detached acc7e1e

feat(omnibox): add terminal-inspired visual styles (#437)

* feat(omnibox): add terminal-inspired visual styles

* test(config): cover omnibox style migration

* fix(omnibox): refine passive status styling

* fix(config): reject empty omnibox styles

* fix(omnibox): align terminal style indicators
commit
acc7e1ee079bb4665eaefa5b35c07fb41081e822
author
bnema · · 2026-09-14 19:57 UTC
committer
GitHub · · 2026-09-14 19:57 UTC
parent
5e1311a
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
20 files · +315 -50
M CHANGELOG.md +2 -0
@@ -6,12 +6,14 @@ All notable changes to this project will be documented in this file.
66
77 ### Added
88
9+- **Omnibox visual styles**: Added configurable `command`, `multiplexer`, and `minimal` GTK treatments. The terminal-inspired `command` palette is the default.
910 - **Vim mode for keyboard-driven page navigation**: Added configurable multi-key sequences for page scrolling, fast scrolling, page-input focus, and live heading navigation. Pressing `Enter` on a selected CEF heading opens its associated link before leaving Vim Mode. CEF accessibility data also provides the foundation for navigating structured page content such as headings, lists, paragraphs, and sections.
1011 - **Native favorites sidebar**: Added a keyboard-driven GTK sidebar toggled with `Ctrl+B`, including search, tag filtering, tag creation, favorite editing, shortcut assignment, deletion, and opening favorites in the current pane or a new split.
1112 - **WebP favicon decoding**: Added safe WebP decoding for sites that publish WebP favicons.
1213
1314 ### Changed
1415
16+- **Omnibox status indicators**: The history-order control is now a passive status badge switched with `Ctrl+R`, and favorites use a terminal-style `*` marker.
1517 - **Favorites organization**: Favorites now use a tags-first model. Existing folder assignments are migrated into deterministic tags, with collision handling and folded-name uniqueness preserved during migration.
1618 - **Page mode configuration**: Page Mode is now called Vim Mode and uses the `workspace.vim_mode` configuration namespace for activation, timeouts, and action sequences. Existing page-mode bindings are migrated by the configuration loader.
1719 - **Browsing-context routing**: External links, floating panes, popups, and standalone omnibox launches now use explicit browsing-context ownership so they open in the appropriate pane or browser window.
@@ -6,12 +6,14 @@ All notable changes to this project will be documented in this file.
6 6
7 ### Added7 ### Added
8 8
9+- **Omnibox visual styles**: Added configurable `command`, `multiplexer`, and `minimal` GTK treatments. The terminal-inspired `command` palette is the default.
9 - **Vim mode for keyboard-driven page navigation**: Added configurable multi-key sequences for page scrolling, fast scrolling, page-input focus, and live heading navigation. Pressing `Enter` on a selected CEF heading opens its associated link before leaving Vim Mode. CEF accessibility data also provides the foundation for navigating structured page content such as headings, lists, paragraphs, and sections.10 - **Vim mode for keyboard-driven page navigation**: Added configurable multi-key sequences for page scrolling, fast scrolling, page-input focus, and live heading navigation. Pressing `Enter` on a selected CEF heading opens its associated link before leaving Vim Mode. CEF accessibility data also provides the foundation for navigating structured page content such as headings, lists, paragraphs, and sections.
10 - **Native favorites sidebar**: Added a keyboard-driven GTK sidebar toggled with `Ctrl+B`, including search, tag filtering, tag creation, favorite editing, shortcut assignment, deletion, and opening favorites in the current pane or a new split.11 - **Native favorites sidebar**: Added a keyboard-driven GTK sidebar toggled with `Ctrl+B`, including search, tag filtering, tag creation, favorite editing, shortcut assignment, deletion, and opening favorites in the current pane or a new split.
11 - **WebP favicon decoding**: Added safe WebP decoding for sites that publish WebP favicons.12 - **WebP favicon decoding**: Added safe WebP decoding for sites that publish WebP favicons.
12 13
13 ### Changed14 ### Changed
14 15
16+- **Omnibox status indicators**: The history-order control is now a passive status badge switched with `Ctrl+R`, and favorites use a terminal-style `*` marker.
15 - **Favorites organization**: Favorites now use a tags-first model. Existing folder assignments are migrated into deterministic tags, with collision handling and folded-name uniqueness preserved during migration.17 - **Favorites organization**: Favorites now use a tags-first model. Existing folder assignments are migrated into deterministic tags, with collision handling and folded-name uniqueness preserved during migration.
16 - **Page mode configuration**: Page Mode is now called Vim Mode and uses the `workspace.vim_mode` configuration namespace for activation, timeouts, and action sequences. Existing page-mode bindings are migrated by the configuration loader.18 - **Page mode configuration**: Page Mode is now called Vim Mode and uses the `workspace.vim_mode` configuration namespace for activation, timeouts, and action sequences. Existing page-mode bindings are migrated by the configuration loader.
17 - **Browsing-context routing**: External links, floating panes, popups, and standalone omnibox launches now use explicit browsing-context ownership so they open in the appropriate pane or browser window.19 - **Browsing-context routing**: External links, floating panes, popups, and standalone omnibox launches now use explicit browsing-context ownership so they open in the appropriate pane or browser window.
M docs/config/index.md +9 -1
@@ -103,17 +103,25 @@ dumber dmenu --days 14 --most-visited
103103
104104 | Key | Type | Default | Valid Values | Description |
105105 |-----|------|---------|--------------|-------------|
106-| `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior |
106+| `omnibox.style` | string | `"command"` | `multiplexer`, `command`, `minimal` | Omnibox visual treatment |
107+| `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior; press `Ctrl+R` in the omnibox to switch |
107108 | `omnibox.most_visited_days` | int | `30` | `>= 0` | Days of history to consider when `initial_behavior = "most_visited"` (`0` = all history) |
108109 | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane |
109110
111+Changing `omnibox.style` takes effect after restarting Dumber.
112+
110113 **Example:**
111114 ```toml
112115 [omnibox]
116+style = "command" # Soft terminal-inspired command palette
113117 initial_behavior = "recent" # Show recent history when omnibox opens
114118 most_visited_days = 30 # Days of history used for most_visited
115119 auto_open_on_new_pane = false
116120
121+# Alternative visual treatments:
122+# style = "multiplexer" # Segmented terminal multiplexer treatment
123+# style = "minimal" # Bare terminal typography and hairlines
124+
117125 # Alternative options:
118126 # initial_behavior = "most_visited" # Show most visited sites
119127 # initial_behavior = "none" # Show no initial suggestions
@@ -103,17 +103,25 @@ dumber dmenu --days 14 --most-visited
103 103
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.style` | string | `"command"` | `multiplexer`, `command`, `minimal` | Omnibox visual treatment |
107+| `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior; press `Ctrl+R` in the omnibox to switch |
107 | `omnibox.most_visited_days` | int | `30` | `>= 0` | Days of history to consider when `initial_behavior = "most_visited"` (`0` = all history) |108 | `omnibox.most_visited_days` | int | `30` | `>= 0` | Days of history to consider when `initial_behavior = "most_visited"` (`0` = all history) |
108 | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane |109 | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane |
109 110
111+Changing `omnibox.style` takes effect after restarting Dumber.
112+
110 **Example:**113 **Example:**
111 ```toml114 ```toml
112 [omnibox]115 [omnibox]
116+style = "command" # Soft terminal-inspired command palette
113 initial_behavior = "recent" # Show recent history when omnibox opens117 initial_behavior = "recent" # Show recent history when omnibox opens
114 most_visited_days = 30 # Days of history used for most_visited118 most_visited_days = 30 # Days of history used for most_visited
115 auto_open_on_new_pane = false119 auto_open_on_new_pane = false
116 120
121+# Alternative visual treatments:
122+# style = "multiplexer" # Segmented terminal multiplexer treatment
123+# style = "minimal" # Bare terminal typography and hairlines
124+
117 # Alternative options:125 # Alternative options:
118 # initial_behavior = "most_visited" # Show most visited sites126 # initial_behavior = "most_visited" # Show most visited sites
119 # initial_behavior = "none" # Show no initial suggestions127 # initial_behavior = "none" # Show no initial suggestions
M docs/reference/configuration.md +1 -0
@@ -19,6 +19,7 @@
1919 | `dmenu.url_prefix` | string | `🌐` | |
2020 | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format |
2121 | `dmenu.sort_by_visit_count` | bool | `true` | |
22+| `omnibox.style` | string | `command` | `multiplexer`, `command`, `minimal` |
2223 | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` |
2324 | `omnibox.most_visited_days` | int | `30` | `>= 0` |
2425 | `omnibox.auto_open_on_new_pane` | bool | `false` | |
@@ -19,6 +19,7 @@
19 | `dmenu.url_prefix` | string | `🌐` | |19 | `dmenu.url_prefix` | string | `🌐` | |
20 | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format |20 | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format |
21 | `dmenu.sort_by_visit_count` | bool | `true` | |21 | `dmenu.sort_by_visit_count` | bool | `true` | |
22+| `omnibox.style` | string | `command` | `multiplexer`, `command`, `minimal` |
22 | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` |23 | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` |
23 | `omnibox.most_visited_days` | int | `30` | `>= 0` |24 | `omnibox.most_visited_days` | int | `30` | `>= 0` |
24 | `omnibox.auto_open_on_new_pane` | bool | `false` | |25 | `omnibox.auto_open_on_new_pane` | bool | `false` | |
M internal/bootstrap/runtime_config.go +1 -0
@@ -96,6 +96,7 @@ func RuntimeConfigSnapshotFromConfig(cfg *config.Config) entity.RuntimeConfigSna
9696 SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts),
9797 DefaultSearchEngine: cfg.DefaultSearchEngine,
9898 Omnibox: entity.RuntimeOmniboxConfig{
99+ Style: cfg.Omnibox.Style,
99100 InitialBehavior: cfg.Omnibox.InitialBehavior,
100101 MostVisitedDays: cfg.Omnibox.MostVisitedDays,
101102 AutoOpenOnNewPane: cfg.Omnibox.AutoOpenOnNewPane,
@@ -96,6 +96,7 @@ func RuntimeConfigSnapshotFromConfig(cfg *config.Config) entity.RuntimeConfigSna
96 SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts),96 SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts),
97 DefaultSearchEngine: cfg.DefaultSearchEngine,97 DefaultSearchEngine: cfg.DefaultSearchEngine,
98 Omnibox: entity.RuntimeOmniboxConfig{98 Omnibox: entity.RuntimeOmniboxConfig{
99+ Style: cfg.Omnibox.Style,
99 InitialBehavior: cfg.Omnibox.InitialBehavior,100 InitialBehavior: cfg.Omnibox.InitialBehavior,
100 MostVisitedDays: cfg.Omnibox.MostVisitedDays,101 MostVisitedDays: cfg.Omnibox.MostVisitedDays,
101 AutoOpenOnNewPane: cfg.Omnibox.AutoOpenOnNewPane,102 AutoOpenOnNewPane: cfg.Omnibox.AutoOpenOnNewPane,
M internal/domain/entity/runtime_config.go +1 -0
@@ -64,6 +64,7 @@ type RuntimeSearchShortcut struct {
6464 }
6565
6666 type RuntimeOmniboxConfig struct {
67+ Style string
6768 InitialBehavior OmniboxInitialBehavior
6869 MostVisitedDays int
6970 AutoOpenOnNewPane bool
@@ -64,6 +64,7 @@ type RuntimeSearchShortcut struct {
64 }64 }
65 65
66 type RuntimeOmniboxConfig struct {66 type RuntimeOmniboxConfig struct {
67+ Style string
67 InitialBehavior OmniboxInitialBehavior68 InitialBehavior OmniboxInitialBehavior
68 MostVisitedDays int69 MostVisitedDays int
69 AutoOpenOnNewPane bool70 AutoOpenOnNewPane bool
M internal/infrastructure/config/defaults.go +2 -0
@@ -33,6 +33,7 @@ const (
3333 defaultNewPaneURL = "about:blank"
3434
3535 // Omnibox defaults
36+ defaultOmniboxStyle = "command"
3637 defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent
3738 defaultOmniboxMostVisitedDays = 30
3839 defaultOmniboxAutoOpenOnNewPane = false
@@ -405,6 +406,7 @@ func DefaultConfig() *Config {
405406 AutoCopyOnSelection: true, // Enabled by default (zellij-style)
406407 },
407408 Omnibox: OmniboxConfig{
409+ Style: defaultOmniboxStyle,
408410 InitialBehavior: defaultOmniboxInitialBehavior,
409411 MostVisitedDays: defaultOmniboxMostVisitedDays,
410412 AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane,
@@ -33,6 +33,7 @@ const (
33 defaultNewPaneURL = "about:blank"33 defaultNewPaneURL = "about:blank"
34 34
35 // Omnibox defaults35 // Omnibox defaults
36+ defaultOmniboxStyle = "command"
36 defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent37 defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent
37 defaultOmniboxMostVisitedDays = 3038 defaultOmniboxMostVisitedDays = 30
38 defaultOmniboxAutoOpenOnNewPane = false39 defaultOmniboxAutoOpenOnNewPane = false
@@ -405,6 +406,7 @@ func DefaultConfig() *Config {
405 AutoCopyOnSelection: true, // Enabled by default (zellij-style)406 AutoCopyOnSelection: true, // Enabled by default (zellij-style)
406 },407 },
407 Omnibox: OmniboxConfig{408 Omnibox: OmniboxConfig{
409+ Style: defaultOmniboxStyle,
408 InitialBehavior: defaultOmniboxInitialBehavior,410 InitialBehavior: defaultOmniboxInitialBehavior,
409 MostVisitedDays: defaultOmniboxMostVisitedDays,411 MostVisitedDays: defaultOmniboxMostVisitedDays,
410 AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane,412 AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane,
M internal/infrastructure/config/loader.go +1 -0
@@ -753,6 +753,7 @@ func (m *Manager) setClipboardDefaults(defaults *Config) {
753753 }
754754
755755 func (m *Manager) setOmniboxDefaults(defaults *Config) {
756+ m.viper.SetDefault("omnibox.style", defaults.Omnibox.Style)
756757 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
757758 m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays)
758759 m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
@@ -753,6 +753,7 @@ func (m *Manager) setClipboardDefaults(defaults *Config) {
753 }753 }
754 754
755 func (m *Manager) setOmniboxDefaults(defaults *Config) {755 func (m *Manager) setOmniboxDefaults(defaults *Config) {
756+ m.viper.SetDefault("omnibox.style", defaults.Omnibox.Style)
756 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)757 m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior)
757 m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays)758 m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays)
758 m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)759 m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane)
M internal/infrastructure/config/migrate_test.go +1 -0
@@ -39,6 +39,7 @@ func TestMigrator_GetAllDefaultKeys(t *testing.T) {
3939 assert.True(t, keySet["history.max_entries"], "should include history.max_entries")
4040 assert.True(t, keySet["logging.level"], "should include logging.level")
4141 assert.True(t, keySet["appearance.sans_font"], "should include appearance.sans_font")
42+ assert.True(t, keySet["omnibox.style"], "should include omnibox.style for config migrate")
4243 }
4344
4445 func TestMigrator_GetUserConfigKeys(t *testing.T) {
@@ -39,6 +39,7 @@ func TestMigrator_GetAllDefaultKeys(t *testing.T) {
39 assert.True(t, keySet["history.max_entries"], "should include history.max_entries")39 assert.True(t, keySet["history.max_entries"], "should include history.max_entries")
40 assert.True(t, keySet["logging.level"], "should include logging.level")40 assert.True(t, keySet["logging.level"], "should include logging.level")
41 assert.True(t, keySet["appearance.sans_font"], "should include appearance.sans_font")41 assert.True(t, keySet["appearance.sans_font"], "should include appearance.sans_font")
42+ assert.True(t, keySet["omnibox.style"], "should include omnibox.style for config migrate")
42 }43 }
43 44
44 func TestMigrator_GetUserConfigKeys(t *testing.T) {45 func TestMigrator_GetUserConfigKeys(t *testing.T) {
M internal/infrastructure/config/schema.go +2 -0
@@ -389,6 +389,8 @@ type ClipboardConfig struct {
389389
390390 // OmniboxConfig holds omnibox behavior preferences
391391 type OmniboxConfig struct {
392+ // Style selects the omnibox visual treatment: "multiplexer", "command", or "minimal".
393+ Style string `mapstructure:"style" yaml:"style" toml:"style"`
392394 // InitialBehavior controls what to show when omnibox opens with empty input
393395 // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing)
394396 InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"`
@@ -389,6 +389,8 @@ type ClipboardConfig struct {
389 389
390 // OmniboxConfig holds omnibox behavior preferences390 // OmniboxConfig holds omnibox behavior preferences
391 type OmniboxConfig struct {391 type OmniboxConfig struct {
392+ // Style selects the omnibox visual treatment: "multiplexer", "command", or "minimal".
393+ Style string `mapstructure:"style" yaml:"style" toml:"style"`
392 // InitialBehavior controls what to show when omnibox opens with empty input394 // InitialBehavior controls what to show when omnibox opens with empty input
393 // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing)395 // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing)
394 InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"`396 InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"`
M internal/infrastructure/config/schema_provider.go +8 -0
@@ -779,6 +779,14 @@ func (*SchemaProvider) getSessionKeys(defaults *Config) []entity.ConfigKeyInfo {
779779
780780 func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo {
781781 return []entity.ConfigKeyInfo{
782+ {
783+ Key: "omnibox.style",
784+ Type: "string",
785+ Default: defaults.Omnibox.Style,
786+ Description: "Visual treatment for the omnibox",
787+ Values: []string{"multiplexer", "command", "minimal"},
788+ Section: SectionOmnibox,
789+ },
782790 {
783791 Key: "omnibox.initial_behavior",
784792 Type: "string",
@@ -779,6 +779,14 @@ func (*SchemaProvider) getSessionKeys(defaults *Config) []entity.ConfigKeyInfo {
779 779
780 func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo {780 func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo {
781 return []entity.ConfigKeyInfo{781 return []entity.ConfigKeyInfo{
782+ {
783+ Key: "omnibox.style",
784+ Type: "string",
785+ Default: defaults.Omnibox.Style,
786+ Description: "Visual treatment for the omnibox",
787+ Values: []string{"multiplexer", "command", "minimal"},
788+ Section: SectionOmnibox,
789+ },
782 {790 {
783 Key: "omnibox.initial_behavior",791 Key: "omnibox.initial_behavior",
784 Type: "string",792 Type: "string",
M internal/infrastructure/config/validation.go +8 -0
@@ -646,6 +646,14 @@ func looksLikeBareRelativeWorkspacePath(value string) bool {
646646
647647 func validateOmnibox(config *Config) []string {
648648 var validationErrors []string
649+ switch config.Omnibox.Style {
650+ case "multiplexer", "command", "minimal":
651+ default:
652+ validationErrors = append(validationErrors, fmt.Sprintf(
653+ "omnibox.style must be one of: multiplexer, command, minimal (got: %s)",
654+ config.Omnibox.Style,
655+ ))
656+ }
649657 if config.Omnibox.MostVisitedDays < 0 {
650658 validationErrors = append(validationErrors, "omnibox.most_visited_days must be non-negative")
651659 }
@@ -646,6 +646,14 @@ func looksLikeBareRelativeWorkspacePath(value string) bool {
646 646
647 func validateOmnibox(config *Config) []string {647 func validateOmnibox(config *Config) []string {
648 var validationErrors []string648 var validationErrors []string
649+ switch config.Omnibox.Style {
650+ case "multiplexer", "command", "minimal":
651+ default:
652+ validationErrors = append(validationErrors, fmt.Sprintf(
653+ "omnibox.style must be one of: multiplexer, command, minimal (got: %s)",
654+ config.Omnibox.Style,
655+ ))
656+ }
649 if config.Omnibox.MostVisitedDays < 0 {657 if config.Omnibox.MostVisitedDays < 0 {
650 validationErrors = append(validationErrors, "omnibox.most_visited_days must be non-negative")658 validationErrors = append(validationErrors, "omnibox.most_visited_days must be non-negative")
651 }659 }
M internal/infrastructure/config/validation_test.go +27 -0
@@ -10,6 +10,33 @@ import (
1010 "github.com/stretchr/testify/require"
1111 )
1212
13+func TestValidateConfig_OmniboxStyle(t *testing.T) {
14+ tests := []struct {
15+ style string
16+ wantErr bool
17+ }{
18+ {style: "command"},
19+ {style: "multiplexer"},
20+ {style: "minimal"},
21+ {style: "", wantErr: true},
22+ {style: "classic", wantErr: true},
23+ }
24+
25+ for _, tt := range tests {
26+ t.Run(tt.style, func(t *testing.T) {
27+ cfg := DefaultConfig()
28+ cfg.Omnibox.Style = tt.style
29+ err := validateConfig(cfg)
30+ if tt.wantErr {
31+ require.Error(t, err)
32+ assert.Contains(t, err.Error(), "omnibox.style")
33+ return
34+ }
35+ require.NoError(t, err)
36+ })
37+ }
38+}
39+
1340 func TestValidateConfig_EngineType(t *testing.T) {
1441 t.Setenv("DUMBER_ENGINE", "")
1542
@@ -10,6 +10,33 @@ import (
10 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
11 )11 )
12 12
13+func TestValidateConfig_OmniboxStyle(t *testing.T) {
14+ tests := []struct {
15+ style string
16+ wantErr bool
17+ }{
18+ {style: "command"},
19+ {style: "multiplexer"},
20+ {style: "minimal"},
21+ {style: "", wantErr: true},
22+ {style: "classic", wantErr: true},
23+ }
24+
25+ for _, tt := range tests {
26+ t.Run(tt.style, func(t *testing.T) {
27+ cfg := DefaultConfig()
28+ cfg.Omnibox.Style = tt.style
29+ err := validateConfig(cfg)
30+ if tt.wantErr {
31+ require.Error(t, err)
32+ assert.Contains(t, err.Error(), "omnibox.style")
33+ return
34+ }
35+ require.NoError(t, err)
36+ })
37+ }
38+}
39+
13 func TestValidateConfig_EngineType(t *testing.T) {40 func TestValidateConfig_EngineType(t *testing.T) {
14 t.Setenv("DUMBER_ENGINE", "")41 t.Setenv("DUMBER_ENGINE", "")
15 42
M internal/ui/app.go +1 -0
@@ -1309,6 +1309,7 @@ func buildOmniboxConfig(
13091309 }
13101310
13111311 return component.OmniboxConfig{
1312+ Style: runtimeCfg.Omnibox.Style,
13121313 HistoryUC: deps.HistoryUC,
13131314 FavoritesUC: deps.FavoritesUC,
13141315 FaviconAdapter: faviconAdapter,
@@ -1309,6 +1309,7 @@ func buildOmniboxConfig(
1309 }1309 }
1310 1310
1311 return component.OmniboxConfig{1311 return component.OmniboxConfig{
1312+ Style: runtimeCfg.Omnibox.Style,
1312 HistoryUC: deps.HistoryUC,1313 HistoryUC: deps.HistoryUC,
1313 FavoritesUC: deps.FavoritesUC,1314 FavoritesUC: deps.FavoritesUC,
1314 FaviconAdapter: faviconAdapter,1315 FaviconAdapter: faviconAdapter,
M internal/ui/app_omnibox_config_test.go +4 -0
@@ -19,6 +19,7 @@ func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) {
1919 },
2020 },
2121 Omnibox: entity.RuntimeOmniboxConfig{
22+ Style: "command",
2223 InitialBehavior: entity.OmniboxInitialBehaviorMostVisited,
2324 MostVisitedDays: 7,
2425 },
@@ -29,6 +30,9 @@ func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) {
2930 return "normalized:" + input
3031 },
3132 })
33+ if got.Style != "command" {
34+ t.Fatalf("Style = %q, want command", got.Style)
35+ }
3236 if got.MostVisitedDays != 7 {
3337 t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays)
3438 }
@@ -19,6 +19,7 @@ func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) {
19 },19 },
20 },20 },
21 Omnibox: entity.RuntimeOmniboxConfig{21 Omnibox: entity.RuntimeOmniboxConfig{
22+ Style: "command",
22 InitialBehavior: entity.OmniboxInitialBehaviorMostVisited,23 InitialBehavior: entity.OmniboxInitialBehaviorMostVisited,
23 MostVisitedDays: 7,24 MostVisitedDays: 7,
24 },25 },
@@ -29,6 +30,9 @@ func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) {
29 return "normalized:" + input30 return "normalized:" + input
30 },31 },
31 })32 })
33+ if got.Style != "command" {
34+ t.Fatalf("Style = %q, want command", got.Style)
35+ }
32 if got.MostVisitedDays != 7 {36 if got.MostVisitedDays != 7 {
33 t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays)37 t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays)
34 }38 }
M internal/ui/component/omnibox.go +21 -24
@@ -31,7 +31,7 @@ const (
3131 endBoxSpacing = 6
3232 defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)"
3333 minGhostInputLength = 1
34- initialBehaviorBadgeTooltip = "Toggle default history order (Ctrl+R)"
34+ initialBehaviorBadgeTooltip = "Default history order · Ctrl+R to switch"
3535 )
3636
3737 type favoriteRowIndicatorUpdate struct {
@@ -140,7 +140,7 @@ type Omnibox struct {
140140 outerBox *gtk.Box // Outer container for positioning
141141 mainBox *gtk.Box // Main content box
142142 headerBox *gtk.Box
143- initialBehaviorBadge *gtk.Button
143+ initialBehaviorBadge *gtk.Label
144144 historyBtn *gtk.Button
145145 favoritesBtn *gtk.Button
146146 zoomLabel *gtk.Label
@@ -151,6 +151,7 @@ type Omnibox struct {
151151
152152 // Parent overlay reference for sizing (set via SetParentOverlay)
153153 parentOverlay layout.OverlayWidget
154+ style string
154155
155156 // State
156157 mu sync.RWMutex
@@ -224,6 +225,7 @@ type Omnibox struct {
224225
225226 // OmniboxConfig holds configuration for creating an Omnibox.
226227 type OmniboxConfig struct {
228+ Style string
227229 HistoryUC *usecase.SearchHistoryUseCase
228230 FavoritesUC *usecase.ManageFavoritesUseCase
229231 FaviconAdapter *adapter.FaviconAdapter
@@ -257,6 +259,7 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox {
257259 sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults)
258260
259261 o := &Omnibox{
262+ style: cfg.Style,
260263 viewMode: ViewModeHistory,
261264 selectedIndex: -1,
262265 historyUC: cfg.HistoryUC,
@@ -509,7 +512,7 @@ func (o *Omnibox) refreshInitialBehaviorBadge() {
509512 if !state.visible {
510513 return
511514 }
512- o.initialBehaviorBadge.SetLabel(state.label)
515+ o.initialBehaviorBadge.SetText(state.label)
513516 tooltip := state.tooltip
514517 o.initialBehaviorBadge.SetTooltipText(&tooltip)
515518 }
@@ -754,6 +757,11 @@ func (o *Omnibox) initMainBox() error {
754757 return errNilWidget("mainBox")
755758 }
756759 o.mainBox.AddCssClass("omnibox-container")
760+ style := strings.TrimSpace(o.style)
761+ if style == "" {
762+ style = "command"
763+ }
764+ o.mainBox.AddCssClass("omnibox-style-" + style)
757765 return nil
758766 }
759767
@@ -780,19 +788,11 @@ func (o *Omnibox) initHeader() error {
780788 o.favoritesBtn.AddCssClass("omnibox-header-btn")
781789 o.favoritesBtn.SetCanFocus(false)
782790
783- o.initialBehaviorBadge = gtk.NewButtonWithLabel("")
791+ o.initialBehaviorBadge = gtk.NewLabel(nil)
784792 if o.initialBehaviorBadge == nil {
785793 return errNilWidget("initialBehaviorBadge")
786794 }
787795 o.initialBehaviorBadge.AddCssClass("omnibox-header-badge")
788- o.initialBehaviorBadge.SetCanFocus(false)
789- o.initialBehaviorBadge.SetFocusOnClick(false)
790-
791- initialBehaviorClickCb := func(_ gtk.Button) {
792- o.toggleInitialBehaviorPreference()
793- }
794- o.retainedCallbacks = append(o.retainedCallbacks, initialBehaviorClickCb)
795- o.initialBehaviorBadge.ConnectClicked(&initialBehaviorClickCb)
796796 o.refreshInitialBehaviorBadge()
797797
798798 historyClickCb := func(_ gtk.Button) {
@@ -2177,11 +2177,8 @@ func (o *Omnibox) createFaviconImage(rawURL, fallbackIcon string) *gtk.Image {
21772177 return favicon
21782178 }
21792179
2180-const favoriteStarBaseSize = 18
21812180 const favoriteStarSlotClass = "omnibox-favorite-star-slot"
21822181
2183-func favoriteStarSize(scale float64) int { return ScaleValue(favoriteStarBaseSize, scale) }
2184-
21852182 func shouldShowFavoriteStar(s Suggestion) bool { return s.IsFavorite }
21862183
21872184 func clearBoxChildren(box *gtk.Box) {
@@ -2224,17 +2221,17 @@ func favoriteStarSlotForRow(row *gtk.ListBoxRow) *gtk.Box {
22242221 return nil
22252222 }
22262223
2227-func (o *Omnibox) createFavoriteStarIcon() *gtk.Image {
2228- star := gtk.NewImage()
2229- if star == nil {
2224+func (*Omnibox) createFavoriteStarIcon() *gtk.Label {
2225+ marker := gtk.NewLabel(nil)
2226+ if marker == nil {
22302227 return nil
22312228 }
2232- iconName := "starred-symbolic"
2233- star.SetFromIconName(&iconName)
2234- star.SetPixelSize(favoriteStarSize(o.uiScale))
2235- star.AddCssClass("omnibox-favorite-star")
2236- star.SetValign(gtk.AlignCenterValue)
2237- return star
2229+ marker.SetText("*")
2230+ marker.AddCssClass("omnibox-favorite-star")
2231+ marker.SetValign(gtk.AlignCenterValue)
2232+ tooltip := "Favorite"
2233+ marker.SetTooltipText(&tooltip)
2234+ return marker
22382235 }
22392236
22402237 func (o *Omnibox) appendSuggestionTitleAndURL(textBox *gtk.Box, title, displayURL string) {
@@ -31,7 +31,7 @@ const (
31 endBoxSpacing = 631 endBoxSpacing = 6
32 defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)"32 defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)"
33 minGhostInputLength = 133 minGhostInputLength = 1
34- initialBehaviorBadgeTooltip = "Toggle default history order (Ctrl+R)"34+ initialBehaviorBadgeTooltip = "Default history order · Ctrl+R to switch"
35 )35 )
36 36
37 type favoriteRowIndicatorUpdate struct {37 type favoriteRowIndicatorUpdate struct {
@@ -140,7 +140,7 @@ type Omnibox struct {
140 outerBox *gtk.Box // Outer container for positioning140 outerBox *gtk.Box // Outer container for positioning
141 mainBox *gtk.Box // Main content box141 mainBox *gtk.Box // Main content box
142 headerBox *gtk.Box142 headerBox *gtk.Box
143- initialBehaviorBadge *gtk.Button143+ initialBehaviorBadge *gtk.Label
144 historyBtn *gtk.Button144 historyBtn *gtk.Button
145 favoritesBtn *gtk.Button145 favoritesBtn *gtk.Button
146 zoomLabel *gtk.Label146 zoomLabel *gtk.Label
@@ -151,6 +151,7 @@ type Omnibox struct {
151 151
152 // Parent overlay reference for sizing (set via SetParentOverlay)152 // Parent overlay reference for sizing (set via SetParentOverlay)
153 parentOverlay layout.OverlayWidget153 parentOverlay layout.OverlayWidget
154+ style string
154 155
155 // State156 // State
156 mu sync.RWMutex157 mu sync.RWMutex
@@ -224,6 +225,7 @@ type Omnibox struct {
224 225
225 // OmniboxConfig holds configuration for creating an Omnibox.226 // OmniboxConfig holds configuration for creating an Omnibox.
226 type OmniboxConfig struct {227 type OmniboxConfig struct {
228+ Style string
227 HistoryUC *usecase.SearchHistoryUseCase229 HistoryUC *usecase.SearchHistoryUseCase
228 FavoritesUC *usecase.ManageFavoritesUseCase230 FavoritesUC *usecase.ManageFavoritesUseCase
229 FaviconAdapter *adapter.FaviconAdapter231 FaviconAdapter *adapter.FaviconAdapter
@@ -257,6 +259,7 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox {
257 sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults)259 sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults)
258 260
259 o := &Omnibox{261 o := &Omnibox{
262+ style: cfg.Style,
260 viewMode: ViewModeHistory,263 viewMode: ViewModeHistory,
261 selectedIndex: -1,264 selectedIndex: -1,
262 historyUC: cfg.HistoryUC,265 historyUC: cfg.HistoryUC,
@@ -509,7 +512,7 @@ func (o *Omnibox) refreshInitialBehaviorBadge() {
509 if !state.visible {512 if !state.visible {
510 return513 return
511 }514 }
512- o.initialBehaviorBadge.SetLabel(state.label)515+ o.initialBehaviorBadge.SetText(state.label)
513 tooltip := state.tooltip516 tooltip := state.tooltip
514 o.initialBehaviorBadge.SetTooltipText(&tooltip)517 o.initialBehaviorBadge.SetTooltipText(&tooltip)
515 }518 }
@@ -754,6 +757,11 @@ func (o *Omnibox) initMainBox() error {
754 return errNilWidget("mainBox")757 return errNilWidget("mainBox")
755 }758 }
756 o.mainBox.AddCssClass("omnibox-container")759 o.mainBox.AddCssClass("omnibox-container")
760+ style := strings.TrimSpace(o.style)
761+ if style == "" {
762+ style = "command"
763+ }
764+ o.mainBox.AddCssClass("omnibox-style-" + style)
757 return nil765 return nil
758 }766 }
759 767
@@ -780,19 +788,11 @@ func (o *Omnibox) initHeader() error {
780 o.favoritesBtn.AddCssClass("omnibox-header-btn")788 o.favoritesBtn.AddCssClass("omnibox-header-btn")
781 o.favoritesBtn.SetCanFocus(false)789 o.favoritesBtn.SetCanFocus(false)
782 790
783- o.initialBehaviorBadge = gtk.NewButtonWithLabel("")791+ o.initialBehaviorBadge = gtk.NewLabel(nil)
784 if o.initialBehaviorBadge == nil {792 if o.initialBehaviorBadge == nil {
785 return errNilWidget("initialBehaviorBadge")793 return errNilWidget("initialBehaviorBadge")
786 }794 }
787 o.initialBehaviorBadge.AddCssClass("omnibox-header-badge")795 o.initialBehaviorBadge.AddCssClass("omnibox-header-badge")
788- o.initialBehaviorBadge.SetCanFocus(false)
789- o.initialBehaviorBadge.SetFocusOnClick(false)
790-
791- initialBehaviorClickCb := func(_ gtk.Button) {
792- o.toggleInitialBehaviorPreference()
793- }
794- o.retainedCallbacks = append(o.retainedCallbacks, initialBehaviorClickCb)
795- o.initialBehaviorBadge.ConnectClicked(&initialBehaviorClickCb)
796 o.refreshInitialBehaviorBadge()796 o.refreshInitialBehaviorBadge()
797 797
798 historyClickCb := func(_ gtk.Button) {798 historyClickCb := func(_ gtk.Button) {
@@ -2177,11 +2177,8 @@ func (o *Omnibox) createFaviconImage(rawURL, fallbackIcon string) *gtk.Image {
2177 return favicon2177 return favicon
2178 }2178 }
2179 2179
2180-const favoriteStarBaseSize = 18
2181 const favoriteStarSlotClass = "omnibox-favorite-star-slot"2180 const favoriteStarSlotClass = "omnibox-favorite-star-slot"
2182 2181
2183-func favoriteStarSize(scale float64) int { return ScaleValue(favoriteStarBaseSize, scale) }
2184-
2185 func shouldShowFavoriteStar(s Suggestion) bool { return s.IsFavorite }2182 func shouldShowFavoriteStar(s Suggestion) bool { return s.IsFavorite }
2186 2183
2187 func clearBoxChildren(box *gtk.Box) {2184 func clearBoxChildren(box *gtk.Box) {
@@ -2224,17 +2221,17 @@ func favoriteStarSlotForRow(row *gtk.ListBoxRow) *gtk.Box {
2224 return nil2221 return nil
2225 }2222 }
2226 2223
2227-func (o *Omnibox) createFavoriteStarIcon() *gtk.Image {2224+func (*Omnibox) createFavoriteStarIcon() *gtk.Label {
2228- star := gtk.NewImage()2225+ marker := gtk.NewLabel(nil)
2229- if star == nil {2226+ if marker == nil {
2230 return nil2227 return nil
2231 }2228 }
2232- iconName := "starred-symbolic"2229+ marker.SetText("*")
2233- star.SetFromIconName(&iconName)2230+ marker.AddCssClass("omnibox-favorite-star")
2234- star.SetPixelSize(favoriteStarSize(o.uiScale))2231+ marker.SetValign(gtk.AlignCenterValue)
2235- star.AddCssClass("omnibox-favorite-star")2232+ tooltip := "Favorite"
2236- star.SetValign(gtk.AlignCenterValue)2233+ marker.SetTooltipText(&tooltip)
2237- return star2234+ return marker
2238 }2235 }
2239 2236
2240 func (o *Omnibox) appendSuggestionTitleAndURL(textBox *gtk.Box, title, displayURL string) {2237 func (o *Omnibox) appendSuggestionTitleAndURL(textBox *gtk.Box, title, displayURL string) {
M internal/ui/component/omnibox_favorite_icon_test.go +0 -5
@@ -6,11 +6,6 @@ import (
66 "github.com/stretchr/testify/assert"
77 )
88
9-func TestOmniboxFavoriteStarSize(t *testing.T) {
10- assert.Equal(t, 18, favoriteStarSize(1.0))
11- assert.Equal(t, 27, favoriteStarSize(1.5))
12-}
13-
149 func TestOmniboxFavoriteStarVisibility(t *testing.T) {
1510 assert.True(t, shouldShowFavoriteStar(Suggestion{IsFavorite: true}))
1611 assert.False(t, shouldShowFavoriteStar(Suggestion{IsFavorite: false}))
@@ -6,11 +6,6 @@ import (
6 "github.com/stretchr/testify/assert"6 "github.com/stretchr/testify/assert"
7 )7 )
8 8
9-func TestOmniboxFavoriteStarSize(t *testing.T) {
10- assert.Equal(t, 18, favoriteStarSize(1.0))
11- assert.Equal(t, 27, favoriteStarSize(1.5))
12-}
13-
14 func TestOmniboxFavoriteStarVisibility(t *testing.T) {9 func TestOmniboxFavoriteStarVisibility(t *testing.T) {
15 assert.True(t, shouldShowFavoriteStar(Suggestion{IsFavorite: true}))10 assert.True(t, shouldShowFavoriteStar(Suggestion{IsFavorite: true}))
16 assert.False(t, shouldShowFavoriteStar(Suggestion{IsFavorite: false}))11 assert.False(t, shouldShowFavoriteStar(Suggestion{IsFavorite: false}))
M internal/ui/component/omnibox_initial_behavior_test.go +2 -2
@@ -27,7 +27,7 @@ func TestOmniboxInitialBehaviorBadgeState(t *testing.T) {
2727 behavior: entity.OmniboxInitialBehaviorRecent,
2828 wantVisible: true,
2929 wantLabel: "Recent",
30- wantTooltip: "Toggle default history order (Ctrl+R)",
30+ wantTooltip: "Default history order · Ctrl+R to switch",
3131 wantNext: entity.OmniboxInitialBehaviorMostVisited,
3232 },
3333 {
@@ -35,7 +35,7 @@ func TestOmniboxInitialBehaviorBadgeState(t *testing.T) {
3535 behavior: entity.OmniboxInitialBehaviorMostVisited,
3636 wantVisible: true,
3737 wantLabel: "Most used",
38- wantTooltip: "Toggle default history order (Ctrl+R)",
38+ wantTooltip: "Default history order · Ctrl+R to switch",
3939 wantNext: entity.OmniboxInitialBehaviorRecent,
4040 },
4141 {
@@ -27,7 +27,7 @@ func TestOmniboxInitialBehaviorBadgeState(t *testing.T) {
27 behavior: entity.OmniboxInitialBehaviorRecent,27 behavior: entity.OmniboxInitialBehaviorRecent,
28 wantVisible: true,28 wantVisible: true,
29 wantLabel: "Recent",29 wantLabel: "Recent",
30- wantTooltip: "Toggle default history order (Ctrl+R)",30+ wantTooltip: "Default history order · Ctrl+R to switch",
31 wantNext: entity.OmniboxInitialBehaviorMostVisited,31 wantNext: entity.OmniboxInitialBehaviorMostVisited,
32 },32 },
33 {33 {
@@ -35,7 +35,7 @@ func TestOmniboxInitialBehaviorBadgeState(t *testing.T) {
35 behavior: entity.OmniboxInitialBehaviorMostVisited,35 behavior: entity.OmniboxInitialBehaviorMostVisited,
36 wantVisible: true,36 wantVisible: true,
37 wantLabel: "Most used",37 wantLabel: "Most used",
38- wantTooltip: "Toggle default history order (Ctrl+R)",38+ wantTooltip: "Default history order · Ctrl+R to switch",
39 wantNext: entity.OmniboxInitialBehaviorRecent,39 wantNext: entity.OmniboxInitialBehaviorRecent,
40 },40 },
41 {41 {
M internal/ui/theme/css.go +205 -8
@@ -406,11 +406,14 @@ entry.omnibox-entry > text:focus-visible {
406406 font-weight: 400;
407407 }
408408
409-.omnibox-favorite-star {
409+.omnibox-row .omnibox-favorite-star {
410410 color: mix(var(--warning), var(--muted), 0.45);
411411 opacity: 0.75;
412412 margin-left: 0.625em;
413413 margin-right: 0.125em;
414+ font-family: var(--font-mono);
415+ font-size: 1em;
416+ font-weight: 700;
414417 }
415418
416419 .omnibox-row:selected .omnibox-favorite-star {
@@ -463,13 +466,6 @@ entry.omnibox-entry > text:focus-visible {
463466 padding: 0.0625em 0.375em;
464467 font-size: 0.6875em;
465468 font-weight: 500;
466- transition: background-color 100ms ease-in-out, border-color 100ms ease-in-out, color 100ms ease-in-out;
467-}
468-
469-.omnibox-header-badge:hover {
470- background-color: shade(var(--surface-variant), 1.08);
471- border-color: var(--border);
472- color: var(--text);
473469 }
474470
475471 /* Zoom indicator in omnibox header */
@@ -508,6 +504,207 @@ entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text {
508504 margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */
509505 background-color: transparent;
510506 }
507+
508+/* Multiplexer: segmented status line, square prompt, and strong active rail. */
509+.omnibox-container.omnibox-style-multiplexer {
510+ border-radius: 0;
511+ border-color: var(--border);
512+ box-shadow: 0 1.125em 3em alpha(black, 0.34);
513+}
514+
515+.omnibox-style-multiplexer .omnibox-header {
516+ padding: 0;
517+ min-height: 2em;
518+ background-color: shade(var(--surface), 0.94);
519+}
520+
521+.omnibox-style-multiplexer .omnibox-header-btn {
522+ margin: 0;
523+ padding: 0.375em 1em;
524+ border: none;
525+ border-right: 0.0625em solid var(--border);
526+ border-radius: 0;
527+ font-family: var(--font-mono);
528+ font-size: 0.75em;
529+ font-weight: 600;
530+}
531+
532+.omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active {
533+ background-color: var(--accent);
534+ color: var(--accent-text);
535+}
536+
537+.omnibox-style-multiplexer .omnibox-header-badge {
538+ padding: 0.1875em 0.5em;
539+ background-color: transparent;
540+ border: none;
541+ color: var(--muted);
542+ font-family: var(--font-mono);
543+ font-size: 0.6875em;
544+ font-weight: 600;
545+}
546+
547+.omnibox-style-multiplexer .omnibox-bang-badge,
548+.omnibox-style-multiplexer .omnibox-zoom-indicator,
549+.omnibox-style-multiplexer .omnibox-shortcut-badge {
550+ border-radius: 0;
551+ font-family: var(--font-mono);
552+}
553+
554+.omnibox-style-multiplexer entry.omnibox-entry {
555+ margin: 0.75em;
556+ border-radius: 0;
557+ border-left: 0.25em solid var(--accent);
558+ font-family: var(--font-mono);
559+}
560+
561+.omnibox-style-multiplexer .omnibox-row {
562+ border-left-width: 0.25em;
563+}
564+
565+.omnibox-style-multiplexer .omnibox-row:selected {
566+ background-color: alpha(var(--accent), 0.14);
567+ box-shadow: 0.1875em 0 0 var(--accent) inset;
568+}
569+
570+.omnibox-style-multiplexer .omnibox-favorite-star {
571+ min-width: 1.25em;
572+ margin: 0 0.375em 0 0.125em;
573+ padding: 0;
574+ color: var(--warning);
575+ opacity: 0.85;
576+}
577+
578+/* Command: a calmer command palette with a floating input and compact tabs. */
579+.omnibox-container.omnibox-style-command {
580+ border-radius: 0.625em;
581+ box-shadow: 0 1.25em 3.5em alpha(black, 0.38), 0 0 0 0.0625em alpha(white, 0.04) inset;
582+ padding: 0.375em;
583+}
584+
585+.omnibox-style-command .omnibox-header {
586+ border-bottom: none;
587+ padding: 0.25em 0.375em 0.125em;
588+}
589+
590+.omnibox-style-command .omnibox-header-btn {
591+ border-radius: 1em;
592+ padding: 0.25em 0.75em;
593+ font-size: 0.75em;
594+}
595+
596+.omnibox-style-command .omnibox-header-btn.omnibox-header-active {
597+ background-color: alpha(var(--accent), 0.13);
598+ border-color: transparent;
599+}
600+
601+.omnibox-style-command .omnibox-header-badge {
602+ padding: 0.1875em 0.5em;
603+ background-color: transparent;
604+ border: none;
605+ color: var(--muted);
606+ font-family: var(--font-mono);
607+ font-size: 0.6875em;
608+ font-weight: 600;
609+}
610+
611+.omnibox-style-command entry.omnibox-entry {
612+ margin: 0.5em 0.375em 0.75em;
613+ padding: 0.75em 0.875em;
614+ border-radius: 0.375em;
615+ font-family: var(--font-mono);
616+ box-shadow: 0 0 0 0.0625em alpha(var(--accent), 0.12), 0 0.5em 1.5em alpha(black, 0.12);
617+}
618+
619+.omnibox-style-command .omnibox-scrolled {
620+ border-top-color: alpha(var(--border), 0.35);
621+}
622+
623+.omnibox-style-command .omnibox-row {
624+ margin: 0.125em 0.25em;
625+ border: none;
626+ border-radius: 0.25em;
627+}
628+
629+.omnibox-style-command .omnibox-row:selected {
630+ background-color: alpha(var(--accent), 0.16);
631+ border-left-color: transparent;
632+}
633+
634+.omnibox-style-command .omnibox-favorite-star {
635+ min-width: 1.25em;
636+ margin: 0 0.375em 0 0.125em;
637+ padding: 0;
638+ color: var(--warning);
639+ opacity: 0.85;
640+}
641+
642+/* Minimal: terminal typography and hairline structure with almost no chrome. */
643+.omnibox-container.omnibox-style-minimal {
644+ border-radius: 0;
645+ box-shadow: 0 0.75em 2em alpha(black, 0.2);
646+}
647+
648+.omnibox-style-minimal .omnibox-header {
649+ padding: 0.25em 0.75em;
650+ background-color: var(--surface);
651+}
652+
653+.omnibox-style-minimal .omnibox-header-btn {
654+ margin-right: 1em;
655+ padding: 0.25em 0;
656+ border: none;
657+ border-bottom: 0.125em solid transparent;
658+ border-radius: 0;
659+ background-color: transparent;
660+ font-family: var(--font-mono);
661+ font-size: 0.75em;
662+}
663+
664+.omnibox-style-minimal .omnibox-header-btn.omnibox-header-active {
665+ background-color: transparent;
666+ border-bottom-color: var(--accent);
667+}
668+
669+.omnibox-style-minimal entry.omnibox-entry {
670+ margin: 0;
671+ padding: 0.75em;
672+ border: none;
673+ border-bottom: 0.0625em solid var(--border);
674+ border-radius: 0;
675+ background-color: var(--surface);
676+ font-family: var(--font-mono);
677+ box-shadow: none;
678+}
679+
680+.omnibox-style-minimal .omnibox-scrolled {
681+ border-top: none;
682+}
683+
684+.omnibox-style-minimal .omnibox-row {
685+ padding-top: 0.4375em;
686+ padding-bottom: 0.4375em;
687+ border-left-width: 0.125em;
688+ font-family: var(--font-mono);
689+}
690+
691+.omnibox-style-minimal .omnibox-favorite-star {
692+ color: var(--warning);
693+ opacity: 0.85;
694+}
695+
696+.omnibox-style-minimal .omnibox-header-badge {
697+ background-color: transparent;
698+ border: none;
699+ color: var(--muted);
700+ font-family: var(--font-mono);
701+}
702+
703+.omnibox-style-minimal .omnibox-shortcut-badge {
704+ background-color: transparent;
705+ border: 0.0625em solid alpha(var(--border), 0.7);
706+ border-radius: 0;
707+}
511708 `
512709 }
513710
@@ -406,11 +406,14 @@ entry.omnibox-entry > text:focus-visible {
406 font-weight: 400;406 font-weight: 400;
407 }407 }
408 408
409-.omnibox-favorite-star {409+.omnibox-row .omnibox-favorite-star {
410 color: mix(var(--warning), var(--muted), 0.45);410 color: mix(var(--warning), var(--muted), 0.45);
411 opacity: 0.75;411 opacity: 0.75;
412 margin-left: 0.625em;412 margin-left: 0.625em;
413 margin-right: 0.125em;413 margin-right: 0.125em;
414+ font-family: var(--font-mono);
415+ font-size: 1em;
416+ font-weight: 700;
414 }417 }
415 418
416 .omnibox-row:selected .omnibox-favorite-star {419 .omnibox-row:selected .omnibox-favorite-star {
@@ -463,13 +466,6 @@ entry.omnibox-entry > text:focus-visible {
463 padding: 0.0625em 0.375em;466 padding: 0.0625em 0.375em;
464 font-size: 0.6875em;467 font-size: 0.6875em;
465 font-weight: 500;468 font-weight: 500;
466- transition: background-color 100ms ease-in-out, border-color 100ms ease-in-out, color 100ms ease-in-out;
467-}
468-
469-.omnibox-header-badge:hover {
470- background-color: shade(var(--surface-variant), 1.08);
471- border-color: var(--border);
472- color: var(--text);
473 }469 }
474 470
475 /* Zoom indicator in omnibox header */471 /* Zoom indicator in omnibox header */
@@ -508,6 +504,207 @@ entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text {
508 margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */504 margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */
509 background-color: transparent;505 background-color: transparent;
510 }506 }
507+
508+/* Multiplexer: segmented status line, square prompt, and strong active rail. */
509+.omnibox-container.omnibox-style-multiplexer {
510+ border-radius: 0;
511+ border-color: var(--border);
512+ box-shadow: 0 1.125em 3em alpha(black, 0.34);
513+}
514+
515+.omnibox-style-multiplexer .omnibox-header {
516+ padding: 0;
517+ min-height: 2em;
518+ background-color: shade(var(--surface), 0.94);
519+}
520+
521+.omnibox-style-multiplexer .omnibox-header-btn {
522+ margin: 0;
523+ padding: 0.375em 1em;
524+ border: none;
525+ border-right: 0.0625em solid var(--border);
526+ border-radius: 0;
527+ font-family: var(--font-mono);
528+ font-size: 0.75em;
529+ font-weight: 600;
530+}
531+
532+.omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active {
533+ background-color: var(--accent);
534+ color: var(--accent-text);
535+}
536+
537+.omnibox-style-multiplexer .omnibox-header-badge {
538+ padding: 0.1875em 0.5em;
539+ background-color: transparent;
540+ border: none;
541+ color: var(--muted);
542+ font-family: var(--font-mono);
543+ font-size: 0.6875em;
544+ font-weight: 600;
545+}
546+
547+.omnibox-style-multiplexer .omnibox-bang-badge,
548+.omnibox-style-multiplexer .omnibox-zoom-indicator,
549+.omnibox-style-multiplexer .omnibox-shortcut-badge {
550+ border-radius: 0;
551+ font-family: var(--font-mono);
552+}
553+
554+.omnibox-style-multiplexer entry.omnibox-entry {
555+ margin: 0.75em;
556+ border-radius: 0;
557+ border-left: 0.25em solid var(--accent);
558+ font-family: var(--font-mono);
559+}
560+
561+.omnibox-style-multiplexer .omnibox-row {
562+ border-left-width: 0.25em;
563+}
564+
565+.omnibox-style-multiplexer .omnibox-row:selected {
566+ background-color: alpha(var(--accent), 0.14);
567+ box-shadow: 0.1875em 0 0 var(--accent) inset;
568+}
569+
570+.omnibox-style-multiplexer .omnibox-favorite-star {
571+ min-width: 1.25em;
572+ margin: 0 0.375em 0 0.125em;
573+ padding: 0;
574+ color: var(--warning);
575+ opacity: 0.85;
576+}
577+
578+/* Command: a calmer command palette with a floating input and compact tabs. */
579+.omnibox-container.omnibox-style-command {
580+ border-radius: 0.625em;
581+ box-shadow: 0 1.25em 3.5em alpha(black, 0.38), 0 0 0 0.0625em alpha(white, 0.04) inset;
582+ padding: 0.375em;
583+}
584+
585+.omnibox-style-command .omnibox-header {
586+ border-bottom: none;
587+ padding: 0.25em 0.375em 0.125em;
588+}
589+
590+.omnibox-style-command .omnibox-header-btn {
591+ border-radius: 1em;
592+ padding: 0.25em 0.75em;
593+ font-size: 0.75em;
594+}
595+
596+.omnibox-style-command .omnibox-header-btn.omnibox-header-active {
597+ background-color: alpha(var(--accent), 0.13);
598+ border-color: transparent;
599+}
600+
601+.omnibox-style-command .omnibox-header-badge {
602+ padding: 0.1875em 0.5em;
603+ background-color: transparent;
604+ border: none;
605+ color: var(--muted);
606+ font-family: var(--font-mono);
607+ font-size: 0.6875em;
608+ font-weight: 600;
609+}
610+
611+.omnibox-style-command entry.omnibox-entry {
612+ margin: 0.5em 0.375em 0.75em;
613+ padding: 0.75em 0.875em;
614+ border-radius: 0.375em;
615+ font-family: var(--font-mono);
616+ box-shadow: 0 0 0 0.0625em alpha(var(--accent), 0.12), 0 0.5em 1.5em alpha(black, 0.12);
617+}
618+
619+.omnibox-style-command .omnibox-scrolled {
620+ border-top-color: alpha(var(--border), 0.35);
621+}
622+
623+.omnibox-style-command .omnibox-row {
624+ margin: 0.125em 0.25em;
625+ border: none;
626+ border-radius: 0.25em;
627+}
628+
629+.omnibox-style-command .omnibox-row:selected {
630+ background-color: alpha(var(--accent), 0.16);
631+ border-left-color: transparent;
632+}
633+
634+.omnibox-style-command .omnibox-favorite-star {
635+ min-width: 1.25em;
636+ margin: 0 0.375em 0 0.125em;
637+ padding: 0;
638+ color: var(--warning);
639+ opacity: 0.85;
640+}
641+
642+/* Minimal: terminal typography and hairline structure with almost no chrome. */
643+.omnibox-container.omnibox-style-minimal {
644+ border-radius: 0;
645+ box-shadow: 0 0.75em 2em alpha(black, 0.2);
646+}
647+
648+.omnibox-style-minimal .omnibox-header {
649+ padding: 0.25em 0.75em;
650+ background-color: var(--surface);
651+}
652+
653+.omnibox-style-minimal .omnibox-header-btn {
654+ margin-right: 1em;
655+ padding: 0.25em 0;
656+ border: none;
657+ border-bottom: 0.125em solid transparent;
658+ border-radius: 0;
659+ background-color: transparent;
660+ font-family: var(--font-mono);
661+ font-size: 0.75em;
662+}
663+
664+.omnibox-style-minimal .omnibox-header-btn.omnibox-header-active {
665+ background-color: transparent;
666+ border-bottom-color: var(--accent);
667+}
668+
669+.omnibox-style-minimal entry.omnibox-entry {
670+ margin: 0;
671+ padding: 0.75em;
672+ border: none;
673+ border-bottom: 0.0625em solid var(--border);
674+ border-radius: 0;
675+ background-color: var(--surface);
676+ font-family: var(--font-mono);
677+ box-shadow: none;
678+}
679+
680+.omnibox-style-minimal .omnibox-scrolled {
681+ border-top: none;
682+}
683+
684+.omnibox-style-minimal .omnibox-row {
685+ padding-top: 0.4375em;
686+ padding-bottom: 0.4375em;
687+ border-left-width: 0.125em;
688+ font-family: var(--font-mono);
689+}
690+
691+.omnibox-style-minimal .omnibox-favorite-star {
692+ color: var(--warning);
693+ opacity: 0.85;
694+}
695+
696+.omnibox-style-minimal .omnibox-header-badge {
697+ background-color: transparent;
698+ border: none;
699+ color: var(--muted);
700+ font-family: var(--font-mono);
701+}
702+
703+.omnibox-style-minimal .omnibox-shortcut-badge {
704+ background-color: transparent;
705+ border: 0.0625em solid alpha(var(--border), 0.7);
706+ border-radius: 0;
707+}
511 `708 `
512 }709 }
513 710
M internal/ui/theme/css_test.go +18 -10
@@ -103,9 +103,6 @@ func TestGenerateCSS_OmniboxHeaderBadgeSelectorsExist(t *testing.T) {
103103 if !strings.Contains(css, ".omnibox-header-badge {") {
104104 t.Fatalf("expected omnibox header badge selector in CSS")
105105 }
106- if !strings.Contains(css, ".omnibox-header-badge:hover {") {
107- t.Fatalf("expected omnibox header badge hover selector in CSS")
108- }
109106 }
110107
111108 func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) {
@@ -116,11 +113,6 @@ func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.
116113 t.Fatalf("expected omnibox header badge to use passive neutral chip styling")
117114 }
118115
119- hoverRe := regexp.MustCompile(`(?s)\.omnibox-header-badge:hover\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);[^}]*border-color:\s*var\(--border\);[^}]*color:\s*var\(--text\);`)
120- if !hoverRe.MatchString(css) {
121- t.Fatalf("expected omnibox header badge hover styling to stay neutral while hinting interactivity")
122- }
123-
124116 badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css)
125117 if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") {
126118 t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling")
@@ -142,8 +134,9 @@ func TestGenerateCSS_FavoriteRowsOnlyTintTrailingAffordance(t *testing.T) {
142134 if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") {
143135 t.Fatalf("expected favorite indicator to move off the right edge")
144136 }
145- if !strings.Contains(css, "color: mix(var(--warning), var(--muted), 0.45);") {
146- t.Fatalf("expected favorite star color to be softened")
137+ favoriteMarker := cssRuleBlock(t, css, ".omnibox-row .omnibox-favorite-star")
138+ if !strings.Contains(favoriteMarker, "color: mix(var(--warning), var(--muted), 0.45);") {
139+ t.Fatalf("expected favorite marker color to be softened")
147140 }
148141 if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") {
149142 t.Fatalf("expected favorite highlight to move off the star slot")
@@ -258,6 +251,21 @@ func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testi
258251 assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;")
259252 }
260253
254+func TestGenerateCSS_OmniboxIncludesVisualConcepts(t *testing.T) {
255+ css := GenerateCSS(DefaultDarkPalette())
256+
257+ for _, style := range []string{"multiplexer", "command", "minimal"} {
258+ assert.Contains(t, css, ".omnibox-container.omnibox-style-"+style)
259+ }
260+ assert.Contains(t, css, ".omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active")
261+ assert.Contains(t, css, ".omnibox-style-command .omnibox-header-badge")
262+ assert.Contains(t, css, ".omnibox-style-command .omnibox-favorite-star")
263+ assert.Contains(t, css, ".omnibox-style-command entry.omnibox-entry")
264+ minimalFavorite := cssRuleBlock(t, css, ".omnibox-style-minimal .omnibox-favorite-star")
265+ assert.Contains(t, minimalFavorite, "color: var(--warning);")
266+ assert.Contains(t, css, ".omnibox-style-minimal .omnibox-shortcut-badge")
267+}
268+
261269 func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) {
262270 css := GenerateCSS(DefaultDarkPalette())
263271
@@ -103,9 +103,6 @@ func TestGenerateCSS_OmniboxHeaderBadgeSelectorsExist(t *testing.T) {
103 if !strings.Contains(css, ".omnibox-header-badge {") {103 if !strings.Contains(css, ".omnibox-header-badge {") {
104 t.Fatalf("expected omnibox header badge selector in CSS")104 t.Fatalf("expected omnibox header badge selector in CSS")
105 }105 }
106- if !strings.Contains(css, ".omnibox-header-badge:hover {") {
107- t.Fatalf("expected omnibox header badge hover selector in CSS")
108- }
109 }106 }
110 107
111 func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) {108 func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) {
@@ -116,11 +113,6 @@ func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.
116 t.Fatalf("expected omnibox header badge to use passive neutral chip styling")113 t.Fatalf("expected omnibox header badge to use passive neutral chip styling")
117 }114 }
118 115
119- hoverRe := regexp.MustCompile(`(?s)\.omnibox-header-badge:hover\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);[^}]*border-color:\s*var\(--border\);[^}]*color:\s*var\(--text\);`)
120- if !hoverRe.MatchString(css) {
121- t.Fatalf("expected omnibox header badge hover styling to stay neutral while hinting interactivity")
122- }
123-
124 badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css)116 badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css)
125 if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") {117 if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") {
126 t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling")118 t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling")
@@ -142,8 +134,9 @@ func TestGenerateCSS_FavoriteRowsOnlyTintTrailingAffordance(t *testing.T) {
142 if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") {134 if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") {
143 t.Fatalf("expected favorite indicator to move off the right edge")135 t.Fatalf("expected favorite indicator to move off the right edge")
144 }136 }
145- if !strings.Contains(css, "color: mix(var(--warning), var(--muted), 0.45);") {137+ favoriteMarker := cssRuleBlock(t, css, ".omnibox-row .omnibox-favorite-star")
146- t.Fatalf("expected favorite star color to be softened")138+ if !strings.Contains(favoriteMarker, "color: mix(var(--warning), var(--muted), 0.45);") {
139+ t.Fatalf("expected favorite marker color to be softened")
147 }140 }
148 if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") {141 if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") {
149 t.Fatalf("expected favorite highlight to move off the star slot")142 t.Fatalf("expected favorite highlight to move off the star slot")
@@ -258,6 +251,21 @@ func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testi
258 assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;")251 assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;")
259 }252 }
260 253
254+func TestGenerateCSS_OmniboxIncludesVisualConcepts(t *testing.T) {
255+ css := GenerateCSS(DefaultDarkPalette())
256+
257+ for _, style := range []string{"multiplexer", "command", "minimal"} {
258+ assert.Contains(t, css, ".omnibox-container.omnibox-style-"+style)
259+ }
260+ assert.Contains(t, css, ".omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active")
261+ assert.Contains(t, css, ".omnibox-style-command .omnibox-header-badge")
262+ assert.Contains(t, css, ".omnibox-style-command .omnibox-favorite-star")
263+ assert.Contains(t, css, ".omnibox-style-command entry.omnibox-entry")
264+ minimalFavorite := cssRuleBlock(t, css, ".omnibox-style-minimal .omnibox-favorite-star")
265+ assert.Contains(t, minimalFavorite, "color: var(--warning);")
266+ assert.Contains(t, css, ".omnibox-style-minimal .omnibox-shortcut-badge")
267+}
268+
261 func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) {269 func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) {
262 css := GenerateCSS(DefaultDarkPalette())270 css := GenerateCSS(DefaultDarkPalette())
263 271
M internal/ui/theme/palette.go +1 -0
@@ -272,6 +272,7 @@ func (p Palette) ToCSSVars() string {
272272 sb.WriteString(" --text: " + p.Text + ";\n")
273273 sb.WriteString(" --muted: " + p.Muted + ";\n")
274274 sb.WriteString(" --accent: " + p.Accent + ";\n")
275+ sb.WriteString(" --accent-text: " + readableTextColor(p.Accent, p.Background) + ";\n")
275276 sb.WriteString(" --border: " + p.Border + ";\n")
276277 sb.WriteString(" --success: " + p.Success + ";\n")
277278 sb.WriteString(" --warning: " + p.Warning + ";\n")
@@ -272,6 +272,7 @@ func (p Palette) ToCSSVars() string {
272 sb.WriteString(" --text: " + p.Text + ";\n")272 sb.WriteString(" --text: " + p.Text + ";\n")
273 sb.WriteString(" --muted: " + p.Muted + ";\n")273 sb.WriteString(" --muted: " + p.Muted + ";\n")
274 sb.WriteString(" --accent: " + p.Accent + ";\n")274 sb.WriteString(" --accent: " + p.Accent + ";\n")
275+ sb.WriteString(" --accent-text: " + readableTextColor(p.Accent, p.Background) + ";\n")
275 sb.WriteString(" --border: " + p.Border + ";\n")276 sb.WriteString(" --border: " + p.Border + ";\n")
276 sb.WriteString(" --success: " + p.Success + ";\n")277 sb.WriteString(" --success: " + p.Success + ";\n")
277 sb.WriteString(" --warning: " + p.Warning + ";\n")278 sb.WriteString(" --warning: " + p.Warning + ";\n")

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