1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
// 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
}
// 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)
}
|