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}