detached e4dbd73

feat(cli): add config schema command to list all keys with valid values

commit
e4dbd73e577dac28268f76f90c1dfef000be33c2
author
bnema · · 2025-12-28 18:29 UTC
parent
01be947
signature
GPG Unverified2B82207D370127A0 The key is unknown
10 files · +1347 -0
M .mockery.yaml +1 -0
@@ -9,6 +9,7 @@ packages:
99 pkgname: mocks
1010 interfaces:
1111 ConfigMigrator: {}
12+ ConfigSchemaProvider: {}
1213 DatabaseProvider: {}
1314 DiffFormatter: {}
1415 SessionLogger: {}
@@ -9,6 +9,7 @@ packages:
9 pkgname: mocks9 pkgname: mocks
10 interfaces:10 interfaces:
11 ConfigMigrator: {}11 ConfigMigrator: {}
12+ ConfigSchemaProvider: {}
12 DatabaseProvider: {}13 DatabaseProvider: {}
13 DiffFormatter: {}14 DiffFormatter: {}
14 SessionLogger: {}15 SessionLogger: {}
M CHANGELOG.md +1 -0
@@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file.
2020 - Reuses existing auto-update infrastructure (check, download, stage, apply on exit).
2121 - Theme-aware styling consistent with other CLI commands.
2222 - **Config open command**: New `dumber config open` command to open the config file in your preferred editor. Uses `$EDITOR`, then `$VISUAL`, with fallback to nano/vim/vi.
23+- **Config schema command**: New `dumber config schema` command that displays all configuration keys with their types, defaults, descriptions, and valid values. Supports `--json` flag for machine-readable output.
2324 - **Move pane to tab**: New pane-mode actions to move the active pane to another tab.
2425 - `Ctrl+P → m`: Opens a tab picker modal.
2526 - `Ctrl+P → M`: Moves the active pane to the next tab (creates a new tab if needed).
@@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file.
20 - Reuses existing auto-update infrastructure (check, download, stage, apply on exit).20 - Reuses existing auto-update infrastructure (check, download, stage, apply on exit).
21 - Theme-aware styling consistent with other CLI commands.21 - Theme-aware styling consistent with other CLI commands.
22 - **Config open command**: New `dumber config open` command to open the config file in your preferred editor. Uses `$EDITOR`, then `$VISUAL`, with fallback to nano/vim/vi.22 - **Config open command**: New `dumber config open` command to open the config file in your preferred editor. Uses `$EDITOR`, then `$VISUAL`, with fallback to nano/vim/vi.
23+- **Config schema command**: New `dumber config schema` command that displays all configuration keys with their types, defaults, descriptions, and valid values. Supports `--json` flag for machine-readable output.
23 - **Move pane to tab**: New pane-mode actions to move the active pane to another tab.24 - **Move pane to tab**: New pane-mode actions to move the active pane to another tab.
24 - `Ctrl+P → m`: Opens a tab picker modal.25 - `Ctrl+P → m`: Opens a tab picker modal.
25 - `Ctrl+P → M`: Moves the active pane to the next tab (creates a new tab if needed).26 - `Ctrl+P → M`: Moves the active pane to the next tab (creates a new tab if needed).
A internal/application/port/config_schema.go +9 -0
@@ -0,0 +1,9 @@
1+package port
2+
3+import "github.com/bnema/dumber/internal/domain/entity"
4+
5+// ConfigSchemaProvider provides configuration schema information.
6+type ConfigSchemaProvider interface {
7+ // GetSchema returns all configuration keys with their metadata.
8+ GetSchema() []entity.ConfigKeyInfo
9+}
@@ -0,0 +1,9 @@
1+package port
2+
3+import "github.com/bnema/dumber/internal/domain/entity"
4+
5+// ConfigSchemaProvider provides configuration schema information.
6+type ConfigSchemaProvider interface {
7+ // GetSchema returns all configuration keys with their metadata.
8+ GetSchema() []entity.ConfigKeyInfo
9+}
M internal/application/port/mocks/mocks.go +73 -0
@@ -388,6 +388,79 @@ func (_c *MockDiffFormatter_FormatChangesAsDiff_Call) RunAndReturn(run func(chan
388388 return _c
389389 }
390390
391+// NewMockConfigSchemaProvider creates a new instance of MockConfigSchemaProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
392+// The first argument is typically a *testing.T value.
393+func NewMockConfigSchemaProvider(t interface {
394+ mock.TestingT
395+ Cleanup(func())
396+}) *MockConfigSchemaProvider {
397+ mock := &MockConfigSchemaProvider{}
398+ mock.Mock.Test(t)
399+
400+ t.Cleanup(func() { mock.AssertExpectations(t) })
401+
402+ return mock
403+}
404+
405+// MockConfigSchemaProvider is an autogenerated mock type for the ConfigSchemaProvider type
406+type MockConfigSchemaProvider struct {
407+ mock.Mock
408+}
409+
410+type MockConfigSchemaProvider_Expecter struct {
411+ mock *mock.Mock
412+}
413+
414+func (_m *MockConfigSchemaProvider) EXPECT() *MockConfigSchemaProvider_Expecter {
415+ return &MockConfigSchemaProvider_Expecter{mock: &_m.Mock}
416+}
417+
418+// GetSchema provides a mock function for the type MockConfigSchemaProvider
419+func (_mock *MockConfigSchemaProvider) GetSchema() []entity.ConfigKeyInfo {
420+ ret := _mock.Called()
421+
422+ if len(ret) == 0 {
423+ panic("no return value specified for GetSchema")
424+ }
425+
426+ var r0 []entity.ConfigKeyInfo
427+ if returnFunc, ok := ret.Get(0).(func() []entity.ConfigKeyInfo); ok {
428+ r0 = returnFunc()
429+ } else {
430+ if ret.Get(0) != nil {
431+ r0 = ret.Get(0).([]entity.ConfigKeyInfo)
432+ }
433+ }
434+ return r0
435+}
436+
437+// MockConfigSchemaProvider_GetSchema_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSchema'
438+type MockConfigSchemaProvider_GetSchema_Call struct {
439+ *mock.Call
440+}
441+
442+// GetSchema is a helper method to define mock.On call
443+func (_e *MockConfigSchemaProvider_Expecter) GetSchema() *MockConfigSchemaProvider_GetSchema_Call {
444+ return &MockConfigSchemaProvider_GetSchema_Call{Call: _e.mock.On("GetSchema")}
445+}
446+
447+func (_c *MockConfigSchemaProvider_GetSchema_Call) Run(run func()) *MockConfigSchemaProvider_GetSchema_Call {
448+ _c.Call.Run(func(args mock.Arguments) {
449+ run()
450+ })
451+ return _c
452+}
453+
454+func (_c *MockConfigSchemaProvider_GetSchema_Call) Return(configKeyInfos []entity.ConfigKeyInfo) *MockConfigSchemaProvider_GetSchema_Call {
455+ _c.Call.Return(configKeyInfos)
456+ return _c
457+}
458+
459+func (_c *MockConfigSchemaProvider_GetSchema_Call) RunAndReturn(run func() []entity.ConfigKeyInfo) *MockConfigSchemaProvider_GetSchema_Call {
460+ _c.Call.Return(run)
461+ return _c
462+}
463+
391464 // NewMockDatabaseProvider creates a new instance of MockDatabaseProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
392465 // The first argument is typically a *testing.T value.
393466 func NewMockDatabaseProvider(t interface {
@@ -388,6 +388,79 @@ func (_c *MockDiffFormatter_FormatChangesAsDiff_Call) RunAndReturn(run func(chan
388 return _c388 return _c
389 }389 }
390 390
391+// NewMockConfigSchemaProvider creates a new instance of MockConfigSchemaProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
392+// The first argument is typically a *testing.T value.
393+func NewMockConfigSchemaProvider(t interface {
394+ mock.TestingT
395+ Cleanup(func())
396+}) *MockConfigSchemaProvider {
397+ mock := &MockConfigSchemaProvider{}
398+ mock.Mock.Test(t)
399+
400+ t.Cleanup(func() { mock.AssertExpectations(t) })
401+
402+ return mock
403+}
404+
405+// MockConfigSchemaProvider is an autogenerated mock type for the ConfigSchemaProvider type
406+type MockConfigSchemaProvider struct {
407+ mock.Mock
408+}
409+
410+type MockConfigSchemaProvider_Expecter struct {
411+ mock *mock.Mock
412+}
413+
414+func (_m *MockConfigSchemaProvider) EXPECT() *MockConfigSchemaProvider_Expecter {
415+ return &MockConfigSchemaProvider_Expecter{mock: &_m.Mock}
416+}
417+
418+// GetSchema provides a mock function for the type MockConfigSchemaProvider
419+func (_mock *MockConfigSchemaProvider) GetSchema() []entity.ConfigKeyInfo {
420+ ret := _mock.Called()
421+
422+ if len(ret) == 0 {
423+ panic("no return value specified for GetSchema")
424+ }
425+
426+ var r0 []entity.ConfigKeyInfo
427+ if returnFunc, ok := ret.Get(0).(func() []entity.ConfigKeyInfo); ok {
428+ r0 = returnFunc()
429+ } else {
430+ if ret.Get(0) != nil {
431+ r0 = ret.Get(0).([]entity.ConfigKeyInfo)
432+ }
433+ }
434+ return r0
435+}
436+
437+// MockConfigSchemaProvider_GetSchema_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSchema'
438+type MockConfigSchemaProvider_GetSchema_Call struct {
439+ *mock.Call
440+}
441+
442+// GetSchema is a helper method to define mock.On call
443+func (_e *MockConfigSchemaProvider_Expecter) GetSchema() *MockConfigSchemaProvider_GetSchema_Call {
444+ return &MockConfigSchemaProvider_GetSchema_Call{Call: _e.mock.On("GetSchema")}
445+}
446+
447+func (_c *MockConfigSchemaProvider_GetSchema_Call) Run(run func()) *MockConfigSchemaProvider_GetSchema_Call {
448+ _c.Call.Run(func(args mock.Arguments) {
449+ run()
450+ })
451+ return _c
452+}
453+
454+func (_c *MockConfigSchemaProvider_GetSchema_Call) Return(configKeyInfos []entity.ConfigKeyInfo) *MockConfigSchemaProvider_GetSchema_Call {
455+ _c.Call.Return(configKeyInfos)
456+ return _c
457+}
458+
459+func (_c *MockConfigSchemaProvider_GetSchema_Call) RunAndReturn(run func() []entity.ConfigKeyInfo) *MockConfigSchemaProvider_GetSchema_Call {
460+ _c.Call.Return(run)
461+ return _c
462+}
463+
391 // NewMockDatabaseProvider creates a new instance of MockDatabaseProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.464 // NewMockDatabaseProvider creates a new instance of MockDatabaseProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
392 // The first argument is typically a *testing.T value.465 // The first argument is typically a *testing.T value.
393 func NewMockDatabaseProvider(t interface {466 func NewMockDatabaseProvider(t interface {
A internal/application/usecase/get_config_schema.go +38 -0
@@ -0,0 +1,38 @@
1+package usecase
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+// GetConfigSchemaUseCase retrieves configuration schema information.
11+type GetConfigSchemaUseCase struct {
12+ provider port.ConfigSchemaProvider
13+}
14+
15+// NewGetConfigSchemaUseCase creates a new GetConfigSchemaUseCase.
16+func NewGetConfigSchemaUseCase(provider port.ConfigSchemaProvider) *GetConfigSchemaUseCase {
17+ return &GetConfigSchemaUseCase{
18+ provider: provider,
19+ }
20+}
21+
22+// GetConfigSchemaInput contains input parameters for schema retrieval.
23+type GetConfigSchemaInput struct {
24+ // Reserved for future filtering options (e.g., by section)
25+}
26+
27+// GetConfigSchemaOutput contains the schema information.
28+type GetConfigSchemaOutput struct {
29+ Keys []entity.ConfigKeyInfo
30+}
31+
32+// Execute retrieves all configuration keys with their metadata.
33+func (uc *GetConfigSchemaUseCase) Execute(_ context.Context, _ GetConfigSchemaInput) (*GetConfigSchemaOutput, error) {
34+ keys := uc.provider.GetSchema()
35+ return &GetConfigSchemaOutput{
36+ Keys: keys,
37+ }, nil
38+}
@@ -0,0 +1,38 @@
1+package usecase
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+// GetConfigSchemaUseCase retrieves configuration schema information.
11+type GetConfigSchemaUseCase struct {
12+ provider port.ConfigSchemaProvider
13+}
14+
15+// NewGetConfigSchemaUseCase creates a new GetConfigSchemaUseCase.
16+func NewGetConfigSchemaUseCase(provider port.ConfigSchemaProvider) *GetConfigSchemaUseCase {
17+ return &GetConfigSchemaUseCase{
18+ provider: provider,
19+ }
20+}
21+
22+// GetConfigSchemaInput contains input parameters for schema retrieval.
23+type GetConfigSchemaInput struct {
24+ // Reserved for future filtering options (e.g., by section)
25+}
26+
27+// GetConfigSchemaOutput contains the schema information.
28+type GetConfigSchemaOutput struct {
29+ Keys []entity.ConfigKeyInfo
30+}
31+
32+// Execute retrieves all configuration keys with their metadata.
33+func (uc *GetConfigSchemaUseCase) Execute(_ context.Context, _ GetConfigSchemaInput) (*GetConfigSchemaOutput, error) {
34+ keys := uc.provider.GetSchema()
35+ return &GetConfigSchemaOutput{
36+ Keys: keys,
37+ }, nil
38+}
A internal/application/usecase/get_config_schema_test.go +106 -0
@@ -0,0 +1,106 @@
1+package usecase_test
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/stretchr/testify/assert"
8+ "github.com/stretchr/testify/mock"
9+ "github.com/stretchr/testify/require"
10+
11+ "github.com/bnema/dumber/internal/application/port/mocks"
12+ "github.com/bnema/dumber/internal/application/usecase"
13+ "github.com/bnema/dumber/internal/domain/entity"
14+)
15+
16+func TestGetConfigSchemaUseCase_Execute(t *testing.T) {
17+ t.Run("returns schema keys from provider", func(t *testing.T) {
18+ // Arrange
19+ mockProvider := mocks.NewMockConfigSchemaProvider(t)
20+ expectedKeys := []entity.ConfigKeyInfo{
21+ {
22+ Key: "appearance.color_scheme",
23+ Type: "string",
24+ Default: "default",
25+ Description: "Theme preference",
26+ Values: []string{"default", "prefer-dark", "prefer-light"},
27+ Section: "Appearance",
28+ },
29+ {
30+ Key: "logging.level",
31+ Type: "string",
32+ Default: "info",
33+ Description: "Log verbosity level",
34+ Values: []string{"trace", "debug", "info", "warn", "error", "fatal"},
35+ Section: "Logging",
36+ },
37+ }
38+ mockProvider.EXPECT().GetSchema().Return(expectedKeys)
39+
40+ uc := usecase.NewGetConfigSchemaUseCase(mockProvider)
41+
42+ // Act
43+ result, err := uc.Execute(context.Background(), usecase.GetConfigSchemaInput{})
44+
45+ // Assert
46+ require.NoError(t, err)
47+ assert.NotNil(t, result)
48+ assert.Len(t, result.Keys, 2)
49+ assert.Equal(t, "appearance.color_scheme", result.Keys[0].Key)
50+ assert.Equal(t, "logging.level", result.Keys[1].Key)
51+ mock.AssertExpectationsForObjects(t, mockProvider)
52+ })
53+
54+ t.Run("returns empty slice when no keys", func(t *testing.T) {
55+ // Arrange
56+ mockProvider := mocks.NewMockConfigSchemaProvider(t)
57+ mockProvider.EXPECT().GetSchema().Return([]entity.ConfigKeyInfo{})
58+
59+ uc := usecase.NewGetConfigSchemaUseCase(mockProvider)
60+
61+ // Act
62+ result, err := uc.Execute(context.Background(), usecase.GetConfigSchemaInput{})
63+
64+ // Assert
65+ require.NoError(t, err)
66+ assert.NotNil(t, result)
67+ assert.Empty(t, result.Keys)
68+ mock.AssertExpectationsForObjects(t, mockProvider)
69+ })
70+
71+ t.Run("keys include all expected fields", func(t *testing.T) {
72+ // Arrange
73+ mockProvider := mocks.NewMockConfigSchemaProvider(t)
74+ expectedKeys := []entity.ConfigKeyInfo{
75+ {
76+ Key: "appearance.default_font_size",
77+ Type: "int",
78+ Default: "16",
79+ Description: "Default font size in CSS pixels",
80+ Range: "1-72",
81+ Section: "Appearance",
82+ },
83+ }
84+ mockProvider.EXPECT().GetSchema().Return(expectedKeys)
85+
86+ uc := usecase.NewGetConfigSchemaUseCase(mockProvider)
87+
88+ // Act
89+ result, err := uc.Execute(context.Background(), usecase.GetConfigSchemaInput{})
90+
91+ // Assert
92+ require.NoError(t, err)
93+ assert.NotNil(t, result)
94+ assert.Len(t, result.Keys, 1)
95+
96+ key := result.Keys[0]
97+ assert.Equal(t, "appearance.default_font_size", key.Key)
98+ assert.Equal(t, "int", key.Type)
99+ assert.Equal(t, "16", key.Default)
100+ assert.Equal(t, "Default font size in CSS pixels", key.Description)
101+ assert.Equal(t, "1-72", key.Range)
102+ assert.Equal(t, "Appearance", key.Section)
103+ assert.Empty(t, key.Values) // No enum values for int type
104+ mock.AssertExpectationsForObjects(t, mockProvider)
105+ })
106+}
@@ -0,0 +1,106 @@
1+package usecase_test
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/stretchr/testify/assert"
8+ "github.com/stretchr/testify/mock"
9+ "github.com/stretchr/testify/require"
10+
11+ "github.com/bnema/dumber/internal/application/port/mocks"
12+ "github.com/bnema/dumber/internal/application/usecase"
13+ "github.com/bnema/dumber/internal/domain/entity"
14+)
15+
16+func TestGetConfigSchemaUseCase_Execute(t *testing.T) {
17+ t.Run("returns schema keys from provider", func(t *testing.T) {
18+ // Arrange
19+ mockProvider := mocks.NewMockConfigSchemaProvider(t)
20+ expectedKeys := []entity.ConfigKeyInfo{
21+ {
22+ Key: "appearance.color_scheme",
23+ Type: "string",
24+ Default: "default",
25+ Description: "Theme preference",
26+ Values: []string{"default", "prefer-dark", "prefer-light"},
27+ Section: "Appearance",
28+ },
29+ {
30+ Key: "logging.level",
31+ Type: "string",
32+ Default: "info",
33+ Description: "Log verbosity level",
34+ Values: []string{"trace", "debug", "info", "warn", "error", "fatal"},
35+ Section: "Logging",
36+ },
37+ }
38+ mockProvider.EXPECT().GetSchema().Return(expectedKeys)
39+
40+ uc := usecase.NewGetConfigSchemaUseCase(mockProvider)
41+
42+ // Act
43+ result, err := uc.Execute(context.Background(), usecase.GetConfigSchemaInput{})
44+
45+ // Assert
46+ require.NoError(t, err)
47+ assert.NotNil(t, result)
48+ assert.Len(t, result.Keys, 2)
49+ assert.Equal(t, "appearance.color_scheme", result.Keys[0].Key)
50+ assert.Equal(t, "logging.level", result.Keys[1].Key)
51+ mock.AssertExpectationsForObjects(t, mockProvider)
52+ })
53+
54+ t.Run("returns empty slice when no keys", func(t *testing.T) {
55+ // Arrange
56+ mockProvider := mocks.NewMockConfigSchemaProvider(t)
57+ mockProvider.EXPECT().GetSchema().Return([]entity.ConfigKeyInfo{})
58+
59+ uc := usecase.NewGetConfigSchemaUseCase(mockProvider)
60+
61+ // Act
62+ result, err := uc.Execute(context.Background(), usecase.GetConfigSchemaInput{})
63+
64+ // Assert
65+ require.NoError(t, err)
66+ assert.NotNil(t, result)
67+ assert.Empty(t, result.Keys)
68+ mock.AssertExpectationsForObjects(t, mockProvider)
69+ })
70+
71+ t.Run("keys include all expected fields", func(t *testing.T) {
72+ // Arrange
73+ mockProvider := mocks.NewMockConfigSchemaProvider(t)
74+ expectedKeys := []entity.ConfigKeyInfo{
75+ {
76+ Key: "appearance.default_font_size",
77+ Type: "int",
78+ Default: "16",
79+ Description: "Default font size in CSS pixels",
80+ Range: "1-72",
81+ Section: "Appearance",
82+ },
83+ }
84+ mockProvider.EXPECT().GetSchema().Return(expectedKeys)
85+
86+ uc := usecase.NewGetConfigSchemaUseCase(mockProvider)
87+
88+ // Act
89+ result, err := uc.Execute(context.Background(), usecase.GetConfigSchemaInput{})
90+
91+ // Assert
92+ require.NoError(t, err)
93+ assert.NotNil(t, result)
94+ assert.Len(t, result.Keys, 1)
95+
96+ key := result.Keys[0]
97+ assert.Equal(t, "appearance.default_font_size", key.Key)
98+ assert.Equal(t, "int", key.Type)
99+ assert.Equal(t, "16", key.Default)
100+ assert.Equal(t, "Default font size in CSS pixels", key.Description)
101+ assert.Equal(t, "1-72", key.Range)
102+ assert.Equal(t, "Appearance", key.Section)
103+ assert.Empty(t, key.Values) // No enum values for int type
104+ mock.AssertExpectationsForObjects(t, mockProvider)
105+ })
106+}
M internal/cli/cmd/config.go +45 -0
@@ -53,12 +53,57 @@ Existing settings are never modified - only missing keys are added with default
5353 RunE: runConfigMigrate,
5454 }
5555
56+var configSchemaJSON bool
57+
58+var configSchemaCmd = &cobra.Command{
59+ Use: "schema",
60+ Short: "Show all config keys with types and valid values",
61+ Long: `Display a reference of all configuration keys with their types, defaults,
62+descriptions, and valid values.
63+
64+Use --json for machine-readable output.`,
65+ RunE: runConfigSchema,
66+}
67+
5668 func init() {
5769 rootCmd.AddCommand(configCmd)
5870 configCmd.AddCommand(configOpenCmd)
5971 configCmd.AddCommand(configStatusCmd)
6072 configCmd.AddCommand(configMigrateCmd)
73+ configCmd.AddCommand(configSchemaCmd)
6174 configMigrateCmd.Flags().BoolVarP(&configYes, "yes", "y", false, "skip confirmation prompt")
75+ configSchemaCmd.Flags().BoolVar(&configSchemaJSON, "json", false, "output as JSON")
76+}
77+
78+// runConfigSchema displays all configuration keys with their metadata.
79+func runConfigSchema(_ *cobra.Command, _ []string) error {
80+ app := GetApp()
81+ if app == nil {
82+ return fmt.Errorf("app not initialized")
83+ }
84+
85+ provider := config.NewSchemaProvider()
86+ uc := usecase.NewGetConfigSchemaUseCase(provider)
87+
88+ ctx := context.Background()
89+ result, err := uc.Execute(ctx, usecase.GetConfigSchemaInput{})
90+ if err != nil {
91+ return fmt.Errorf("get config schema: %w", err)
92+ }
93+
94+ renderer := styles.NewConfigSchemaRenderer(app.Theme)
95+
96+ if configSchemaJSON {
97+ output, err := renderer.RenderJSON(result.Keys)
98+ if err != nil {
99+ return err
100+ }
101+ fmt.Println(output)
102+ return nil
103+ }
104+
105+ fmt.Println(renderer.Render(result.Keys))
106+ return nil
62107 }
63108
64109 // runConfigOpen opens the config file in the user's preferred editor.
@@ -53,12 +53,57 @@ Existing settings are never modified - only missing keys are added with default
53 RunE: runConfigMigrate,53 RunE: runConfigMigrate,
54 }54 }
55 55
56+var configSchemaJSON bool
57+
58+var configSchemaCmd = &cobra.Command{
59+ Use: "schema",
60+ Short: "Show all config keys with types and valid values",
61+ Long: `Display a reference of all configuration keys with their types, defaults,
62+descriptions, and valid values.
63+
64+Use --json for machine-readable output.`,
65+ RunE: runConfigSchema,
66+}
67+
56 func init() {68 func init() {
57 rootCmd.AddCommand(configCmd)69 rootCmd.AddCommand(configCmd)
58 configCmd.AddCommand(configOpenCmd)70 configCmd.AddCommand(configOpenCmd)
59 configCmd.AddCommand(configStatusCmd)71 configCmd.AddCommand(configStatusCmd)
60 configCmd.AddCommand(configMigrateCmd)72 configCmd.AddCommand(configMigrateCmd)
73+ configCmd.AddCommand(configSchemaCmd)
61 configMigrateCmd.Flags().BoolVarP(&configYes, "yes", "y", false, "skip confirmation prompt")74 configMigrateCmd.Flags().BoolVarP(&configYes, "yes", "y", false, "skip confirmation prompt")
75+ configSchemaCmd.Flags().BoolVar(&configSchemaJSON, "json", false, "output as JSON")
76+}
77+
78+// runConfigSchema displays all configuration keys with their metadata.
79+func runConfigSchema(_ *cobra.Command, _ []string) error {
80+ app := GetApp()
81+ if app == nil {
82+ return fmt.Errorf("app not initialized")
83+ }
84+
85+ provider := config.NewSchemaProvider()
86+ uc := usecase.NewGetConfigSchemaUseCase(provider)
87+
88+ ctx := context.Background()
89+ result, err := uc.Execute(ctx, usecase.GetConfigSchemaInput{})
90+ if err != nil {
91+ return fmt.Errorf("get config schema: %w", err)
92+ }
93+
94+ renderer := styles.NewConfigSchemaRenderer(app.Theme)
95+
96+ if configSchemaJSON {
97+ output, err := renderer.RenderJSON(result.Keys)
98+ if err != nil {
99+ return err
100+ }
101+ fmt.Println(output)
102+ return nil
103+ }
104+
105+ fmt.Println(renderer.Render(result.Keys))
106+ return nil
62 }107 }
63 108
64 // runConfigOpen opens the config file in the user's preferred editor.109 // runConfigOpen opens the config file in the user's preferred editor.
A internal/cli/styles/config_schema.go +144 -0
@@ -0,0 +1,144 @@
1+package styles
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "strings"
7+
8+ "github.com/charmbracelet/lipgloss"
9+
10+ "github.com/bnema/dumber/internal/domain/entity"
11+)
12+
13+// ConfigSchemaRenderer renders configuration schema information.
14+type ConfigSchemaRenderer struct {
15+ theme *Theme
16+}
17+
18+// NewConfigSchemaRenderer creates a new ConfigSchemaRenderer.
19+func NewConfigSchemaRenderer(theme *Theme) *ConfigSchemaRenderer {
20+ return &ConfigSchemaRenderer{theme: theme}
21+}
22+
23+// Render renders the configuration schema in styled format.
24+func (r *ConfigSchemaRenderer) Render(keys []entity.ConfigKeyInfo) string {
25+ if len(keys) == 0 {
26+ return r.theme.Subtle.Render("No configuration keys found")
27+ }
28+
29+ // Group keys by section
30+ sections := groupBySection(keys)
31+
32+ // Build output
33+ var parts []string
34+
35+ // Header
36+ header := r.renderHeader()
37+ parts = append(parts, header, "")
38+
39+ // Render each section
40+ sectionOrder := []string{
41+ "Appearance",
42+ "Logging",
43+ "History",
44+ "Search",
45+ "Dmenu",
46+ "Workspace",
47+ "Session",
48+ "Omnibox",
49+ "Content Filtering",
50+ "Rendering",
51+ "Media",
52+ "Update",
53+ "Debug",
54+ "Performance",
55+ "Runtime",
56+ "Database",
57+ }
58+
59+ for _, section := range sectionOrder {
60+ if sectionKeys, ok := sections[section]; ok {
61+ parts = append(parts, r.renderSection(section, sectionKeys), "")
62+ }
63+ }
64+
65+ return strings.Join(parts, "\n")
66+}
67+
68+// RenderJSON renders the configuration schema as JSON.
69+func (*ConfigSchemaRenderer) RenderJSON(keys []entity.ConfigKeyInfo) (string, error) {
70+ data, err := json.MarshalIndent(keys, "", " ")
71+ if err != nil {
72+ return "", fmt.Errorf("marshal schema: %w", err)
73+ }
74+ return string(data), nil
75+}
76+
77+func (r *ConfigSchemaRenderer) renderHeader() string {
78+ iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
79+ title := fmt.Sprintf("%s %s", iconStyle.Render(IconConfig), r.theme.Title.Render("Config Schema Reference"))
80+ return title
81+}
82+
83+func groupBySection(keys []entity.ConfigKeyInfo) map[string][]entity.ConfigKeyInfo {
84+ sections := make(map[string][]entity.ConfigKeyInfo)
85+ for _, key := range keys {
86+ sections[key.Section] = append(sections[key.Section], key)
87+ }
88+ return sections
89+}
90+
91+func (r *ConfigSchemaRenderer) renderSection(name string, keys []entity.ConfigKeyInfo) string {
92+ var lines []string
93+
94+ for _, key := range keys {
95+ lines = append(lines, r.renderKey(key))
96+ }
97+
98+ body := strings.Join(lines, "\n")
99+
100+ // Create section header (no extra padding/margin)
101+ sectionHeader := r.theme.Highlight.Render(name)
102+
103+ // Build box content: header directly followed by keys
104+ boxContent := sectionHeader + "\n" + body
105+
106+ // Use box style without top padding
107+ boxStyle := r.theme.Box.PaddingTop(0)
108+
109+ return boxStyle.Render(boxContent)
110+}
111+
112+func (r *ConfigSchemaRenderer) renderKey(key entity.ConfigKeyInfo) string {
113+ keyStyle := r.theme.Normal.Bold(true)
114+ typeStyle := r.theme.Subtle
115+ defaultStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
116+ descStyle := r.theme.Subtle
117+ valuesStyle := r.theme.Normal
118+
119+ // Line 1: key name, type, default
120+ line1 := fmt.Sprintf(
121+ "%s %s %s",
122+ keyStyle.Render(key.Key),
123+ typeStyle.Render(key.Type),
124+ defaultStyle.Render(key.Default),
125+ )
126+
127+ // Line 2: description (indented)
128+ line2 := fmt.Sprintf(" %s", descStyle.Render(key.Description))
129+
130+ result := line1 + "\n" + line2
131+
132+ // Line 3 (optional): values or range
133+ if len(key.Values) > 0 {
134+ valuesText := "Values: " + strings.Join(key.Values, ", ")
135+ line3 := fmt.Sprintf(" %s", valuesStyle.Render(valuesText))
136+ result += "\n" + line3
137+ } else if key.Range != "" {
138+ rangeText := "Range: " + key.Range
139+ line3 := fmt.Sprintf(" %s", valuesStyle.Render(rangeText))
140+ result += "\n" + line3
141+ }
142+
143+ return result
144+}
@@ -0,0 +1,144 @@
1+package styles
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "strings"
7+
8+ "github.com/charmbracelet/lipgloss"
9+
10+ "github.com/bnema/dumber/internal/domain/entity"
11+)
12+
13+// ConfigSchemaRenderer renders configuration schema information.
14+type ConfigSchemaRenderer struct {
15+ theme *Theme
16+}
17+
18+// NewConfigSchemaRenderer creates a new ConfigSchemaRenderer.
19+func NewConfigSchemaRenderer(theme *Theme) *ConfigSchemaRenderer {
20+ return &ConfigSchemaRenderer{theme: theme}
21+}
22+
23+// Render renders the configuration schema in styled format.
24+func (r *ConfigSchemaRenderer) Render(keys []entity.ConfigKeyInfo) string {
25+ if len(keys) == 0 {
26+ return r.theme.Subtle.Render("No configuration keys found")
27+ }
28+
29+ // Group keys by section
30+ sections := groupBySection(keys)
31+
32+ // Build output
33+ var parts []string
34+
35+ // Header
36+ header := r.renderHeader()
37+ parts = append(parts, header, "")
38+
39+ // Render each section
40+ sectionOrder := []string{
41+ "Appearance",
42+ "Logging",
43+ "History",
44+ "Search",
45+ "Dmenu",
46+ "Workspace",
47+ "Session",
48+ "Omnibox",
49+ "Content Filtering",
50+ "Rendering",
51+ "Media",
52+ "Update",
53+ "Debug",
54+ "Performance",
55+ "Runtime",
56+ "Database",
57+ }
58+
59+ for _, section := range sectionOrder {
60+ if sectionKeys, ok := sections[section]; ok {
61+ parts = append(parts, r.renderSection(section, sectionKeys), "")
62+ }
63+ }
64+
65+ return strings.Join(parts, "\n")
66+}
67+
68+// RenderJSON renders the configuration schema as JSON.
69+func (*ConfigSchemaRenderer) RenderJSON(keys []entity.ConfigKeyInfo) (string, error) {
70+ data, err := json.MarshalIndent(keys, "", " ")
71+ if err != nil {
72+ return "", fmt.Errorf("marshal schema: %w", err)
73+ }
74+ return string(data), nil
75+}
76+
77+func (r *ConfigSchemaRenderer) renderHeader() string {
78+ iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
79+ title := fmt.Sprintf("%s %s", iconStyle.Render(IconConfig), r.theme.Title.Render("Config Schema Reference"))
80+ return title
81+}
82+
83+func groupBySection(keys []entity.ConfigKeyInfo) map[string][]entity.ConfigKeyInfo {
84+ sections := make(map[string][]entity.ConfigKeyInfo)
85+ for _, key := range keys {
86+ sections[key.Section] = append(sections[key.Section], key)
87+ }
88+ return sections
89+}
90+
91+func (r *ConfigSchemaRenderer) renderSection(name string, keys []entity.ConfigKeyInfo) string {
92+ var lines []string
93+
94+ for _, key := range keys {
95+ lines = append(lines, r.renderKey(key))
96+ }
97+
98+ body := strings.Join(lines, "\n")
99+
100+ // Create section header (no extra padding/margin)
101+ sectionHeader := r.theme.Highlight.Render(name)
102+
103+ // Build box content: header directly followed by keys
104+ boxContent := sectionHeader + "\n" + body
105+
106+ // Use box style without top padding
107+ boxStyle := r.theme.Box.PaddingTop(0)
108+
109+ return boxStyle.Render(boxContent)
110+}
111+
112+func (r *ConfigSchemaRenderer) renderKey(key entity.ConfigKeyInfo) string {
113+ keyStyle := r.theme.Normal.Bold(true)
114+ typeStyle := r.theme.Subtle
115+ defaultStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
116+ descStyle := r.theme.Subtle
117+ valuesStyle := r.theme.Normal
118+
119+ // Line 1: key name, type, default
120+ line1 := fmt.Sprintf(
121+ "%s %s %s",
122+ keyStyle.Render(key.Key),
123+ typeStyle.Render(key.Type),
124+ defaultStyle.Render(key.Default),
125+ )
126+
127+ // Line 2: description (indented)
128+ line2 := fmt.Sprintf(" %s", descStyle.Render(key.Description))
129+
130+ result := line1 + "\n" + line2
131+
132+ // Line 3 (optional): values or range
133+ if len(key.Values) > 0 {
134+ valuesText := "Values: " + strings.Join(key.Values, ", ")
135+ line3 := fmt.Sprintf(" %s", valuesStyle.Render(valuesText))
136+ result += "\n" + line3
137+ } else if key.Range != "" {
138+ rangeText := "Range: " + key.Range
139+ line3 := fmt.Sprintf(" %s", valuesStyle.Render(rangeText))
140+ result += "\n" + line3
141+ }
142+
143+ return result
144+}
A internal/domain/entity/config_key.go +27 -0
@@ -0,0 +1,27 @@
1+package entity
2+
3+// ConfigKeyInfo describes a single configuration key for schema documentation.
4+type ConfigKeyInfo struct {
5+ // Key is the full dotted path to the config key (e.g., "appearance.color_scheme")
6+ Key string `json:"key"`
7+
8+ // Type is the Go type name (e.g., "string", "int", "bool", "float64")
9+ Type string `json:"type"`
10+
11+ // Default is the default value as a string representation
12+ Default string `json:"default"`
13+
14+ // Description explains the purpose of this config key
15+ Description string `json:"description"`
16+
17+ // Values contains valid enum values (for string enums)
18+ // Empty if not an enum type
19+ Values []string `json:"values,omitempty"`
20+
21+ // Range describes numeric constraints (e.g., "1-72", "0.1-5.0")
22+ // Empty if no range constraint
23+ Range string `json:"range,omitempty"`
24+
25+ // Section groups related keys (e.g., "Appearance", "Logging")
26+ Section string `json:"section"`
27+}
@@ -0,0 +1,27 @@
1+package entity
2+
3+// ConfigKeyInfo describes a single configuration key for schema documentation.
4+type ConfigKeyInfo struct {
5+ // Key is the full dotted path to the config key (e.g., "appearance.color_scheme")
6+ Key string `json:"key"`
7+
8+ // Type is the Go type name (e.g., "string", "int", "bool", "float64")
9+ Type string `json:"type"`
10+
11+ // Default is the default value as a string representation
12+ Default string `json:"default"`
13+
14+ // Description explains the purpose of this config key
15+ Description string `json:"description"`
16+
17+ // Values contains valid enum values (for string enums)
18+ // Empty if not an enum type
19+ Values []string `json:"values,omitempty"`
20+
21+ // Range describes numeric constraints (e.g., "1-72", "0.1-5.0")
22+ // Empty if no range constraint
23+ Range string `json:"range,omitempty"`
24+
25+ // Section groups related keys (e.g., "Appearance", "Logging")
26+ Section string `json:"section"`
27+}
A internal/infrastructure/config/schema_provider.go +903 -0
@@ -0,0 +1,903 @@
1+package config
2+
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/dumber/internal/domain/entity"
7+)
8+
9+// Section names for grouping config keys.
10+const (
11+ SectionAppearance = "Appearance"
12+ SectionLogging = "Logging"
13+ SectionHistory = "History"
14+ SectionWorkspace = "Workspace"
15+ SectionSession = "Session"
16+ SectionOmnibox = "Omnibox"
17+ SectionContentFiltering = "Content Filtering"
18+ SectionRendering = "Rendering"
19+ SectionMedia = "Media"
20+ SectionUpdate = "Update"
21+ SectionDmenu = "Dmenu"
22+ SectionDebug = "Debug"
23+ SectionPerformance = "Performance"
24+ SectionRuntime = "Runtime"
25+ SectionDatabase = "Database"
26+ SectionSearch = "Search"
27+)
28+
29+// SchemaProvider implements port.ConfigSchemaProvider.
30+type SchemaProvider struct{}
31+
32+// NewSchemaProvider creates a new SchemaProvider.
33+func NewSchemaProvider() *SchemaProvider {
34+ return &SchemaProvider{}
35+}
36+
37+// GetSchema returns all configuration keys with their metadata.
38+func (p *SchemaProvider) GetSchema() []entity.ConfigKeyInfo {
39+ defaults := DefaultConfig()
40+
41+ keys := make([]entity.ConfigKeyInfo, 0, 100)
42+
43+ // Appearance section
44+ keys = append(keys, p.getAppearanceKeys(defaults)...)
45+
46+ // Logging section
47+ keys = append(keys, p.getLoggingKeys(defaults)...)
48+
49+ // History section
50+ keys = append(keys, p.getHistoryKeys(defaults)...)
51+
52+ // Search section
53+ keys = append(keys, p.getSearchKeys(defaults)...)
54+
55+ // Dmenu section
56+ keys = append(keys, p.getDmenuKeys(defaults)...)
57+
58+ // Workspace section
59+ keys = append(keys, p.getWorkspaceKeys(defaults)...)
60+
61+ // Session section
62+ keys = append(keys, p.getSessionKeys(defaults)...)
63+
64+ // Omnibox section
65+ keys = append(keys, p.getOmniboxKeys(defaults)...)
66+
67+ // Content Filtering section
68+ keys = append(keys, p.getContentFilteringKeys(defaults)...)
69+
70+ // Rendering section
71+ keys = append(keys, p.getRenderingKeys(defaults)...)
72+
73+ // Media section
74+ keys = append(keys, p.getMediaKeys(defaults)...)
75+
76+ // Update section
77+ keys = append(keys, p.getUpdateKeys(defaults)...)
78+
79+ // Debug section
80+ keys = append(keys, p.getDebugKeys(defaults)...)
81+
82+ // Performance section
83+ keys = append(keys, p.getPerformanceKeys(defaults)...)
84+
85+ // Runtime section
86+ keys = append(keys, p.getRuntimeKeys(defaults)...)
87+
88+ // Database section
89+ keys = append(keys, p.getDatabaseKeys()...)
90+
91+ return keys
92+}
93+
94+func (*SchemaProvider) getAppearanceKeys(defaults *Config) []entity.ConfigKeyInfo {
95+ return []entity.ConfigKeyInfo{
96+ {
97+ Key: "appearance.color_scheme",
98+ Type: "string",
99+ Default: defaults.Appearance.ColorScheme,
100+ Description: "Theme preference (follows system by default)",
101+ Values: []string{"default", "prefer-dark", "prefer-light"},
102+ Section: SectionAppearance,
103+ },
104+ {
105+ Key: "appearance.default_font_size",
106+ Type: "int",
107+ Default: fmt.Sprintf("%d", defaults.Appearance.DefaultFontSize),
108+ Description: "Default font size in CSS pixels",
109+ Range: "1-72",
110+ Section: SectionAppearance,
111+ },
112+ {
113+ Key: "appearance.sans_font",
114+ Type: "string",
115+ Default: defaults.Appearance.SansFont,
116+ Description: "Sans-serif font family for web pages",
117+ Section: SectionAppearance,
118+ },
119+ {
120+ Key: "appearance.serif_font",
121+ Type: "string",
122+ Default: defaults.Appearance.SerifFont,
123+ Description: "Serif font family for web pages",
124+ Section: SectionAppearance,
125+ },
126+ {
127+ Key: "appearance.monospace_font",
128+ Type: "string",
129+ Default: defaults.Appearance.MonospaceFont,
130+ Description: "Monospace font family for code",
131+ Section: SectionAppearance,
132+ },
133+ {
134+ Key: "default_webpage_zoom",
135+ Type: "float64",
136+ Default: fmt.Sprintf("%.1f", defaults.DefaultWebpageZoom),
137+ Description: "Default zoom level for web pages (1.0 = 100%%)",
138+ Range: "0.1-5.0",
139+ Section: SectionAppearance,
140+ },
141+ {
142+ Key: "default_ui_scale",
143+ Type: "float64",
144+ Default: fmt.Sprintf("%.1f", defaults.DefaultUIScale),
145+ Description: "UI scale multiplier for GTK widgets",
146+ Range: "0.5-3.0",
147+ Section: SectionAppearance,
148+ },
149+ {
150+ Key: "appearance.light_palette.*",
151+ Type: "string",
152+ Default: "(hex colors)",
153+ Description: "Light theme color palette (background, surface, surface_variant, text, muted, accent, border)",
154+ Section: SectionAppearance,
155+ },
156+ {
157+ Key: "appearance.dark_palette.*",
158+ Type: "string",
159+ Default: "(hex colors)",
160+ Description: "Dark theme color palette (background, surface, surface_variant, text, muted, accent, border)",
161+ Section: SectionAppearance,
162+ },
163+ }
164+}
165+
166+func (*SchemaProvider) getLoggingKeys(defaults *Config) []entity.ConfigKeyInfo {
167+ return []entity.ConfigKeyInfo{
168+ {
169+ Key: "logging.level",
170+ Type: "string",
171+ Default: defaults.Logging.Level,
172+ Description: "Log verbosity level",
173+ Values: []string{"trace", "debug", "info", "warn", "error", "fatal"},
174+ Section: SectionLogging,
175+ },
176+ {
177+ Key: "logging.format",
178+ Type: "string",
179+ Default: defaults.Logging.Format,
180+ Description: "Log output format",
181+ Values: []string{"text", "json", "console"},
182+ Section: SectionLogging,
183+ },
184+ {
185+ Key: "logging.max_age",
186+ Type: "int",
187+ Default: fmt.Sprintf("%d", defaults.Logging.MaxAge),
188+ Description: "Maximum age of log files in days",
189+ Range: ">=0",
190+ Section: SectionLogging,
191+ },
192+ {
193+ Key: "logging.log_dir",
194+ Type: "string",
195+ Default: defaults.Logging.LogDir,
196+ Description: "Directory for log files",
197+ Section: SectionLogging,
198+ },
199+ {
200+ Key: "logging.enable_file_log",
201+ Type: "bool",
202+ Default: fmt.Sprintf("%t", defaults.Logging.EnableFileLog),
203+ Description: "Enable logging to file",
204+ Section: SectionLogging,
205+ },
206+ {
207+ Key: "logging.capture_console",
208+ Type: "bool",
209+ Default: fmt.Sprintf("%t", defaults.Logging.CaptureConsole),
210+ Description: "Capture browser console messages to log",
211+ Section: SectionLogging,
212+ },
213+ }
214+}
215+
216+func (*SchemaProvider) getHistoryKeys(defaults *Config) []entity.ConfigKeyInfo {
217+ return []entity.ConfigKeyInfo{
218+ {
219+ Key: "history.max_entries",
220+ Type: "int",
221+ Default: fmt.Sprintf("%d", defaults.History.MaxEntries),
222+ Description: "Maximum number of history entries to keep",
223+ Range: ">=0",
224+ Section: SectionHistory,
225+ },
226+ {
227+ Key: "history.retention_period_days",
228+ Type: "int",
229+ Default: fmt.Sprintf("%d", defaults.History.RetentionPeriodDays),
230+ Description: "Number of days to retain history",
231+ Range: ">=0",
232+ Section: SectionHistory,
233+ },
234+ {
235+ Key: "history.cleanup_interval_days",
236+ Type: "int",
237+ Default: fmt.Sprintf("%d", defaults.History.CleanupIntervalDays),
238+ Description: "Days between automatic history cleanup",
239+ Range: ">=0",
240+ Section: SectionHistory,
241+ },
242+ }
243+}
244+
245+func (*SchemaProvider) getSearchKeys(defaults *Config) []entity.ConfigKeyInfo {
246+ return []entity.ConfigKeyInfo{
247+ {
248+ Key: "default_search_engine",
249+ Type: "string",
250+ Default: defaults.DefaultSearchEngine,
251+ Description: "Default search engine URL (must contain %s placeholder)",
252+ Section: SectionSearch,
253+ },
254+ {
255+ Key: "search_shortcuts.<key>",
256+ Type: "object",
257+ Default: "(see defaults)",
258+ Description: "Search shortcuts map with url and description fields",
259+ Section: SectionSearch,
260+ },
261+ }
262+}
263+
264+func (*SchemaProvider) getDmenuKeys(defaults *Config) []entity.ConfigKeyInfo {
265+ return []entity.ConfigKeyInfo{
266+ {
267+ Key: "dmenu.max_history_items",
268+ Type: "int",
269+ Default: fmt.Sprintf("%d", defaults.Dmenu.MaxHistoryItems),
270+ Description: "Maximum history items shown in dmenu",
271+ Range: ">=0",
272+ Section: SectionDmenu,
273+ },
274+ {
275+ Key: "dmenu.show_visit_count",
276+ Type: "bool",
277+ Default: fmt.Sprintf("%t", defaults.Dmenu.ShowVisitCount),
278+ Description: "Show visit count in dmenu entries",
279+ Section: SectionDmenu,
280+ },
281+ {
282+ Key: "dmenu.show_last_visited",
283+ Type: "bool",
284+ Default: fmt.Sprintf("%t", defaults.Dmenu.ShowLastVisited),
285+ Description: "Show last visited date in dmenu entries",
286+ Section: SectionDmenu,
287+ },
288+ {
289+ Key: "dmenu.history_prefix",
290+ Type: "string",
291+ Default: defaults.Dmenu.HistoryPrefix,
292+ Description: "Prefix for history entries in dmenu",
293+ Section: SectionDmenu,
294+ },
295+ {
296+ Key: "dmenu.shortcut_prefix",
297+ Type: "string",
298+ Default: defaults.Dmenu.ShortcutPrefix,
299+ Description: "Prefix for search shortcuts in dmenu",
300+ Section: SectionDmenu,
301+ },
302+ {
303+ Key: "dmenu.url_prefix",
304+ Type: "string",
305+ Default: defaults.Dmenu.URLPrefix,
306+ Description: "Prefix for URL entries in dmenu",
307+ Section: SectionDmenu,
308+ },
309+ {
310+ Key: "dmenu.date_format",
311+ Type: "string",
312+ Default: defaults.Dmenu.DateFormat,
313+ Description: "Go time format for dates in dmenu",
314+ Section: SectionDmenu,
315+ },
316+ {
317+ Key: "dmenu.sort_by_visit_count",
318+ Type: "bool",
319+ Default: fmt.Sprintf("%t", defaults.Dmenu.SortByVisitCount),
320+ Description: "Sort dmenu entries by visit count",
321+ Section: SectionDmenu,
322+ },
323+ }
324+}
325+
326+func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo {
327+ return []entity.ConfigKeyInfo{
328+ {
329+ Key: "workspace.new_pane_url",
330+ Type: "string",
331+ Default: defaults.Workspace.NewPaneURL,
332+ Description: "URL loaded when creating a new pane",
333+ Section: SectionWorkspace,
334+ },
335+ {
336+ Key: "workspace.tab_bar_position",
337+ Type: "string",
338+ Default: defaults.Workspace.TabBarPosition,
339+ Description: "Position of the tab bar",
340+ Values: []string{"top", "bottom"},
341+ Section: SectionWorkspace,
342+ },
343+ {
344+ Key: "workspace.hide_tab_bar_when_single_tab",
345+ Type: "bool",
346+ Default: fmt.Sprintf("%t", defaults.Workspace.HideTabBarWhenSingleTab),
347+ Description: "Hide tab bar when only one tab exists",
348+ Section: SectionWorkspace,
349+ },
350+ {
351+ Key: "workspace.switch_to_tab_on_move",
352+ Type: "bool",
353+ Default: fmt.Sprintf("%t", defaults.Workspace.SwitchToTabOnMove),
354+ Description: "Switch focus to tab when moving pane to it",
355+ Section: SectionWorkspace,
356+ },
357+ // Pane mode
358+ {
359+ Key: "workspace.pane_mode.activation_shortcut",
360+ Type: "string",
361+ Default: defaults.Workspace.PaneMode.ActivationShortcut,
362+ Description: "Shortcut to enter pane mode",
363+ Section: SectionWorkspace,
364+ },
365+ {
366+ Key: "workspace.pane_mode.timeout_ms",
367+ Type: "int",
368+ Default: fmt.Sprintf("%d", defaults.Workspace.PaneMode.TimeoutMilliseconds),
369+ Description: "Pane mode timeout in milliseconds (0 = no timeout)",
370+ Range: ">=0",
371+ Section: SectionWorkspace,
372+ },
373+ {
374+ Key: "workspace.pane_mode.actions.<action>",
375+ Type: "[]string",
376+ Default: "(see defaults)",
377+ Description: "Key bindings for pane mode actions",
378+ Section: SectionWorkspace,
379+ },
380+ // Tab mode
381+ {
382+ Key: "workspace.tab_mode.activation_shortcut",
383+ Type: "string",
384+ Default: defaults.Workspace.TabMode.ActivationShortcut,
385+ Description: "Shortcut to enter tab mode",
386+ Section: SectionWorkspace,
387+ },
388+ {
389+ Key: "workspace.tab_mode.timeout_ms",
390+ Type: "int",
391+ Default: fmt.Sprintf("%d", defaults.Workspace.TabMode.TimeoutMilliseconds),
392+ Description: "Tab mode timeout in milliseconds (0 = no timeout)",
393+ Range: ">=0",
394+ Section: SectionWorkspace,
395+ },
396+ {
397+ Key: "workspace.tab_mode.actions.<action>",
398+ Type: "[]string",
399+ Default: "(see defaults)",
400+ Description: "Key bindings for tab mode actions",
401+ Section: SectionWorkspace,
402+ },
403+ // Resize mode
404+ {
405+ Key: "workspace.resize_mode.activation_shortcut",
406+ Type: "string",
407+ Default: defaults.Workspace.ResizeMode.ActivationShortcut,
408+ Description: "Shortcut to enter resize mode",
409+ Section: SectionWorkspace,
410+ },
411+ {
412+ Key: "workspace.resize_mode.timeout_ms",
413+ Type: "int",
414+ Default: fmt.Sprintf("%d", defaults.Workspace.ResizeMode.TimeoutMilliseconds),
415+ Description: "Resize mode timeout in milliseconds (0 = no timeout)",
416+ Range: ">=0",
417+ Section: SectionWorkspace,
418+ },
419+ {
420+ Key: "workspace.resize_mode.step_percent",
421+ Type: "float64",
422+ Default: fmt.Sprintf("%.1f", defaults.Workspace.ResizeMode.StepPercent),
423+ Description: "Percentage to resize panes per step",
424+ Section: SectionWorkspace,
425+ },
426+ {
427+ Key: "workspace.resize_mode.min_pane_percent",
428+ Type: "float64",
429+ Default: fmt.Sprintf("%.1f", defaults.Workspace.ResizeMode.MinPanePercent),
430+ Description: "Minimum pane size as percentage",
431+ Section: SectionWorkspace,
432+ },
433+ {
434+ Key: "workspace.resize_mode.actions.<action>",
435+ Type: "[]string",
436+ Default: "(see defaults)",
437+ Description: "Key bindings for resize mode actions",
438+ Section: SectionWorkspace,
439+ },
440+ // Global shortcuts
441+ {
442+ Key: "workspace.shortcuts.close_pane",
443+ Type: "string",
444+ Default: defaults.Workspace.Shortcuts.ClosePane,
445+ Description: "Global shortcut to close current pane",
446+ Section: SectionWorkspace,
447+ },
448+ {
449+ Key: "workspace.shortcuts.next_tab",
450+ Type: "string",
451+ Default: defaults.Workspace.Shortcuts.NextTab,
452+ Description: "Global shortcut to switch to next tab",
453+ Section: SectionWorkspace,
454+ },
455+ {
456+ Key: "workspace.shortcuts.previous_tab",
457+ Type: "string",
458+ Default: defaults.Workspace.Shortcuts.PreviousTab,
459+ Description: "Global shortcut to switch to previous tab",
460+ Section: SectionWorkspace,
461+ },
462+ // Popups
463+ {
464+ Key: "workspace.popups.behavior",
465+ Type: "string",
466+ Default: string(defaults.Workspace.Popups.Behavior),
467+ Description: "How to handle JavaScript popup windows",
468+ Values: []string{"split", "stacked", "tabbed", "windowed"},
469+ Section: SectionWorkspace,
470+ },
471+ {
472+ Key: "workspace.popups.placement",
473+ Type: "string",
474+ Default: defaults.Workspace.Popups.Placement,
475+ Description: "Placement direction for split popups",
476+ Values: []string{"right", "left", "top", "bottom"},
477+ Section: SectionWorkspace,
478+ },
479+ {
480+ Key: "workspace.popups.blank_target_behavior",
481+ Type: "string",
482+ Default: defaults.Workspace.Popups.BlankTargetBehavior,
483+ Description: "How to handle target=\"_blank\" links",
484+ Values: []string{"split", "stacked", "tabbed"},
485+ Section: SectionWorkspace,
486+ },
487+ {
488+ Key: "workspace.popups.open_in_new_pane",
489+ Type: "bool",
490+ Default: fmt.Sprintf("%t", defaults.Workspace.Popups.OpenInNewPane),
491+ Description: "Open popups in workspace instead of blocking",
492+ Section: SectionWorkspace,
493+ },
494+ {
495+ Key: "workspace.popups.follow_pane_context",
496+ Type: "bool",
497+ Default: fmt.Sprintf("%t", defaults.Workspace.Popups.FollowPaneContext),
498+ Description: "Popup placement follows parent pane context",
499+ Section: SectionWorkspace,
500+ },
501+ {
502+ Key: "workspace.popups.enable_smart_detection",
503+ Type: "bool",
504+ Default: fmt.Sprintf("%t", defaults.Workspace.Popups.EnableSmartDetection),
505+ Description: "Use window properties to detect popup intent",
506+ Section: SectionWorkspace,
507+ },
508+ {
509+ Key: "workspace.popups.oauth_auto_close",
510+ Type: "bool",
511+ Default: fmt.Sprintf("%t", defaults.Workspace.Popups.OAuthAutoClose),
512+ Description: "Auto-close OAuth popups after success",
513+ Section: SectionWorkspace,
514+ },
515+ // Styling
516+ {
517+ Key: "workspace.styling.border_width",
518+ Type: "int",
519+ Default: fmt.Sprintf("%d", defaults.Workspace.Styling.BorderWidth),
520+ Description: "Active pane border width in pixels",
521+ Range: ">=0",
522+ Section: SectionWorkspace,
523+ },
524+ {
525+ Key: "workspace.styling.border_color",
526+ Type: "string",
527+ Default: defaults.Workspace.Styling.BorderColor,
528+ Description: "Active pane border color (CSS or @theme variable)",
529+ Section: SectionWorkspace,
530+ },
531+ {
532+ Key: "workspace.styling.mode_border_width",
533+ Type: "int",
534+ Default: fmt.Sprintf("%d", defaults.Workspace.Styling.ModeBorderWidth),
535+ Description: "Border width for modal mode indicators",
536+ Range: ">=0",
537+ Section: SectionWorkspace,
538+ },
539+ {
540+ Key: "workspace.styling.pane_mode_color",
541+ Type: "string",
542+ Default: defaults.Workspace.Styling.PaneModeColor,
543+ Description: "Color for pane mode indicator",
544+ Section: SectionWorkspace,
545+ },
546+ {
547+ Key: "workspace.styling.tab_mode_color",
548+ Type: "string",
549+ Default: defaults.Workspace.Styling.TabModeColor,
550+ Description: "Color for tab mode indicator",
551+ Section: SectionWorkspace,
552+ },
553+ {
554+ Key: "workspace.styling.session_mode_color",
555+ Type: "string",
556+ Default: defaults.Workspace.Styling.SessionModeColor,
557+ Description: "Color for session mode indicator",
558+ Section: SectionWorkspace,
559+ },
560+ {
561+ Key: "workspace.styling.resize_mode_color",
562+ Type: "string",
563+ Default: defaults.Workspace.Styling.ResizeModeColor,
564+ Description: "Color for resize mode indicator",
565+ Section: SectionWorkspace,
566+ },
567+ {
568+ Key: "workspace.styling.mode_indicator_toaster_enabled",
569+ Type: "bool",
570+ Default: fmt.Sprintf("%t", defaults.Workspace.Styling.ModeIndicatorToasterEnabled),
571+ Description: "Show toaster notification for modal modes",
572+ Section: SectionWorkspace,
573+ },
574+ {
575+ Key: "workspace.styling.transition_duration",
576+ Type: "int",
577+ Default: fmt.Sprintf("%d", defaults.Workspace.Styling.TransitionDuration),
578+ Description: "Border animation duration in milliseconds",
579+ Range: ">=0",
580+ Section: SectionWorkspace,
581+ },
582+ }
583+}
584+
585+func (*SchemaProvider) getSessionKeys(defaults *Config) []entity.ConfigKeyInfo {
586+ return []entity.ConfigKeyInfo{
587+ {
588+ Key: "session.auto_restore",
589+ Type: "bool",
590+ Default: fmt.Sprintf("%t", defaults.Session.AutoRestore),
591+ Description: "Automatically restore last session on startup",
592+ Section: SectionSession,
593+ },
594+ {
595+ Key: "session.snapshot_interval_ms",
596+ Type: "int",
597+ Default: fmt.Sprintf("%d", defaults.Session.SnapshotIntervalMs),
598+ Description: "Minimum interval between session snapshots",
599+ Range: ">=0",
600+ Section: SectionSession,
601+ },
602+ {
603+ Key: "session.max_exited_sessions",
604+ Type: "int",
605+ Default: fmt.Sprintf("%d", defaults.Session.MaxExitedSessions),
606+ Description: "Maximum number of exited sessions to keep",
607+ Range: ">=0",
608+ Section: SectionSession,
609+ },
610+ {
611+ Key: "session.max_exited_session_age_days",
612+ Type: "int",
613+ Default: fmt.Sprintf("%d", defaults.Session.MaxExitedSessionAgeDays),
614+ Description: "Maximum age of exited sessions in days",
615+ Range: ">=0",
616+ Section: SectionSession,
617+ },
618+ {
619+ Key: "session.session_mode.activation_shortcut",
620+ Type: "string",
621+ Default: defaults.Session.SessionMode.ActivationShortcut,
622+ Description: "Shortcut to enter session mode",
623+ Section: SectionSession,
624+ },
625+ {
626+ Key: "session.session_mode.timeout_ms",
627+ Type: "int",
628+ Default: fmt.Sprintf("%d", defaults.Session.SessionMode.TimeoutMilliseconds),
629+ Description: "Session mode timeout in milliseconds",
630+ Range: ">=0",
631+ Section: SectionSession,
632+ },
633+ {
634+ Key: "session.session_mode.actions.<action>",
635+ Type: "[]string",
636+ Default: "(see defaults)",
637+ Description: "Key bindings for session mode actions",
638+ Section: SectionSession,
639+ },
640+ }
641+}
642+
643+func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo {
644+ return []entity.ConfigKeyInfo{
645+ {
646+ Key: "omnibox.initial_behavior",
647+ Type: "string",
648+ Default: defaults.Omnibox.InitialBehavior,
649+ Description: "What to show when omnibox opens with empty input",
650+ Values: []string{"recent", "most_visited", "none"},
651+ Section: SectionOmnibox,
652+ },
653+ {
654+ Key: "omnibox.auto_open_on_new_pane",
655+ Type: "bool",
656+ Default: fmt.Sprintf("%t", defaults.Omnibox.AutoOpenOnNewPane),
657+ Description: "Auto-open omnibox when creating new pane",
658+ Section: SectionOmnibox,
659+ },
660+ }
661+}
662+
663+func (*SchemaProvider) getContentFilteringKeys(defaults *Config) []entity.ConfigKeyInfo {
664+ return []entity.ConfigKeyInfo{
665+ {
666+ Key: "content_filtering.enabled",
667+ Type: "bool",
668+ Default: fmt.Sprintf("%t", defaults.ContentFiltering.Enabled),
669+ Description: "Enable ad blocking and content filtering",
670+ Section: SectionContentFiltering,
671+ },
672+ {
673+ Key: "content_filtering.auto_update",
674+ Type: "bool",
675+ Default: fmt.Sprintf("%t", defaults.ContentFiltering.AutoUpdate),
676+ Description: "Auto-update filter lists from GitHub",
677+ Section: SectionContentFiltering,
678+ },
679+ }
680+}
681+
682+func (*SchemaProvider) getRenderingKeys(defaults *Config) []entity.ConfigKeyInfo {
683+ return []entity.ConfigKeyInfo{
684+ {
685+ Key: "rendering.mode",
686+ Type: "string",
687+ Default: string(defaults.Rendering.Mode),
688+ Description: "GPU/CPU rendering selection for WebKit",
689+ Values: []string{"auto", "gpu", "cpu"},
690+ Section: SectionRendering,
691+ },
692+ {
693+ Key: "rendering.gsk_renderer",
694+ Type: "string",
695+ Default: string(defaults.Rendering.GSKRenderer),
696+ Description: "GTK Scene Kit renderer selection",
697+ Values: []string{"auto", "opengl", "vulkan", "cairo"},
698+ Section: SectionRendering,
699+ },
700+ {
701+ Key: "rendering.disable_dmabuf_renderer",
702+ Type: "bool",
703+ Default: fmt.Sprintf("%t", defaults.Rendering.DisableDMABufRenderer),
704+ Description: "Disable DMA-BUF accelerated renderer",
705+ Section: SectionRendering,
706+ },
707+ {
708+ Key: "rendering.force_compositing_mode",
709+ Type: "bool",
710+ Default: fmt.Sprintf("%t", defaults.Rendering.ForceCompositingMode),
711+ Description: "Force accelerated compositing",
712+ Section: SectionRendering,
713+ },
714+ {
715+ Key: "rendering.disable_compositing_mode",
716+ Type: "bool",
717+ Default: fmt.Sprintf("%t", defaults.Rendering.DisableCompositingMode),
718+ Description: "Disable accelerated compositing",
719+ Section: SectionRendering,
720+ },
721+ {
722+ Key: "rendering.disable_mipmaps",
723+ Type: "bool",
724+ Default: fmt.Sprintf("%t", defaults.Rendering.DisableMipmaps),
725+ Description: "Disable mipmap generation",
726+ Section: SectionRendering,
727+ },
728+ {
729+ Key: "rendering.prefer_gl",
730+ Type: "bool",
731+ Default: fmt.Sprintf("%t", defaults.Rendering.PreferGL),
732+ Description: "Force OpenGL over GLES",
733+ Section: SectionRendering,
734+ },
735+ {
736+ Key: "rendering.draw_compositing_indicators",
737+ Type: "bool",
738+ Default: fmt.Sprintf("%t", defaults.Rendering.DrawCompositingIndicators),
739+ Description: "Show compositing layer borders",
740+ Section: SectionRendering,
741+ },
742+ {
743+ Key: "rendering.show_fps",
744+ Type: "bool",
745+ Default: fmt.Sprintf("%t", defaults.Rendering.ShowFPS),
746+ Description: "Display WebKit FPS counter",
747+ Section: SectionRendering,
748+ },
749+ {
750+ Key: "rendering.sample_memory",
751+ Type: "bool",
752+ Default: fmt.Sprintf("%t", defaults.Rendering.SampleMemory),
753+ Description: "Enable memory sampling",
754+ Section: SectionRendering,
755+ },
756+ {
757+ Key: "rendering.debug_frames",
758+ Type: "bool",
759+ Default: fmt.Sprintf("%t", defaults.Rendering.DebugFrames),
760+ Description: "Enable frame timing debug output",
761+ Section: SectionRendering,
762+ },
763+ }
764+}
765+
766+func (*SchemaProvider) getMediaKeys(defaults *Config) []entity.ConfigKeyInfo {
767+ return []entity.ConfigKeyInfo{
768+ {
769+ Key: "media.hardware_decoding",
770+ Type: "string",
771+ Default: string(defaults.Media.HardwareDecodingMode),
772+ Description: "Hardware vs software video decoding",
773+ Values: []string{"auto", "force", "disable"},
774+ Section: SectionMedia,
775+ },
776+ {
777+ Key: "media.gl_rendering_mode",
778+ Type: "string",
779+ Default: string(defaults.Media.GLRenderingMode),
780+ Description: "OpenGL API selection for video rendering",
781+ Values: []string{"auto", "gles2", "gl3", "none"},
782+ Section: SectionMedia,
783+ },
784+ {
785+ Key: "media.prefer_av1",
786+ Type: "bool",
787+ Default: fmt.Sprintf("%t", defaults.Media.PreferAV1),
788+ Description: "Prioritize AV1 codec when available",
789+ Section: SectionMedia,
790+ },
791+ {
792+ Key: "media.show_diagnostics",
793+ Type: "bool",
794+ Default: fmt.Sprintf("%t", defaults.Media.ShowDiagnosticsOnStartup),
795+ Description: "Show media capability warnings at startup",
796+ Section: SectionMedia,
797+ },
798+ {
799+ Key: "media.force_vsync",
800+ Type: "bool",
801+ Default: fmt.Sprintf("%t", defaults.Media.ForceVSync),
802+ Description: "Force vertical sync for video playback",
803+ Section: SectionMedia,
804+ },
805+ {
806+ Key: "media.gstreamer_debug_level",
807+ Type: "int",
808+ Default: fmt.Sprintf("%d", defaults.Media.GStreamerDebugLevel),
809+ Description: "GStreamer debug verbosity (0=off, 1-5)",
810+ Range: "0-5",
811+ Section: SectionMedia,
812+ },
813+ }
814+}
815+
816+func (*SchemaProvider) getUpdateKeys(defaults *Config) []entity.ConfigKeyInfo {
817+ return []entity.ConfigKeyInfo{
818+ {
819+ Key: "update.enable_on_startup",
820+ Type: "bool",
821+ Default: fmt.Sprintf("%t", defaults.Update.EnableOnStartup),
822+ Description: "Check for updates on browser startup",
823+ Section: SectionUpdate,
824+ },
825+ {
826+ Key: "update.auto_download",
827+ Type: "bool",
828+ Default: fmt.Sprintf("%t", defaults.Update.AutoDownload),
829+ Description: "Auto-download updates in background",
830+ Section: SectionUpdate,
831+ },
832+ {
833+ Key: "update.notify_on_new_settings",
834+ Type: "bool",
835+ Default: fmt.Sprintf("%t", defaults.Update.NotifyOnNewSettings),
836+ Description: "Show toast when new config settings available",
837+ Section: SectionUpdate,
838+ },
839+ }
840+}
841+
842+func (*SchemaProvider) getDebugKeys(defaults *Config) []entity.ConfigKeyInfo {
843+ return []entity.ConfigKeyInfo{
844+ {
845+ Key: "debug.enable_devtools",
846+ Type: "bool",
847+ Default: fmt.Sprintf("%t", defaults.Debug.EnableDevTools),
848+ Description: "Enable browser developer tools (F12)",
849+ Section: SectionDebug,
850+ },
851+ }
852+}
853+
854+func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyInfo {
855+ return []entity.ConfigKeyInfo{
856+ {
857+ Key: "performance.zoom_cache_size",
858+ Type: "int",
859+ Default: fmt.Sprintf("%d", defaults.Performance.ZoomCacheSize),
860+ Description: "Number of domain zoom levels to cache",
861+ Range: ">=0",
862+ Section: SectionPerformance,
863+ },
864+ {
865+ Key: "performance.webview_pool_prewarm_count",
866+ Type: "int",
867+ Default: fmt.Sprintf("%d", defaults.Performance.WebViewPoolPrewarmCount),
868+ Description: "WebViews to pre-create at startup",
869+ Range: ">=0",
870+ Section: SectionPerformance,
871+ },
872+ }
873+}
874+
875+func (*SchemaProvider) getRuntimeKeys(_ *Config) []entity.ConfigKeyInfo {
876+ return []entity.ConfigKeyInfo{
877+ {
878+ Key: "runtime.prefix",
879+ Type: "string",
880+ Default: "(empty)",
881+ Description: "Custom runtime prefix for WebKitGTK/GTK",
882+ Section: SectionRuntime,
883+ },
884+ }
885+}
886+
887+func (*SchemaProvider) getDatabaseKeys() []entity.ConfigKeyInfo {
888+ // Get actual default path for display
889+ dbPath := "$XDG_DATA_HOME/" + appName + "/" + databaseName
890+ if path, err := GetDatabaseFile(); err == nil {
891+ dbPath = path
892+ }
893+
894+ return []entity.ConfigKeyInfo{
895+ {
896+ Key: "database.path",
897+ Type: "string",
898+ Default: dbPath,
899+ Description: "Path to SQLite database file",
900+ Section: SectionDatabase,
901+ },
902+ }
903+}
@@ -0,0 +1,903 @@
1+package config
2+
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/dumber/internal/domain/entity"
7+)
8+
9+// Section names for grouping config keys.
10+const (
11+ SectionAppearance = "Appearance"
12+ SectionLogging = "Logging"
13+ SectionHistory = "History"
14+ SectionWorkspace = "Workspace"
15+ SectionSession = "Session"
16+ SectionOmnibox = "Omnibox"
17+ SectionContentFiltering = "Content Filtering"
18+ SectionRendering = "Rendering"
19+ SectionMedia = "Media"
20+ SectionUpdate = "Update"
21+ SectionDmenu = "Dmenu"
22+ SectionDebug = "Debug"
23+ SectionPerformance = "Performance"
24+ SectionRuntime = "Runtime"
25+ SectionDatabase = "Database"
26+ SectionSearch = "Search"
27+)
28+
29+// SchemaProvider implements port.ConfigSchemaProvider.
30+type SchemaProvider struct{}
31+
32+// NewSchemaProvider creates a new SchemaProvider.
33+func NewSchemaProvider() *SchemaProvider {
34+ return &SchemaProvider{}
35+}
36+
37+// GetSchema returns all configuration keys with their metadata.
38+func (p *SchemaProvider) GetSchema() []entity.ConfigKeyInfo {
39+ defaults := DefaultConfig()
40+
41+ keys := make([]entity.ConfigKeyInfo, 0, 100)
42+
43+ // Appearance section
44+ keys = append(keys, p.getAppearanceKeys(defaults)...)
45+
46+ // Logging section
47+ keys = append(keys, p.getLoggingKeys(defaults)...)
48+
49+ // History section
50+ keys = append(keys, p.getHistoryKeys(defaults)...)
51+
52+ // Search section
53+ keys = append(keys, p.getSearchKeys(defaults)...)
54+
55+ // Dmenu section
56+ keys = append(keys, p.getDmenuKeys(defaults)...)
57+
58+ // Workspace section
59+ keys = append(keys, p.getWorkspaceKeys(defaults)...)
60+
61+ // Session section
62+ keys = append(keys, p.getSessionKeys(defaults)...)
63+
64+ // Omnibox section
65+ keys = append(keys, p.getOmniboxKeys(defaults)...)
66+
67+ // Content Filtering section
68+ keys = append(keys, p.getContentFilteringKeys(defaults)...)
69+
70+ // Rendering section
71+ keys = append(keys, p.getRenderingKeys(defaults)...)
72+
73+ // Media section
74+ keys = append(keys, p.getMediaKeys(defaults)...)
75+
76+ // Update section
77+ keys = append(keys, p.getUpdateKeys(defaults)...)
78+
79+ // Debug section
80+ keys = append(keys, p.getDebugKeys(defaults)...)
81+
82+ // Performance section
83+ keys = append(keys, p.getPerformanceKeys(defaults)...)
84+
85+ // Runtime section
86+ keys = append(keys, p.getRuntimeKeys(defaults)...)
87+
88+ // Database section
89+ keys = append(keys, p.getDatabaseKeys()...)
90+
91+ return keys
92+}
93+
94+func (*SchemaProvider) getAppearanceKeys(defaults *Config) []entity.ConfigKeyInfo {
95+ return []entity.ConfigKeyInfo{
96+ {
97+ Key: "appearance.color_scheme",
98+ Type: "string",
99+ Default: defaults.Appearance.ColorScheme,
100+ Description: "Theme preference (follows system by default)",
101+ Values: []string{"default", "prefer-dark", "prefer-light"},
102+ Section: SectionAppearance,
103+ },
104+ {
105+ Key: "appearance.default_font_size",
106+ Type: "int",
107+ Default: fmt.Sprintf("%d", defaults.Appearance.DefaultFontSize),
108+ Description: "Default font size in CSS pixels",
109+ Range: "1-72",
110+ Section: SectionAppearance,
111+ },
112+ {
113+ Key: "appearance.sans_font",
114+ Type: "string",
115+ Default: defaults.Appearance.SansFont,
116+ Description: "Sans-serif font family for web pages",
117+ Section: SectionAppearance,
118+ },
119+ {
120+ Key: "appearance.serif_font",
121+ Type: "string",
122+ Default: defaults.Appearance.SerifFont,
123+ Description: "Serif font family for web pages",
124+ Section: SectionAppearance,
125+ },
126+ {
127+ Key: "appearance.monospace_font",
128+ Type: "string",
129+ Default: defaults.Appearance.MonospaceFont,
130+ Description: "Monospace font family for code",
131+ Section: SectionAppearance,
132+ },
133+ {
134+ Key: "default_webpage_zoom",
135+ Type: "float64",
136+ Default: fmt.Sprintf("%.1f", defaults.DefaultWebpageZoom),
137+ Description: "Default zoom level for web pages (1.0 = 100%%)",
138+ Range: "0.1-5.0",
139+ Section: SectionAppearance,
140+ },
141+ {
142+ Key: "default_ui_scale",
143+ Type: "float64",
144+ Default: fmt.Sprintf("%.1f", defaults.DefaultUIScale),
145+ Description: "UI scale multiplier for GTK widgets",
146+ Range: "0.5-3.0",
147+ Section: SectionAppearance,
148+ },
149+ {
150+ Key: "appearance.light_palette.*",
151+ Type: "string",
152+ Default: "(hex colors)",
153+ Description: "Light theme color palette (background, surface, surface_variant, text, muted, accent, border)",
154+ Section: SectionAppearance,
155+ },
156+ {
157+ Key: "appearance.dark_palette.*",
158+ Type: "string",
159+ Default: "(hex colors)",
160+ Description: "Dark theme color palette (background, surface, surface_variant, text, muted, accent, border)",
161+ Section: SectionAppearance,
162+ },
163+ }
164+}
165+
166+func (*SchemaProvider) getLoggingKeys(defaults *Config) []entity.ConfigKeyInfo {
167+ return []entity.ConfigKeyInfo{
168+ {
169+ Key: "logging.level",
170+ Type: "string",
171+ Default: defaults.Logging.Level,
172+ Description: "Log verbosity level",
173+ Values: []string{"trace", "debug", "info", "warn", "error", "fatal"},
174+ Section: SectionLogging,
175+ },
176+ {
177+ Key: "logging.format",
178+ Type: "string",
179+ Default: defaults.Logging.Format,
180+ Description: "Log output format",
181+ Values: []string{"text", "json", "console"},
182+ Section: SectionLogging,
183+ },
184+ {
185+ Key: "logging.max_age",
186+ Type: "int",
187+ Default: fmt.Sprintf("%d", defaults.Logging.MaxAge),
188+ Description: "Maximum age of log files in days",
189+ Range: ">=0",
190+ Section: SectionLogging,
191+ },
192+ {
193+ Key: "logging.log_dir",
194+ Type: "string",
195+ Default: defaults.Logging.LogDir,
196+ Description: "Directory for log files",
197+ Section: SectionLogging,
198+ },
199+ {
200+ Key: "logging.enable_file_log",
201+ Type: "bool",
202+ Default: fmt.Sprintf("%t", defaults.Logging.EnableFileLog),
203+ Description: "Enable logging to file",
204+ Section: SectionLogging,
205+ },
206+ {
207+ Key: "logging.capture_console",
208+ Type: "bool",
209+ Default: fmt.Sprintf("%t", defaults.Logging.CaptureConsole),
210+ Description: "Capture browser console messages to log",
211+ Section: SectionLogging,
212+ },
213+ }
214+}
215+
216+func (*SchemaProvider) getHistoryKeys(defaults *Config) []entity.ConfigKeyInfo {
217+ return []entity.ConfigKeyInfo{
218+ {
219+ Key: "history.max_entries",
220+ Type: "int",
221+ Default: fmt.Sprintf("%d", defaults.History.MaxEntries),
222+ Description: "Maximum number of history entries to keep",
223+ Range: ">=0",
224+ Section: SectionHistory,
225+ },
226+ {
227+ Key: "history.retention_period_days",
228+ Type: "int",
229+ Default: fmt.Sprintf("%d", defaults.History.RetentionPeriodDays),
230+ Description: "Number of days to retain history",
231+ Range: ">=0",
232+ Section: SectionHistory,
233+ },
234+ {
235+ Key: "history.cleanup_interval_days",
236+ Type: "int",
237+ Default: fmt.Sprintf("%d", defaults.History.CleanupIntervalDays),
238+ Description: "Days between automatic history cleanup",
239+ Range: ">=0",
240+ Section: SectionHistory,
241+ },
242+ }
243+}
244+
245+func (*SchemaProvider) getSearchKeys(defaults *Config) []entity.ConfigKeyInfo {
246+ return []entity.ConfigKeyInfo{
247+ {
248+ Key: "default_search_engine",
249+ Type: "string",
250+ Default: defaults.DefaultSearchEngine,
251+ Description: "Default search engine URL (must contain %s placeholder)",
252+ Section: SectionSearch,
253+ },
254+ {
255+ Key: "search_shortcuts.<key>",
256+ Type: "object",
257+ Default: "(see defaults)",
258+ Description: "Search shortcuts map with url and description fields",
259+ Section: SectionSearch,
260+ },
261+ }
262+}
263+
264+func (*SchemaProvider) getDmenuKeys(defaults *Config) []entity.ConfigKeyInfo {
265+ return []entity.ConfigKeyInfo{
266+ {
267+ Key: "dmenu.max_history_items",
268+ Type: "int",
269+ Default: fmt.Sprintf("%d", defaults.Dmenu.MaxHistoryItems),
270+ Description: "Maximum history items shown in dmenu",
271+ Range: ">=0",
272+ Section: SectionDmenu,
273+ },
274+ {
275+ Key: "dmenu.show_visit_count",
276+ Type: "bool",
277+ Default: fmt.Sprintf("%t", defaults.Dmenu.ShowVisitCount),
278+ Description: "Show visit count in dmenu entries",
279+ Section: SectionDmenu,
280+ },
281+ {
282+ Key: "dmenu.show_last_visited",
283+ Type: "bool",
284+ Default: fmt.Sprintf("%t", defaults.Dmenu.ShowLastVisited),
285+ Description: "Show last visited date in dmenu entries",
286+ Section: SectionDmenu,
287+ },
288+ {
289+ Key: "dmenu.history_prefix",
290+ Type: "string",
291+ Default: defaults.Dmenu.HistoryPrefix,
292+ Description: "Prefix for history entries in dmenu",
293+ Section: SectionDmenu,
294+ },
295+ {
296+ Key: "dmenu.shortcut_prefix",
297+ Type: "string",
298+ Default: defaults.Dmenu.ShortcutPrefix,
299+ Description: "Prefix for search shortcuts in dmenu",
300+ Section: SectionDmenu,
301+ },
302+ {
303+ Key: "dmenu.url_prefix",
304+ Type: "string",
305+ Default: defaults.Dmenu.URLPrefix,
306+ Description: "Prefix for URL entries in dmenu",
307+ Section: SectionDmenu,
308+ },
309+ {
310+ Key: "dmenu.date_format",
311+ Type: "string",
312+ Default: defaults.Dmenu.DateFormat,
313+ Description: "Go time format for dates in dmenu",
314+ Section: SectionDmenu,
315+ },
316+ {
317+ Key: "dmenu.sort_by_visit_count",
318+ Type: "bool",
319+ Default: fmt.Sprintf("%t", defaults.Dmenu.SortByVisitCount),
320+ Description: "Sort dmenu entries by visit count",
321+ Section: SectionDmenu,
322+ },
323+ }
324+}
325+
326+func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo {
327+ return []entity.ConfigKeyInfo{
328+ {
329+ Key: "workspace.new_pane_url",
330+ Type: "string",
331+ Default: defaults.Workspace.NewPaneURL,
332+ Description: "URL loaded when creating a new pane",
333+ Section: SectionWorkspace,
334+ },
335+ {
336+ Key: "workspace.tab_bar_position",
337+ Type: "string",
338+ Default: defaults.Workspace.TabBarPosition,
339+ Description: "Position of the tab bar",
340+ Values: []string{"top", "bottom"},
341+ Section: SectionWorkspace,
342+ },
343+ {
344+ Key: "workspace.hide_tab_bar_when_single_tab",
345+ Type: "bool",
346+ Default: fmt.Sprintf("%t", defaults.Workspace.HideTabBarWhenSingleTab),
347+ Description: "Hide tab bar when only one tab exists",
348+ Section: SectionWorkspace,
349+ },
350+ {
351+ Key: "workspace.switch_to_tab_on_move",
352+ Type: "bool",
353+ Default: fmt.Sprintf("%t", defaults.Workspace.SwitchToTabOnMove),
354+ Description: "Switch focus to tab when moving pane to it",
355+ Section: SectionWorkspace,
356+ },
357+ // Pane mode
358+ {
359+ Key: "workspace.pane_mode.activation_shortcut",
360+ Type: "string",
361+ Default: defaults.Workspace.PaneMode.ActivationShortcut,
362+ Description: "Shortcut to enter pane mode",
363+ Section: SectionWorkspace,
364+ },
365+ {
366+ Key: "workspace.pane_mode.timeout_ms",
367+ Type: "int",
368+ Default: fmt.Sprintf("%d", defaults.Workspace.PaneMode.TimeoutMilliseconds),
369+ Description: "Pane mode timeout in milliseconds (0 = no timeout)",
370+ Range: ">=0",
371+ Section: SectionWorkspace,
372+ },
373+ {
374+ Key: "workspace.pane_mode.actions.<action>",
375+ Type: "[]string",
376+ Default: "(see defaults)",
377+ Description: "Key bindings for pane mode actions",
378+ Section: SectionWorkspace,
379+ },
380+ // Tab mode
381+ {
382+ Key: "workspace.tab_mode.activation_shortcut",
383+ Type: "string",
384+ Default: defaults.Workspace.TabMode.ActivationShortcut,
385+ Description: "Shortcut to enter tab mode",
386+ Section: SectionWorkspace,
387+ },
388+ {
389+ Key: "workspace.tab_mode.timeout_ms",
390+ Type: "int",
391+ Default: fmt.Sprintf("%d", defaults.Workspace.TabMode.TimeoutMilliseconds),
392+ Description: "Tab mode timeout in milliseconds (0 = no timeout)",
393+ Range: ">=0",
394+ Section: SectionWorkspace,
395+ },
396+ {
397+ Key: "workspace.tab_mode.actions.<action>",
398+ Type: "[]string",
399+ Default: "(see defaults)",
400+ Description: "Key bindings for tab mode actions",
401+ Section: SectionWorkspace,
402+ },
403+ // Resize mode
404+ {
405+ Key: "workspace.resize_mode.activation_shortcut",
406+ Type: "string",
407+ Default: defaults.Workspace.ResizeMode.ActivationShortcut,
408+ Description: "Shortcut to enter resize mode",
409+ Section: SectionWorkspace,
410+ },
411+ {
412+ Key: "workspace.resize_mode.timeout_ms",
413+ Type: "int",
414+ Default: fmt.Sprintf("%d", defaults.Workspace.ResizeMode.TimeoutMilliseconds),
415+ Description: "Resize mode timeout in milliseconds (0 = no timeout)",
416+ Range: ">=0",
417+ Section: SectionWorkspace,
418+ },
419+ {
420+ Key: "workspace.resize_mode.step_percent",
421+ Type: "float64",
422+ Default: fmt.Sprintf("%.1f", defaults.Workspace.ResizeMode.StepPercent),
423+ Description: "Percentage to resize panes per step",
424+ Section: SectionWorkspace,
425+ },
426+ {
427+ Key: "workspace.resize_mode.min_pane_percent",
428+ Type: "float64",
429+ Default: fmt.Sprintf("%.1f", defaults.Workspace.ResizeMode.MinPanePercent),
430+ Description: "Minimum pane size as percentage",
431+ Section: SectionWorkspace,
432+ },
433+ {
434+ Key: "workspace.resize_mode.actions.<action>",
435+ Type: "[]string",
436+ Default: "(see defaults)",
437+ Description: "Key bindings for resize mode actions",
438+ Section: SectionWorkspace,
439+ },
440+ // Global shortcuts
441+ {
442+ Key: "workspace.shortcuts.close_pane",
443+ Type: "string",
444+ Default: defaults.Workspace.Shortcuts.ClosePane,
445+ Description: "Global shortcut to close current pane",
446+ Section: SectionWorkspace,
447+ },
448+ {
449+ Key: "workspace.shortcuts.next_tab",
450+ Type: "string",
451+ Default: defaults.Workspace.Shortcuts.NextTab,
452+ Description: "Global shortcut to switch to next tab",
453+ Section: SectionWorkspace,
454+ },
455+ {
456+ Key: "workspace.shortcuts.previous_tab",
457+ Type: "string",
458+ Default: defaults.Workspace.Shortcuts.PreviousTab,
459+ Description: "Global shortcut to switch to previous tab",
460+ Section: SectionWorkspace,
461+ },
462+ // Popups
463+ {
464+ Key: "workspace.popups.behavior",
465+ Type: "string",
466+ Default: string(defaults.Workspace.Popups.Behavior),
467+ Description: "How to handle JavaScript popup windows",
468+ Values: []string{"split", "stacked", "tabbed", "windowed"},
469+ Section: SectionWorkspace,
470+ },
471+ {
472+ Key: "workspace.popups.placement",
473+ Type: "string",
474+ Default: defaults.Workspace.Popups.Placement,
475+ Description: "Placement direction for split popups",
476+ Values: []string{"right", "left", "top", "bottom"},
477+ Section: SectionWorkspace,
478+ },
479+ {
480+ Key: "workspace.popups.blank_target_behavior",
481+ Type: "string",
482+ Default: defaults.Workspace.Popups.BlankTargetBehavior,
483+ Description: "How to handle target=\"_blank\" links",
484+ Values: []string{"split", "stacked", "tabbed"},
485+ Section: SectionWorkspace,
486+ },
487+ {
488+ Key: "workspace.popups.open_in_new_pane",
489+ Type: "bool",
490+ Default: fmt.Sprintf("%t", defaults.Workspace.Popups.OpenInNewPane),
491+ Description: "Open popups in workspace instead of blocking",
492+ Section: SectionWorkspace,
493+ },
494+ {
495+ Key: "workspace.popups.follow_pane_context",
496+ Type: "bool",
497+ Default: fmt.Sprintf("%t", defaults.Workspace.Popups.FollowPaneContext),
498+ Description: "Popup placement follows parent pane context",
499+ Section: SectionWorkspace,
500+ },
501+ {
502+ Key: "workspace.popups.enable_smart_detection",
503+ Type: "bool",
504+ Default: fmt.Sprintf("%t", defaults.Workspace.Popups.EnableSmartDetection),
505+ Description: "Use window properties to detect popup intent",
506+ Section: SectionWorkspace,
507+ },
508+ {
509+ Key: "workspace.popups.oauth_auto_close",
510+ Type: "bool",
511+ Default: fmt.Sprintf("%t", defaults.Workspace.Popups.OAuthAutoClose),
512+ Description: "Auto-close OAuth popups after success",
513+ Section: SectionWorkspace,
514+ },
515+ // Styling
516+ {
517+ Key: "workspace.styling.border_width",
518+ Type: "int",
519+ Default: fmt.Sprintf("%d", defaults.Workspace.Styling.BorderWidth),
520+ Description: "Active pane border width in pixels",
521+ Range: ">=0",
522+ Section: SectionWorkspace,
523+ },
524+ {
525+ Key: "workspace.styling.border_color",
526+ Type: "string",
527+ Default: defaults.Workspace.Styling.BorderColor,
528+ Description: "Active pane border color (CSS or @theme variable)",
529+ Section: SectionWorkspace,
530+ },
531+ {
532+ Key: "workspace.styling.mode_border_width",
533+ Type: "int",
534+ Default: fmt.Sprintf("%d", defaults.Workspace.Styling.ModeBorderWidth),
535+ Description: "Border width for modal mode indicators",
536+ Range: ">=0",
537+ Section: SectionWorkspace,
538+ },
539+ {
540+ Key: "workspace.styling.pane_mode_color",
541+ Type: "string",
542+ Default: defaults.Workspace.Styling.PaneModeColor,
543+ Description: "Color for pane mode indicator",
544+ Section: SectionWorkspace,
545+ },
546+ {
547+ Key: "workspace.styling.tab_mode_color",
548+ Type: "string",
549+ Default: defaults.Workspace.Styling.TabModeColor,
550+ Description: "Color for tab mode indicator",
551+ Section: SectionWorkspace,
552+ },
553+ {
554+ Key: "workspace.styling.session_mode_color",
555+ Type: "string",
556+ Default: defaults.Workspace.Styling.SessionModeColor,
557+ Description: "Color for session mode indicator",
558+ Section: SectionWorkspace,
559+ },
560+ {
561+ Key: "workspace.styling.resize_mode_color",
562+ Type: "string",
563+ Default: defaults.Workspace.Styling.ResizeModeColor,
564+ Description: "Color for resize mode indicator",
565+ Section: SectionWorkspace,
566+ },
567+ {
568+ Key: "workspace.styling.mode_indicator_toaster_enabled",
569+ Type: "bool",
570+ Default: fmt.Sprintf("%t", defaults.Workspace.Styling.ModeIndicatorToasterEnabled),
571+ Description: "Show toaster notification for modal modes",
572+ Section: SectionWorkspace,
573+ },
574+ {
575+ Key: "workspace.styling.transition_duration",
576+ Type: "int",
577+ Default: fmt.Sprintf("%d", defaults.Workspace.Styling.TransitionDuration),
578+ Description: "Border animation duration in milliseconds",
579+ Range: ">=0",
580+ Section: SectionWorkspace,
581+ },
582+ }
583+}
584+
585+func (*SchemaProvider) getSessionKeys(defaults *Config) []entity.ConfigKeyInfo {
586+ return []entity.ConfigKeyInfo{
587+ {
588+ Key: "session.auto_restore",
589+ Type: "bool",
590+ Default: fmt.Sprintf("%t", defaults.Session.AutoRestore),
591+ Description: "Automatically restore last session on startup",
592+ Section: SectionSession,
593+ },
594+ {
595+ Key: "session.snapshot_interval_ms",
596+ Type: "int",
597+ Default: fmt.Sprintf("%d", defaults.Session.SnapshotIntervalMs),
598+ Description: "Minimum interval between session snapshots",
599+ Range: ">=0",
600+ Section: SectionSession,
601+ },
602+ {
603+ Key: "session.max_exited_sessions",
604+ Type: "int",
605+ Default: fmt.Sprintf("%d", defaults.Session.MaxExitedSessions),
606+ Description: "Maximum number of exited sessions to keep",
607+ Range: ">=0",
608+ Section: SectionSession,
609+ },
610+ {
611+ Key: "session.max_exited_session_age_days",
612+ Type: "int",
613+ Default: fmt.Sprintf("%d", defaults.Session.MaxExitedSessionAgeDays),
614+ Description: "Maximum age of exited sessions in days",
615+ Range: ">=0",
616+ Section: SectionSession,
617+ },
618+ {
619+ Key: "session.session_mode.activation_shortcut",
620+ Type: "string",
621+ Default: defaults.Session.SessionMode.ActivationShortcut,
622+ Description: "Shortcut to enter session mode",
623+ Section: SectionSession,
624+ },
625+ {
626+ Key: "session.session_mode.timeout_ms",
627+ Type: "int",
628+ Default: fmt.Sprintf("%d", defaults.Session.SessionMode.TimeoutMilliseconds),
629+ Description: "Session mode timeout in milliseconds",
630+ Range: ">=0",
631+ Section: SectionSession,
632+ },
633+ {
634+ Key: "session.session_mode.actions.<action>",
635+ Type: "[]string",
636+ Default: "(see defaults)",
637+ Description: "Key bindings for session mode actions",
638+ Section: SectionSession,
639+ },
640+ }
641+}
642+
643+func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo {
644+ return []entity.ConfigKeyInfo{
645+ {
646+ Key: "omnibox.initial_behavior",
647+ Type: "string",
648+ Default: defaults.Omnibox.InitialBehavior,
649+ Description: "What to show when omnibox opens with empty input",
650+ Values: []string{"recent", "most_visited", "none"},
651+ Section: SectionOmnibox,
652+ },
653+ {
654+ Key: "omnibox.auto_open_on_new_pane",
655+ Type: "bool",
656+ Default: fmt.Sprintf("%t", defaults.Omnibox.AutoOpenOnNewPane),
657+ Description: "Auto-open omnibox when creating new pane",
658+ Section: SectionOmnibox,
659+ },
660+ }
661+}
662+
663+func (*SchemaProvider) getContentFilteringKeys(defaults *Config) []entity.ConfigKeyInfo {
664+ return []entity.ConfigKeyInfo{
665+ {
666+ Key: "content_filtering.enabled",
667+ Type: "bool",
668+ Default: fmt.Sprintf("%t", defaults.ContentFiltering.Enabled),
669+ Description: "Enable ad blocking and content filtering",
670+ Section: SectionContentFiltering,
671+ },
672+ {
673+ Key: "content_filtering.auto_update",
674+ Type: "bool",
675+ Default: fmt.Sprintf("%t", defaults.ContentFiltering.AutoUpdate),
676+ Description: "Auto-update filter lists from GitHub",
677+ Section: SectionContentFiltering,
678+ },
679+ }
680+}
681+
682+func (*SchemaProvider) getRenderingKeys(defaults *Config) []entity.ConfigKeyInfo {
683+ return []entity.ConfigKeyInfo{
684+ {
685+ Key: "rendering.mode",
686+ Type: "string",
687+ Default: string(defaults.Rendering.Mode),
688+ Description: "GPU/CPU rendering selection for WebKit",
689+ Values: []string{"auto", "gpu", "cpu"},
690+ Section: SectionRendering,
691+ },
692+ {
693+ Key: "rendering.gsk_renderer",
694+ Type: "string",
695+ Default: string(defaults.Rendering.GSKRenderer),
696+ Description: "GTK Scene Kit renderer selection",
697+ Values: []string{"auto", "opengl", "vulkan", "cairo"},
698+ Section: SectionRendering,
699+ },
700+ {
701+ Key: "rendering.disable_dmabuf_renderer",
702+ Type: "bool",
703+ Default: fmt.Sprintf("%t", defaults.Rendering.DisableDMABufRenderer),
704+ Description: "Disable DMA-BUF accelerated renderer",
705+ Section: SectionRendering,
706+ },
707+ {
708+ Key: "rendering.force_compositing_mode",
709+ Type: "bool",
710+ Default: fmt.Sprintf("%t", defaults.Rendering.ForceCompositingMode),
711+ Description: "Force accelerated compositing",
712+ Section: SectionRendering,
713+ },
714+ {
715+ Key: "rendering.disable_compositing_mode",
716+ Type: "bool",
717+ Default: fmt.Sprintf("%t", defaults.Rendering.DisableCompositingMode),
718+ Description: "Disable accelerated compositing",
719+ Section: SectionRendering,
720+ },
721+ {
722+ Key: "rendering.disable_mipmaps",
723+ Type: "bool",
724+ Default: fmt.Sprintf("%t", defaults.Rendering.DisableMipmaps),
725+ Description: "Disable mipmap generation",
726+ Section: SectionRendering,
727+ },
728+ {
729+ Key: "rendering.prefer_gl",
730+ Type: "bool",
731+ Default: fmt.Sprintf("%t", defaults.Rendering.PreferGL),
732+ Description: "Force OpenGL over GLES",
733+ Section: SectionRendering,
734+ },
735+ {
736+ Key: "rendering.draw_compositing_indicators",
737+ Type: "bool",
738+ Default: fmt.Sprintf("%t", defaults.Rendering.DrawCompositingIndicators),
739+ Description: "Show compositing layer borders",
740+ Section: SectionRendering,
741+ },
742+ {
743+ Key: "rendering.show_fps",
744+ Type: "bool",
745+ Default: fmt.Sprintf("%t", defaults.Rendering.ShowFPS),
746+ Description: "Display WebKit FPS counter",
747+ Section: SectionRendering,
748+ },
749+ {
750+ Key: "rendering.sample_memory",
751+ Type: "bool",
752+ Default: fmt.Sprintf("%t", defaults.Rendering.SampleMemory),
753+ Description: "Enable memory sampling",
754+ Section: SectionRendering,
755+ },
756+ {
757+ Key: "rendering.debug_frames",
758+ Type: "bool",
759+ Default: fmt.Sprintf("%t", defaults.Rendering.DebugFrames),
760+ Description: "Enable frame timing debug output",
761+ Section: SectionRendering,
762+ },
763+ }
764+}
765+
766+func (*SchemaProvider) getMediaKeys(defaults *Config) []entity.ConfigKeyInfo {
767+ return []entity.ConfigKeyInfo{
768+ {
769+ Key: "media.hardware_decoding",
770+ Type: "string",
771+ Default: string(defaults.Media.HardwareDecodingMode),
772+ Description: "Hardware vs software video decoding",
773+ Values: []string{"auto", "force", "disable"},
774+ Section: SectionMedia,
775+ },
776+ {
777+ Key: "media.gl_rendering_mode",
778+ Type: "string",
779+ Default: string(defaults.Media.GLRenderingMode),
780+ Description: "OpenGL API selection for video rendering",
781+ Values: []string{"auto", "gles2", "gl3", "none"},
782+ Section: SectionMedia,
783+ },
784+ {
785+ Key: "media.prefer_av1",
786+ Type: "bool",
787+ Default: fmt.Sprintf("%t", defaults.Media.PreferAV1),
788+ Description: "Prioritize AV1 codec when available",
789+ Section: SectionMedia,
790+ },
791+ {
792+ Key: "media.show_diagnostics",
793+ Type: "bool",
794+ Default: fmt.Sprintf("%t", defaults.Media.ShowDiagnosticsOnStartup),
795+ Description: "Show media capability warnings at startup",
796+ Section: SectionMedia,
797+ },
798+ {
799+ Key: "media.force_vsync",
800+ Type: "bool",
801+ Default: fmt.Sprintf("%t", defaults.Media.ForceVSync),
802+ Description: "Force vertical sync for video playback",
803+ Section: SectionMedia,
804+ },
805+ {
806+ Key: "media.gstreamer_debug_level",
807+ Type: "int",
808+ Default: fmt.Sprintf("%d", defaults.Media.GStreamerDebugLevel),
809+ Description: "GStreamer debug verbosity (0=off, 1-5)",
810+ Range: "0-5",
811+ Section: SectionMedia,
812+ },
813+ }
814+}
815+
816+func (*SchemaProvider) getUpdateKeys(defaults *Config) []entity.ConfigKeyInfo {
817+ return []entity.ConfigKeyInfo{
818+ {
819+ Key: "update.enable_on_startup",
820+ Type: "bool",
821+ Default: fmt.Sprintf("%t", defaults.Update.EnableOnStartup),
822+ Description: "Check for updates on browser startup",
823+ Section: SectionUpdate,
824+ },
825+ {
826+ Key: "update.auto_download",
827+ Type: "bool",
828+ Default: fmt.Sprintf("%t", defaults.Update.AutoDownload),
829+ Description: "Auto-download updates in background",
830+ Section: SectionUpdate,
831+ },
832+ {
833+ Key: "update.notify_on_new_settings",
834+ Type: "bool",
835+ Default: fmt.Sprintf("%t", defaults.Update.NotifyOnNewSettings),
836+ Description: "Show toast when new config settings available",
837+ Section: SectionUpdate,
838+ },
839+ }
840+}
841+
842+func (*SchemaProvider) getDebugKeys(defaults *Config) []entity.ConfigKeyInfo {
843+ return []entity.ConfigKeyInfo{
844+ {
845+ Key: "debug.enable_devtools",
846+ Type: "bool",
847+ Default: fmt.Sprintf("%t", defaults.Debug.EnableDevTools),
848+ Description: "Enable browser developer tools (F12)",
849+ Section: SectionDebug,
850+ },
851+ }
852+}
853+
854+func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyInfo {
855+ return []entity.ConfigKeyInfo{
856+ {
857+ Key: "performance.zoom_cache_size",
858+ Type: "int",
859+ Default: fmt.Sprintf("%d", defaults.Performance.ZoomCacheSize),
860+ Description: "Number of domain zoom levels to cache",
861+ Range: ">=0",
862+ Section: SectionPerformance,
863+ },
864+ {
865+ Key: "performance.webview_pool_prewarm_count",
866+ Type: "int",
867+ Default: fmt.Sprintf("%d", defaults.Performance.WebViewPoolPrewarmCount),
868+ Description: "WebViews to pre-create at startup",
869+ Range: ">=0",
870+ Section: SectionPerformance,
871+ },
872+ }
873+}
874+
875+func (*SchemaProvider) getRuntimeKeys(_ *Config) []entity.ConfigKeyInfo {
876+ return []entity.ConfigKeyInfo{
877+ {
878+ Key: "runtime.prefix",
879+ Type: "string",
880+ Default: "(empty)",
881+ Description: "Custom runtime prefix for WebKitGTK/GTK",
882+ Section: SectionRuntime,
883+ },
884+ }
885+}
886+
887+func (*SchemaProvider) getDatabaseKeys() []entity.ConfigKeyInfo {
888+ // Get actual default path for display
889+ dbPath := "$XDG_DATA_HOME/" + appName + "/" + databaseName
890+ if path, err := GetDatabaseFile(); err == nil {
891+ dbPath = path
892+ }
893+
894+ return []entity.ConfigKeyInfo{
895+ {
896+ Key: "database.path",
897+ Type: "string",
898+ Default: dbPath,
899+ Description: "Path to SQLite database file",
900+ Section: SectionDatabase,
901+ },
902+ }
903+}

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