Merge pull request #38 from bnema/feat/new-pane-defaults
feat(config): defaults for new panes
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. | |||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | 7 | ### 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 | 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 | 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 | 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. | |
| @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. | |||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | ### Added | 7 | ### 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 | |||
| 121 | 121 | # 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 | 138 | ## Logging | |
| 125 | 139 | ||
| 126 | 140 | | 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 suggestions | 121 | # 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 | ## Logging | 138 | ## 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 ( | |||
| 8 | 8 | "sort" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/dumber/internal/domain/entity" | |
| 11 | + | domainurl "github.com/bnema/dumber/internal/domain/url" | |
| 11 | 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 | 114 | paneID := entity.PaneID(uc.idGenerator()) | |
| 114 | 115 | newPane = entity.NewPane(paneID) | |
| 115 | 116 | if input.InitialURL != "" { | |
| 116 | - | newPane.URI = input.InitialURL | |
| 117 | + | newPane.URI = domainurl.Normalize(input.InitialURL) | |
| 117 | 118 | } else { | |
| 118 | 119 | newPane.URI = "about:blank" | |
| 119 | 120 | } | |
| @@ -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.InitialURL | 117 | + | 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 ( | |||
| 15 | 15 | // Appearance defaults | |
| 16 | 16 | defaultFontSize = 16 // points | |
| 17 | 17 | ||
| 18 | + | // Defaults | |
| 19 | + | defaultNewPaneURL = "about:blank" | |
| 20 | + | defaultAutoOpenOmniboxOnNewPane = false | |
| 21 | + | ||
| 18 | 22 | // Omnibox defaults | |
| 19 | 23 | defaultOmniboxInitialBehavior = "recent" | |
| 20 | 24 | ||
| @@ -150,6 +154,10 @@ func DefaultConfig() *Config { | |||
| 150 | 154 | }, | |
| 151 | 155 | DefaultWebpageZoom: 1.2, // 120% default zoom for better readability | |
| 152 | 156 | DefaultUIScale: defaultUIScale, // 1.0 = 100%, 2.0 = 200% | |
| 157 | + | Defaults: DefaultsConfig{ | |
| 158 | + | NewPaneURL: defaultNewPaneURL, | |
| 159 | + | AutoOpenOmniboxOnNewPane: defaultAutoOpenOmniboxOnNewPane, | |
| 160 | + | }, | |
| 153 | 161 | Workspace: WorkspaceConfig{ | |
| 154 | 162 | PaneMode: PaneModeConfig{ | |
| 155 | 163 | ActivationShortcut: defaultPaneActivationShortcut, | |
| @@ -15,6 +15,10 @@ const ( | |||||
| 15 | // Appearance defaults | 15 | // Appearance defaults | ||
| 16 | defaultFontSize = 16 // points | 16 | defaultFontSize = 16 // points | ||
| 17 | 17 | ||||
| 18 | + | // Defaults | |||
| 19 | + | defaultNewPaneURL = "about:blank" | |||
| 20 | + | defaultAutoOpenOmniboxOnNewPane = false | |||
| 21 | + | ||||
| 18 | // Omnibox defaults | 22 | // 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 readability | 155 | 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() { | |||
| 309 | 309 | m.setDebugDefaults(defaults) | |
| 310 | 310 | m.setAppearanceDefaults(defaults) | |
| 311 | 311 | m.setRenderingDefaults(defaults) | |
| 312 | + | m.setDefaultsDefaults(defaults) | |
| 312 | 313 | m.setWorkspaceDefaults(defaults) | |
| 313 | 314 | m.setContentFilteringDefaults(defaults) | |
| 314 | 315 | m.setOmniboxDefaults(defaults) | |
| @@ -379,6 +380,11 @@ func (m *Manager) setRenderingDefaults(defaults *Config) { | |||
| 379 | 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 | 388 | func (m *Manager) setWorkspaceDefaults(defaults *Config) { | |
| 383 | 389 | m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut) | |
| 384 | 390 | 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 { | |||
| 15 | 15 | DefaultWebpageZoom float64 `mapstructure:"default_webpage_zoom" yaml:"default_webpage_zoom" toml:"default_webpage_zoom"` | |
| 16 | 16 | // DefaultUIScale sets the default UI scale for GTK widgets (1.0 = 100%, 2.0 = 200%) | |
| 17 | 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 | 20 | // Workspace defines workspace, pane, and tab handling behavior. | |
| 19 | 21 | Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace" toml:"workspace"` | |
| 20 | 22 | // Session controls session persistence and restoration. | |
| @@ -35,6 +37,19 @@ type Config struct { | |||
| 35 | 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 | 53 | // RenderingMode selects GPU vs CPU rendering. | |
| 39 | 54 | type RenderingMode string | |
| 40 | 55 | ||
| @@ -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 string | 54 | type RenderingMode string | ||
| 40 | 55 | ||||
M internal/infrastructure/config/validation.go +34 -0
| @@ -2,8 +2,10 @@ package config | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "net/url" | |
| 5 | 6 | "strings" | |
| 6 | 7 | ||
| 8 | + | domainurl "github.com/bnema/dumber/internal/domain/url" | |
| 7 | 9 | domainvalidation "github.com/bnema/dumber/internal/domain/validation" | |
| 8 | 10 | ) | |
| 9 | 11 | ||
| @@ -21,6 +23,7 @@ func validateConfig(config *Config) error { | |||
| 21 | 23 | validationErrors = append(validationErrors, validateTabBar(config)...) | |
| 22 | 24 | validationErrors = append(validationErrors, validateTabMode(config)...) | |
| 23 | 25 | validationErrors = append(validationErrors, validateLogging(config)...) | |
| 26 | + | validationErrors = append(validationErrors, validateDefaults(config)...) | |
| 24 | 27 | validationErrors = append(validationErrors, validateOmnibox(config)...) | |
| 25 | 28 | validationErrors = append(validationErrors, validateRendering(config)...) | |
| 26 | 29 | validationErrors = append(validationErrors, validateColorScheme(config)...) | |
| @@ -246,6 +249,37 @@ func validateLogging(config *Config) []string { | |||
| 246 | 249 | 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 | 283 | func validateOmnibox(config *Config) []string { | |
| 250 | 284 | switch config.Omnibox.InitialBehavior { | |
| 251 | 285 | 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 validationErrors | 249 | 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 ( | |||
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | 9 | "github.com/bnema/dumber/internal/domain/entity" | |
| 10 | + | domainurl "github.com/bnema/dumber/internal/domain/url" | |
| 10 | 11 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 11 | 12 | "github.com/bnema/dumber/internal/logging" | |
| 12 | 13 | "github.com/bnema/dumber/internal/ui/component" | |
| @@ -126,6 +127,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli | |||
| 126 | 127 | Workspace: splitCtx.ws, | |
| 127 | 128 | TargetPane: splitCtx.activePane, | |
| 128 | 129 | Direction: direction, | |
| 130 | + | InitialURL: domainurl.Normalize(config.Get().Defaults.NewPaneURL), | |
| 129 | 131 | }) | |
| 130 | 132 | if err != nil { | |
| 131 | 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 | 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 | 152 | // Notify state change for session snapshots | |
| 147 | 153 | c.notifyStateChanged() | |
| 148 | 154 | ||
| @@ -1216,7 +1222,7 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||
| 1216 | 1222 | ||
| 1217 | 1223 | newPaneID := entity.PaneID(c.generateID()) | |
| 1218 | 1224 | newPane := entity.NewPane(newPaneID) | |
| 1219 | - | newPane.URI = "about:blank" | |
| 1225 | + | newPane.URI = domainurl.Normalize(config.Get().Defaults.NewPaneURL) | |
| 1220 | 1226 | newPane.Title = defaultPaneTitle | |
| 1221 | 1227 | ||
| 1222 | 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 | 1259 | if widget != nil { | |
| 1254 | 1260 | newPaneView.SetWebViewWidget(widget) | |
| 1255 | 1261 | } | |
| 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") | |
| 1259 | 1265 | } | |
| 1260 | 1266 | } | |
| 1261 | 1267 | ||
| @@ -1267,6 +1273,10 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||
| 1267 | 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 | 1280 | // Set up title bar click callback | |
| 1271 | 1281 | tr := stackCtx.wsView.TreeRenderer() | |
| 1272 | 1282 | 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 snapshots | 152 | // 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 = defaultPaneTitle | 1226 | 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 page | 1262 | + | // 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 callback | 1280 | // 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 ( | |||
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/dumber/internal/application/usecase" | |
| 8 | 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 | 11 | "github.com/bnema/dumber/internal/logging" | |
| 10 | 12 | "github.com/bnema/dumber/internal/ui/component" | |
| 11 | 13 | "github.com/bnema/dumber/internal/ui/coordinator" | |
| @@ -97,7 +99,11 @@ func (d *KeyboardDispatcher) initActionHandlers() { | |||
| 97 | 99 | ) | |
| 98 | 100 | d.actionHandlers = map[input.Action]func(ctx context.Context) error{ | |
| 99 | 101 | // 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 | 107 | input.ActionCloseTab: d.tabCoord.Close, | |
| 102 | 108 | input.ActionNextTab: d.tabCoord.SwitchNext, | |
| 103 | 109 | 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 actions | 101 | // 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, | ||