fix(omnibox): honor configured most-visited window
- commit
- b65c4d924cd889de509f605f47961c0aef26eb8f
- author
- brice · · 2026-04-12 06:26 UTC
- parent
- 14d9030
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +156 -9
M docs/config/index.md +2 -0
| @@ -104,12 +104,14 @@ dumber dmenu --days 14 --most-visited | |||
| 104 | 104 | | Key | Type | Default | Valid Values | Description | | |
| 105 | 105 | |-----|------|---------|--------------|-------------| | |
| 106 | 106 | | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior | | |
| 107 | + | | `omnibox.most_visited_days` | int | `30` | `>= 0` | Days of history to consider when `initial_behavior = "most_visited"` (`0` = all history) | | |
| 107 | 108 | | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane | | |
| 108 | 109 | ||
| 109 | 110 | **Example:** | |
| 110 | 111 | ```toml | |
| 111 | 112 | [omnibox] | |
| 112 | 113 | initial_behavior = "recent" # Show recent history when omnibox opens | |
| 114 | + | most_visited_days = 30 # Days of history used for most_visited | |
| 113 | 115 | auto_open_on_new_pane = false | |
| 114 | 116 | ||
| 115 | 117 | # Alternative options: | |
| @@ -104,12 +104,14 @@ dumber dmenu --days 14 --most-visited | |||||
| 104 | | Key | Type | Default | Valid Values | Description | | 104 | | Key | Type | Default | Valid Values | Description | | ||
| 105 | |-----|------|---------|--------------|-------------| | 105 | |-----|------|---------|--------------|-------------| | ||
| 106 | | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior | | 106 | | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior | | ||
| 107 | + | | `omnibox.most_visited_days` | int | `30` | `>= 0` | Days of history to consider when `initial_behavior = "most_visited"` (`0` = all history) | | |||
| 107 | | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane | | 108 | | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane | | ||
| 108 | 109 | ||||
| 109 | **Example:** | 110 | **Example:** | ||
| 110 | ```toml | 111 | ```toml | ||
| 111 | [omnibox] | 112 | [omnibox] | ||
| 112 | initial_behavior = "recent" # Show recent history when omnibox opens | 113 | initial_behavior = "recent" # Show recent history when omnibox opens | ||
| 114 | + | most_visited_days = 30 # Days of history used for most_visited | |||
| 113 | auto_open_on_new_pane = false | 115 | auto_open_on_new_pane = false | ||
| 114 | 116 | ||||
| 115 | # Alternative options: | 117 | # Alternative options: | ||
M docs/reference/configuration.md +1 -0
| @@ -18,6 +18,7 @@ | |||
| 18 | 18 | | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format | | |
| 19 | 19 | | `dmenu.sort_by_visit_count` | bool | `true` | | | |
| 20 | 20 | | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` | | |
| 21 | + | | `omnibox.most_visited_days` | int | `30` | `>= 0` | | |
| 21 | 22 | | `omnibox.auto_open_on_new_pane` | bool | `false` | | | |
| 22 | 23 | | `logging.level` | string | `info` | `debug`, `info`, `warn`, `error` | | |
| 23 | 24 | | `logging.format` | string | `text` | `text`, `json` | | |
| @@ -18,6 +18,7 @@ | |||||
| 18 | | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format | | 18 | | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format | | ||
| 19 | | `dmenu.sort_by_visit_count` | bool | `true` | | | 19 | | `dmenu.sort_by_visit_count` | bool | `true` | | | ||
| 20 | | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` | | 20 | | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` | | ||
| 21 | + | | `omnibox.most_visited_days` | int | `30` | `>= 0` | | |||
| 21 | | `omnibox.auto_open_on_new_pane` | bool | `false` | | | 22 | | `omnibox.auto_open_on_new_pane` | bool | `false` | | | ||
| 22 | | `logging.level` | string | `info` | `debug`, `info`, `warn`, `error` | | 23 | | `logging.level` | string | `info` | `debug`, `info`, `warn`, `error` | | ||
| 23 | | `logging.format` | string | `text` | `text`, `json` | | 24 | | `logging.format` | string | `text` | `text`, `json` | | ||
M internal/infrastructure/config/defaults.go +2 -0
| @@ -20,6 +20,7 @@ const ( | |||
| 20 | 20 | ||
| 21 | 21 | // Omnibox defaults | |
| 22 | 22 | defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent | |
| 23 | + | defaultOmniboxMostVisitedDays = 30 | |
| 23 | 24 | defaultOmniboxAutoOpenOnNewPane = false | |
| 24 | 25 | ||
| 25 | 26 | // Workspace defaults | |
| @@ -284,6 +285,7 @@ func DefaultConfig() *Config { | |||
| 284 | 285 | }, | |
| 285 | 286 | Omnibox: OmniboxConfig{ | |
| 286 | 287 | InitialBehavior: defaultOmniboxInitialBehavior, | |
| 288 | + | MostVisitedDays: defaultOmniboxMostVisitedDays, | |
| 287 | 289 | AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane, | |
| 288 | 290 | }, | |
| 289 | 291 | Session: SessionConfig{ | |
| @@ -20,6 +20,7 @@ const ( | |||||
| 20 | 20 | ||||
| 21 | // Omnibox defaults | 21 | // Omnibox defaults | ||
| 22 | defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent | 22 | defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent | ||
| 23 | + | defaultOmniboxMostVisitedDays = 30 | |||
| 23 | defaultOmniboxAutoOpenOnNewPane = false | 24 | defaultOmniboxAutoOpenOnNewPane = false | ||
| 24 | 25 | ||||
| 25 | // Workspace defaults | 26 | // Workspace defaults | ||
| @@ -284,6 +285,7 @@ func DefaultConfig() *Config { | |||||
| 284 | }, | 285 | }, | ||
| 285 | Omnibox: OmniboxConfig{ | 286 | Omnibox: OmniboxConfig{ | ||
| 286 | InitialBehavior: defaultOmniboxInitialBehavior, | 287 | InitialBehavior: defaultOmniboxInitialBehavior, | ||
| 288 | + | MostVisitedDays: defaultOmniboxMostVisitedDays, | |||
| 287 | AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane, | 289 | AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane, | ||
| 288 | }, | 290 | }, | ||
| 289 | Session: SessionConfig{ | 291 | Session: SessionConfig{ | ||
M internal/infrastructure/config/loader.go +1 -0
| @@ -526,6 +526,7 @@ func (m *Manager) setClipboardDefaults(defaults *Config) { | |||
| 526 | 526 | ||
| 527 | 527 | func (m *Manager) setOmniboxDefaults(defaults *Config) { | |
| 528 | 528 | m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior) | |
| 529 | + | m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays) | |
| 529 | 530 | m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane) | |
| 530 | 531 | } | |
| 531 | 532 | ||
| @@ -526,6 +526,7 @@ func (m *Manager) setClipboardDefaults(defaults *Config) { | |||||
| 526 | 526 | ||||
| 527 | func (m *Manager) setOmniboxDefaults(defaults *Config) { | 527 | func (m *Manager) setOmniboxDefaults(defaults *Config) { | ||
| 528 | m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior) | 528 | m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior) | ||
| 529 | + | m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays) | |||
| 529 | m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane) | 530 | m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane) | ||
| 530 | } | 531 | } | ||
| 531 | 532 | ||||
M internal/infrastructure/config/schema.go +4 -0
| @@ -359,6 +359,10 @@ type OmniboxConfig struct { | |||
| 359 | 359 | // InitialBehavior controls what to show when omnibox opens with empty input | |
| 360 | 360 | // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing) | |
| 361 | 361 | InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"` | |
| 362 | + | // MostVisitedDays controls how many days of history are considered when initial behavior is most_visited. | |
| 363 | + | // 0 means all history. | |
| 364 | + | // Default: 30. | |
| 365 | + | MostVisitedDays int `mapstructure:"most_visited_days" yaml:"most_visited_days" toml:"most_visited_days"` | |
| 362 | 366 | // AutoOpenOnNewPane opens the omnibox automatically when a new pane is created. | |
| 363 | 367 | // Default: false | |
| 364 | 368 | AutoOpenOnNewPane bool `mapstructure:"auto_open_on_new_pane" yaml:"auto_open_on_new_pane" toml:"auto_open_on_new_pane"` | |
| @@ -359,6 +359,10 @@ type OmniboxConfig struct { | |||||
| 359 | // InitialBehavior controls what to show when omnibox opens with empty input | 359 | // InitialBehavior controls what to show when omnibox opens with empty input | ||
| 360 | // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing) | 360 | // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing) | ||
| 361 | InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"` | 361 | InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"` | ||
| 362 | + | // MostVisitedDays controls how many days of history are considered when initial behavior is most_visited. | |||
| 363 | + | // 0 means all history. | |||
| 364 | + | // Default: 30. | |||
| 365 | + | MostVisitedDays int `mapstructure:"most_visited_days" yaml:"most_visited_days" toml:"most_visited_days"` | |||
| 362 | // AutoOpenOnNewPane opens the omnibox automatically when a new pane is created. | 366 | // AutoOpenOnNewPane opens the omnibox automatically when a new pane is created. | ||
| 363 | // Default: false | 367 | // Default: false | ||
| 364 | AutoOpenOnNewPane bool `mapstructure:"auto_open_on_new_pane" yaml:"auto_open_on_new_pane" toml:"auto_open_on_new_pane"` | 368 | AutoOpenOnNewPane bool `mapstructure:"auto_open_on_new_pane" yaml:"auto_open_on_new_pane" toml:"auto_open_on_new_pane"` | ||
M internal/infrastructure/config/schema_provider.go +8 -0
| @@ -672,6 +672,14 @@ func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo { | |||
| 672 | 672 | }, | |
| 673 | 673 | Section: SectionOmnibox, | |
| 674 | 674 | }, | |
| 675 | + | { | |
| 676 | + | Key: "omnibox.most_visited_days", | |
| 677 | + | Type: "int", | |
| 678 | + | Default: fmt.Sprintf("%d", defaults.Omnibox.MostVisitedDays), | |
| 679 | + | Description: "Days of history to consider for most_visited (0 = all history)", | |
| 680 | + | Range: ">=0", | |
| 681 | + | Section: SectionOmnibox, | |
| 682 | + | }, | |
| 675 | 683 | { | |
| 676 | 684 | Key: "omnibox.auto_open_on_new_pane", | |
| 677 | 685 | Type: "bool", | |
| @@ -672,6 +672,14 @@ func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo { | |||||
| 672 | }, | 672 | }, | ||
| 673 | Section: SectionOmnibox, | 673 | Section: SectionOmnibox, | ||
| 674 | }, | 674 | }, | ||
| 675 | + | { | |||
| 676 | + | Key: "omnibox.most_visited_days", | |||
| 677 | + | Type: "int", | |||
| 678 | + | Default: fmt.Sprintf("%d", defaults.Omnibox.MostVisitedDays), | |||
| 679 | + | Description: "Days of history to consider for most_visited (0 = all history)", | |||
| 680 | + | Range: ">=0", | |||
| 681 | + | Section: SectionOmnibox, | |||
| 682 | + | }, | |||
| 675 | { | 683 | { | ||
| 676 | Key: "omnibox.auto_open_on_new_pane", | 684 | Key: "omnibox.auto_open_on_new_pane", | ||
| 677 | Type: "bool", | 685 | Type: "bool", | ||
M internal/infrastructure/config/validation.go +7 -3
| @@ -388,18 +388,22 @@ func validateWorkspaceURLValue(fieldPath, value string) []string { | |||
| 388 | 388 | } | |
| 389 | 389 | ||
| 390 | 390 | func validateOmnibox(config *Config) []string { | |
| 391 | + | var validationErrors []string | |
| 392 | + | if config.Omnibox.MostVisitedDays < 0 { | |
| 393 | + | validationErrors = append(validationErrors, "omnibox.most_visited_days must be non-negative") | |
| 394 | + | } | |
| 391 | 395 | switch config.Omnibox.InitialBehavior { | |
| 392 | 396 | case OmniboxInitialBehaviorRecent, OmniboxInitialBehaviorMostVisited, OmniboxInitialBehaviorNone: | |
| 393 | - | return nil | |
| 394 | 397 | default: | |
| 395 | - | return []string{fmt.Sprintf( | |
| 398 | + | validationErrors = append(validationErrors, fmt.Sprintf( | |
| 396 | 399 | "omnibox.initial_behavior must be one of: %s, %s, %s (got: %s)", | |
| 397 | 400 | OmniboxInitialBehaviorRecent, | |
| 398 | 401 | OmniboxInitialBehaviorMostVisited, | |
| 399 | 402 | OmniboxInitialBehaviorNone, | |
| 400 | 403 | config.Omnibox.InitialBehavior, | |
| 401 | - | )} | |
| 404 | + | )) | |
| 402 | 405 | } | |
| 406 | + | return validationErrors | |
| 403 | 407 | } | |
| 404 | 408 | ||
| 405 | 409 | func validateRendering(config *Config) []string { | |
| @@ -388,18 +388,22 @@ func validateWorkspaceURLValue(fieldPath, value string) []string { | |||||
| 388 | } | 388 | } | ||
| 389 | 389 | ||||
| 390 | func validateOmnibox(config *Config) []string { | 390 | func validateOmnibox(config *Config) []string { | ||
| 391 | + | var validationErrors []string | |||
| 392 | + | if config.Omnibox.MostVisitedDays < 0 { | |||
| 393 | + | validationErrors = append(validationErrors, "omnibox.most_visited_days must be non-negative") | |||
| 394 | + | } | |||
| 391 | switch config.Omnibox.InitialBehavior { | 395 | switch config.Omnibox.InitialBehavior { | ||
| 392 | case OmniboxInitialBehaviorRecent, OmniboxInitialBehaviorMostVisited, OmniboxInitialBehaviorNone: | 396 | case OmniboxInitialBehaviorRecent, OmniboxInitialBehaviorMostVisited, OmniboxInitialBehaviorNone: | ||
| 393 | - | return nil | |||
| 394 | default: | 397 | default: | ||
| 395 | - | return []string{fmt.Sprintf( | 398 | + | validationErrors = append(validationErrors, fmt.Sprintf( |
| 396 | "omnibox.initial_behavior must be one of: %s, %s, %s (got: %s)", | 399 | "omnibox.initial_behavior must be one of: %s, %s, %s (got: %s)", | ||
| 397 | OmniboxInitialBehaviorRecent, | 400 | OmniboxInitialBehaviorRecent, | ||
| 398 | OmniboxInitialBehaviorMostVisited, | 401 | OmniboxInitialBehaviorMostVisited, | ||
| 399 | OmniboxInitialBehaviorNone, | 402 | OmniboxInitialBehaviorNone, | ||
| 400 | config.Omnibox.InitialBehavior, | 403 | config.Omnibox.InitialBehavior, | ||
| 401 | - | )} | 404 | + | )) |
| 402 | } | 405 | } | ||
| 406 | + | return validationErrors | |||
| 403 | } | 407 | } | ||
| 404 | 408 | ||||
| 405 | func validateRendering(config *Config) []string { | 409 | func validateRendering(config *Config) []string { | ||
M internal/ui/app.go +2 -0
| @@ -802,6 +802,8 @@ func buildOmniboxConfig( | |||
| 802 | 802 | ShortcutsUC: usecase.NewSearchShortcutsUseCase(shortcuts), | |
| 803 | 803 | DefaultSearch: deps.Config.DefaultSearchEngine, | |
| 804 | 804 | InitialBehavior: deps.Config.Omnibox.InitialBehavior, | |
| 805 | + | MostVisitedDays: deps.Config.Omnibox.MostVisitedDays, | |
| 806 | + | MostVisitedDaysSet: true, | |
| 805 | 807 | SaveInitialBehavior: deps.HandlerDeps.SaveOmniboxInitialBehavior, | |
| 806 | 808 | UIScale: deps.Config.DefaultUIScale, | |
| 807 | 809 | OnNavigate: callbacks.OnNavigate, | |
| @@ -802,6 +802,8 @@ func buildOmniboxConfig( | |||||
| 802 | ShortcutsUC: usecase.NewSearchShortcutsUseCase(shortcuts), | 802 | ShortcutsUC: usecase.NewSearchShortcutsUseCase(shortcuts), | ||
| 803 | DefaultSearch: deps.Config.DefaultSearchEngine, | 803 | DefaultSearch: deps.Config.DefaultSearchEngine, | ||
| 804 | InitialBehavior: deps.Config.Omnibox.InitialBehavior, | 804 | InitialBehavior: deps.Config.Omnibox.InitialBehavior, | ||
| 805 | + | MostVisitedDays: deps.Config.Omnibox.MostVisitedDays, | |||
| 806 | + | MostVisitedDaysSet: true, | |||
| 805 | SaveInitialBehavior: deps.HandlerDeps.SaveOmniboxInitialBehavior, | 807 | SaveInitialBehavior: deps.HandlerDeps.SaveOmniboxInitialBehavior, | ||
| 806 | UIScale: deps.Config.DefaultUIScale, | 808 | UIScale: deps.Config.DefaultUIScale, | ||
| 807 | OnNavigate: callbacks.OnNavigate, | 809 | OnNavigate: callbacks.OnNavigate, | ||
A internal/ui/app_omnibox_config_test.go +25 -0
| @@ -0,0 +1,25 @@ | |||
| 1 | + | package ui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | configpkg "github.com/bnema/dumber/internal/infrastructure/config" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestBuildOmniboxConfig_UsesConfiguredMostVisitedDays(t *testing.T) { | |
| 10 | + | deps := &Dependencies{ | |
| 11 | + | Config: &configpkg.Config{ | |
| 12 | + | Omnibox: configpkg.OmniboxConfig{ | |
| 13 | + | MostVisitedDays: 7, | |
| 14 | + | }, | |
| 15 | + | }, | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | got := buildOmniboxConfig(deps, nil, omniboxCallbacks{}) | |
| 19 | + | if got.MostVisitedDays != 7 { | |
| 20 | + | t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays) | |
| 21 | + | } | |
| 22 | + | if !got.MostVisitedDaysSet { | |
| 23 | + | t.Fatal("MostVisitedDaysSet = false, want true") | |
| 24 | + | } | |
| 25 | + | } | |
| @@ -0,0 +1,25 @@ | |||||
| 1 | + | package ui | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | configpkg "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestBuildOmniboxConfig_UsesConfiguredMostVisitedDays(t *testing.T) { | |||
| 10 | + | deps := &Dependencies{ | |||
| 11 | + | Config: &configpkg.Config{ | |||
| 12 | + | Omnibox: configpkg.OmniboxConfig{ | |||
| 13 | + | MostVisitedDays: 7, | |||
| 14 | + | }, | |||
| 15 | + | }, | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | got := buildOmniboxConfig(deps, nil, omniboxCallbacks{}) | |||
| 19 | + | if got.MostVisitedDays != 7 { | |||
| 20 | + | t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays) | |||
| 21 | + | } | |||
| 22 | + | if !got.MostVisitedDaysSet { | |||
| 23 | + | t.Fatal("MostVisitedDaysSet = false, want true") | |||
| 24 | + | } | |||
| 25 | + | } | |||
M internal/ui/component/omnibox.go +25 -6
| @@ -27,11 +27,12 @@ import ( | |||
| 27 | 27 | ) | |
| 28 | 28 | ||
| 29 | 29 | const ( | |
| 30 | - | debounceDelayMs = 50 | |
| 31 | - | endBoxSpacing = 6 | |
| 32 | - | defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)" | |
| 33 | - | minGhostInputLength = 1 | |
| 34 | - | initialBehaviorBadgeTooltip = "Toggle default history order (Ctrl+R)" | |
| 30 | + | debounceDelayMs = 50 | |
| 31 | + | defaultOmniboxMostVisitedDays = 30 | |
| 32 | + | endBoxSpacing = 6 | |
| 33 | + | defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)" | |
| 34 | + | minGhostInputLength = 1 | |
| 35 | + | initialBehaviorBadgeTooltip = "Toggle default history order (Ctrl+R)" | |
| 35 | 36 | ) | |
| 36 | 37 | ||
| 37 | 38 | type favoriteRowIndicatorUpdate struct { | |
| @@ -115,6 +116,8 @@ type Omnibox struct { | |||
| 115 | 116 | shortcutsUC *usecase.SearchShortcutsUseCase | |
| 116 | 117 | defaultSearch string | |
| 117 | 118 | initialBehavior entity.OmniboxInitialBehavior | |
| 119 | + | mostVisitedDays int | |
| 120 | + | mostVisitedDaysSet bool | |
| 118 | 121 | saveInitialBehaviorFn func(context.Context, entity.OmniboxInitialBehavior) error | |
| 119 | 122 | ctx context.Context | |
| 120 | 123 | ||
| @@ -161,6 +164,8 @@ type OmniboxConfig struct { | |||
| 161 | 164 | ShortcutsUC *usecase.SearchShortcutsUseCase | |
| 162 | 165 | DefaultSearch string | |
| 163 | 166 | InitialBehavior entity.OmniboxInitialBehavior | |
| 167 | + | MostVisitedDays int | |
| 168 | + | MostVisitedDaysSet bool | |
| 164 | 169 | SaveInitialBehavior func(ctx context.Context, behavior entity.OmniboxInitialBehavior) error | |
| 165 | 170 | UIScale float64 // UI scale for favicon sizing | |
| 166 | 171 | OnNavigate func(url string) // Callback when user navigates via omnibox | |
| @@ -181,6 +186,10 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||
| 181 | 186 | if uiScale <= 0 { | |
| 182 | 187 | uiScale = 1.0 | |
| 183 | 188 | } | |
| 189 | + | mostVisitedDays := cfg.MostVisitedDays | |
| 190 | + | if !cfg.MostVisitedDaysSet { | |
| 191 | + | mostVisitedDays = defaultOmniboxMostVisitedDays | |
| 192 | + | } | |
| 184 | 193 | ||
| 185 | 194 | sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults) | |
| 186 | 195 | ||
| @@ -194,6 +203,8 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||
| 194 | 203 | shortcutsUC: cfg.ShortcutsUC, | |
| 195 | 204 | defaultSearch: cfg.DefaultSearch, | |
| 196 | 205 | initialBehavior: cfg.InitialBehavior, | |
| 206 | + | mostVisitedDays: mostVisitedDays, | |
| 207 | + | mostVisitedDaysSet: cfg.MostVisitedDaysSet, | |
| 197 | 208 | saveInitialBehaviorFn: cfg.SaveInitialBehavior, | |
| 198 | 209 | onToast: cfg.OnToast, | |
| 199 | 210 | onAccentKeyPress: cfg.OnAccentKeyPress, | |
| @@ -1590,6 +1601,8 @@ func (o *Omnibox) loadInitialHistory(token uint64) { | |||
| 1590 | 1601 | // Capture effective result limit on the GTK main thread before spawning goroutine | |
| 1591 | 1602 | initialLimit := o.effectiveMaxRows() | |
| 1592 | 1603 | initialBehavior := o.initialBehavior | |
| 1604 | + | mostVisitedDays := o.mostVisitedDays | |
| 1605 | + | mostVisitedDaysSet := o.mostVisitedDaysSet | |
| 1593 | 1606 | ||
| 1594 | 1607 | go func() { | |
| 1595 | 1608 | ctx := o.ctx | |
| @@ -1616,7 +1629,13 @@ func (o *Omnibox) loadInitialHistory(token uint64) { | |||
| 1616 | 1629 | err error | |
| 1617 | 1630 | ) | |
| 1618 | 1631 | if initialBehavior == entity.OmniboxInitialBehaviorMostVisited { | |
| 1619 | - | results, err = o.historyUC.GetMostVisited(ctx, 0) | |
| 1632 | + | if !mostVisitedDaysSet { | |
| 1633 | + | results, err = o.historyUC.GetMostVisited(ctx, defaultOmniboxMostVisitedDays) | |
| 1634 | + | } else if mostVisitedDays == 0 { | |
| 1635 | + | results, err = o.historyUC.GetMostVisited(ctx, 0) | |
| 1636 | + | } else { | |
| 1637 | + | results, err = o.historyUC.GetMostVisited(ctx, mostVisitedDays) | |
| 1638 | + | } | |
| 1620 | 1639 | if err == nil && len(results) > initialLimit { | |
| 1621 | 1640 | results = results[:initialLimit] | |
| 1622 | 1641 | } | |
| @@ -27,11 +27,12 @@ import ( | |||||
| 27 | ) | 27 | ) | ||
| 28 | 28 | ||||
| 29 | const ( | 29 | const ( | ||
| 30 | - | debounceDelayMs = 50 | 30 | + | debounceDelayMs = 50 |
| 31 | - | endBoxSpacing = 6 | 31 | + | defaultOmniboxMostVisitedDays = 30 |
| 32 | - | defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)" | 32 | + | endBoxSpacing = 6 |
| 33 | - | minGhostInputLength = 1 | 33 | + | defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)" |
| 34 | - | initialBehaviorBadgeTooltip = "Toggle default history order (Ctrl+R)" | 34 | + | minGhostInputLength = 1 |
| 35 | + | initialBehaviorBadgeTooltip = "Toggle default history order (Ctrl+R)" | |||
| 35 | ) | 36 | ) | ||
| 36 | 37 | ||||
| 37 | type favoriteRowIndicatorUpdate struct { | 38 | type favoriteRowIndicatorUpdate struct { | ||
| @@ -115,6 +116,8 @@ type Omnibox struct { | |||||
| 115 | shortcutsUC *usecase.SearchShortcutsUseCase | 116 | shortcutsUC *usecase.SearchShortcutsUseCase | ||
| 116 | defaultSearch string | 117 | defaultSearch string | ||
| 117 | initialBehavior entity.OmniboxInitialBehavior | 118 | initialBehavior entity.OmniboxInitialBehavior | ||
| 119 | + | mostVisitedDays int | |||
| 120 | + | mostVisitedDaysSet bool | |||
| 118 | saveInitialBehaviorFn func(context.Context, entity.OmniboxInitialBehavior) error | 121 | saveInitialBehaviorFn func(context.Context, entity.OmniboxInitialBehavior) error | ||
| 119 | ctx context.Context | 122 | ctx context.Context | ||
| 120 | 123 | ||||
| @@ -161,6 +164,8 @@ type OmniboxConfig struct { | |||||
| 161 | ShortcutsUC *usecase.SearchShortcutsUseCase | 164 | ShortcutsUC *usecase.SearchShortcutsUseCase | ||
| 162 | DefaultSearch string | 165 | DefaultSearch string | ||
| 163 | InitialBehavior entity.OmniboxInitialBehavior | 166 | InitialBehavior entity.OmniboxInitialBehavior | ||
| 167 | + | MostVisitedDays int | |||
| 168 | + | MostVisitedDaysSet bool | |||
| 164 | SaveInitialBehavior func(ctx context.Context, behavior entity.OmniboxInitialBehavior) error | 169 | SaveInitialBehavior func(ctx context.Context, behavior entity.OmniboxInitialBehavior) error | ||
| 165 | UIScale float64 // UI scale for favicon sizing | 170 | UIScale float64 // UI scale for favicon sizing | ||
| 166 | OnNavigate func(url string) // Callback when user navigates via omnibox | 171 | OnNavigate func(url string) // Callback when user navigates via omnibox | ||
| @@ -181,6 +186,10 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||||
| 181 | if uiScale <= 0 { | 186 | if uiScale <= 0 { | ||
| 182 | uiScale = 1.0 | 187 | uiScale = 1.0 | ||
| 183 | } | 188 | } | ||
| 189 | + | mostVisitedDays := cfg.MostVisitedDays | |||
| 190 | + | if !cfg.MostVisitedDaysSet { | |||
| 191 | + | mostVisitedDays = defaultOmniboxMostVisitedDays | |||
| 192 | + | } | |||
| 184 | 193 | ||||
| 185 | sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults) | 194 | sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults) | ||
| 186 | 195 | ||||
| @@ -194,6 +203,8 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||||
| 194 | shortcutsUC: cfg.ShortcutsUC, | 203 | shortcutsUC: cfg.ShortcutsUC, | ||
| 195 | defaultSearch: cfg.DefaultSearch, | 204 | defaultSearch: cfg.DefaultSearch, | ||
| 196 | initialBehavior: cfg.InitialBehavior, | 205 | initialBehavior: cfg.InitialBehavior, | ||
| 206 | + | mostVisitedDays: mostVisitedDays, | |||
| 207 | + | mostVisitedDaysSet: cfg.MostVisitedDaysSet, | |||
| 197 | saveInitialBehaviorFn: cfg.SaveInitialBehavior, | 208 | saveInitialBehaviorFn: cfg.SaveInitialBehavior, | ||
| 198 | onToast: cfg.OnToast, | 209 | onToast: cfg.OnToast, | ||
| 199 | onAccentKeyPress: cfg.OnAccentKeyPress, | 210 | onAccentKeyPress: cfg.OnAccentKeyPress, | ||
| @@ -1590,6 +1601,8 @@ func (o *Omnibox) loadInitialHistory(token uint64) { | |||||
| 1590 | // Capture effective result limit on the GTK main thread before spawning goroutine | 1601 | // Capture effective result limit on the GTK main thread before spawning goroutine | ||
| 1591 | initialLimit := o.effectiveMaxRows() | 1602 | initialLimit := o.effectiveMaxRows() | ||
| 1592 | initialBehavior := o.initialBehavior | 1603 | initialBehavior := o.initialBehavior | ||
| 1604 | + | mostVisitedDays := o.mostVisitedDays | |||
| 1605 | + | mostVisitedDaysSet := o.mostVisitedDaysSet | |||
| 1593 | 1606 | ||||
| 1594 | go func() { | 1607 | go func() { | ||
| 1595 | ctx := o.ctx | 1608 | ctx := o.ctx | ||
| @@ -1616,7 +1629,13 @@ func (o *Omnibox) loadInitialHistory(token uint64) { | |||||
| 1616 | err error | 1629 | err error | ||
| 1617 | ) | 1630 | ) | ||
| 1618 | if initialBehavior == entity.OmniboxInitialBehaviorMostVisited { | 1631 | if initialBehavior == entity.OmniboxInitialBehaviorMostVisited { | ||
| 1619 | - | results, err = o.historyUC.GetMostVisited(ctx, 0) | 1632 | + | if !mostVisitedDaysSet { |
| 1633 | + | results, err = o.historyUC.GetMostVisited(ctx, defaultOmniboxMostVisitedDays) | |||
| 1634 | + | } else if mostVisitedDays == 0 { | |||
| 1635 | + | results, err = o.historyUC.GetMostVisited(ctx, 0) | |||
| 1636 | + | } else { | |||
| 1637 | + | results, err = o.historyUC.GetMostVisited(ctx, mostVisitedDays) | |||
| 1638 | + | } | |||
| 1620 | if err == nil && len(results) > initialLimit { | 1639 | if err == nil && len(results) > initialLimit { | ||
| 1621 | results = results[:initialLimit] | 1640 | results = results[:initialLimit] | ||
| 1622 | } | 1641 | } | ||
M internal/ui/component/omnibox_initial_behavior_test.go +79 -0
| @@ -178,3 +178,82 @@ func TestOmniboxLoadInitialHistory_UsesCapturedInitialBehavior(t *testing.T) { | |||
| 178 | 178 | t.Fatal("timed out waiting for recent-history load") | |
| 179 | 179 | } | |
| 180 | 180 | } | |
| 181 | + | ||
| 182 | + | func TestOmniboxLoadInitialHistory_MostVisitedUsesThirtyDayWindow(t *testing.T) { | |
| 183 | + | repo := repomocks.NewMockHistoryRepository(t) | |
| 184 | + | done := make(chan struct{}) | |
| 185 | + | repo.EXPECT().GetMostVisited(mock.Anything, 30).RunAndReturn( | |
| 186 | + | func(context.Context, int) ([]*entity.HistoryEntry, error) { | |
| 187 | + | close(done) | |
| 188 | + | return []*entity.HistoryEntry{{URL: "https://example.com"}}, nil | |
| 189 | + | }, | |
| 190 | + | ) | |
| 191 | + | ||
| 192 | + | o := &Omnibox{ | |
| 193 | + | historyUC: appusecase.NewSearchHistoryUseCase(repo), | |
| 194 | + | initialBehavior: entity.OmniboxInitialBehaviorMostVisited, | |
| 195 | + | ctx: context.Background(), | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | o.loadInitialHistory(1) | |
| 199 | + | ||
| 200 | + | select { | |
| 201 | + | case <-done: | |
| 202 | + | case <-time.After(2 * time.Second): | |
| 203 | + | t.Fatal("timed out waiting for most-visited load") | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | func TestOmniboxLoadInitialHistory_MostVisitedUsesConfiguredWindow(t *testing.T) { | |
| 208 | + | repo := repomocks.NewMockHistoryRepository(t) | |
| 209 | + | done := make(chan struct{}) | |
| 210 | + | repo.EXPECT().GetMostVisited(mock.Anything, 7).RunAndReturn( | |
| 211 | + | func(context.Context, int) ([]*entity.HistoryEntry, error) { | |
| 212 | + | close(done) | |
| 213 | + | return []*entity.HistoryEntry{{URL: "https://example.com"}}, nil | |
| 214 | + | }, | |
| 215 | + | ) | |
| 216 | + | ||
| 217 | + | o := &Omnibox{ | |
| 218 | + | historyUC: appusecase.NewSearchHistoryUseCase(repo), | |
| 219 | + | initialBehavior: entity.OmniboxInitialBehaviorMostVisited, | |
| 220 | + | mostVisitedDays: 7, | |
| 221 | + | mostVisitedDaysSet: true, | |
| 222 | + | ctx: context.Background(), | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | o.loadInitialHistory(1) | |
| 226 | + | ||
| 227 | + | select { | |
| 228 | + | case <-done: | |
| 229 | + | case <-time.After(2 * time.Second): | |
| 230 | + | t.Fatal("timed out waiting for configured most-visited load") | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | func TestOmniboxLoadInitialHistory_MostVisitedZeroWindowUsesAllHistory(t *testing.T) { | |
| 235 | + | repo := repomocks.NewMockHistoryRepository(t) | |
| 236 | + | done := make(chan struct{}) | |
| 237 | + | repo.EXPECT().GetAllMostVisited(mock.Anything).RunAndReturn( | |
| 238 | + | func(context.Context) ([]*entity.HistoryEntry, error) { | |
| 239 | + | close(done) | |
| 240 | + | return []*entity.HistoryEntry{{URL: "https://example.com"}}, nil | |
| 241 | + | }, | |
| 242 | + | ) | |
| 243 | + | ||
| 244 | + | o := &Omnibox{ | |
| 245 | + | historyUC: appusecase.NewSearchHistoryUseCase(repo), | |
| 246 | + | initialBehavior: entity.OmniboxInitialBehaviorMostVisited, | |
| 247 | + | mostVisitedDays: 0, | |
| 248 | + | mostVisitedDaysSet: true, | |
| 249 | + | ctx: context.Background(), | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | o.loadInitialHistory(1) | |
| 253 | + | ||
| 254 | + | select { | |
| 255 | + | case <-done: | |
| 256 | + | case <-time.After(2 * time.Second): | |
| 257 | + | t.Fatal("timed out waiting for all-history most-visited load") | |
| 258 | + | } | |
| 259 | + | } | |
| @@ -178,3 +178,82 @@ func TestOmniboxLoadInitialHistory_UsesCapturedInitialBehavior(t *testing.T) { | |||||
| 178 | t.Fatal("timed out waiting for recent-history load") | 178 | t.Fatal("timed out waiting for recent-history load") | ||
| 179 | } | 179 | } | ||
| 180 | } | 180 | } | ||
| 181 | + | ||||
| 182 | + | func TestOmniboxLoadInitialHistory_MostVisitedUsesThirtyDayWindow(t *testing.T) { | |||
| 183 | + | repo := repomocks.NewMockHistoryRepository(t) | |||
| 184 | + | done := make(chan struct{}) | |||
| 185 | + | repo.EXPECT().GetMostVisited(mock.Anything, 30).RunAndReturn( | |||
| 186 | + | func(context.Context, int) ([]*entity.HistoryEntry, error) { | |||
| 187 | + | close(done) | |||
| 188 | + | return []*entity.HistoryEntry{{URL: "https://example.com"}}, nil | |||
| 189 | + | }, | |||
| 190 | + | ) | |||
| 191 | + | ||||
| 192 | + | o := &Omnibox{ | |||
| 193 | + | historyUC: appusecase.NewSearchHistoryUseCase(repo), | |||
| 194 | + | initialBehavior: entity.OmniboxInitialBehaviorMostVisited, | |||
| 195 | + | ctx: context.Background(), | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | o.loadInitialHistory(1) | |||
| 199 | + | ||||
| 200 | + | select { | |||
| 201 | + | case <-done: | |||
| 202 | + | case <-time.After(2 * time.Second): | |||
| 203 | + | t.Fatal("timed out waiting for most-visited load") | |||
| 204 | + | } | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | func TestOmniboxLoadInitialHistory_MostVisitedUsesConfiguredWindow(t *testing.T) { | |||
| 208 | + | repo := repomocks.NewMockHistoryRepository(t) | |||
| 209 | + | done := make(chan struct{}) | |||
| 210 | + | repo.EXPECT().GetMostVisited(mock.Anything, 7).RunAndReturn( | |||
| 211 | + | func(context.Context, int) ([]*entity.HistoryEntry, error) { | |||
| 212 | + | close(done) | |||
| 213 | + | return []*entity.HistoryEntry{{URL: "https://example.com"}}, nil | |||
| 214 | + | }, | |||
| 215 | + | ) | |||
| 216 | + | ||||
| 217 | + | o := &Omnibox{ | |||
| 218 | + | historyUC: appusecase.NewSearchHistoryUseCase(repo), | |||
| 219 | + | initialBehavior: entity.OmniboxInitialBehaviorMostVisited, | |||
| 220 | + | mostVisitedDays: 7, | |||
| 221 | + | mostVisitedDaysSet: true, | |||
| 222 | + | ctx: context.Background(), | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | o.loadInitialHistory(1) | |||
| 226 | + | ||||
| 227 | + | select { | |||
| 228 | + | case <-done: | |||
| 229 | + | case <-time.After(2 * time.Second): | |||
| 230 | + | t.Fatal("timed out waiting for configured most-visited load") | |||
| 231 | + | } | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | func TestOmniboxLoadInitialHistory_MostVisitedZeroWindowUsesAllHistory(t *testing.T) { | |||
| 235 | + | repo := repomocks.NewMockHistoryRepository(t) | |||
| 236 | + | done := make(chan struct{}) | |||
| 237 | + | repo.EXPECT().GetAllMostVisited(mock.Anything).RunAndReturn( | |||
| 238 | + | func(context.Context) ([]*entity.HistoryEntry, error) { | |||
| 239 | + | close(done) | |||
| 240 | + | return []*entity.HistoryEntry{{URL: "https://example.com"}}, nil | |||
| 241 | + | }, | |||
| 242 | + | ) | |||
| 243 | + | ||||
| 244 | + | o := &Omnibox{ | |||
| 245 | + | historyUC: appusecase.NewSearchHistoryUseCase(repo), | |||
| 246 | + | initialBehavior: entity.OmniboxInitialBehaviorMostVisited, | |||
| 247 | + | mostVisitedDays: 0, | |||
| 248 | + | mostVisitedDaysSet: true, | |||
| 249 | + | ctx: context.Background(), | |||
| 250 | + | } | |||
| 251 | + | ||||
| 252 | + | o.loadInitialHistory(1) | |||
| 253 | + | ||||
| 254 | + | select { | |||
| 255 | + | case <-done: | |||
| 256 | + | case <-time.After(2 * time.Second): | |||
| 257 | + | t.Fatal("timed out waiting for all-history most-visited load") | |||
| 258 | + | } | |||
| 259 | + | } | |||