mode_toaster_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)
11
12// legendGroupOrder is the fixed display order of legend groups.
13var legendGroupOrder = []string{"SPLIT", "FOCUS", "SCROLL", "RESIZE", "SWITCH", "JUMP", "SELECT", "MANAGE", "MODES", "EXIT"}
14
15// legendRow is one action of a legend group.
16type legendRow struct {
17	Action string
18	Keys   []string // as configured; the view formats them for display
19	Desc   string
20}
21
22// legendGroup is a titled run of related actions. A group is never split
23// across columns.
24type legendGroup struct {
25	Name string
26	Rows []legendRow
27}
28
29func legendActions(mode input.Mode, workspace entity.WorkspaceConfig, session entity.SessionConfig) map[string]entity.ActionBinding {
30	switch mode {
31	case input.ModePane:
32		return workspace.PaneMode.Actions
33	case input.ModeResize:
34		return workspace.ResizeMode.Actions
35	case input.ModeVim:
36		return workspace.VimMode.Actions
37	case input.ModeTab:
38		return workspace.TabMode.Actions
39	case input.ModeSession:
40		return session.SessionMode.Actions
41	case input.ModePrefix:
42		return workspace.PrefixMode.Actions
43	default:
44		return nil
45	}
46}
47
48// buildLegendGroups arranges the bound actions of mode in legendGroupOrder,
49// each group sorted by action name. Actions without keys are left out.
50func buildLegendGroups(mode input.Mode, actions map[string]entity.ActionBinding) []legendGroup {
51	names := make(map[string][]string)
52	for name, binding := range actions {
53		if len(binding.Keys) == 0 {
54			continue
55		}
56		group := modeLegendGroup(mode, name)
57		names[group] = append(names[group], name)
58	}
59	var groups []legendGroup
60	for _, groupName := range legendGroupOrder {
61		members := names[groupName]
62		if len(members) == 0 {
63			continue
64		}
65		slices.Sort(members)
66		group := legendGroup{Name: groupName}
67		for _, name := range members {
68			binding := actions[name]
69			desc := binding.Desc
70			if desc == "" {
71				desc = strings.ReplaceAll(name, "-", " ")
72			}
73			group.Rows = append(group.Rows, legendRow{Action: name, Keys: binding.Keys, Desc: desc})
74		}
75		groups = append(groups, group)
76	}
77	return groups
78}
79
80// legendGroupHeights measures groups in rows: the title counts as one.
81func legendGroupHeights(groups []legendGroup) []int {
82	heights := make([]int, len(groups))
83	for i, group := range groups {
84		heights[i] = 1 + len(group.Rows)
85	}
86	return heights
87}
88
89func displayLegendKey(key string) string {
90	switch strings.ToLower(key) {
91	case "arrowright":
92		return "→"
93	case "arrowleft":
94		return "←"
95	case "arrowup":
96		return "↑"
97	case "arrowdown":
98		return "↓"
99	case "escape":
100		return "esc"
101	case "enter":
102		return "↵"
103	default:
104		return key
105	}
106}
107
108func legendSequenceMatches(pending string, keys []string) bool {
109	sequence := strings.TrimLeftFunc(pending, unicode.IsDigit)
110	if sequence == "" {
111		return true
112	}
113	for _, key := range keys {
114		if strings.HasPrefix(key, sequence) {
115			return true
116		}
117	}
118	return false
119}
120
121// vimLegendGroup returns the legend group of a Vim Mode action, or "" when the
122// action belongs to a generic group.
123func vimLegendGroup(name string) string {
124	switch {
125	case strings.HasPrefix(name, "hint-") || strings.HasPrefix(name, "yank-") || name == "visual":
126		return "SELECT"
127	case strings.Contains(name, "next") || strings.Contains(name, "prev") || name == "outline":
128		return "JUMP"
129	default:
130		return ""
131	}
132}
133
134func modeLegendGroup(mode input.Mode, name string) string {
135	switch {
136	case name == "confirm" || name == "cancel":
137		return "EXIT"
138	case strings.HasPrefix(name, "split-"):
139		return "SPLIT"
140	case strings.HasPrefix(name, "enter-"):
141		return "MODES"
142	case strings.HasPrefix(name, "focus-"):
143		return "FOCUS"
144	case strings.HasPrefix(name, "resize-"):
145		return "RESIZE"
146	case strings.HasPrefix(name, "vim-scroll-") || strings.HasPrefix(name, "half-page-"):
147		return "SCROLL"
148	case mode == input.ModeVim && vimLegendGroup(name) != "":
149		return vimLegendGroup(name)
150	case (mode == input.ModeTab || mode == input.ModePrefix) && (name == "next-tab" || name == "previous-tab"):
151		return "SWITCH"
152	default:
153		return "MANAGE"
154	}
155}

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