main 5041353

config_view.go RawHistoryFolder

1package systemviews
2
3import (
4	"fmt"
5	"reflect"
6	"sort"
7	"strings"
8
9	"github.com/bnema/dumber/internal/application/dto"
10	"github.com/bnema/dumber/internal/application/port"
11)
12
13type configRenderData struct {
14	Config      dto.SystemviewConfigPayload
15	Keybindings port.KeybindingsConfig
16	Notice      string
17	Error       string
18}
19
20// maxFlattenDepth caps recursive config flattening to avoid runaway nesting.
21const maxFlattenDepth = 16
22
23func configHTML(data configRenderData) string {
24	return mustRenderComponent(ConfigView(data))
25}
26
27func configKeybindingSummary(keybindings port.KeybindingsConfig) string {
28	if keybindings.Groups == nil {
29		return "Keybindings unavailable"
30	}
31	groups, bindings := summarizeKeybindings(keybindings)
32	return fmt.Sprintf(
33		"Keybindings loaded (%d %s, %d %s)",
34		groups,
35		plural(groups, "group"),
36		bindings,
37		plural(bindings, "binding"),
38	)
39}
40
41func configKeybindingGroups(keybindings port.KeybindingsConfig) []renderedKeybindingGroup {
42	if keybindings.Groups == nil {
43		return nil
44	}
45	return convertPortKeybindingGroups(keybindings.Groups)
46}
47
48func keybindingGroupTitle(group renderedKeybindingGroup) string {
49	title := strings.TrimSpace(group.DisplayName)
50	if title == "" {
51		title = strings.TrimSpace(group.Mode)
52	}
53	if title == "" {
54		return "Keybinding group"
55	}
56	return title
57}
58
59func keybindingGroupMeta(group renderedKeybindingGroup) string {
60	meta := make([]string, 0, 2)
61	if strings.TrimSpace(group.Mode) != "" {
62		meta = append(meta, "mode: "+group.Mode)
63	}
64	if strings.TrimSpace(group.Activation) != "" {
65		meta = append(meta, "activation: "+group.Activation)
66	}
67	return strings.Join(meta, " • ")
68}
69
70func keybindingDescription(binding renderedKeybinding) string {
71	description := strings.TrimSpace(binding.Description)
72	if description != "" {
73		return description
74	}
75	if strings.TrimSpace(binding.Action) != "" {
76		return strings.TrimSpace(binding.Action)
77	}
78	return "Unnamed binding"
79}
80
81func keybindingAction(binding renderedKeybinding) string {
82	action := strings.TrimSpace(binding.Action)
83	if action == "" {
84		return "binding"
85	}
86	return action
87}
88
89func keybindingIsCustom(binding renderedKeybinding) bool {
90	return binding.IsCustom || !sameStringSlice(binding.Keys, binding.DefaultKeys)
91}
92
93func keybindingStatus(binding renderedKeybinding) string {
94	if keybindingIsCustom(binding) {
95		return "custom"
96	}
97	return "default"
98}
99
100func keybindingKeysValue(keys []string) string {
101	return strings.Join(keys, ", ")
102}
103
104type searchShortcutRow struct {
105	Key         string
106	URL         string
107	Description string
108}
109
110func searchShortcutRows(shortcuts map[string]dto.SearchShortcut) []searchShortcutRow {
111	if len(shortcuts) == 0 {
112		return nil
113	}
114	keys := make([]string, 0, len(shortcuts))
115	for key := range shortcuts {
116		keys = append(keys, key)
117	}
118	sort.Strings(keys)
119	rows := make([]searchShortcutRow, 0, len(keys))
120	for _, key := range keys {
121		shortcut := shortcuts[key]
122		rows = append(rows, searchShortcutRow{
123			Key:         key,
124			URL:         shortcut.URL,
125			Description: shortcut.Description,
126		})
127	}
128	return rows
129}
130
131func formatConfigInt(value int) string {
132	return fmt.Sprintf("%d", value)
133}
134
135func formatConfigFloat(value float64) string {
136	return fmt.Sprintf("%g", value)
137}
138
139func colorSchemeSelected(current, option string) bool {
140	return strings.EqualFold(strings.TrimSpace(current), option)
141}
142
143func performanceProfileSelected(current, option string) bool {
144	return strings.EqualFold(strings.TrimSpace(current), option)
145}
146
147func engineIsWebKit(engine string) bool {
148	return strings.EqualFold(strings.TrimSpace(engine), "webkit")
149}
150
151func flattenValues(value any) []kvPair {
152	rows := make([]kvPair, 0)
153	flattenValue(&rows, "", reflect.ValueOf(value), 0)
154	return rows
155}
156
157func flattenValue(rows *[]kvPair, path string, value reflect.Value, depth int) {
158	if !value.IsValid() {
159		appendFlatValue(rows, path, "null")
160		return
161	}
162	if depth >= maxFlattenDepth {
163		appendFlatValue(rows, path, "...")
164		return
165	}
166
167	for value.Kind() == reflect.Interface {
168		if value.IsNil() {
169			appendFlatValue(rows, path, "null")
170			return
171		}
172		value = value.Elem()
173	}
174
175	for value.Kind() == reflect.Pointer {
176		if value.IsNil() {
177			appendFlatValue(rows, path, "null")
178			return
179		}
180		value = value.Elem()
181	}
182
183	switch value.Kind() {
184	case reflect.Struct:
185		flattenStructValue(rows, path, value, depth)
186	case reflect.Map:
187		flattenMapValue(rows, path, value, depth)
188	case reflect.Slice, reflect.Array:
189		flattenListValue(rows, path, value, depth)
190	default:
191		appendFlatValue(rows, path, formatFlatValue(value))
192	}
193}
194
195func flattenStructValue(rows *[]kvPair, path string, value reflect.Value, depth int) {
196	typeOfValue := value.Type()
197	for i := 0; i < value.NumField(); i++ {
198		field := typeOfValue.Field(i)
199		if !field.IsExported() {
200			continue
201		}
202		name := jsonFieldName(field)
203		if name == "-" {
204			continue
205		}
206		flattenValue(rows, childFlatPath(path, name), value.Field(i), depth+1)
207	}
208}
209
210func flattenMapValue(rows *[]kvPair, path string, value reflect.Value, depth int) {
211	if value.Len() == 0 {
212		appendFlatValue(rows, path, "{}")
213		return
214	}
215
216	keys := value.MapKeys()
217	sort.Slice(keys, func(i, j int) bool {
218		return mapKeyString(keys[i]) < mapKeyString(keys[j])
219	})
220
221	for _, key := range keys {
222		name := mapKeyString(key)
223		flattenValue(rows, childFlatPath(path, name), value.MapIndex(key), depth+1)
224	}
225}
226
227func flattenListValue(rows *[]kvPair, path string, value reflect.Value, depth int) {
228	if value.Len() == 0 {
229		appendFlatValue(rows, path, "[]")
230		return
231	}
232
233	for i := 0; i < value.Len(); i++ {
234		nextPath := fmt.Sprintf("%s[%d]", path, i)
235		if path == "" {
236			nextPath = fmt.Sprintf("[%d]", i)
237		}
238		flattenValue(rows, nextPath, value.Index(i), depth+1)
239	}
240}
241
242func childFlatPath(path, name string) string {
243	if path == "" {
244		return name
245	}
246	return path + "." + name
247}
248
249func appendFlatValue(rows *[]kvPair, path, value string) {
250	if strings.TrimSpace(path) == "" {
251		path = "value"
252	}
253	*rows = append(*rows, kvPair{Label: path, Value: value})
254}
255
256func formatFlatValue(value reflect.Value) string {
257	if !value.IsValid() {
258		return "null"
259	}
260
261	switch value.Kind() {
262	case reflect.String:
263		return value.String()
264	case reflect.Bool:
265		return fmt.Sprint(value.Bool())
266	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
267		return fmt.Sprint(value.Int())
268	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
269		return fmt.Sprint(value.Uint())
270	case reflect.Float32, reflect.Float64:
271		return fmt.Sprint(value.Float())
272	case reflect.Complex64, reflect.Complex128:
273		return fmt.Sprint(value.Complex())
274	default:
275		if !value.CanInterface() {
276			return "<unexported>"
277		}
278		return fmt.Sprint(value.Interface())
279	}
280}
281
282func jsonFieldName(field reflect.StructField) string {
283	name := field.Tag.Get("json")
284	if name == "" {
285		return field.Name
286	}
287	if idx := strings.Index(name, ","); idx >= 0 {
288		name = name[:idx]
289	}
290	if name == "" {
291		return field.Name
292	}
293	return name
294}
295
296func mapKeyString(value reflect.Value) string {
297	if value.Kind() == reflect.String {
298		return value.String()
299	}
300	if value.CanInterface() {
301		return fmt.Sprint(value.Interface())
302	}
303	return formatFlatValue(value)
304}
305
306func summarizeKeybindings(keybindings port.KeybindingsConfig) (groups, bindings int) {
307	for _, group := range keybindings.Groups {
308		groups++
309		bindings += len(group.Bindings)
310	}
311	return groups, bindings
312}
313
314func convertPortKeybindingGroups(groups []port.KeybindingGroup) []renderedKeybindingGroup {
315	rendered := make([]renderedKeybindingGroup, 0, len(groups))
316	for _, group := range groups {
317		rendered = append(rendered, renderedKeybindingGroup{
318			DisplayName: strings.TrimSpace(group.DisplayName),
319			Mode:        strings.TrimSpace(group.Mode),
320			Activation:  strings.TrimSpace(group.Activation),
321			Bindings:    convertPortKeybindingEntries(group.Bindings),
322		})
323	}
324	return rendered
325}
326
327func convertPortKeybindingEntries(bindings []port.KeybindingEntry) []renderedKeybinding {
328	rendered := make([]renderedKeybinding, 0, len(bindings))
329	for _, binding := range bindings {
330		rendered = append(rendered, renderedKeybinding{
331			Description: strings.TrimSpace(binding.Description),
332			Action:      strings.TrimSpace(binding.Action),
333			Keys:        append([]string(nil), binding.Keys...),
334			DefaultKeys: append([]string(nil), binding.DefaultKeys...),
335			IsCustom:    binding.IsCustom,
336		})
337	}
338	return rendered
339}
340
341type renderedKeybindingGroup struct {
342	DisplayName string
343	Mode        string
344	Activation  string
345	Bindings    []renderedKeybinding
346}
347
348type renderedKeybinding struct {
349	Description string
350	Action      string
351	Keys        []string
352	DefaultKeys []string
353	IsCustom    bool
354}
355
356func sameStringSlice(a, b []string) bool {
357	if len(a) != len(b) {
358		return false
359	}
360	for i := range a {
361		if a[i] != b[i] {
362			return false
363		}
364	}
365	return true
366}
367
368func plural(count int, singular string) string {
369	if count == 1 {
370		return singular
371	}
372	return singular + "s"
373}

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