refactor(systemviews): render config with templ
- commit
- 18449b5dbe6e034f682566aca06b92e844697c43
- author
- brice · · 2026-04-24 02:53 UTC
- parent
- a743a66
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | - | "html" | |
| 6 | 5 | "reflect" | |
| 7 | 6 | "sort" | |
| 8 | 7 | "strings" | |
| @@ -10,13 +9,22 @@ import ( | |||
| 10 | 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 | 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 | 22 | groups, bindings, loaded := summarizeKeybindings(keybindings) | |
| 15 | 23 | status := "not loaded" | |
| 16 | 24 | if loaded { | |
| 17 | 25 | status = "loaded" | |
| 18 | 26 | } | |
| 19 | - | keybindingSummary := fmt.Sprintf( | |
| 27 | + | return fmt.Sprintf( | |
| 20 | 28 | "Keybindings %s (%d %s, %d %s)", | |
| 21 | 29 | status, | |
| 22 | 30 | groups, | |
| @@ -24,13 +32,66 @@ func configHTML(cfg port.SystemviewConfigPayload, keybindings any) string { | |||
| 24 | 32 | bindings, | |
| 25 | 33 | plural(bindings, "binding"), | |
| 26 | 34 | ) | |
| 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) | |
| 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 | 97 | func flattenValues(value any) []kvPair { | |
| @@ -181,18 +242,6 @@ func summarizeKeybindings(keybindings any) (groups, bindings int, loaded bool) { | |||
| 181 | 242 | 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 | 245 | func collectKeybindingGroups(keybindings any) ([]renderedKeybindingGroup, bool) { | |
| 197 | 246 | switch v := keybindings.(type) { | |
| 198 | 247 | case nil: | |
| @@ -351,14 +400,6 @@ func boolValue(value any) bool { | |||
| 351 | 400 | 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 | 403 | type renderedKeybindingGroup struct { | |
| 363 | 404 | DisplayName string | |
| 364 | 405 | Mode string | |
| @@ -374,94 +415,6 @@ type renderedKeybinding struct { | |||
| 374 | 415 | 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 | 418 | func sameStringSlice(a, b []string) bool { | |
| 466 | 419 | if len(a) != len(b) { | |
| 467 | 420 | 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, true | 242 | 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 b | 400 | 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 string | 404 | DisplayName string | ||
| 364 | Mode string | 405 | Mode string | ||
| @@ -374,94 +415,6 @@ type renderedKeybinding struct { | |||||
| 374 | IsCustom bool | 415 | 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 false | 420 | return false | ||