detached 18449b5

refactor(systemviews): render config with templ

commit
18449b5dbe6e034f682566aca06b92e844697c43
author
brice · · 2026-04-24 02:53 UTC
parent
a743a66
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +446 -115
A internal/ui/systemviews/config.templ +67 -0
@@ -0,0 +1,67 @@
1+package systemviews
2+
3+templ ConfigView(data configRenderData) {
4+ @Section("sv-config-values", "Config values") {
5+ @KV(flattenValues(data.Config))
6+ }
7+ @Section("sv-config-keybindings", "Keybindings") {
8+ @Meta(configKeybindingSummary(data.Keybindings))
9+ @ConfigKeybindings(data.Keybindings)
10+ }
11+}
12+
13+templ ConfigKeybindings(keybindings any) {
14+ {{ groups := configKeybindingGroups(keybindings) }}
15+ if groups == nil {
16+ @EmptyState("Keybindings unavailable")
17+ } else if len(groups) == 0 {
18+ @EmptyState("No keybindings configured")
19+ } else {
20+ for _, group := range groups {
21+ @ConfigKeybindingGroup(group)
22+ }
23+ }
24+}
25+
26+templ ConfigKeybindingGroup(group renderedKeybindingGroup) {
27+ <div class="sv-group">
28+ <div class="sv-group-header">
29+ <div>
30+ <h4>{ keybindingGroupTitle(group) }</h4>
31+ if keybindingGroupMeta(group) != "" {
32+ <p class="sv-meta">{ keybindingGroupMeta(group) }</p>
33+ }
34+ </div>
35+ </div>
36+ <div class="sv-group-body">
37+ if len(group.Bindings) == 0 {
38+ @EmptyState("No bindings in this group")
39+ } else {
40+ <ul class="sv-list">
41+ for _, binding := range group.Bindings {
42+ @ConfigKeybinding(binding)
43+ }
44+ </ul>
45+ }
46+ </div>
47+ </div>
48+}
49+
50+templ ConfigKeybinding(binding renderedKeybinding) {
51+ <li class="sv-list-row">
52+ <div class="sv-keybinding-row">
53+ <div>
54+ <strong>{ keybindingDescription(binding) }</strong>
55+ <p class="sv-meta">{ keybindingAction(binding) }</p>
56+ </div>
57+ <div class="sv-keybinding-keys">
58+ @KeyList(binding.Keys)
59+ <span class="sv-badge">{ keybindingStatus(binding) }</span>
60+ if keybindingIsCustom(binding) && len(binding.DefaultKeys) > 0 {
61+ <span class="sv-meta">default:</span>
62+ @KeyList(binding.DefaultKeys)
63+ }
64+ </div>
65+ </div>
66+ </li>
67+}
@@ -0,0 +1,67 @@
1+package systemviews
2+
3+templ ConfigView(data configRenderData) {
4+ @Section("sv-config-values", "Config values") {
5+ @KV(flattenValues(data.Config))
6+ }
7+ @Section("sv-config-keybindings", "Keybindings") {
8+ @Meta(configKeybindingSummary(data.Keybindings))
9+ @ConfigKeybindings(data.Keybindings)
10+ }
11+}
12+
13+templ ConfigKeybindings(keybindings any) {
14+ {{ groups := configKeybindingGroups(keybindings) }}
15+ if groups == nil {
16+ @EmptyState("Keybindings unavailable")
17+ } else if len(groups) == 0 {
18+ @EmptyState("No keybindings configured")
19+ } else {
20+ for _, group := range groups {
21+ @ConfigKeybindingGroup(group)
22+ }
23+ }
24+}
25+
26+templ ConfigKeybindingGroup(group renderedKeybindingGroup) {
27+ <div class="sv-group">
28+ <div class="sv-group-header">
29+ <div>
30+ <h4>{ keybindingGroupTitle(group) }</h4>
31+ if keybindingGroupMeta(group) != "" {
32+ <p class="sv-meta">{ keybindingGroupMeta(group) }</p>
33+ }
34+ </div>
35+ </div>
36+ <div class="sv-group-body">
37+ if len(group.Bindings) == 0 {
38+ @EmptyState("No bindings in this group")
39+ } else {
40+ <ul class="sv-list">
41+ for _, binding := range group.Bindings {
42+ @ConfigKeybinding(binding)
43+ }
44+ </ul>
45+ }
46+ </div>
47+ </div>
48+}
49+
50+templ ConfigKeybinding(binding renderedKeybinding) {
51+ <li class="sv-list-row">
52+ <div class="sv-keybinding-row">
53+ <div>
54+ <strong>{ keybindingDescription(binding) }</strong>
55+ <p class="sv-meta">{ keybindingAction(binding) }</p>
56+ </div>
57+ <div class="sv-keybinding-keys">
58+ @KeyList(binding.Keys)
59+ <span class="sv-badge">{ keybindingStatus(binding) }</span>
60+ if keybindingIsCustom(binding) && len(binding.DefaultKeys) > 0 {
61+ <span class="sv-meta">default:</span>
62+ @KeyList(binding.DefaultKeys)
63+ }
64+ </div>
65+ </div>
66+ </li>
67+}
A internal/ui/systemviews/config_templ.go +311 -0 generated file: collapsed
M internal/ui/systemviews/config_view.go +68 -115
@@ -2,7 +2,6 @@ package systemviews
22
33 import (
44 "fmt"
5- "html"
65 "reflect"
76 "sort"
87 "strings"
@@ -10,13 +9,22 @@ import (
109 "github.com/bnema/dumber/internal/application/port"
1110 )
1211
12+type configRenderData struct {
13+ Config port.SystemviewConfigPayload
14+ Keybindings any
15+}
16+
1317 func configHTML(cfg port.SystemviewConfigPayload, keybindings any) string {
18+ return mustRenderComponent(ConfigView(configRenderData{Config: cfg, Keybindings: keybindings}))
19+}
20+
21+func configKeybindingSummary(keybindings any) string {
1422 groups, bindings, loaded := summarizeKeybindings(keybindings)
1523 status := "not loaded"
1624 if loaded {
1725 status = "loaded"
1826 }
19- keybindingSummary := fmt.Sprintf(
27+ return fmt.Sprintf(
2028 "Keybindings %s (%d %s, %d %s)",
2129 status,
2230 groups,
@@ -24,13 +32,66 @@ func configHTML(cfg port.SystemviewConfigPayload, keybindings any) string {
2432 bindings,
2533 plural(bindings, "binding"),
2634 )
27- content := []string{
28- metaHTML(keybindingSummary),
29- "<h3>Config values</h3>" + kvHTML(flattenValues(cfg)),
30- "<h3>Keybindings</h3>" + renderKeybindingsSection(keybindings),
35+}
36+
37+func configKeybindingGroups(keybindings any) []renderedKeybindingGroup {
38+ groups, ok := collectKeybindingGroups(keybindings)
39+ if !ok {
40+ return nil
41+ }
42+ return groups
43+}
44+
45+func keybindingGroupTitle(group renderedKeybindingGroup) string {
46+ title := strings.TrimSpace(group.DisplayName)
47+ if title == "" {
48+ title = strings.TrimSpace(group.Mode)
49+ }
50+ if title == "" {
51+ return "Keybinding group"
52+ }
53+ return title
54+}
55+
56+func keybindingGroupMeta(group renderedKeybindingGroup) string {
57+ meta := make([]string, 0, 2)
58+ if strings.TrimSpace(group.Mode) != "" {
59+ meta = append(meta, "mode: "+group.Mode)
60+ }
61+ if strings.TrimSpace(group.Activation) != "" {
62+ meta = append(meta, "activation: "+group.Activation)
3163 }
64+ return strings.Join(meta, " • ")
65+}
3266
33- return sectionHTML("", "Config", strings.Join(content, ""))
67+func keybindingDescription(binding renderedKeybinding) string {
68+ description := strings.TrimSpace(binding.Description)
69+ if description != "" {
70+ return description
71+ }
72+ if strings.TrimSpace(binding.Action) != "" {
73+ return strings.TrimSpace(binding.Action)
74+ }
75+ return "Unnamed binding"
76+}
77+
78+func keybindingAction(binding renderedKeybinding) string {
79+ action := strings.TrimSpace(binding.Action)
80+ if action == "" {
81+ return "binding"
82+ }
83+ return action
84+}
85+
86+func keybindingIsCustom(binding renderedKeybinding) bool {
87+ return binding.IsCustom || !sameStringSlice(binding.Keys, binding.DefaultKeys)
88+}
89+
90+func keybindingStatus(binding renderedKeybinding) string {
91+ if keybindingIsCustom(binding) {
92+ return "custom"
93+ }
94+ return "default"
3495 }
3596
3697 func flattenValues(value any) []kvPair {
@@ -181,18 +242,6 @@ func summarizeKeybindings(keybindings any) (groups, bindings int, loaded bool) {
181242 return groups, bindings, true
182243 }
183244
184-func renderKeybindingsSection(keybindings any) string {
185- groups, ok := collectKeybindingGroups(keybindings)
186- if !ok {
187- return emptyStateHTML("Keybindings unavailable")
188- }
189- if len(groups) == 0 {
190- return emptyStateHTML("No keybindings configured")
191- }
192-
193- return renderKeybindingGroups(groups)
194-}
195-
196245 func collectKeybindingGroups(keybindings any) ([]renderedKeybindingGroup, bool) {
197246 switch v := keybindings.(type) {
198247 case nil:
@@ -351,14 +400,6 @@ func boolValue(value any) bool {
351400 return b
352401 }
353402
354-func renderKeybindingGroups(groups []renderedKeybindingGroup) string {
355- var rendered strings.Builder
356- for _, group := range groups {
357- rendered.WriteString(renderKeybindingGroup(group))
358- }
359- return rendered.String()
360-}
361-
362403 type renderedKeybindingGroup struct {
363404 DisplayName string
364405 Mode string
@@ -374,94 +415,6 @@ type renderedKeybinding struct {
374415 IsCustom bool
375416 }
376417
377-func renderKeybindingGroup(group renderedKeybindingGroup) string {
378- title := group.DisplayName
379- if strings.TrimSpace(title) == "" {
380- title = group.Mode
381- }
382- if strings.TrimSpace(title) == "" {
383- title = "Keybinding group"
384- }
385-
386- var body strings.Builder
387- body.WriteString(`<div class="sv-group">`)
388- body.WriteString(`<div class="sv-group-header">`)
389- body.WriteString(`<div>`)
390- body.WriteString(`<h4>` + html.EscapeString(title) + `</h4>`)
391-
392- meta := make([]string, 0, 2)
393- if strings.TrimSpace(group.Mode) != "" {
394- meta = append(meta, "mode: "+group.Mode)
395- }
396- if strings.TrimSpace(group.Activation) != "" {
397- meta = append(meta, "activation: "+group.Activation)
398- }
399- if len(meta) > 0 {
400- body.WriteString(`<p class="sv-meta">` + html.EscapeString(strings.Join(meta, " • ")) + `</p>`)
401- }
402- body.WriteString(`</div></div>`)
403- body.WriteString(`<div class="sv-group-body">`)
404- if len(group.Bindings) == 0 {
405- body.WriteString(emptyStateHTML("No bindings in this group"))
406- } else {
407- for _, binding := range group.Bindings {
408- body.WriteString(renderKeybinding(binding))
409- }
410- }
411- body.WriteString(`</div></div>`)
412-
413- return body.String()
414-}
415-
416-func renderKeybinding(binding renderedKeybinding) string {
417- description := strings.TrimSpace(binding.Description)
418- if description == "" {
419- description = strings.TrimSpace(binding.Action)
420- }
421- if description == "" {
422- description = "Unnamed binding"
423- }
424-
425- action := strings.TrimSpace(binding.Action)
426- if action == "" {
427- action = "binding"
428- }
429-
430- custom := binding.IsCustom || !sameStringSlice(binding.Keys, binding.DefaultKeys)
431- status := "default"
432- if custom {
433- status = "custom"
434- }
435-
436- var body strings.Builder
437- body.WriteString(`<div class="sv-binding">`)
438- body.WriteString(`<div class="sv-binding-header">`)
439- body.WriteString(`<div class="sv-binding-description">` + html.EscapeString(description) + `</div>`)
440- body.WriteString(`<div class="sv-binding-keys">` + keyListHTML(binding.Keys) + `</div>`)
441- body.WriteString(`</div>`)
442- body.WriteString(`<div class="sv-binding-meta">`)
443- body.WriteString(`<span>` + html.EscapeString("action: "+action) + `</span>`)
444- body.WriteString(`<span>` + html.EscapeString(status) + `</span>`)
445- if custom && len(binding.DefaultKeys) > 0 {
446- body.WriteString(`<span>default: ` + keyListHTML(binding.DefaultKeys) + `</span>`)
447- }
448- body.WriteString(`</div></div>`)
449-
450- return body.String()
451-}
452-
453-func keyListHTML(keys []string) string {
454- if len(keys) == 0 {
455- return `<span class="sv-key sv-key-empty">Unassigned</span>`
456- }
457-
458- parts := make([]string, 0, len(keys))
459- for _, key := range keys {
460- parts = append(parts, `<span class="sv-key">`+html.EscapeString(key)+`</span>`)
461- }
462- return strings.Join(parts, " ")
463-}
464-
465418 func sameStringSlice(a, b []string) bool {
466419 if len(a) != len(b) {
467420 return false
@@ -2,7 +2,6 @@ package systemviews
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5- "html"
6 "reflect"5 "reflect"
7 "sort"6 "sort"
8 "strings"7 "strings"
@@ -10,13 +9,22 @@ import (
10 "github.com/bnema/dumber/internal/application/port"9 "github.com/bnema/dumber/internal/application/port"
11 )10 )
12 11
12+type configRenderData struct {
13+ Config port.SystemviewConfigPayload
14+ Keybindings any
15+}
16+
13 func configHTML(cfg port.SystemviewConfigPayload, keybindings any) string {17 func configHTML(cfg port.SystemviewConfigPayload, keybindings any) string {
18+ return mustRenderComponent(ConfigView(configRenderData{Config: cfg, Keybindings: keybindings}))
19+}
20+
21+func configKeybindingSummary(keybindings any) string {
14 groups, bindings, loaded := summarizeKeybindings(keybindings)22 groups, bindings, loaded := summarizeKeybindings(keybindings)
15 status := "not loaded"23 status := "not loaded"
16 if loaded {24 if loaded {
17 status = "loaded"25 status = "loaded"
18 }26 }
19- keybindingSummary := fmt.Sprintf(27+ return fmt.Sprintf(
20 "Keybindings %s (%d %s, %d %s)",28 "Keybindings %s (%d %s, %d %s)",
21 status,29 status,
22 groups,30 groups,
@@ -24,13 +32,66 @@ func configHTML(cfg port.SystemviewConfigPayload, keybindings any) string {
24 bindings,32 bindings,
25 plural(bindings, "binding"),33 plural(bindings, "binding"),
26 )34 )
27- content := []string{35+}
28- metaHTML(keybindingSummary),36+
29- "<h3>Config values</h3>" + kvHTML(flattenValues(cfg)),37+func configKeybindingGroups(keybindings any) []renderedKeybindingGroup {
30- "<h3>Keybindings</h3>" + renderKeybindingsSection(keybindings),38+ groups, ok := collectKeybindingGroups(keybindings)
39+ if !ok {
40+ return nil
41+ }
42+ return groups
43+}
44+
45+func keybindingGroupTitle(group renderedKeybindingGroup) string {
46+ title := strings.TrimSpace(group.DisplayName)
47+ if title == "" {
48+ title = strings.TrimSpace(group.Mode)
49+ }
50+ if title == "" {
51+ return "Keybinding group"
52+ }
53+ return title
54+}
55+
56+func keybindingGroupMeta(group renderedKeybindingGroup) string {
57+ meta := make([]string, 0, 2)
58+ if strings.TrimSpace(group.Mode) != "" {
59+ meta = append(meta, "mode: "+group.Mode)
60+ }
61+ if strings.TrimSpace(group.Activation) != "" {
62+ meta = append(meta, "activation: "+group.Activation)
31 }63 }
64+ return strings.Join(meta, " • ")
65+}
32 66
33- return sectionHTML("", "Config", strings.Join(content, ""))67+func keybindingDescription(binding renderedKeybinding) string {
68+ description := strings.TrimSpace(binding.Description)
69+ if description != "" {
70+ return description
71+ }
72+ if strings.TrimSpace(binding.Action) != "" {
73+ return strings.TrimSpace(binding.Action)
74+ }
75+ return "Unnamed binding"
76+}
77+
78+func keybindingAction(binding renderedKeybinding) string {
79+ action := strings.TrimSpace(binding.Action)
80+ if action == "" {
81+ return "binding"
82+ }
83+ return action
84+}
85+
86+func keybindingIsCustom(binding renderedKeybinding) bool {
87+ return binding.IsCustom || !sameStringSlice(binding.Keys, binding.DefaultKeys)
88+}
89+
90+func keybindingStatus(binding renderedKeybinding) string {
91+ if keybindingIsCustom(binding) {
92+ return "custom"
93+ }
94+ return "default"
34 }95 }
35 96
36 func flattenValues(value any) []kvPair {97 func flattenValues(value any) []kvPair {
@@ -181,18 +242,6 @@ func summarizeKeybindings(keybindings any) (groups, bindings int, loaded bool) {
181 return groups, bindings, true242 return groups, bindings, true
182 }243 }
183 244
184-func renderKeybindingsSection(keybindings any) string {
185- groups, ok := collectKeybindingGroups(keybindings)
186- if !ok {
187- return emptyStateHTML("Keybindings unavailable")
188- }
189- if len(groups) == 0 {
190- return emptyStateHTML("No keybindings configured")
191- }
192-
193- return renderKeybindingGroups(groups)
194-}
195-
196 func collectKeybindingGroups(keybindings any) ([]renderedKeybindingGroup, bool) {245 func collectKeybindingGroups(keybindings any) ([]renderedKeybindingGroup, bool) {
197 switch v := keybindings.(type) {246 switch v := keybindings.(type) {
198 case nil:247 case nil:
@@ -351,14 +400,6 @@ func boolValue(value any) bool {
351 return b400 return b
352 }401 }
353 402
354-func renderKeybindingGroups(groups []renderedKeybindingGroup) string {
355- var rendered strings.Builder
356- for _, group := range groups {
357- rendered.WriteString(renderKeybindingGroup(group))
358- }
359- return rendered.String()
360-}
361-
362 type renderedKeybindingGroup struct {403 type renderedKeybindingGroup struct {
363 DisplayName string404 DisplayName string
364 Mode string405 Mode string
@@ -374,94 +415,6 @@ type renderedKeybinding struct {
374 IsCustom bool415 IsCustom bool
375 }416 }
376 417
377-func renderKeybindingGroup(group renderedKeybindingGroup) string {
378- title := group.DisplayName
379- if strings.TrimSpace(title) == "" {
380- title = group.Mode
381- }
382- if strings.TrimSpace(title) == "" {
383- title = "Keybinding group"
384- }
385-
386- var body strings.Builder
387- body.WriteString(`<div class="sv-group">`)
388- body.WriteString(`<div class="sv-group-header">`)
389- body.WriteString(`<div>`)
390- body.WriteString(`<h4>` + html.EscapeString(title) + `</h4>`)
391-
392- meta := make([]string, 0, 2)
393- if strings.TrimSpace(group.Mode) != "" {
394- meta = append(meta, "mode: "+group.Mode)
395- }
396- if strings.TrimSpace(group.Activation) != "" {
397- meta = append(meta, "activation: "+group.Activation)
398- }
399- if len(meta) > 0 {
400- body.WriteString(`<p class="sv-meta">` + html.EscapeString(strings.Join(meta, " • ")) + `</p>`)
401- }
402- body.WriteString(`</div></div>`)
403- body.WriteString(`<div class="sv-group-body">`)
404- if len(group.Bindings) == 0 {
405- body.WriteString(emptyStateHTML("No bindings in this group"))
406- } else {
407- for _, binding := range group.Bindings {
408- body.WriteString(renderKeybinding(binding))
409- }
410- }
411- body.WriteString(`</div></div>`)
412-
413- return body.String()
414-}
415-
416-func renderKeybinding(binding renderedKeybinding) string {
417- description := strings.TrimSpace(binding.Description)
418- if description == "" {
419- description = strings.TrimSpace(binding.Action)
420- }
421- if description == "" {
422- description = "Unnamed binding"
423- }
424-
425- action := strings.TrimSpace(binding.Action)
426- if action == "" {
427- action = "binding"
428- }
429-
430- custom := binding.IsCustom || !sameStringSlice(binding.Keys, binding.DefaultKeys)
431- status := "default"
432- if custom {
433- status = "custom"
434- }
435-
436- var body strings.Builder
437- body.WriteString(`<div class="sv-binding">`)
438- body.WriteString(`<div class="sv-binding-header">`)
439- body.WriteString(`<div class="sv-binding-description">` + html.EscapeString(description) + `</div>`)
440- body.WriteString(`<div class="sv-binding-keys">` + keyListHTML(binding.Keys) + `</div>`)
441- body.WriteString(`</div>`)
442- body.WriteString(`<div class="sv-binding-meta">`)
443- body.WriteString(`<span>` + html.EscapeString("action: "+action) + `</span>`)
444- body.WriteString(`<span>` + html.EscapeString(status) + `</span>`)
445- if custom && len(binding.DefaultKeys) > 0 {
446- body.WriteString(`<span>default: ` + keyListHTML(binding.DefaultKeys) + `</span>`)
447- }
448- body.WriteString(`</div></div>`)
449-
450- return body.String()
451-}
452-
453-func keyListHTML(keys []string) string {
454- if len(keys) == 0 {
455- return `<span class="sv-key sv-key-empty">Unassigned</span>`
456- }
457-
458- parts := make([]string, 0, len(keys))
459- for _, key := range keys {
460- parts = append(parts, `<span class="sv-key">`+html.EscapeString(key)+`</span>`)
461- }
462- return strings.Join(parts, " ")
463-}
464-
465 func sameStringSlice(a, b []string) bool {418 func sameStringSlice(a, b []string) bool {
466 if len(a) != len(b) {419 if len(a) != len(b) {
467 return false420 return false

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