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}