// Package dialog is glint's centered naming prompt: the modal box that asks for // a filename when a note is created or renamed. It is a self-contained // sub-model — it knows nothing about files, directories, or the app's modes, // and only reports whether the user confirmed or cancelled. package dialog import ( "strings" "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. type Result int const ( ResultNone Result = iota // still typing ResultConfirm // Enter — the caller reads Value() ResultCancel // Esc — the caller restores the previous mode ) // Model is the naming prompt. type Model struct { th theme.Theme input textinput.Model title string // "New note" / "Rename" dirHint string // the folder the name resolves under, shown under the title err string // validation message shown under the input width int // content-column width the box is sized against } // New builds an empty dialog. Open puts it into a usable state. func New(th theme.Theme) *Model { ti := textinput.New() ti.Prompt = "› " ti.Placeholder = "name…" return &Model{th: th, input: ti} } // Open resets the dialog for one use: title above, dirHint under it, prefill in // the input with the cursor at the end, no error. func (m *Model) Open(title, dirHint, prefill string) { m.title = title m.dirHint = dirHint m.err = "" m.input.SetValue(prefill) m.input.CursorEnd() m.input.Focus() } // Title is the dialog's heading. func (m *Model) Title() string { return m.title } // Value is the typed name with surrounding whitespace trimmed. func (m *Model) Value() string { return strings.TrimSpace(m.input.Value()) } // SetError shows msg under the input; the dialog stays open. Typing clears it. func (m *Model) SetError(msg string) { m.err = msg } // SetTheme repaints the dialog after a theme cycle. func (m *Model) SetTheme(th theme.Theme) { m.th = th } // SetWidth sets the content-column width the box is sized against. func (m *Model) SetWidth(w int) { m.width = w } // Update handles one message. Enter confirms, Esc cancels, other key presses // edit the input and clear a stale validation message. Non-key messages do not // affect the error display. func (m *Model) Update(msg tea.Msg) (Result, tea.Cmd) { if k, ok := msg.(tea.KeyMsg); ok { switch k.Type { case tea.KeyEnter: return ResultConfirm, nil case tea.KeyEsc: return ResultCancel, nil } m.err = "" // typing clears a stale validation message } var cmd tea.Cmd m.input, cmd = m.input.Update(msg) return ResultNone, cmd } // 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 < 1 { w = 1 } 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) }