v0.3.9 966d045

settings_dialog.go RawHistoryFolder

1package modes
2
3import (
4	"strings"
5
6	"github.com/mattn/go-runewidth"
7
8	"github.com/patriceckhart/zot/packages/tui"
9)
10
11type settingsDialog struct {
12	active       bool
13	title        string
14	items        []settingsItem
15	cursor       int
16	selecting    bool
17	optionCursor int
18	parentItems  []settingsItem
19	parentCursor int
20}
21
22type settingsItem struct {
23	key      string
24	label    string
25	desc     string
26	value    bool
27	options  []settingsOption
28	children []settingsItem
29	picker   bool
30	choice   int
31	disabled bool
32	hint     string
33}
34
35type settingsOption struct {
36	value string
37	label string
38	desc  string
39}
40
41type settingsAction struct {
42	Toggle            bool
43	Key               string
44	Value             bool
45	StringValue       string
46	ModelShortcutSlot int
47	Close             bool
48}
49
50func newSettingsDialog() *settingsDialog { return &settingsDialog{} }
51
52func (d *settingsDialog) Open(items []settingsItem) bool {
53	if len(items) == 0 {
54		return false
55	}
56	d.title = "settings"
57	d.items = items
58	d.cursor = 0
59	d.selecting = false
60	d.optionCursor = 0
61	d.parentItems = nil
62	d.parentCursor = 0
63	d.active = true
64	return true
65}
66
67func (d *settingsDialog) Close() {
68	d.active = false
69	d.selecting = false
70	d.parentItems = nil
71}
72func (d *settingsDialog) Active() bool { return d != nil && d.active }
73
74func (d *settingsDialog) HandleKey(k tui.Key) settingsAction {
75	if d.selecting {
76		return d.handleOptionKey(k)
77	}
78	switch k.Kind {
79	case tui.KeyUp:
80		if d.cursor > 0 {
81			d.cursor--
82		}
83	case tui.KeyDown:
84		if d.cursor < len(d.items)-1 {
85			d.cursor++
86		}
87	case tui.KeyBackspace:
88		if len(d.items) > 0 {
89			it := d.items[d.cursor]
90			if strings.HasPrefix(it.key, "quick_model_") {
91				return settingsAction{Toggle: true, Key: it.key, StringValue: ""}
92			}
93		}
94	case tui.KeyEsc:
95		if len(d.parentItems) > 0 {
96			d.items = d.parentItems
97			d.cursor = d.parentCursor
98			d.parentItems = nil
99			d.parentCursor = 0
100			d.title = "settings"
101			return settingsAction{}
102		}
103		d.Close()
104		return settingsAction{Close: true}
105	case tui.KeyEnter:
106		return d.toggleCurrent()
107	case tui.KeyRune:
108		if k.Rune == ' ' {
109			return d.toggleCurrent()
110		}
111	}
112	return settingsAction{}
113}
114
115func (d *settingsDialog) handleOptionKey(k tui.Key) settingsAction {
116	it := d.items[d.cursor]
117	switch k.Kind {
118	case tui.KeyUp:
119		if d.optionCursor > 0 {
120			d.optionCursor--
121		}
122	case tui.KeyDown:
123		if d.optionCursor < len(it.options)-1 {
124			d.optionCursor++
125		}
126	case tui.KeyEsc:
127		d.selecting = false
128	case tui.KeyEnter:
129		return d.selectCurrentOption()
130	case tui.KeyRune:
131		if k.Rune == ' ' {
132			return d.selectCurrentOption()
133		}
134	}
135	return settingsAction{}
136}
137
138func (d *settingsDialog) toggleCurrent() settingsAction {
139	if len(d.items) == 0 {
140		d.Close()
141		return settingsAction{Close: true}
142	}
143	it := d.items[d.cursor]
144	if it.disabled {
145		return settingsAction{}
146	}
147	if it.picker {
148		slotText := strings.TrimPrefix(it.key, "quick_model_")
149		slot := 0
150		for _, r := range slotText {
151			if r < '0' || r > '9' {
152				slot = 0
153				break
154			}
155			slot = slot*10 + int(r-'0')
156		}
157		return settingsAction{ModelShortcutSlot: slot}
158	}
159	if len(it.children) > 0 {
160		d.parentItems = d.items
161		d.parentCursor = d.cursor
162		d.items = it.children
163		d.cursor = 0
164		d.optionCursor = 0
165		d.title = "settings: " + it.label
166		return settingsAction{}
167	}
168	if len(it.options) > 0 {
169		d.optionCursor = it.choice
170		if d.optionCursor < 0 || d.optionCursor >= len(it.options) {
171			d.optionCursor = 0
172		}
173		d.selecting = true
174		return settingsAction{}
175	}
176	it.value = !it.value
177	d.items[d.cursor] = it
178	return settingsAction{Toggle: true, Key: it.key, Value: it.value}
179}
180
181func (d *settingsDialog) selectCurrentOption() settingsAction {
182	if len(d.items) == 0 {
183		d.Close()
184		return settingsAction{Close: true}
185	}
186	it := d.items[d.cursor]
187	if len(it.options) == 0 {
188		d.selecting = false
189		return settingsAction{}
190	}
191	if d.optionCursor < 0 || d.optionCursor >= len(it.options) {
192		d.optionCursor = 0
193	}
194	it.choice = d.optionCursor
195	d.items[d.cursor] = it
196	d.selecting = false
197	return settingsAction{Toggle: true, Key: it.key, StringValue: it.options[it.choice].value}
198}
199
200func (d *settingsDialog) Render(th tui.Theme, width int) []string {
201	if !d.Active() {
202		return nil
203	}
204	if d.selecting {
205		return d.renderOptions(th, width)
206	}
207	var lines []string
208	lines = append(lines, frameHeader(th, d.title, width))
209	if len(d.parentItems) > 0 {
210		lines = append(lines, th.FG256(th.Muted, "change with enter/space, esc to go back:"))
211	} else {
212		lines = append(lines, th.FG256(th.Muted, "change with enter/space, esc to close:"))
213	}
214	for i, it := range d.items {
215		box := "[ ]"
216		if it.value {
217			box = "[✓]"
218		}
219		plain := "  " + box + " " + it.label
220		if it.picker || len(it.children) > 0 {
221			box = "[→]"
222			plain = "  " + box + " " + it.label
223		} else if len(it.options) > 0 {
224			box = "[→]"
225			if it.choice < 0 || it.choice >= len(it.options) {
226				it.choice = 0
227			}
228			plain = "  " + box + " " + it.label + ": " + it.options[it.choice].label
229		}
230		if it.hint != "" {
231			plain += "  " + th.FG256(th.Muted, "("+it.hint+")")
232		}
233		if it.disabled {
234			lines = append(lines, th.FG256(th.Muted, plain))
235		} else if i == d.cursor {
236			lines = append(lines, th.PadHighlight(plain, width))
237		} else {
238			lines = append(lines, plain)
239		}
240		if it.desc != "" {
241			for _, desc := range wrapSettingDescription(it.desc, width, 6) {
242				lines = append(lines, th.FG256(th.Muted, desc))
243			}
244		}
245	}
246	lines = append(lines, frameRule(th, width))
247	return lines
248}
249
250func (d *settingsDialog) renderOptions(th tui.Theme, width int) []string {
251	if len(d.items) == 0 || d.cursor < 0 || d.cursor >= len(d.items) {
252		d.selecting = false
253		return d.Render(th, width)
254	}
255	it := d.items[d.cursor]
256	lines := []string{frameHeader(th, "settings: "+it.label, width)}
257	if it.desc != "" {
258		lines = append(lines, th.FG256(th.Muted, it.desc))
259	}
260	lines = append(lines, th.FG256(th.Muted, "select with enter/space, esc to go back:"))
261	for idx, opt := range it.options {
262		marker := "  "
263		if idx == it.choice {
264			marker = "✓ "
265		}
266		plain := "  " + marker + opt.label
267		if idx == d.optionCursor {
268			lines = append(lines, th.PadHighlight(plain, width))
269		} else {
270			lines = append(lines, plain)
271		}
272		if opt.desc != "" {
273			for _, desc := range wrapSettingDescription(opt.desc, width, 6) {
274				lines = append(lines, th.FG256(th.Muted, desc))
275			}
276		}
277	}
278	lines = append(lines, frameRule(th, width))
279	return lines
280}
281
282func wrapSettingDescription(desc string, width, indent int) []string {
283	prefix := strings.Repeat(" ", indent)
284	limit := width - indent
285	if limit < 20 {
286		limit = 20
287	}
288	words := strings.Fields(desc)
289	if len(words) == 0 {
290		return nil
291	}
292	var lines []string
293	line := words[0]
294	for _, word := range words[1:] {
295		candidate := line + " " + word
296		if runewidth.StringWidth(candidate) <= limit {
297			line = candidate
298			continue
299		}
300		lines = append(lines, prefix+line)
301		line = word
302	}
303	lines = append(lines, prefix+line)
304	return lines
305}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close