okami.mom/albedo

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

Aly Raffaufperf(cli): reduce struct padding3922552

main
3.3 KiB130 linesraw
1package tui
2
3import (
4	"slices"
5	"strings"
6
7	"charm.land/bubbles/v2/textinput"
8	tea "charm.land/bubbletea/v2"
9	"github.com/charmbracelet/x/ansi"
10)
11
12// form is a column of text fields with one focused; the MCP and webhook
13// forms both build on it.
14type form struct {
15	Inputs map[string]*textinput.Model
16	Focus  int
17}
18
19// newField is a text input that follows a label instead of a prompt.
20func newField() textinput.Model {
21	input := newTextInput()
22	input.Prompt = ""
23	return input
24}
25
26// ask re-arms input for the next question, masking a secret, and reports
27// the blink command that shows its cursor.
28func ask(input *textinput.Model, value string, secret bool) tea.Cmd {
29	input.Reset()
30	input.EchoMode = echo(secret)
31	input.SetValue(value)
32	input.Focus()
33	return textinput.Blink
34}
35
36func echo(secret bool) textinput.EchoMode {
37	if secret {
38		return textinput.EchoPassword
39	}
40	return textinput.EchoNormal
41}
42
43// newForm makes a field for each key, the masked ones echoing dots, with a
44// length limit no pasted secret reaches.
45func newForm(keys []string, masked ...string) form {
46	f := form{Inputs: map[string]*textinput.Model{}}
47	for _, key := range keys {
48		input := newField()
49		input.CharLimit, input.EchoMode = 4096, echo(slices.Contains(masked, key))
50		f.Inputs[key] = &input
51	}
52	return f
53}
54
55// focus focuses the field named current and blurs the rest.
56func (f *form) focus(current string) {
57	for key, input := range f.Inputs {
58		if key == current {
59			input.Focus()
60		} else {
61			input.Blur()
62		}
63	}
64}
65
66// key applies the movement keys every form shares, cycling through fields
67// from first on; submit reports that the form should be saved.
68func (f *form) key(msg tea.Msg, fields []string, first int) (submit, handled bool) {
69	key, ok := msg.(tea.KeyPressMsg)
70	if !ok {
71		return false, false
72	}
73	delta := 0
74	switch key.String() {
75	case "tab", "down":
76		delta = 1
77	case "shift+tab", "up":
78		delta = -1
79	case "ctrl+s":
80		return true, true
81	case "enter":
82		if f.Focus == len(fields)-1 {
83			return true, true
84		}
85		delta = 1
86	default:
87		return false, false
88	}
89	n := len(fields) - first
90	f.Focus = first + (f.Focus-first+delta+n)%n
91	f.focus(fields[f.Focus])
92	return false, true
93}
94
95// value is a field's text without surrounding space.
96func (f form) value(key string) string { return strings.TrimSpace(f.Inputs[key].Value()) }
97
98// fit sizes the fields to the columns their rows leave for a value; a
99// textinput without a width shows only the first rune of its placeholder.
100func (f form) fit(width int) {
101	for _, input := range f.Inputs {
102		input.SetWidth(max(1, width))
103	}
104}
105
106// formRow is one labelled field, marked when focused.
107func formRow(focused bool, label string, pad int, value string, width int) string {
108	lead := "  "
109	if focused {
110		lead = promptLead()
111	}
112	return ansi.Truncate(lead+padRight(label, pad)+value, width, "…")
113}
114
115// formFooter explains the focused field, then offers the keys every form
116// shares after any of its own.
117func formFooter(note string, width int, own ...hint) string {
118	line := keyHints(append(own, hint{"tab/↑↓", "move"}, hint{"enter", "next"}, hint{"ctrl+s", "save"}, hint{"esc", "cancel"})...)
119	if note != "" {
120		line = DefaultStyles.Faint.Render(note) + DefaultStyles.Decor.Render(" · ") + line
121	}
122	return ansi.Truncate(line, width, "…")
123}
124
125func padRight(s string, n int) string {
126	if len(s) >= n {
127		return s + " "
128	}
129	return s + strings.Repeat(" ", n-len(s))
130}