detached 8b8ac5d

refactor(config): move defaults to workspace and omnibox sections

commit
8b8ac5d3894e1d51ec5f248189aa42b52a073508
author
brice · · 2025-12-26 10:23 UTC
parent
9481ec4
signature
unsigned
8 files · +43 -58
M CHANGELOG.md +1 -1
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55 ## [Unreleased]
66
77 ### Added
8-- **Configurable new-pane defaults**: New `defaults.new_pane_url` (default: `about:blank`, supports `dumb://`) and `defaults.auto_open_omnibox_on_new_pane` (default: false).
8+- **Configurable new-pane defaults**: New `workspace.new_pane_url` (default: `about:blank`, supports `dumb://`) and `omnibox.auto_open_on_new_pane` (default: false).
99 - **Zellij-style pane resize mode**: New modal resize mode (`Ctrl+N`) to adjust split sizes with keyboard (`h/j/k/l` or arrow keys, `+/-` smart resize). Shows a per-pane border indicator and keeps the timeout alive while you resize.
1010 - **Mouse-driven pane resizing**: Drag split dividers with the mouse; split ratios are persisted in session snapshots (rounded to 2 decimals) and restored on resurrection.
1111 - **Dynamic window title**: Window title now displays the active pane's page title in format `<Page Title> - Dumber`. Updates when switching tabs/panes or navigating. Truncated at 255 characters.
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
5 ## [Unreleased]5 ## [Unreleased]
6 6
7 ### Added7 ### Added
8-- **Configurable new-pane defaults**: New `defaults.new_pane_url` (default: `about:blank`, supports `dumb://`) and `defaults.auto_open_omnibox_on_new_pane` (default: false).8+- **Configurable new-pane defaults**: New `workspace.new_pane_url` (default: `about:blank`, supports `dumb://`) and `omnibox.auto_open_on_new_pane` (default: false).
9 - **Zellij-style pane resize mode**: New modal resize mode (`Ctrl+N`) to adjust split sizes with keyboard (`h/j/k/l` or arrow keys, `+/-` smart resize). Shows a per-pane border indicator and keeps the timeout alive while you resize.9 - **Zellij-style pane resize mode**: New modal resize mode (`Ctrl+N`) to adjust split sizes with keyboard (`h/j/k/l` or arrow keys, `+/-` smart resize). Shows a per-pane border indicator and keeps the timeout alive while you resize.
10 - **Mouse-driven pane resizing**: Drag split dividers with the mouse; split ratios are persisted in session snapshots (rounded to 2 decimals) and restored on resurrection.10 - **Mouse-driven pane resizing**: Drag split dividers with the mouse; split ratios are persisted in session snapshots (rounded to 2 decimals) and restored on resurrection.
11 - **Dynamic window title**: Window title now displays the active pane's page title in format `<Page Title> - Dumber`. Updates when switching tabs/panes or navigating. Truncated at 255 characters.11 - **Dynamic window title**: Window title now displays the active pane's page title in format `<Page Title> - Dumber`. Updates when switching tabs/panes or navigating. Truncated at 255 characters.
M docs/CONFIG.md +14 -14
@@ -110,31 +110,19 @@ dumber dmenu --max 50
110110 | Key | Type | Default | Valid Values | Description |
111111 |-----|------|---------|--------------|-------------|
112112 | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior |
113+| `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane |
113114
114115 **Example:**
115116 ```toml
116117 [omnibox]
117118 initial_behavior = "recent" # Show recent history when omnibox opens
119+auto_open_on_new_pane = false
118120
119121 # Alternative options:
120122 # initial_behavior = "most_visited" # Show most visited sites
121123 # initial_behavior = "none" # Show no initial suggestions
122124 ```
123125
124-## Defaults
125-
126-| Key | Type | Default | Description |
127-|-----|------|---------|-------------|
128-| `defaults.new_pane_url` | string | `"about:blank"` | URL loaded for new panes/tabs (supports `http(s)://`, `dumb://`, `file://`, `about:`) |
129-| `defaults.auto_open_omnibox_on_new_pane` | bool | `false` | Automatically open the omnibox after creating a new pane |
130-
131-**Example:**
132-```toml
133-[defaults]
134-new_pane_url = "dumb://home"
135-auto_open_omnibox_on_new_pane = false
136-```
137-
138126 ## Logging
139127
140128 | Key | Type | Default | Valid Values | Description |
@@ -208,6 +196,18 @@ border = "#363636"
208196
209197 ## Workspace Configuration
210198
199+### General
200+
201+| Key | Type | Default | Description |
202+|-----|------|---------|-------------|
203+| `workspace.new_pane_url` | string | `"about:blank"` | URL loaded for new panes/tabs (supports `http(s)://`, `dumb://`, `file://`, `about:`) |
204+
205+**Example:**
206+```toml
207+[workspace]
208+new_pane_url = "dumb://home"
209+```
210+
211211 ### Pane Mode
212212
213213 | Key | Type | Default | Description |
@@ -110,31 +110,19 @@ dumber dmenu --max 50
110 | Key | Type | Default | Valid Values | Description |110 | Key | Type | Default | Valid Values | Description |
111 |-----|------|---------|--------------|-------------|111 |-----|------|---------|--------------|-------------|
112 | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior |112 | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior |
113+| `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane |
113 114
114 **Example:**115 **Example:**
115 ```toml116 ```toml
116 [omnibox]117 [omnibox]
117 initial_behavior = "recent" # Show recent history when omnibox opens118 initial_behavior = "recent" # Show recent history when omnibox opens
119+auto_open_on_new_pane = false
118 120
119 # Alternative options:121 # Alternative options:
120 # initial_behavior = "most_visited" # Show most visited sites122 # initial_behavior = "most_visited" # Show most visited sites
121 # initial_behavior = "none" # Show no initial suggestions123 # initial_behavior = "none" # Show no initial suggestions
122 ```124 ```
123 125
124-## Defaults
125-
126-| Key | Type | Default | Description |
127-|-----|------|---------|-------------|
128-| `defaults.new_pane_url` | string | `"about:blank"` | URL loaded for new panes/tabs (supports `http(s)://`, `dumb://`, `file://`, `about:`) |
129-| `defaults.auto_open_omnibox_on_new_pane` | bool | `false` | Automatically open the omnibox after creating a new pane |
130-
131-**Example:**
132-```toml
133-[defaults]
134-new_pane_url = "dumb://home"
135-auto_open_omnibox_on_new_pane = false
136-```
137-
138 ## Logging126 ## Logging
139 127
140 | Key | Type | Default | Valid Values | Description |128 | Key | Type | Default | Valid Values | Description |
@@ -208,6 +196,18 @@ border = "#363636"
208 196
209 ## Workspace Configuration197 ## Workspace Configuration
210 198
199+### General
200+
201+| Key | Type | Default | Description |
202+|-----|------|---------|-------------|
203+| `workspace.new_pane_url` | string | `"about:blank"` | URL loaded for new panes/tabs (supports `http(s)://`, `dumb://`, `file://`, `about:`) |
204+
205+**Example:**
206+```toml
207+[workspace]
208+new_pane_url = "dumb://home"
209+```
210+
211 ### Pane Mode211 ### Pane Mode
212 212
213 | Key | Type | Default | Description |213 | Key | Type | Default | Description |
M internal/infrastructure/config/defaults.go +7 -9
@@ -15,12 +15,12 @@ const (
1515 // Appearance defaults
1616 defaultFontSize = 16 // points
1717
18- // Defaults
19- defaultNewPaneURL = "about:blank"
20- defaultAutoOpenOmniboxOnNewPane = false
18+ // Workspace defaults
19+ defaultNewPaneURL = "about:blank"
2120
2221 // Omnibox defaults
23- defaultOmniboxInitialBehavior = "recent"
22+ defaultOmniboxInitialBehavior = "recent"
23+ defaultOmniboxAutoOpenOnNewPane = false
2424
2525 // Workspace defaults
2626 defaultPaneActivationShortcut = "ctrl+p"
@@ -154,11 +154,8 @@ func DefaultConfig() *Config {
154154 },
155155 DefaultWebpageZoom: 1.2, // 120% default zoom for better readability
156156 DefaultUIScale: defaultUIScale, // 1.0 = 100%, 2.0 = 200%
157- Defaults: DefaultsConfig{
158- NewPaneURL: defaultNewPaneURL,
159- AutoOpenOmniboxOnNewPane: defaultAutoOpenOmniboxOnNewPane,
160- },
161157 Workspace: WorkspaceConfig{
158+ NewPaneURL: defaultNewPaneURL,
162159 PaneMode: PaneModeConfig{
163160 ActivationShortcut: defaultPaneActivationShortcut,
164161 TimeoutMilliseconds: defaultPaneTimeoutMilliseconds,
@@ -245,7 +242,8 @@ func DefaultConfig() *Config {
245242 AutoUpdate: true, // Auto-update filters from GitHub releases
246243 },
247244 Omnibox: OmniboxConfig{
248- InitialBehavior: defaultOmniboxInitialBehavior,
245+ InitialBehavior: defaultOmniboxInitialBehavior,
246+ AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane,
249247 },
250248 Session: SessionConfig{
251249 AutoRestore: false,
@@ -15,12 +15,12 @@ const (
15 // Appearance defaults15 // Appearance defaults
16 defaultFontSize = 16 // points16 defaultFontSize = 16 // points
17 17
18- // Defaults18+ // Workspace defaults
19- defaultNewPaneURL = "about:blank"19+ defaultNewPaneURL = "about:blank"
20- defaultAutoOpenOmniboxOnNewPane = false
21 20
22 // Omnibox defaults21 // Omnibox defaults
23- defaultOmniboxInitialBehavior = "recent"22+ defaultOmniboxInitialBehavior = "recent"
23+ defaultOmniboxAutoOpenOnNewPane = false
24 24
25 // Workspace defaults25 // Workspace defaults
26 defaultPaneActivationShortcut = "ctrl+p"26 defaultPaneActivationShortcut = "ctrl+p"
@@ -154,11 +154,8 @@ func DefaultConfig() *Config {
154 },154 },
155 DefaultWebpageZoom: 1.2, // 120% default zoom for better readability155 DefaultWebpageZoom: 1.2, // 120% default zoom for better readability
156 DefaultUIScale: defaultUIScale, // 1.0 = 100%, 2.0 = 200%156 DefaultUIScale: defaultUIScale, // 1.0 = 100%, 2.0 = 200%
157- Defaults: DefaultsConfig{
158- NewPaneURL: defaultNewPaneURL,
159- AutoOpenOmniboxOnNewPane: defaultAutoOpenOmniboxOnNewPane,
160- },
161 Workspace: WorkspaceConfig{157 Workspace: WorkspaceConfig{
158+ NewPaneURL: defaultNewPaneURL,
162 PaneMode: PaneModeConfig{159 PaneMode: PaneModeConfig{
163 ActivationShortcut: defaultPaneActivationShortcut,160 ActivationShortcut: defaultPaneActivationShortcut,
164 TimeoutMilliseconds: defaultPaneTimeoutMilliseconds,161 TimeoutMilliseconds: defaultPaneTimeoutMilliseconds,
@@ -245,7 +242,8 @@ func DefaultConfig() *Config {
245 AutoUpdate: true, // Auto-update filters from GitHub releases242 AutoUpdate: true, // Auto-update filters from GitHub releases
246 },243 },
247 Omnibox: OmniboxConfig{244 Omnibox: OmniboxConfig{
248- InitialBehavior: defaultOmniboxInitialBehavior,245+ InitialBehavior: defaultOmniboxInitialBehavior,
246+ AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane,
249 },247 },
250 Session: SessionConfig{248 Session: SessionConfig{
251 AutoRestore: false,249 AutoRestore: false,
M internal/infrastructure/config/loader.go +2 -6
@@ -309,7 +309,6 @@ func (m *Manager) setDefaults() {
309309 m.setDebugDefaults(defaults)
310310 m.setAppearanceDefaults(defaults)
311311 m.setRenderingDefaults(defaults)
312- m.setDefaultsDefaults(defaults)
313312 m.setWorkspaceDefaults(defaults)
314313 m.setContentFilteringDefaults(defaults)
315314 m.setOmniboxDefaults(defaults)
@@ -380,12 +379,8 @@ func (m *Manager) setRenderingDefaults(defaults *Config) {
380379 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
381380 }
382381
383-func (m *Manager) setDefaultsDefaults(defaults *Config) {
384- m.viper.SetDefault("defaults.new_pane_url", defaults.Defaults.NewPaneURL)
385- m.viper.SetDefault("defaults.auto_open_omnibox_on_new_pane", defaults.Defaults.AutoOpenOmniboxOnNewPane)
386-}
387-
388382 func (m *Manager) setWorkspaceDefaults(defaults *Config) {
383+ m.viper.SetDefault("workspace.new_pane_url", defaults.Workspace.NewPaneURL)
389384 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
390385 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
391386 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
@@ -429,6 +424,7 @@ func (m *Manager) setContentFilteringDefaults(defaults *Config) {
429424
430425 func (m *Manager) setOmniboxDefaults(defaults *Config) {
431426 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
427+ m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
432428 }
433429
434430 func (m *Manager) setMediaDefaults(defaults *Config) {
@@ -309,7 +309,6 @@ func (m *Manager) setDefaults() {
309 m.setDebugDefaults(defaults)309 m.setDebugDefaults(defaults)
310 m.setAppearanceDefaults(defaults)310 m.setAppearanceDefaults(defaults)
311 m.setRenderingDefaults(defaults)311 m.setRenderingDefaults(defaults)
312- m.setDefaultsDefaults(defaults)
313 m.setWorkspaceDefaults(defaults)312 m.setWorkspaceDefaults(defaults)
314 m.setContentFilteringDefaults(defaults)313 m.setContentFilteringDefaults(defaults)
315 m.setOmniboxDefaults(defaults)314 m.setOmniboxDefaults(defaults)
@@ -380,12 +379,8 @@ func (m *Manager) setRenderingDefaults(defaults *Config) {
380 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)379 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
381 }380 }
382 381
383-func (m *Manager) setDefaultsDefaults(defaults *Config) {
384- m.viper.SetDefault("defaults.new_pane_url", defaults.Defaults.NewPaneURL)
385- m.viper.SetDefault("defaults.auto_open_omnibox_on_new_pane", defaults.Defaults.AutoOpenOmniboxOnNewPane)
386-}
387-
388 func (m *Manager) setWorkspaceDefaults(defaults *Config) {382 func (m *Manager) setWorkspaceDefaults(defaults *Config) {
383+ m.viper.SetDefault("workspace.new_pane_url", defaults.Workspace.NewPaneURL)
389 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)384 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
390 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)385 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
391 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)386 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
@@ -429,6 +424,7 @@ func (m *Manager) setContentFilteringDefaults(defaults *Config) {
429 424
430 func (m *Manager) setOmniboxDefaults(defaults *Config) {425 func (m *Manager) setOmniboxDefaults(defaults *Config) {
431 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)426 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
427+ m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
432 }428 }
433 429
434 func (m *Manager) setMediaDefaults(defaults *Config) {430 func (m *Manager) setMediaDefaults(defaults *Config) {
M internal/infrastructure/config/schema.go +6 -15
@@ -15,8 +15,6 @@ type Config struct {
1515 DefaultWebpageZoom float64 `mapstructure:"default_webpage_zoom" yaml:"default_webpage_zoom" toml:"default_webpage_zoom"`
1616 // DefaultUIScale sets the default UI scale for GTK widgets (1.0 = 100%, 2.0 = 200%)
1717 DefaultUIScale float64 `mapstructure:"default_ui_scale" yaml:"default_ui_scale" toml:"default_ui_scale"`
18- // Defaults controls default behavior like new-pane URL.
19- Defaults DefaultsConfig `mapstructure:"defaults" yaml:"defaults" toml:"defaults"`
2018 // Workspace defines workspace, pane, and tab handling behavior.
2119 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace" toml:"workspace"`
2220 // Session controls session persistence and restoration.
@@ -37,19 +35,6 @@ type Config struct {
3735 Update UpdateConfig `mapstructure:"update" yaml:"update" toml:"update"`
3836 }
3937
40-// DefaultsConfig controls default behavior like new-pane URL.
41-//
42-// These defaults are applied when a new pane/tab is created without an explicit URL.
43-// This is not currently exposed in dumb://config UI.
44-type DefaultsConfig struct {
45- // NewPaneURL is the URL loaded when creating a new empty pane.
46- // Default: "about:blank"
47- NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url"`
48- // AutoOpenOmniboxOnNewPane opens the omnibox automatically when a new pane is created.
49- // Default: false
50- AutoOpenOmniboxOnNewPane bool `mapstructure:"auto_open_omnibox_on_new_pane" yaml:"auto_open_omnibox_on_new_pane" toml:"auto_open_omnibox_on_new_pane"` //nolint:lll // struct tags must stay on one line
51-}
52-
5338 // RenderingMode selects GPU vs CPU rendering.
5439 type RenderingMode string
5540
@@ -289,6 +274,9 @@ type OmniboxConfig struct {
289274 // InitialBehavior controls what to show when omnibox opens with empty input
290275 // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing)
291276 InitialBehavior string `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"`
277+ // AutoOpenOnNewPane opens the omnibox automatically when a new pane is created.
278+ // Default: false
279+ AutoOpenOnNewPane bool `mapstructure:"auto_open_on_new_pane" yaml:"auto_open_on_new_pane" toml:"auto_open_on_new_pane"`
292280 }
293281
294282 // DebugConfig holds debug and troubleshooting options
@@ -310,6 +298,9 @@ type UpdateConfig struct {
310298
311299 // WorkspaceConfig captures layout, pane, and tab behavior preferences.
312300 type WorkspaceConfig struct {
301+ // NewPaneURL is the URL loaded when creating a new empty pane/tab.
302+ // Default: "about:blank"
303+ NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url"`
313304 // PaneMode defines modal pane behavior and bindings.
314305 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"`
315306 // TabMode defines modal tab behavior and bindings (Alt+T).
@@ -15,8 +15,6 @@ type Config struct {
15 DefaultWebpageZoom float64 `mapstructure:"default_webpage_zoom" yaml:"default_webpage_zoom" toml:"default_webpage_zoom"`15 DefaultWebpageZoom float64 `mapstructure:"default_webpage_zoom" yaml:"default_webpage_zoom" toml:"default_webpage_zoom"`
16 // DefaultUIScale sets the default UI scale for GTK widgets (1.0 = 100%, 2.0 = 200%)16 // DefaultUIScale sets the default UI scale for GTK widgets (1.0 = 100%, 2.0 = 200%)
17 DefaultUIScale float64 `mapstructure:"default_ui_scale" yaml:"default_ui_scale" toml:"default_ui_scale"`17 DefaultUIScale float64 `mapstructure:"default_ui_scale" yaml:"default_ui_scale" toml:"default_ui_scale"`
18- // Defaults controls default behavior like new-pane URL.
19- Defaults DefaultsConfig `mapstructure:"defaults" yaml:"defaults" toml:"defaults"`
20 // Workspace defines workspace, pane, and tab handling behavior.18 // Workspace defines workspace, pane, and tab handling behavior.
21 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace" toml:"workspace"`19 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace" toml:"workspace"`
22 // Session controls session persistence and restoration.20 // Session controls session persistence and restoration.
@@ -37,19 +35,6 @@ type Config struct {
37 Update UpdateConfig `mapstructure:"update" yaml:"update" toml:"update"`35 Update UpdateConfig `mapstructure:"update" yaml:"update" toml:"update"`
38 }36 }
39 37
40-// DefaultsConfig controls default behavior like new-pane URL.
41-//
42-// These defaults are applied when a new pane/tab is created without an explicit URL.
43-// This is not currently exposed in dumb://config UI.
44-type DefaultsConfig struct {
45- // NewPaneURL is the URL loaded when creating a new empty pane.
46- // Default: "about:blank"
47- NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url"`
48- // AutoOpenOmniboxOnNewPane opens the omnibox automatically when a new pane is created.
49- // Default: false
50- AutoOpenOmniboxOnNewPane bool `mapstructure:"auto_open_omnibox_on_new_pane" yaml:"auto_open_omnibox_on_new_pane" toml:"auto_open_omnibox_on_new_pane"` //nolint:lll // struct tags must stay on one line
51-}
52-
53 // RenderingMode selects GPU vs CPU rendering.38 // RenderingMode selects GPU vs CPU rendering.
54 type RenderingMode string39 type RenderingMode string
55 40
@@ -289,6 +274,9 @@ type OmniboxConfig struct {
289 // InitialBehavior controls what to show when omnibox opens with empty input274 // InitialBehavior controls what to show when omnibox opens with empty input
290 // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing)275 // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing)
291 InitialBehavior string `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"`276 InitialBehavior string `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"`
277+ // AutoOpenOnNewPane opens the omnibox automatically when a new pane is created.
278+ // Default: false
279+ AutoOpenOnNewPane bool `mapstructure:"auto_open_on_new_pane" yaml:"auto_open_on_new_pane" toml:"auto_open_on_new_pane"`
292 }280 }
293 281
294 // DebugConfig holds debug and troubleshooting options282 // DebugConfig holds debug and troubleshooting options
@@ -310,6 +298,9 @@ type UpdateConfig struct {
310 298
311 // WorkspaceConfig captures layout, pane, and tab behavior preferences.299 // WorkspaceConfig captures layout, pane, and tab behavior preferences.
312 type WorkspaceConfig struct {300 type WorkspaceConfig struct {
301+ // NewPaneURL is the URL loaded when creating a new empty pane/tab.
302+ // Default: "about:blank"
303+ NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url"`
313 // PaneMode defines modal pane behavior and bindings.304 // PaneMode defines modal pane behavior and bindings.
314 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"`305 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"`
315 // TabMode defines modal tab behavior and bindings (Alt+T).306 // TabMode defines modal tab behavior and bindings (Alt+T).
M internal/infrastructure/config/validation.go +8 -8
@@ -23,7 +23,7 @@ func validateConfig(config *Config) error {
2323 validationErrors = append(validationErrors, validateTabBar(config)...)
2424 validationErrors = append(validationErrors, validateTabMode(config)...)
2525 validationErrors = append(validationErrors, validateLogging(config)...)
26- validationErrors = append(validationErrors, validateDefaults(config)...)
26+ validationErrors = append(validationErrors, validateWorkspaceNewPaneURL(config)...)
2727 validationErrors = append(validationErrors, validateOmnibox(config)...)
2828 validationErrors = append(validationErrors, validateRendering(config)...)
2929 validationErrors = append(validationErrors, validateColorScheme(config)...)
@@ -249,20 +249,20 @@ func validateLogging(config *Config) []string {
249249 return validationErrors
250250 }
251251
252-func validateDefaults(config *Config) []string {
252+func validateWorkspaceNewPaneURL(config *Config) []string {
253253 var validationErrors []string
254254
255- if config.Defaults.NewPaneURL == "" {
256- validationErrors = append(validationErrors, "defaults.new_pane_url cannot be empty")
255+ if config.Workspace.NewPaneURL == "" {
256+ validationErrors = append(validationErrors, "workspace.new_pane_url cannot be empty")
257257 return validationErrors
258258 }
259259
260- normalized := domainurl.Normalize(config.Defaults.NewPaneURL)
260+ normalized := domainurl.Normalize(config.Workspace.NewPaneURL)
261261 parsed, err := url.Parse(normalized)
262262 if err != nil {
263263 validationErrors = append(validationErrors, fmt.Sprintf(
264- "defaults.new_pane_url must be a valid URL (got: %s)",
265- config.Defaults.NewPaneURL,
264+ "workspace.new_pane_url must be a valid URL (got: %s)",
265+ config.Workspace.NewPaneURL,
266266 ))
267267 return validationErrors
268268 }
@@ -272,7 +272,7 @@ func validateDefaults(config *Config) []string {
272272 // ok
273273 default:
274274 validationErrors = append(validationErrors, fmt.Sprintf(
275- "defaults.new_pane_url must use one of: http, https, dumb, file, about (got: %s)",
275+ "workspace.new_pane_url must use one of: http, https, dumb, file, about (got: %s)",
276276 parsed.Scheme,
277277 ))
278278 }
@@ -23,7 +23,7 @@ func validateConfig(config *Config) error {
23 validationErrors = append(validationErrors, validateTabBar(config)...)23 validationErrors = append(validationErrors, validateTabBar(config)...)
24 validationErrors = append(validationErrors, validateTabMode(config)...)24 validationErrors = append(validationErrors, validateTabMode(config)...)
25 validationErrors = append(validationErrors, validateLogging(config)...)25 validationErrors = append(validationErrors, validateLogging(config)...)
26- validationErrors = append(validationErrors, validateDefaults(config)...)26+ validationErrors = append(validationErrors, validateWorkspaceNewPaneURL(config)...)
27 validationErrors = append(validationErrors, validateOmnibox(config)...)27 validationErrors = append(validationErrors, validateOmnibox(config)...)
28 validationErrors = append(validationErrors, validateRendering(config)...)28 validationErrors = append(validationErrors, validateRendering(config)...)
29 validationErrors = append(validationErrors, validateColorScheme(config)...)29 validationErrors = append(validationErrors, validateColorScheme(config)...)
@@ -249,20 +249,20 @@ func validateLogging(config *Config) []string {
249 return validationErrors249 return validationErrors
250 }250 }
251 251
252-func validateDefaults(config *Config) []string {252+func validateWorkspaceNewPaneURL(config *Config) []string {
253 var validationErrors []string253 var validationErrors []string
254 254
255- if config.Defaults.NewPaneURL == "" {255+ if config.Workspace.NewPaneURL == "" {
256- validationErrors = append(validationErrors, "defaults.new_pane_url cannot be empty")256+ validationErrors = append(validationErrors, "workspace.new_pane_url cannot be empty")
257 return validationErrors257 return validationErrors
258 }258 }
259 259
260- normalized := domainurl.Normalize(config.Defaults.NewPaneURL)260+ normalized := domainurl.Normalize(config.Workspace.NewPaneURL)
261 parsed, err := url.Parse(normalized)261 parsed, err := url.Parse(normalized)
262 if err != nil {262 if err != nil {
263 validationErrors = append(validationErrors, fmt.Sprintf(263 validationErrors = append(validationErrors, fmt.Sprintf(
264- "defaults.new_pane_url must be a valid URL (got: %s)",264+ "workspace.new_pane_url must be a valid URL (got: %s)",
265- config.Defaults.NewPaneURL,265+ config.Workspace.NewPaneURL,
266 ))266 ))
267 return validationErrors267 return validationErrors
268 }268 }
@@ -272,7 +272,7 @@ func validateDefaults(config *Config) []string {
272 // ok272 // ok
273 default:273 default:
274 validationErrors = append(validationErrors, fmt.Sprintf(274 validationErrors = append(validationErrors, fmt.Sprintf(
275- "defaults.new_pane_url must use one of: http, https, dumb, file, about (got: %s)",275+ "workspace.new_pane_url must use one of: http, https, dumb, file, about (got: %s)",
276 parsed.Scheme,276 parsed.Scheme,
277 ))277 ))
278 }278 }
M internal/ui/coordinator/workspace.go +4 -4
@@ -127,7 +127,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
127127 Workspace: splitCtx.ws,
128128 TargetPane: splitCtx.activePane,
129129 Direction: direction,
130- InitialURL: domainurl.Normalize(config.Get().Defaults.NewPaneURL),
130+ InitialURL: domainurl.Normalize(config.Get().Workspace.NewPaneURL),
131131 })
132132 if err != nil {
133133 log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane")
@@ -145,7 +145,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
145145 c.applySplitToView(ctx, splitCtx.wsView, splitCtx.ws, output, direction, splitCtx.existingWidget, splitCtx.isStackSplit, oldActivePaneID)
146146 }
147147
148- if splitCtx.wsView != nil && config.Get().Defaults.AutoOpenOmniboxOnNewPane {
148+ if splitCtx.wsView != nil && config.Get().Omnibox.AutoOpenOnNewPane {
149149 splitCtx.wsView.ShowOmnibox(ctx, "")
150150 }
151151
@@ -1222,7 +1222,7 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
12221222
12231223 newPaneID := entity.PaneID(c.generateID())
12241224 newPane := entity.NewPane(newPaneID)
1225- newPane.URI = domainurl.Normalize(config.Get().Defaults.NewPaneURL)
1225+ newPane.URI = domainurl.Normalize(config.Get().Workspace.NewPaneURL)
12261226 newPane.Title = defaultPaneTitle
12271227
12281228 stackNode, needsFirstPaneTitleUpdate := c.ensureStackNode(ctx, stackCtx.activeNode, stackCtx.activePaneID, stackCtx.originalTitle)
@@ -1273,7 +1273,7 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
12731273 log.Warn().Err(err).Msg("failed to set active pane")
12741274 }
12751275
1276- if config.Get().Defaults.AutoOpenOmniboxOnNewPane {
1276+ if config.Get().Omnibox.AutoOpenOnNewPane {
12771277 stackCtx.wsView.ShowOmnibox(ctx, "")
12781278 }
12791279
@@ -127,7 +127,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
127 Workspace: splitCtx.ws,127 Workspace: splitCtx.ws,
128 TargetPane: splitCtx.activePane,128 TargetPane: splitCtx.activePane,
129 Direction: direction,129 Direction: direction,
130- InitialURL: domainurl.Normalize(config.Get().Defaults.NewPaneURL),130+ InitialURL: domainurl.Normalize(config.Get().Workspace.NewPaneURL),
131 })131 })
132 if err != nil {132 if err != nil {
133 log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane")133 log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane")
@@ -145,7 +145,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
145 c.applySplitToView(ctx, splitCtx.wsView, splitCtx.ws, output, direction, splitCtx.existingWidget, splitCtx.isStackSplit, oldActivePaneID)145 c.applySplitToView(ctx, splitCtx.wsView, splitCtx.ws, output, direction, splitCtx.existingWidget, splitCtx.isStackSplit, oldActivePaneID)
146 }146 }
147 147
148- if splitCtx.wsView != nil && config.Get().Defaults.AutoOpenOmniboxOnNewPane {148+ if splitCtx.wsView != nil && config.Get().Omnibox.AutoOpenOnNewPane {
149 splitCtx.wsView.ShowOmnibox(ctx, "")149 splitCtx.wsView.ShowOmnibox(ctx, "")
150 }150 }
151 151
@@ -1222,7 +1222,7 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
1222 1222
1223 newPaneID := entity.PaneID(c.generateID())1223 newPaneID := entity.PaneID(c.generateID())
1224 newPane := entity.NewPane(newPaneID)1224 newPane := entity.NewPane(newPaneID)
1225- newPane.URI = domainurl.Normalize(config.Get().Defaults.NewPaneURL)1225+ newPane.URI = domainurl.Normalize(config.Get().Workspace.NewPaneURL)
1226 newPane.Title = defaultPaneTitle1226 newPane.Title = defaultPaneTitle
1227 1227
1228 stackNode, needsFirstPaneTitleUpdate := c.ensureStackNode(ctx, stackCtx.activeNode, stackCtx.activePaneID, stackCtx.originalTitle)1228 stackNode, needsFirstPaneTitleUpdate := c.ensureStackNode(ctx, stackCtx.activeNode, stackCtx.activePaneID, stackCtx.originalTitle)
@@ -1273,7 +1273,7 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
1273 log.Warn().Err(err).Msg("failed to set active pane")1273 log.Warn().Err(err).Msg("failed to set active pane")
1274 }1274 }
1275 1275
1276- if config.Get().Defaults.AutoOpenOmniboxOnNewPane {1276+ if config.Get().Omnibox.AutoOpenOnNewPane {
1277 stackCtx.wsView.ShowOmnibox(ctx, "")1277 stackCtx.wsView.ShowOmnibox(ctx, "")
1278 }1278 }
1279 1279
M internal/ui/dispatcher/keyboard.go +1 -1
@@ -101,7 +101,7 @@ func (d *KeyboardDispatcher) initActionHandlers() {
101101 // Tab actions
102102 input.ActionNewTab: func(ctx context.Context) error {
103103 cfg := config.Get()
104- _, err := d.tabCoord.Create(ctx, domainurl.Normalize(cfg.Defaults.NewPaneURL))
104+ _, err := d.tabCoord.Create(ctx, domainurl.Normalize(cfg.Workspace.NewPaneURL))
105105 return err
106106 },
107107 input.ActionCloseTab: d.tabCoord.Close,
@@ -101,7 +101,7 @@ func (d *KeyboardDispatcher) initActionHandlers() {
101 // Tab actions101 // Tab actions
102 input.ActionNewTab: func(ctx context.Context) error {102 input.ActionNewTab: func(ctx context.Context) error {
103 cfg := config.Get()103 cfg := config.Get()
104- _, err := d.tabCoord.Create(ctx, domainurl.Normalize(cfg.Defaults.NewPaneURL))104+ _, err := d.tabCoord.Create(ctx, domainurl.Normalize(cfg.Workspace.NewPaneURL))
105 return err105 return err
106 },106 },
107 input.ActionCloseTab: d.tabCoord.Close,107 input.ActionCloseTab: d.tabCoord.Close,

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