detached 32b8423

test(config): guard reference docs against schema drift

commit
32b8423545df43d94a5ab54b2625f414411b9c44
author
brice · · 2026-06-25 12:23 UTC
parent
41c038a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +83 -4
M docs/reference/configuration.md +19 -4
@@ -31,6 +31,7 @@
3131 | `appearance.sans_font` | string | `Fira Sans` | |
3232 | `appearance.serif_font` | string | `Fira Sans` | |
3333 | `appearance.monospace_font` | string | `Fira Code` | |
34+| `appearance.gtk_font` | string | `Adwaita Sans` | |
3435 | `appearance.default_font_size` | int | `16` | |
3536 | `appearance.color_scheme` | string | `default` | `prefer-dark`, `prefer-light`, `default` |
3637 | `appearance.external_theme.enabled` | bool | `false` | |
@@ -52,7 +53,13 @@
5253 | `appearance.dark_palette.accent` | string | `#a8a8a8` | |
5354 | `appearance.dark_palette.border` | string | `#363636` | |
5455 | `debug.enable_devtools` | bool | `true` | |
56+| `engine.cef.log_file` | string | `` | CEF runtime log path |
57+| `engine.cef.log_severity` | int32 | `0` | `0`, `1`, `2`, `3`, `4`, `99` |
58+| `engine.cef.trace_handlers` | bool | `false` | |
59+| `engine.cef.enable_audio_handler` | bool | `true` | experimental |
5560 | `engine.type` | string | `cef` | `cef`, `webkit` (CEF default; WebKitGTK fallback) |
61+| `engine.cookie_policy` | string | `always` | `always`, `no_third_party`, `never` |
62+| `engine.webkit.itp_enabled` | bool | `true` | WebKit fallback only |
5663 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |
5764 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |
5865 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
@@ -79,21 +86,24 @@
7986 | `engine.webkit.gl_rendering_mode` | string | `auto` | `auto`, `gles2`, `gl3`, `none` (WebKit fallback only) |
8087 | `engine.webkit.gstreamer_debug_level` | int | `0` | `0-5` (WebKit fallback only) |
8188 | `default_ui_scale` | float | `1.0` | > 0 |
82-
83-Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
84-
85-`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `200.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
8689 | `default_webpage_zoom` | float | `1.2` | > 0 |
90+| `sidebar_width` | int | `320` | `0` or `280-380` |
8791 | `workspace.new_pane_url` | string | `about:blank` | |
8892 | `workspace.switch_to_tab_on_move` | bool | `true` | |
93+| `workspace.tab_bar_position` | string | `bottom` | `top`, `bottom` |
94+| `workspace.hide_tab_bar_when_single_tab` | bool | `true` | |
8995 | `workspace.pane_mode.activation_shortcut` | string | `ctrl+p` | |
9096 | `workspace.pane_mode.timeout_ms` | int | `3000` | |
97+| `workspace.pane_mode.actions.<action>` | []string | see defaults | pane mode key mappings |
9198 | `workspace.tab_mode.activation_shortcut` | string | `ctrl+t` | |
9299 | `workspace.tab_mode.timeout_ms` | int | `3000` | |
100+| `workspace.tab_mode.actions.<action>` | []string | see defaults | tab mode key mappings |
93101 | `workspace.resize_mode.activation_shortcut` | string | `ctrl+n` | |
94102 | `workspace.resize_mode.timeout_ms` | int | `3000` | |
103+| `workspace.resize_mode.actions.<action>` | []string | see defaults | resize mode key mappings |
95104 | `workspace.resize_mode.step_percent` | float | `5.0` | |
96105 | `workspace.resize_mode.min_pane_percent` | float | `10.0` | |
106+| `workspace.shortcuts.actions` | map | see defaults | global shortcut action mappings |
97107 | `workspace.shortcuts.actions.toggle_floating_pane.keys` | []string | `alt+f` | key strings |
98108 | `workspace.shortcuts.actions.toggle_floating_pane.desc` | string | `Toggle floating pane` | |
99109 | `workspace.floating_pane.width_pct` | float | `0.82` | `(0,1]` |
@@ -123,6 +133,7 @@ Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise
123133 | `session.max_exited_session_age_days` | int | `7` | |
124134 | `session.session_mode.activation_shortcut` | string | `ctrl+o` | |
125135 | `session.session_mode.timeout_ms` | int | `3000` | |
136+| `session.session_mode.actions.<action>` | []string | see defaults | session mode key mappings |
126137 | `media.hardware_decoding` | string | `auto` | `auto`, `force`, `disable` |
127138 | `media.prefer_av1` | bool | `false` | |
128139 | `media.show_diagnostics` | bool | `false` | |
@@ -150,6 +161,10 @@ Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise
150161 | `engine.zoom_cache_size` | int | `256` | >= 0 |
151162 | `downloads.path` | string | `` | |
152163
164+Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
165+
166+`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `200.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
167+
153168 CEF is the default engine. WebKitGTK is a fallback selected with `engine.type = "webkit"`; `engine.webkit.*` keys are WebKit-specific.
154169
155170 Legacy key migration:
@@ -31,6 +31,7 @@
31 | `appearance.sans_font` | string | `Fira Sans` | |31 | `appearance.sans_font` | string | `Fira Sans` | |
32 | `appearance.serif_font` | string | `Fira Sans` | |32 | `appearance.serif_font` | string | `Fira Sans` | |
33 | `appearance.monospace_font` | string | `Fira Code` | |33 | `appearance.monospace_font` | string | `Fira Code` | |
34+| `appearance.gtk_font` | string | `Adwaita Sans` | |
34 | `appearance.default_font_size` | int | `16` | |35 | `appearance.default_font_size` | int | `16` | |
35 | `appearance.color_scheme` | string | `default` | `prefer-dark`, `prefer-light`, `default` |36 | `appearance.color_scheme` | string | `default` | `prefer-dark`, `prefer-light`, `default` |
36 | `appearance.external_theme.enabled` | bool | `false` | |37 | `appearance.external_theme.enabled` | bool | `false` | |
@@ -52,7 +53,13 @@
52 | `appearance.dark_palette.accent` | string | `#a8a8a8` | |53 | `appearance.dark_palette.accent` | string | `#a8a8a8` | |
53 | `appearance.dark_palette.border` | string | `#363636` | |54 | `appearance.dark_palette.border` | string | `#363636` | |
54 | `debug.enable_devtools` | bool | `true` | |55 | `debug.enable_devtools` | bool | `true` | |
56+| `engine.cef.log_file` | string | `` | CEF runtime log path |
57+| `engine.cef.log_severity` | int32 | `0` | `0`, `1`, `2`, `3`, `4`, `99` |
58+| `engine.cef.trace_handlers` | bool | `false` | |
59+| `engine.cef.enable_audio_handler` | bool | `true` | experimental |
55 | `engine.type` | string | `cef` | `cef`, `webkit` (CEF default; WebKitGTK fallback) |60 | `engine.type` | string | `cef` | `cef`, `webkit` (CEF default; WebKitGTK fallback) |
61+| `engine.cookie_policy` | string | `always` | `always`, `no_third_party`, `never` |
62+| `engine.webkit.itp_enabled` | bool | `true` | WebKit fallback only |
56 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |63 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |
57 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |64 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |
58 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |65 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
@@ -79,21 +86,24 @@
79 | `engine.webkit.gl_rendering_mode` | string | `auto` | `auto`, `gles2`, `gl3`, `none` (WebKit fallback only) |86 | `engine.webkit.gl_rendering_mode` | string | `auto` | `auto`, `gles2`, `gl3`, `none` (WebKit fallback only) |
80 | `engine.webkit.gstreamer_debug_level` | int | `0` | `0-5` (WebKit fallback only) |87 | `engine.webkit.gstreamer_debug_level` | int | `0` | `0-5` (WebKit fallback only) |
81 | `default_ui_scale` | float | `1.0` | > 0 |88 | `default_ui_scale` | float | `1.0` | > 0 |
82-
83-Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
84-
85-`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `200.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
86 | `default_webpage_zoom` | float | `1.2` | > 0 |89 | `default_webpage_zoom` | float | `1.2` | > 0 |
90+| `sidebar_width` | int | `320` | `0` or `280-380` |
87 | `workspace.new_pane_url` | string | `about:blank` | |91 | `workspace.new_pane_url` | string | `about:blank` | |
88 | `workspace.switch_to_tab_on_move` | bool | `true` | |92 | `workspace.switch_to_tab_on_move` | bool | `true` | |
93+| `workspace.tab_bar_position` | string | `bottom` | `top`, `bottom` |
94+| `workspace.hide_tab_bar_when_single_tab` | bool | `true` | |
89 | `workspace.pane_mode.activation_shortcut` | string | `ctrl+p` | |95 | `workspace.pane_mode.activation_shortcut` | string | `ctrl+p` | |
90 | `workspace.pane_mode.timeout_ms` | int | `3000` | |96 | `workspace.pane_mode.timeout_ms` | int | `3000` | |
97+| `workspace.pane_mode.actions.<action>` | []string | see defaults | pane mode key mappings |
91 | `workspace.tab_mode.activation_shortcut` | string | `ctrl+t` | |98 | `workspace.tab_mode.activation_shortcut` | string | `ctrl+t` | |
92 | `workspace.tab_mode.timeout_ms` | int | `3000` | |99 | `workspace.tab_mode.timeout_ms` | int | `3000` | |
100+| `workspace.tab_mode.actions.<action>` | []string | see defaults | tab mode key mappings |
93 | `workspace.resize_mode.activation_shortcut` | string | `ctrl+n` | |101 | `workspace.resize_mode.activation_shortcut` | string | `ctrl+n` | |
94 | `workspace.resize_mode.timeout_ms` | int | `3000` | |102 | `workspace.resize_mode.timeout_ms` | int | `3000` | |
103+| `workspace.resize_mode.actions.<action>` | []string | see defaults | resize mode key mappings |
95 | `workspace.resize_mode.step_percent` | float | `5.0` | |104 | `workspace.resize_mode.step_percent` | float | `5.0` | |
96 | `workspace.resize_mode.min_pane_percent` | float | `10.0` | |105 | `workspace.resize_mode.min_pane_percent` | float | `10.0` | |
106+| `workspace.shortcuts.actions` | map | see defaults | global shortcut action mappings |
97 | `workspace.shortcuts.actions.toggle_floating_pane.keys` | []string | `alt+f` | key strings |107 | `workspace.shortcuts.actions.toggle_floating_pane.keys` | []string | `alt+f` | key strings |
98 | `workspace.shortcuts.actions.toggle_floating_pane.desc` | string | `Toggle floating pane` | |108 | `workspace.shortcuts.actions.toggle_floating_pane.desc` | string | `Toggle floating pane` | |
99 | `workspace.floating_pane.width_pct` | float | `0.82` | `(0,1]` |109 | `workspace.floating_pane.width_pct` | float | `0.82` | `(0,1]` |
@@ -123,6 +133,7 @@ Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise
123 | `session.max_exited_session_age_days` | int | `7` | |133 | `session.max_exited_session_age_days` | int | `7` | |
124 | `session.session_mode.activation_shortcut` | string | `ctrl+o` | |134 | `session.session_mode.activation_shortcut` | string | `ctrl+o` | |
125 | `session.session_mode.timeout_ms` | int | `3000` | |135 | `session.session_mode.timeout_ms` | int | `3000` | |
136+| `session.session_mode.actions.<action>` | []string | see defaults | session mode key mappings |
126 | `media.hardware_decoding` | string | `auto` | `auto`, `force`, `disable` |137 | `media.hardware_decoding` | string | `auto` | `auto`, `force`, `disable` |
127 | `media.prefer_av1` | bool | `false` | |138 | `media.prefer_av1` | bool | `false` | |
128 | `media.show_diagnostics` | bool | `false` | |139 | `media.show_diagnostics` | bool | `false` | |
@@ -150,6 +161,10 @@ Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise
150 | `engine.zoom_cache_size` | int | `256` | >= 0 |161 | `engine.zoom_cache_size` | int | `256` | >= 0 |
151 | `downloads.path` | string | `` | |162 | `downloads.path` | string | `` | |
152 163
164+Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
165+
166+`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `200.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
167+
153 CEF is the default engine. WebKitGTK is a fallback selected with `engine.type = "webkit"`; `engine.webkit.*` keys are WebKit-specific.168 CEF is the default engine. WebKitGTK is a fallback selected with `engine.type = "webkit"`; `engine.webkit.*` keys are WebKit-specific.
154 169
155 Legacy key migration:170 Legacy key migration:
M internal/infrastructure/config/schema_provider_test.go +64 -0
@@ -1,13 +1,77 @@
11 package config
22
33 import (
4+ "os"
5+ "path/filepath"
6+ "regexp"
7+ "runtime"
48 "sort"
9+ "strings"
510 "testing"
611
712 "github.com/spf13/viper"
813 "github.com/stretchr/testify/require"
914 )
1015
16+func TestConfigurationReferenceCoversSchemaKeys(t *testing.T) {
17+ docKeys := configurationReferenceKeys(t)
18+
19+ var missing []string
20+ for _, key := range NewSchemaProvider().GetSchema() {
21+ if configurationReferenceCoversKey(docKeys, key.Key) {
22+ continue
23+ }
24+ missing = append(missing, key.Key)
25+ }
26+
27+ sort.Strings(missing)
28+ require.Empty(t, missing, "docs/reference/configuration.md should cover every SchemaProvider key")
29+}
30+
31+func configurationReferenceKeys(t *testing.T) map[string]struct{} {
32+ t.Helper()
33+
34+ _, filename, _, ok := runtime.Caller(0)
35+ require.True(t, ok, "locate current test file")
36+ docPath := filepath.Join(filepath.Dir(filename), "..", "..", "..", "docs", "reference", "configuration.md")
37+ data, err := os.ReadFile(docPath)
38+ require.NoError(t, err)
39+
40+ canonicalReference, _, _ := strings.Cut(string(data), "Legacy key migration:")
41+ rowRE := regexp.MustCompile(`(?m)^\| ` + "`" + `([^` + "`" + `]+)` + "`" + ` \|`)
42+ keys := make(map[string]struct{})
43+ for _, match := range rowRE.FindAllStringSubmatch(canonicalReference, -1) {
44+ keys[match[1]] = struct{}{}
45+ }
46+ return keys
47+}
48+
49+func configurationReferenceCoversKey(docKeys map[string]struct{}, schemaKey string) bool {
50+ if _, ok := docKeys[schemaKey]; ok {
51+ return true
52+ }
53+
54+ if strings.HasSuffix(schemaKey, ".*") {
55+ prefix := strings.TrimSuffix(schemaKey, "*")
56+ for docKey := range docKeys {
57+ if strings.HasPrefix(docKey, prefix) {
58+ return true
59+ }
60+ }
61+ }
62+
63+ if idx := strings.Index(schemaKey, "<"); idx >= 0 {
64+ prefix := schemaKey[:idx]
65+ for docKey := range docKeys {
66+ if strings.HasPrefix(docKey, prefix) {
67+ return true
68+ }
69+ }
70+ }
71+
72+ return false
73+}
74+
1175 func TestSchemaProviderCoversViperDefaults(t *testing.T) {
1276 m := &Manager{viper: viper.New()}
1377 m.setDefaults()
@@ -1,13 +1,77 @@
1 package config1 package config
2 2
3 import (3 import (
4+ "os"
5+ "path/filepath"
6+ "regexp"
7+ "runtime"
4 "sort"8 "sort"
9+ "strings"
5 "testing"10 "testing"
6 11
7 "github.com/spf13/viper"12 "github.com/spf13/viper"
8 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
9 )14 )
10 15
16+func TestConfigurationReferenceCoversSchemaKeys(t *testing.T) {
17+ docKeys := configurationReferenceKeys(t)
18+
19+ var missing []string
20+ for _, key := range NewSchemaProvider().GetSchema() {
21+ if configurationReferenceCoversKey(docKeys, key.Key) {
22+ continue
23+ }
24+ missing = append(missing, key.Key)
25+ }
26+
27+ sort.Strings(missing)
28+ require.Empty(t, missing, "docs/reference/configuration.md should cover every SchemaProvider key")
29+}
30+
31+func configurationReferenceKeys(t *testing.T) map[string]struct{} {
32+ t.Helper()
33+
34+ _, filename, _, ok := runtime.Caller(0)
35+ require.True(t, ok, "locate current test file")
36+ docPath := filepath.Join(filepath.Dir(filename), "..", "..", "..", "docs", "reference", "configuration.md")
37+ data, err := os.ReadFile(docPath)
38+ require.NoError(t, err)
39+
40+ canonicalReference, _, _ := strings.Cut(string(data), "Legacy key migration:")
41+ rowRE := regexp.MustCompile(`(?m)^\| ` + "`" + `([^` + "`" + `]+)` + "`" + ` \|`)
42+ keys := make(map[string]struct{})
43+ for _, match := range rowRE.FindAllStringSubmatch(canonicalReference, -1) {
44+ keys[match[1]] = struct{}{}
45+ }
46+ return keys
47+}
48+
49+func configurationReferenceCoversKey(docKeys map[string]struct{}, schemaKey string) bool {
50+ if _, ok := docKeys[schemaKey]; ok {
51+ return true
52+ }
53+
54+ if strings.HasSuffix(schemaKey, ".*") {
55+ prefix := strings.TrimSuffix(schemaKey, "*")
56+ for docKey := range docKeys {
57+ if strings.HasPrefix(docKey, prefix) {
58+ return true
59+ }
60+ }
61+ }
62+
63+ if idx := strings.Index(schemaKey, "<"); idx >= 0 {
64+ prefix := schemaKey[:idx]
65+ for docKey := range docKeys {
66+ if strings.HasPrefix(docKey, prefix) {
67+ return true
68+ }
69+ }
70+ }
71+
72+ return false
73+}
74+
11 func TestSchemaProviderCoversViperDefaults(t *testing.T) {75 func TestSchemaProviderCoversViperDefaults(t *testing.T) {
12 m := &Manager{viper: viper.New()}76 m := &Manager{viper: viper.New()}
13 m.setDefaults()77 m.setDefaults()

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