mode_frame_legend.go RawHistoryFolder

1package ui
2
3import (
4	"slices"
5	"strings"
6	"unicode"
7
8	"github.com/bnema/dumber/internal/domain/entity"
9	"github.com/bnema/dumber/internal/ui/input"
10	"github.com/bnema/puregotk/v4/glib"
11	"github.com/bnema/puregotk/v4/gtk"
12)
13
14const legendFlashDurationMs = 120
15
16func legendActions(mode input.Mode, workspace entity.WorkspaceConfig, session entity.SessionConfig) map[string]entity.ActionBinding {
17	switch mode {
18	case input.ModePane:
19		return workspace.PaneMode.Actions
20	case input.ModeResize:
21		return workspace.ResizeMode.Actions
22	case input.ModeVim:
23		return workspace.VimMode.Actions
24	case input.ModeTab:
25		return workspace.TabMode.Actions
26	case input.ModeSession:
27		return session.SessionMode.Actions
28	case input.ModePrefix:
29		return workspace.PrefixMode.Actions
30	default:
31		return nil
32	}
33}
34
35func (f *modeFrame) build(actions map[string]entity.ActionBinding) {
36	if f.flashTimer != 0 {
37		glib.SourceRemove(f.flashTimer)
38		f.flashTimer = 0
39	}
40	f.content.RemoveAll()
41	f.rows = make(map[string][]*gtk.Widget)
42	f.keycaps = make(map[string][]*gtk.Widget)
43	f.heading.SetText(f.mode.DisplayName() + "  ·  ESC to exit")
44	groups := make(map[string][]string)
45	for name, binding := range actions {
46		if len(binding.Keys) == 0 {
47			continue
48		}
49		group := modeLegendGroup(f.mode, name)
50		groups[group] = append(groups[group], name)
51	}
52	order := []string{"SPLIT", "FOCUS", "SCROLL", "RESIZE", "SWITCH", "JUMP", "MANAGE", "MODES", "EXIT"}
53	index := 0
54	for _, groupName := range order {
55		names := groups[groupName]
56		if len(names) == 0 {
57			continue
58		}
59		slices.Sort(names)
60		column := gtk.NewBox(gtk.OrientationVerticalValue, 0)
61		column.SetCanFocus(false)
62		column.SetCanTarget(false)
63		column.AddCssClass("mode-legend-group")
64		title := gtk.NewLabel(&groupName)
65		title.SetCanFocus(false)
66		title.SetCanTarget(false)
67		title.SetHalign(gtk.AlignStartValue)
68		title.AddCssClass("mode-legend-group-title")
69		column.Append(&title.Widget)
70		for _, name := range names {
71			column.Append(&f.actionRow(name, actions[name]).Widget)
72		}
73		f.content.Append(&column.Widget)
74		if child := f.content.GetChildAtIndex(index); child != nil {
75			child.SetCanFocus(false)
76			child.SetCanTarget(false)
77		}
78		index++
79	}
80}
81
82func (f *modeFrame) actionRow(name string, binding entity.ActionBinding) *gtk.Box {
83	row := gtk.NewBox(gtk.OrientationHorizontalValue, 0)
84	row.SetCanFocus(false)
85	row.SetCanTarget(false)
86	row.AddCssClass("mode-legend-row")
87	f.rows[name] = append(f.rows[name], &row.Widget)
88	for _, key := range binding.Keys {
89		displayKey := displayLegendKey(key)
90		keycap := gtk.NewLabel(&displayKey)
91		keycap.SetCanFocus(false)
92		keycap.SetCanTarget(false)
93		keycap.AddCssClass("mode-legend-keycap")
94		row.Append(&keycap.Widget)
95		f.keycaps[name] = append(f.keycaps[name], &keycap.Widget)
96	}
97	label := binding.Desc
98	if label == "" {
99		label = strings.ReplaceAll(name, "-", " ")
100	}
101	text := gtk.NewLabel(&label)
102	text.SetCanFocus(false)
103	text.SetCanTarget(false)
104	text.SetHalign(gtk.AlignStartValue)
105	text.SetEllipsize(3) // PangoEllipsizeEnd
106	text.AddCssClass("mode-legend-description")
107	row.Append(&text.Widget)
108	return row
109}
110
111func displayLegendKey(key string) string {
112	switch strings.ToLower(key) {
113	case "arrowright":
114		return "→"
115	case "arrowleft":
116		return "←"
117	case "arrowup":
118		return "↑"
119	case "arrowdown":
120		return "↓"
121	case "escape":
122		return "esc"
123	case "enter":
124		return "↵"
125	default:
126		return key
127	}
128}
129
130func (f *modeFrame) setPending(pending string, actions map[string]entity.ActionBinding) {
131	if f == nil || f.lingering || f.mode != input.ModeVim {
132		return
133	}
134	f.pending = pending
135	for name, rows := range f.rows {
136		matches := legendSequenceMatches(pending, actions[name].Keys)
137		for _, row := range rows {
138			if matches {
139				row.RemoveCssClass("mode-legend-dim")
140			} else {
141				row.AddCssClass("mode-legend-dim")
142			}
143		}
144	}
145}
146
147func legendSequenceMatches(pending string, keys []string) bool {
148	sequence := strings.TrimLeftFunc(pending, unicode.IsDigit)
149	if sequence == "" {
150		return true
151	}
152	for _, key := range keys {
153		if strings.HasPrefix(key, sequence) {
154			return true
155		}
156	}
157	return false
158}
159
160func (f *modeFrame) flash(action input.Action) {
161	if f == nil || f.lingering || !f.visible || !f.config.ModeLegendAnimations {
162		return
163	}
164	name := string(action)
165	keycaps := f.keycaps[name]
166	if len(keycaps) == 0 {
167		name = strings.ReplaceAll(name, "_", "-")
168		keycaps = f.keycaps[name]
169	}
170	if len(keycaps) == 0 {
171		return
172	}
173	if f.flashTimer != 0 {
174		glib.SourceRemove(f.flashTimer)
175		f.flashTimer = 0
176	}
177	for _, caps := range f.keycaps {
178		for _, cap := range caps {
179			cap.RemoveCssClass("mode-legend-flash")
180		}
181	}
182	for _, cap := range keycaps {
183		cap.AddCssClass("mode-legend-flash")
184	}
185	cb := glib.SourceFunc(func(_ uintptr) bool {
186		for _, cap := range keycaps {
187			cap.RemoveCssClass("mode-legend-flash")
188		}
189		f.flashTimer = 0
190		return false
191	})
192	f.flashTimer = glib.TimeoutAdd(legendFlashDurationMs, &cb, 0)
193}

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 8

Tags 42

↑↓ moveenter openesc close