v0.23.2 bfd5eb4

save_webui_config.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"fmt"
6	"strings"
7
8	"github.com/bnema/dumber/internal/application/port"
9	"github.com/bnema/dumber/internal/domain/validation"
10)
11
12type SaveWebUIConfigUseCase struct {
13	saver port.WebUIConfigSaver
14}
15
16func NewSaveWebUIConfigUseCase(saver port.WebUIConfigSaver) *SaveWebUIConfigUseCase {
17	return &SaveWebUIConfigUseCase{saver: saver}
18}
19
20func (uc *SaveWebUIConfigUseCase) Execute(ctx context.Context, cfg port.WebUIConfig) error {
21	if uc == nil || uc.saver == nil {
22		return fmt.Errorf("config saver is nil")
23	}
24
25	normalized := normalizeWebUIConfig(cfg)
26	if err := validateWebUIConfig(normalized); err != nil {
27		return err
28	}
29
30	return uc.saver.SaveWebUIConfig(ctx, normalized)
31}
32
33func normalizeWebUIConfig(cfg port.WebUIConfig) port.WebUIConfig {
34	cfg.Appearance.SansFont = strings.TrimSpace(cfg.Appearance.SansFont)
35	cfg.Appearance.SerifFont = strings.TrimSpace(cfg.Appearance.SerifFont)
36	cfg.Appearance.MonospaceFont = strings.TrimSpace(cfg.Appearance.MonospaceFont)
37	cfg.Appearance.ColorScheme = strings.TrimSpace(cfg.Appearance.ColorScheme)
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	}
51	return cfg
52}
53
54func validateWebUIConfig(cfg port.WebUIConfig) error {
55	var errs []string
56
57	if cfg.Appearance.DefaultFontSize < 1 || cfg.Appearance.DefaultFontSize > 72 {
58		errs = append(errs, "appearance.default_font_size must be between 1 and 72")
59	}
60	if cfg.DefaultUIScale < 0.5 || cfg.DefaultUIScale > 3.0 {
61		errs = append(errs, "default_ui_scale must be between 0.5 and 3.0")
62	}
63
64	errs = append(errs, validation.ValidateFontFamily("appearance.sans_font", cfg.Appearance.SansFont)...)
65	errs = append(errs, validation.ValidateFontFamily("appearance.serif_font", cfg.Appearance.SerifFont)...)
66	errs = append(errs, validation.ValidateFontFamily("appearance.monospace_font", cfg.Appearance.MonospaceFont)...)
67
68	if cfg.DefaultSearchEngine == "" {
69		errs = append(errs, "default_search_engine cannot be empty")
70	} else if !strings.Contains(cfg.DefaultSearchEngine, "%s") {
71		errs = append(errs, "default_search_engine must contain %s placeholder for the search query")
72	}
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
86	switch cfg.Appearance.ColorScheme {
87	case "default", "prefer-dark", "prefer-light":
88		// ok
89	default:
90		errs = append(errs, fmt.Sprintf(
91			"appearance.color_scheme must be one of: prefer-dark, prefer-light, default (got: %s)",
92			cfg.Appearance.ColorScheme,
93		))
94	}
95
96	errs = append(errs, validation.ValidatePaletteHex(
97		"appearance.light_palette",
98		cfg.Appearance.LightPalette.Background,
99		cfg.Appearance.LightPalette.Surface,
100		cfg.Appearance.LightPalette.SurfaceVariant,
101		cfg.Appearance.LightPalette.Text,
102		cfg.Appearance.LightPalette.Muted,
103		cfg.Appearance.LightPalette.Accent,
104		cfg.Appearance.LightPalette.Border,
105	)...)
106	errs = append(errs, validation.ValidatePaletteHex(
107		"appearance.dark_palette",
108		cfg.Appearance.DarkPalette.Background,
109		cfg.Appearance.DarkPalette.Surface,
110		cfg.Appearance.DarkPalette.SurfaceVariant,
111		cfg.Appearance.DarkPalette.Text,
112		cfg.Appearance.DarkPalette.Muted,
113		cfg.Appearance.DarkPalette.Accent,
114		cfg.Appearance.DarkPalette.Border,
115	)...)
116
117	if len(errs) > 0 {
118		return fmt.Errorf("config validation failed:\n  - %s", strings.Join(errs, "\n  - "))
119	}
120	return nil
121}

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