▍ humdrum codex / glint v1.1.2
license AGPL-3.0

feat: naming dialog view (centered bordered box)

c722d38c9615fc5ddb44650db262174cad2d16c7
humdrum-tiv <45084903+humdrum-tiv@users.noreply.github.com> · 2026-07-30 19:04

parent d94299a4

feat: naming dialog view (centered bordered box)

Title, target-folder hint, input, validation line, and key footer in the
same visual family as the help overlay (TASK-046).

2 files changed

internal/dialog/dialog.go +33 −0
@@ -11,6 +11,7 @@ 	"glint/internal/theme"
 
 	"github.com/charmbracelet/bubbles/textinput"
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
 )
 
 // Result is what a key press did to the dialog.
@@ -83,3 +84,35 @@ 	var cmd tea.Cmd
 	m.input, cmd = m.input.Update(msg)
 	return ResultNone, cmd
 }
+
+// boxMin is the narrowest the dialog box gets before it just takes the column.
+const boxMin = 24
+
+// View renders the dialog as a centered bordered box: title, the folder the
+// name lands in, the input, an optional validation line, then the key footer.
+func (m *Model) View() string {
+	inner := lipgloss.NewStyle().Foreground(m.th.Heading).Bold(true).Render(m.title)
+	if m.dirHint != "" {
+		inner += "\n" + lipgloss.NewStyle().Foreground(m.th.Muted).Render("in "+m.dirHint)
+	}
+	inner += "\n\n" + m.input.View()
+	if m.err != "" {
+		inner += "\n\n" + lipgloss.NewStyle().Foreground(m.th.Spell).Render(m.err)
+	}
+	inner += "\n\n" + lipgloss.NewStyle().Foreground(m.th.Muted).
+		Render("Enter to confirm · Esc to cancel")
+
+	w := m.width - 2 // the border takes one cell each side
+	if w < boxMin {
+		w = boxMin
+	}
+	box := lipgloss.NewStyle().
+		Border(lipgloss.RoundedBorder()).
+		BorderForeground(m.th.Heading).
+		BorderBackground(m.th.Background).
+		Background(m.th.Background).
+		Foreground(m.th.Text).
+		Padding(0, 1).
+		Width(w)
+	return box.Render(inner)
+}
internal/dialog/dialog_test.go +38 −0
@@ -1,12 +1,14 @@
 package dialog
 
 import (
+	"strings"
 	"testing"
 
 	"glint/internal/theme"
 
 	"github.com/charmbracelet/bubbles/cursor"
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
 )
 
 func newModel() *Model { return New(theme.FlexokiDark()) }
@@ -106,3 +108,39 @@ 	if m.err == "" {
 		t.Error("err cleared by a cursor blink; only typing should clear it")
 	}
 }
+
+func TestViewShowsTitleDirAndFooter(t *testing.T) {
+	m := newModel()
+	m.SetWidth(60)
+	m.Open("New note", "~/Humdrum/Inbox/", "")
+	out := m.View()
+	for _, want := range []string{"New note", "~/Humdrum/Inbox/", "Enter", "Esc"} {
+		if !strings.Contains(out, want) {
+			t.Errorf("View() missing %q:\n%s", want, out)
+		}
+	}
+}
+
+func TestViewShowsErrorWhenSet(t *testing.T) {
+	m := newModel()
+	m.SetWidth(60)
+	m.Open("New note", "~/Notes/", "")
+	if strings.Contains(m.View(), "Name taken") {
+		t.Fatal("View() shows the error before SetError")
+	}
+	m.SetError("Name taken")
+	if !strings.Contains(m.View(), "Name taken") {
+		t.Errorf("View() missing the error line:\n%s", m.View())
+	}
+}
+
+func TestViewFitsContentWidth(t *testing.T) {
+	m := newModel()
+	m.SetWidth(40)
+	m.Open("New note", "~/Notes/", "")
+	for i, ln := range strings.Split(m.View(), "\n") {
+		if w := lipgloss.Width(ln); w > 40 {
+			t.Errorf("line %d width = %d, want <= 40", i, w)
+		}
+	}
+}