v0.2.12 7a7bf0b

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

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