detached 6a2e839

feat(config): add defaults for new panes

commit
6a2e839161ccf19fe51caa4387e4c7706ad2a24c
author
brice · · 2025-12-26 09:01 UTC
parent
f1c9159
signature
unsigned
9 files · +101 -6
M CHANGELOG.md +1 -0
@@ -5,6 +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).
89 - **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.
910 - **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.
1011 - **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,6 +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 - **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.
9 - **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.
10 - **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 -0
@@ -121,6 +121,20 @@ initial_behavior = "recent" # Show recent history when omnibox opens
121121 # initial_behavior = "none" # Show no initial suggestions
122122 ```
123123
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+
124138 ## Logging
125139
126140 | Key | Type | Default | Valid Values | Description |
@@ -121,6 +121,20 @@ initial_behavior = "recent" # Show recent history when omnibox opens
121 # initial_behavior = "none" # Show no initial suggestions121 # initial_behavior = "none" # Show no initial suggestions
122 ```122 ```
123 123
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+
124 ## Logging138 ## Logging
125 139
126 | Key | Type | Default | Valid Values | Description |140 | Key | Type | Default | Valid Values | Description |
M internal/application/usecase/manage_panes.go +2 -1
@@ -8,6 +8,7 @@ import (
88 "sort"
99
1010 "github.com/bnema/dumber/internal/domain/entity"
11+ domainurl "github.com/bnema/dumber/internal/domain/url"
1112 "github.com/bnema/dumber/internal/logging"
1213 )
1314
@@ -113,7 +114,7 @@ func (uc *ManagePanesUseCase) Split(ctx context.Context, input SplitPaneInput) (
113114 paneID := entity.PaneID(uc.idGenerator())
114115 newPane = entity.NewPane(paneID)
115116 if input.InitialURL != "" {
116- newPane.URI = input.InitialURL
117+ newPane.URI = domainurl.Normalize(input.InitialURL)
117118 } else {
118119 newPane.URI = "about:blank"
119120 }
@@ -8,6 +8,7 @@ import (
8 "sort"8 "sort"
9 9
10 "github.com/bnema/dumber/internal/domain/entity"10 "github.com/bnema/dumber/internal/domain/entity"
11+ domainurl "github.com/bnema/dumber/internal/domain/url"
11 "github.com/bnema/dumber/internal/logging"12 "github.com/bnema/dumber/internal/logging"
12 )13 )
13 14
@@ -113,7 +114,7 @@ func (uc *ManagePanesUseCase) Split(ctx context.Context, input SplitPaneInput) (
113 paneID := entity.PaneID(uc.idGenerator())114 paneID := entity.PaneID(uc.idGenerator())
114 newPane = entity.NewPane(paneID)115 newPane = entity.NewPane(paneID)
115 if input.InitialURL != "" {116 if input.InitialURL != "" {
116- newPane.URI = input.InitialURL117+ newPane.URI = domainurl.Normalize(input.InitialURL)
117 } else {118 } else {
118 newPane.URI = "about:blank"119 newPane.URI = "about:blank"
119 }120 }
M internal/infrastructure/config/defaults.go +8 -0
@@ -15,6 +15,10 @@ const (
1515 // Appearance defaults
1616 defaultFontSize = 16 // points
1717
18+ // Defaults
19+ defaultNewPaneURL = "about:blank"
20+ defaultAutoOpenOmniboxOnNewPane = false
21+
1822 // Omnibox defaults
1923 defaultOmniboxInitialBehavior = "recent"
2024
@@ -150,6 +154,10 @@ func DefaultConfig() *Config {
150154 },
151155 DefaultWebpageZoom: 1.2, // 120% default zoom for better readability
152156 DefaultUIScale: defaultUIScale, // 1.0 = 100%, 2.0 = 200%
157+ Defaults: DefaultsConfig{
158+ NewPaneURL: defaultNewPaneURL,
159+ AutoOpenOmniboxOnNewPane: defaultAutoOpenOmniboxOnNewPane,
160+ },
153161 Workspace: WorkspaceConfig{
154162 PaneMode: PaneModeConfig{
155163 ActivationShortcut: defaultPaneActivationShortcut,
@@ -15,6 +15,10 @@ const (
15 // Appearance defaults15 // Appearance defaults
16 defaultFontSize = 16 // points16 defaultFontSize = 16 // points
17 17
18+ // Defaults
19+ defaultNewPaneURL = "about:blank"
20+ defaultAutoOpenOmniboxOnNewPane = false
21+
18 // Omnibox defaults22 // Omnibox defaults
19 defaultOmniboxInitialBehavior = "recent"23 defaultOmniboxInitialBehavior = "recent"
20 24
@@ -150,6 +154,10 @@ func DefaultConfig() *Config {
150 },154 },
151 DefaultWebpageZoom: 1.2, // 120% default zoom for better readability155 DefaultWebpageZoom: 1.2, // 120% default zoom for better readability
152 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+ },
153 Workspace: WorkspaceConfig{161 Workspace: WorkspaceConfig{
154 PaneMode: PaneModeConfig{162 PaneMode: PaneModeConfig{
155 ActivationShortcut: defaultPaneActivationShortcut,163 ActivationShortcut: defaultPaneActivationShortcut,
M internal/infrastructure/config/loader.go +6 -0
@@ -309,6 +309,7 @@ func (m *Manager) setDefaults() {
309309 m.setDebugDefaults(defaults)
310310 m.setAppearanceDefaults(defaults)
311311 m.setRenderingDefaults(defaults)
312+ m.setDefaultsDefaults(defaults)
312313 m.setWorkspaceDefaults(defaults)
313314 m.setContentFilteringDefaults(defaults)
314315 m.setOmniboxDefaults(defaults)
@@ -379,6 +380,11 @@ func (m *Manager) setRenderingDefaults(defaults *Config) {
379380 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
380381 }
381382
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+
382388 func (m *Manager) setWorkspaceDefaults(defaults *Config) {
383389 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
384390 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
@@ -309,6 +309,7 @@ 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)
312 m.setWorkspaceDefaults(defaults)313 m.setWorkspaceDefaults(defaults)
313 m.setContentFilteringDefaults(defaults)314 m.setContentFilteringDefaults(defaults)
314 m.setOmniboxDefaults(defaults)315 m.setOmniboxDefaults(defaults)
@@ -379,6 +380,11 @@ func (m *Manager) setRenderingDefaults(defaults *Config) {
379 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)380 m.viper.SetDefault("default_ui_scale", defaults.DefaultUIScale)
380 }381 }
381 382
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+
382 func (m *Manager) setWorkspaceDefaults(defaults *Config) {388 func (m *Manager) setWorkspaceDefaults(defaults *Config) {
383 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)389 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
384 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)390 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
M internal/infrastructure/config/schema.go +15 -0
@@ -15,6 +15,8 @@ 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"`
1820 // Workspace defines workspace, pane, and tab handling behavior.
1921 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace" toml:"workspace"`
2022 // Session controls session persistence and restoration.
@@ -35,6 +37,19 @@ type Config struct {
3537 Update UpdateConfig `mapstructure:"update" yaml:"update" toml:"update"`
3638 }
3739
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+
3853 // RenderingMode selects GPU vs CPU rendering.
3954 type RenderingMode string
4055
@@ -15,6 +15,8 @@ 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"`
18 // Workspace defines workspace, pane, and tab handling behavior.20 // Workspace defines workspace, pane, and tab handling behavior.
19 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace" toml:"workspace"`21 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace" toml:"workspace"`
20 // Session controls session persistence and restoration.22 // Session controls session persistence and restoration.
@@ -35,6 +37,19 @@ type Config struct {
35 Update UpdateConfig `mapstructure:"update" yaml:"update" toml:"update"`37 Update UpdateConfig `mapstructure:"update" yaml:"update" toml:"update"`
36 }38 }
37 39
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+
38 // RenderingMode selects GPU vs CPU rendering.53 // RenderingMode selects GPU vs CPU rendering.
39 type RenderingMode string54 type RenderingMode string
40 55
M internal/infrastructure/config/validation.go +34 -0
@@ -2,8 +2,10 @@ package config
22
33 import (
44 "fmt"
5+ "net/url"
56 "strings"
67
8+ domainurl "github.com/bnema/dumber/internal/domain/url"
79 domainvalidation "github.com/bnema/dumber/internal/domain/validation"
810 )
911
@@ -21,6 +23,7 @@ func validateConfig(config *Config) error {
2123 validationErrors = append(validationErrors, validateTabBar(config)...)
2224 validationErrors = append(validationErrors, validateTabMode(config)...)
2325 validationErrors = append(validationErrors, validateLogging(config)...)
26+ validationErrors = append(validationErrors, validateDefaults(config)...)
2427 validationErrors = append(validationErrors, validateOmnibox(config)...)
2528 validationErrors = append(validationErrors, validateRendering(config)...)
2629 validationErrors = append(validationErrors, validateColorScheme(config)...)
@@ -246,6 +249,37 @@ func validateLogging(config *Config) []string {
246249 return validationErrors
247250 }
248251
252+func validateDefaults(config *Config) []string {
253+ var validationErrors []string
254+
255+ if config.Defaults.NewPaneURL == "" {
256+ validationErrors = append(validationErrors, "defaults.new_pane_url cannot be empty")
257+ return validationErrors
258+ }
259+
260+ normalized := domainurl.Normalize(config.Defaults.NewPaneURL)
261+ parsed, err := url.Parse(normalized)
262+ if err != nil {
263+ validationErrors = append(validationErrors, fmt.Sprintf(
264+ "defaults.new_pane_url must be a valid URL (got: %s)",
265+ config.Defaults.NewPaneURL,
266+ ))
267+ return validationErrors
268+ }
269+
270+ switch parsed.Scheme {
271+ case "http", "https", "dumb", "file", "about":
272+ // ok
273+ default:
274+ validationErrors = append(validationErrors, fmt.Sprintf(
275+ "defaults.new_pane_url must use one of: http, https, dumb, file, about (got: %s)",
276+ parsed.Scheme,
277+ ))
278+ }
279+
280+ return validationErrors
281+}
282+
249283 func validateOmnibox(config *Config) []string {
250284 switch config.Omnibox.InitialBehavior {
251285 case "recent", "most_visited", "none":
@@ -2,8 +2,10 @@ package config
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "net/url"
5 "strings"6 "strings"
6 7
8+ domainurl "github.com/bnema/dumber/internal/domain/url"
7 domainvalidation "github.com/bnema/dumber/internal/domain/validation"9 domainvalidation "github.com/bnema/dumber/internal/domain/validation"
8 )10 )
9 11
@@ -21,6 +23,7 @@ func validateConfig(config *Config) error {
21 validationErrors = append(validationErrors, validateTabBar(config)...)23 validationErrors = append(validationErrors, validateTabBar(config)...)
22 validationErrors = append(validationErrors, validateTabMode(config)...)24 validationErrors = append(validationErrors, validateTabMode(config)...)
23 validationErrors = append(validationErrors, validateLogging(config)...)25 validationErrors = append(validationErrors, validateLogging(config)...)
26+ validationErrors = append(validationErrors, validateDefaults(config)...)
24 validationErrors = append(validationErrors, validateOmnibox(config)...)27 validationErrors = append(validationErrors, validateOmnibox(config)...)
25 validationErrors = append(validationErrors, validateRendering(config)...)28 validationErrors = append(validationErrors, validateRendering(config)...)
26 validationErrors = append(validationErrors, validateColorScheme(config)...)29 validationErrors = append(validationErrors, validateColorScheme(config)...)
@@ -246,6 +249,37 @@ func validateLogging(config *Config) []string {
246 return validationErrors249 return validationErrors
247 }250 }
248 251
252+func validateDefaults(config *Config) []string {
253+ var validationErrors []string
254+
255+ if config.Defaults.NewPaneURL == "" {
256+ validationErrors = append(validationErrors, "defaults.new_pane_url cannot be empty")
257+ return validationErrors
258+ }
259+
260+ normalized := domainurl.Normalize(config.Defaults.NewPaneURL)
261+ parsed, err := url.Parse(normalized)
262+ if err != nil {
263+ validationErrors = append(validationErrors, fmt.Sprintf(
264+ "defaults.new_pane_url must be a valid URL (got: %s)",
265+ config.Defaults.NewPaneURL,
266+ ))
267+ return validationErrors
268+ }
269+
270+ switch parsed.Scheme {
271+ case "http", "https", "dumb", "file", "about":
272+ // ok
273+ default:
274+ validationErrors = append(validationErrors, fmt.Sprintf(
275+ "defaults.new_pane_url must use one of: http, https, dumb, file, about (got: %s)",
276+ parsed.Scheme,
277+ ))
278+ }
279+
280+ return validationErrors
281+}
282+
249 func validateOmnibox(config *Config) []string {283 func validateOmnibox(config *Config) []string {
250 switch config.Omnibox.InitialBehavior {284 switch config.Omnibox.InitialBehavior {
251 case "recent", "most_visited", "none":285 case "recent", "most_visited", "none":
M internal/ui/coordinator/workspace.go +14 -4
@@ -7,6 +7,7 @@ import (
77
88 "github.com/bnema/dumber/internal/application/usecase"
99 "github.com/bnema/dumber/internal/domain/entity"
10+ domainurl "github.com/bnema/dumber/internal/domain/url"
1011 "github.com/bnema/dumber/internal/infrastructure/config"
1112 "github.com/bnema/dumber/internal/logging"
1213 "github.com/bnema/dumber/internal/ui/component"
@@ -126,6 +127,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
126127 Workspace: splitCtx.ws,
127128 TargetPane: splitCtx.activePane,
128129 Direction: direction,
130+ InitialURL: domainurl.Normalize(config.Get().Defaults.NewPaneURL),
129131 })
130132 if err != nil {
131133 log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane")
@@ -143,6 +145,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
143145 c.applySplitToView(ctx, splitCtx.wsView, splitCtx.ws, output, direction, splitCtx.existingWidget, splitCtx.isStackSplit, oldActivePaneID)
144146 }
145147
148+ if splitCtx.wsView != nil && config.Get().Defaults.AutoOpenOmniboxOnNewPane {
149+ splitCtx.wsView.ShowOmnibox(ctx, "")
150+ }
151+
146152 // Notify state change for session snapshots
147153 c.notifyStateChanged()
148154
@@ -1216,7 +1222,7 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
12161222
12171223 newPaneID := entity.PaneID(c.generateID())
12181224 newPane := entity.NewPane(newPaneID)
1219- newPane.URI = "about:blank"
1225+ newPane.URI = domainurl.Normalize(config.Get().Defaults.NewPaneURL)
12201226 newPane.Title = defaultPaneTitle
12211227
12221228 stackNode, needsFirstPaneTitleUpdate := c.ensureStackNode(ctx, stackCtx.activeNode, stackCtx.activePaneID, stackCtx.originalTitle)
@@ -1253,9 +1259,9 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
12531259 if widget != nil {
12541260 newPaneView.SetWebViewWidget(widget)
12551261 }
1256- // Load blank page
1257- if err := wv.LoadURI(ctx, "about:blank"); err != nil {
1258- log.Warn().Err(err).Msg("failed to load blank page")
1262+ // Load initial page for the new pane
1263+ if err := wv.LoadURI(ctx, newPane.URI); err != nil {
1264+ log.Warn().Err(err).Str("uri", newPane.URI).Msg("failed to load initial page")
12591265 }
12601266 }
12611267
@@ -1267,6 +1273,10 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
12671273 log.Warn().Err(err).Msg("failed to set active pane")
12681274 }
12691275
1276+ if config.Get().Defaults.AutoOpenOmniboxOnNewPane {
1277+ stackCtx.wsView.ShowOmnibox(ctx, "")
1278+ }
1279+
12701280 // Set up title bar click callback
12711281 tr := stackCtx.wsView.TreeRenderer()
12721282 if tr != nil {
@@ -7,6 +7,7 @@ import (
7 7
8 "github.com/bnema/dumber/internal/application/usecase"8 "github.com/bnema/dumber/internal/application/usecase"
9 "github.com/bnema/dumber/internal/domain/entity"9 "github.com/bnema/dumber/internal/domain/entity"
10+ domainurl "github.com/bnema/dumber/internal/domain/url"
10 "github.com/bnema/dumber/internal/infrastructure/config"11 "github.com/bnema/dumber/internal/infrastructure/config"
11 "github.com/bnema/dumber/internal/logging"12 "github.com/bnema/dumber/internal/logging"
12 "github.com/bnema/dumber/internal/ui/component"13 "github.com/bnema/dumber/internal/ui/component"
@@ -126,6 +127,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
126 Workspace: splitCtx.ws,127 Workspace: splitCtx.ws,
127 TargetPane: splitCtx.activePane,128 TargetPane: splitCtx.activePane,
128 Direction: direction,129 Direction: direction,
130+ InitialURL: domainurl.Normalize(config.Get().Defaults.NewPaneURL),
129 })131 })
130 if err != nil {132 if err != nil {
131 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")
@@ -143,6 +145,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
143 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)
144 }146 }
145 147
148+ if splitCtx.wsView != nil && config.Get().Defaults.AutoOpenOmniboxOnNewPane {
149+ splitCtx.wsView.ShowOmnibox(ctx, "")
150+ }
151+
146 // Notify state change for session snapshots152 // Notify state change for session snapshots
147 c.notifyStateChanged()153 c.notifyStateChanged()
148 154
@@ -1216,7 +1222,7 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
1216 1222
1217 newPaneID := entity.PaneID(c.generateID())1223 newPaneID := entity.PaneID(c.generateID())
1218 newPane := entity.NewPane(newPaneID)1224 newPane := entity.NewPane(newPaneID)
1219- newPane.URI = "about:blank"1225+ newPane.URI = domainurl.Normalize(config.Get().Defaults.NewPaneURL)
1220 newPane.Title = defaultPaneTitle1226 newPane.Title = defaultPaneTitle
1221 1227
1222 stackNode, needsFirstPaneTitleUpdate := c.ensureStackNode(ctx, stackCtx.activeNode, stackCtx.activePaneID, stackCtx.originalTitle)1228 stackNode, needsFirstPaneTitleUpdate := c.ensureStackNode(ctx, stackCtx.activeNode, stackCtx.activePaneID, stackCtx.originalTitle)
@@ -1253,9 +1259,9 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
1253 if widget != nil {1259 if widget != nil {
1254 newPaneView.SetWebViewWidget(widget)1260 newPaneView.SetWebViewWidget(widget)
1255 }1261 }
1256- // Load blank page1262+ // Load initial page for the new pane
1257- if err := wv.LoadURI(ctx, "about:blank"); err != nil {1263+ if err := wv.LoadURI(ctx, newPane.URI); err != nil {
1258- log.Warn().Err(err).Msg("failed to load blank page")1264+ log.Warn().Err(err).Str("uri", newPane.URI).Msg("failed to load initial page")
1259 }1265 }
1260 }1266 }
1261 1267
@@ -1267,6 +1273,10 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
1267 log.Warn().Err(err).Msg("failed to set active pane")1273 log.Warn().Err(err).Msg("failed to set active pane")
1268 }1274 }
1269 1275
1276+ if config.Get().Defaults.AutoOpenOmniboxOnNewPane {
1277+ stackCtx.wsView.ShowOmnibox(ctx, "")
1278+ }
1279+
1270 // Set up title bar click callback1280 // Set up title bar click callback
1271 tr := stackCtx.wsView.TreeRenderer()1281 tr := stackCtx.wsView.TreeRenderer()
1272 if tr != nil {1282 if tr != nil {
M internal/ui/dispatcher/keyboard.go +7 -1
@@ -6,6 +6,8 @@ import (
66
77 "github.com/bnema/dumber/internal/application/usecase"
88 "github.com/bnema/dumber/internal/domain/entity"
9+ domainurl "github.com/bnema/dumber/internal/domain/url"
10+ "github.com/bnema/dumber/internal/infrastructure/config"
911 "github.com/bnema/dumber/internal/logging"
1012 "github.com/bnema/dumber/internal/ui/component"
1113 "github.com/bnema/dumber/internal/ui/coordinator"
@@ -97,7 +99,11 @@ func (d *KeyboardDispatcher) initActionHandlers() {
9799 )
98100 d.actionHandlers = map[input.Action]func(ctx context.Context) error{
99101 // Tab actions
100- input.ActionNewTab: func(ctx context.Context) error { _, err := d.tabCoord.Create(ctx, "about:blank"); return err },
102+ input.ActionNewTab: func(ctx context.Context) error {
103+ cfg := config.Get()
104+ _, err := d.tabCoord.Create(ctx, domainurl.Normalize(cfg.Defaults.NewPaneURL))
105+ return err
106+ },
101107 input.ActionCloseTab: d.tabCoord.Close,
102108 input.ActionNextTab: d.tabCoord.SwitchNext,
103109 input.ActionPreviousTab: d.tabCoord.SwitchPrev,
@@ -6,6 +6,8 @@ import (
6 6
7 "github.com/bnema/dumber/internal/application/usecase"7 "github.com/bnema/dumber/internal/application/usecase"
8 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
9+ domainurl "github.com/bnema/dumber/internal/domain/url"
10+ "github.com/bnema/dumber/internal/infrastructure/config"
9 "github.com/bnema/dumber/internal/logging"11 "github.com/bnema/dumber/internal/logging"
10 "github.com/bnema/dumber/internal/ui/component"12 "github.com/bnema/dumber/internal/ui/component"
11 "github.com/bnema/dumber/internal/ui/coordinator"13 "github.com/bnema/dumber/internal/ui/coordinator"
@@ -97,7 +99,11 @@ func (d *KeyboardDispatcher) initActionHandlers() {
97 )99 )
98 d.actionHandlers = map[input.Action]func(ctx context.Context) error{100 d.actionHandlers = map[input.Action]func(ctx context.Context) error{
99 // Tab actions101 // Tab actions
100- input.ActionNewTab: func(ctx context.Context) error { _, err := d.tabCoord.Create(ctx, "about:blank"); return err },102+ input.ActionNewTab: func(ctx context.Context) error {
103+ cfg := config.Get()
104+ _, err := d.tabCoord.Create(ctx, domainurl.Normalize(cfg.Defaults.NewPaneURL))
105+ return err
106+ },
101 input.ActionCloseTab: d.tabCoord.Close,107 input.ActionCloseTab: d.tabCoord.Close,
102 input.ActionNextTab: d.tabCoord.SwitchNext,108 input.ActionNextTab: d.tabCoord.SwitchNext,
103 input.ActionPreviousTab: d.tabCoord.SwitchPrev,109 input.ActionPreviousTab: d.tabCoord.SwitchPrev,

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