detached 42bc673

feat(config): add search shortcuts to webui config with validation and persistence

commit
42bc6731386a9e86345f9d280d5b6e48f251b4fb
author
bnema · · 2025-12-21 13:53 UTC
parent
a258d03
signature
unsigned
3 files · +42 -0
M internal/application/port/webui_config.go +6 -0
@@ -7,6 +7,7 @@ type WebUIConfig struct {
77 Appearance WebUIAppearanceConfig `json:"appearance"`
88 DefaultUIScale float64 `json:"default_ui_scale"`
99 DefaultSearchEngine string `json:"default_search_engine"`
10+ SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
1011 }
1112
1213 type WebUIAppearanceConfig struct {
@@ -29,6 +30,11 @@ type ColorPalette struct {
2930 Border string `json:"border"`
3031 }
3132
33+type SearchShortcut struct {
34+ URL string `json:"url"`
35+ Description string `json:"description"`
36+}
37+
3238 // WebUIConfigSaver persists WebUI configuration changes.
3339 type WebUIConfigSaver interface {
3440 SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error
@@ -7,6 +7,7 @@ type WebUIConfig struct {
7 Appearance WebUIAppearanceConfig `json:"appearance"`7 Appearance WebUIAppearanceConfig `json:"appearance"`
8 DefaultUIScale float64 `json:"default_ui_scale"`8 DefaultUIScale float64 `json:"default_ui_scale"`
9 DefaultSearchEngine string `json:"default_search_engine"`9 DefaultSearchEngine string `json:"default_search_engine"`
10+ SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
10 }11 }
11 12
12 type WebUIAppearanceConfig struct {13 type WebUIAppearanceConfig struct {
@@ -29,6 +30,11 @@ type ColorPalette struct {
29 Border string `json:"border"`30 Border string `json:"border"`
30 }31 }
31 32
33+type SearchShortcut struct {
34+ URL string `json:"url"`
35+ Description string `json:"description"`
36+}
37+
32 // WebUIConfigSaver persists WebUI configuration changes.38 // WebUIConfigSaver persists WebUI configuration changes.
33 type WebUIConfigSaver interface {39 type WebUIConfigSaver interface {
34 SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error40 SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error
M internal/application/usecase/save_webui_config.go +24 -0
@@ -36,6 +36,18 @@ func normalizeWebUIConfig(cfg port.WebUIConfig) port.WebUIConfig {
3636 cfg.Appearance.MonospaceFont = strings.TrimSpace(cfg.Appearance.MonospaceFont)
3737 cfg.Appearance.ColorScheme = strings.TrimSpace(cfg.Appearance.ColorScheme)
3838 cfg.DefaultSearchEngine = strings.TrimSpace(cfg.DefaultSearchEngine)
39+ if len(cfg.SearchShortcuts) > 0 {
40+ normalized := make(map[string]port.SearchShortcut, len(cfg.SearchShortcuts))
41+ for key, shortcut := range cfg.SearchShortcuts {
42+ trimmedKey := strings.TrimSpace(key)
43+ shortcut.URL = strings.TrimSpace(shortcut.URL)
44+ shortcut.Description = strings.TrimSpace(shortcut.Description)
45+ if trimmedKey != "" {
46+ normalized[trimmedKey] = shortcut
47+ }
48+ }
49+ cfg.SearchShortcuts = normalized
50+ }
3951 return cfg
4052 }
4153
@@ -59,6 +71,18 @@ func validateWebUIConfig(cfg port.WebUIConfig) error {
5971 errs = append(errs, "default_search_engine must contain %s placeholder for the search query")
6072 }
6173
74+ for key, shortcut := range cfg.SearchShortcuts {
75+ for _, err := range validation.ValidateShortcutKey(key) {
76+ errs = append(errs, fmt.Sprintf("search_shortcuts.%s: %s", key, err))
77+ }
78+ for _, err := range validation.ValidateShortcutURL(shortcut.URL) {
79+ errs = append(errs, fmt.Sprintf("search_shortcuts.%s.url: %s", key, err))
80+ }
81+ for _, err := range validation.ValidateShortcutDescription(shortcut.Description) {
82+ errs = append(errs, fmt.Sprintf("search_shortcuts.%s.description: %s", key, err))
83+ }
84+ }
85+
6286 switch cfg.Appearance.ColorScheme {
6387 case "default", "prefer-dark", "prefer-light":
6488 // ok
@@ -36,6 +36,18 @@ func normalizeWebUIConfig(cfg port.WebUIConfig) port.WebUIConfig {
36 cfg.Appearance.MonospaceFont = strings.TrimSpace(cfg.Appearance.MonospaceFont)36 cfg.Appearance.MonospaceFont = strings.TrimSpace(cfg.Appearance.MonospaceFont)
37 cfg.Appearance.ColorScheme = strings.TrimSpace(cfg.Appearance.ColorScheme)37 cfg.Appearance.ColorScheme = strings.TrimSpace(cfg.Appearance.ColorScheme)
38 cfg.DefaultSearchEngine = strings.TrimSpace(cfg.DefaultSearchEngine)38 cfg.DefaultSearchEngine = strings.TrimSpace(cfg.DefaultSearchEngine)
39+ if len(cfg.SearchShortcuts) > 0 {
40+ normalized := make(map[string]port.SearchShortcut, len(cfg.SearchShortcuts))
41+ for key, shortcut := range cfg.SearchShortcuts {
42+ trimmedKey := strings.TrimSpace(key)
43+ shortcut.URL = strings.TrimSpace(shortcut.URL)
44+ shortcut.Description = strings.TrimSpace(shortcut.Description)
45+ if trimmedKey != "" {
46+ normalized[trimmedKey] = shortcut
47+ }
48+ }
49+ cfg.SearchShortcuts = normalized
50+ }
39 return cfg51 return cfg
40 }52 }
41 53
@@ -59,6 +71,18 @@ func validateWebUIConfig(cfg port.WebUIConfig) error {
59 errs = append(errs, "default_search_engine must contain %s placeholder for the search query")71 errs = append(errs, "default_search_engine must contain %s placeholder for the search query")
60 }72 }
61 73
74+ for key, shortcut := range cfg.SearchShortcuts {
75+ for _, err := range validation.ValidateShortcutKey(key) {
76+ errs = append(errs, fmt.Sprintf("search_shortcuts.%s: %s", key, err))
77+ }
78+ for _, err := range validation.ValidateShortcutURL(shortcut.URL) {
79+ errs = append(errs, fmt.Sprintf("search_shortcuts.%s.url: %s", key, err))
80+ }
81+ for _, err := range validation.ValidateShortcutDescription(shortcut.Description) {
82+ errs = append(errs, fmt.Sprintf("search_shortcuts.%s.description: %s", key, err))
83+ }
84+ }
85+
62 switch cfg.Appearance.ColorScheme {86 switch cfg.Appearance.ColorScheme {
63 case "default", "prefer-dark", "prefer-light":87 case "default", "prefer-dark", "prefer-light":
64 // ok88 // ok
M internal/infrastructure/config/webui_gateway.go +12 -0
@@ -47,6 +47,18 @@ func (g *WebUIConfigGateway) SaveWebUIConfig(ctx context.Context, cfg port.WebUI
4747 }
4848 current.DefaultUIScale = cfg.DefaultUIScale
4949 current.DefaultSearchEngine = cfg.DefaultSearchEngine
50+ if len(cfg.SearchShortcuts) == 0 {
51+ current.SearchShortcuts = nil
52+ } else {
53+ shortcuts := make(map[string]SearchShortcut, len(cfg.SearchShortcuts))
54+ for key, shortcut := range cfg.SearchShortcuts {
55+ shortcuts[key] = SearchShortcut{
56+ URL: shortcut.URL,
57+ Description: shortcut.Description,
58+ }
59+ }
60+ current.SearchShortcuts = shortcuts
61+ }
5062
5163 return g.mgr.Save(current)
5264 }
@@ -47,6 +47,18 @@ func (g *WebUIConfigGateway) SaveWebUIConfig(ctx context.Context, cfg port.WebUI
47 }47 }
48 current.DefaultUIScale = cfg.DefaultUIScale48 current.DefaultUIScale = cfg.DefaultUIScale
49 current.DefaultSearchEngine = cfg.DefaultSearchEngine49 current.DefaultSearchEngine = cfg.DefaultSearchEngine
50+ if len(cfg.SearchShortcuts) == 0 {
51+ current.SearchShortcuts = nil
52+ } else {
53+ shortcuts := make(map[string]SearchShortcut, len(cfg.SearchShortcuts))
54+ for key, shortcut := range cfg.SearchShortcuts {
55+ shortcuts[key] = SearchShortcut{
56+ URL: shortcut.URL,
57+ Description: shortcut.Description,
58+ }
59+ }
60+ current.SearchShortcuts = shortcuts
61+ }
50 62
51 return g.mgr.Save(current)63 return g.mgr.Save(current)
52 }64 }

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