okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawnadd charm tui skillf7fcae1

main
3.6 KiB136 linesraw
1package main
2
3import (
4	"charm.land/bubbles/v2/textinput"
5	tea "charm.land/bubbletea/v2"
6	"context"
7	"example.com/charm-tui-worked/internal/display"
8	"fmt"
9	"strings"
10	"time"
11)
12
13type debounceMsg struct {
14	ticket ticket
15	err    error
16}
17type loadedMsg struct {
18	generation uint64
19	rows       []string
20	err        error
21}
22
23func debounceCmd(t ticket) tea.Cmd {
24	return func() tea.Msg { return debounceMsg{ticket: t, err: await(t.ctx, 150*time.Millisecond)} }
25}
26func loadCmd(t ticket, search searchFunc) tea.Cmd {
27	// The closure captures values and a service, never the live model.
28	return func() tea.Msg {
29		rows, err := search(t.ctx, t.query)
30		return loadedMsg{generation: t.generation, rows: rows, err: err}
31	}
32}
33
34type model struct {
35	input         textinput.Model
36	state         searchState
37	search        searchFunc
38	initial       ticket
39	width, height int
40}
41
42func newModel(ctx context.Context, search searchFunc) *model {
43	input := textinput.New()
44	input.Prompt = "/ "
45	input.Placeholder = "try slow, error, or queue"
46	input.SetVirtualCursor(true)
47	input.SetWidth(55)
48	m := &model{input: input, state: searchState{root: ctx}, search: search, width: 60, height: 12}
49	m.initial = m.state.change("")
50	return m
51}
52
53func (m *model) Init() tea.Cmd {
54	return tea.Batch(m.input.Focus(), debounceCmd(m.initial), tea.RequestBackgroundColor)
55}
56
57func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
58	switch msg := msg.(type) {
59	case tea.WindowSizeMsg:
60		m.width = min(72, max(0, msg.Width))
61		m.height = min(12, max(0, msg.Height))
62		m.input.SetWidth(max(1, m.width-3))
63		return m, nil
64	case tea.BackgroundColorMsg:
65		m.input.SetStyles(textinput.DefaultStyles(msg.IsDark()))
66		return m, nil
67	case tea.KeyPressMsg:
68		switch msg.String() {
69		case "esc", "ctrl+c":
70			m.state.stop()
71			return m, tea.Quit
72		case "ctrl+r":
73			return m, debounceCmd(m.state.change(m.input.Value()))
74		}
75	case debounceMsg:
76		// begin also rejects a duplicated current-generation debounce delivery.
77		if !m.state.begin(msg.ticket.generation) {
78			return m, nil
79		}
80		if msg.err != nil {
81			m.state.complete(msg.ticket.generation, nil, msg.err)
82			return m, nil
83		}
84		return m, loadCmd(msg.ticket, m.search)
85	case loadedMsg:
86		m.state.complete(msg.generation, msg.rows, msg.err)
87		return m, nil
88	}
89	before := m.input.Value()
90	var childCmd tea.Cmd
91	m.input, childCmd = m.input.Update(msg)
92	if m.input.Value() != before {
93		t := m.state.change(m.input.Value())
94		return m, tea.Batch(childCmd, debounceCmd(t))
95	}
96	return m, childCmd
97}
98
99func (m *model) View() tea.View {
100	if m.width <= 0 || m.height <= 0 {
101		return tea.NewView("")
102	}
103	if m.width < 20 || m.height < 6 {
104		return tea.NewView(display.Line("resize · esc quit", m.width))
105	}
106	status := "waiting for input"
107	switch m.state.phase {
108	case waiting:
109		status = "waiting for typing to settle"
110	case loading:
111		status = "searching…"
112	case ready:
113		status = fmt.Sprintf("%d matches", len(m.state.rows))
114	case failed:
115		status = "search failed · ctrl+r retry"
116	}
117	rows := make([]string, 0, len(m.state.rows))
118	for _, row := range m.state.rows {
119		rows = append(rows, display.SingleLine(row))
120	}
121	if m.state.phase == ready && len(rows) == 0 {
122		rows = append(rows, "no matches · edit the query")
123	}
124	if m.state.err != nil {
125		rows = append(rows, display.SingleLine(m.state.err.Error()))
126	}
127	// All four chrome rows are explicitly clipped to one line.
128	content := []string{
129		display.Line("server search · simulated service", m.width),
130		display.Line(m.input.View(), m.width),
131		display.Line(status, m.width),
132		display.FillRows(rows, m.width, display.BodyRows(m.height, 3, 1)),
133		display.Line("esc quit · ctrl+r retry", m.width),
134	}
135	return tea.NewView(strings.Join(content, "\n"))
136}