▍ humdrum codex / glint v1.1.2
license AGPL-3.0
3.7 KB raw
  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)
}