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

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