detached 9f085d4

feat(config): add configurable default search engine

- Added default_search_engine config option with explicit URL template format
- Changed default from Google to DuckDuckGo
- Users can set any search engine with %s placeholder
- Added validation to ensure URL contains %s placeholder
- Updated parser to use config value instead of hardcoded Google URLs
- Refactored URL type detection to use cleaner switch statement
commit
9f085d4fa2ad2865e347aa400328c531590deafe
author
bnema · · 2025-11-17 06:35 UTC
parent
4a28a17
signature
unsigned
6 files · +59 -23
M CHANGELOG.md +1 -0
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
77 ### Added
88 - **Middle-click and Ctrl+click link handling**: Links can now be opened in new workspace panes using standard browser gestures (middle-click or Ctrl+left-click). New panes split in the configured direction (default: right) and respect `workspace.popups.placement` configuration. Implemented via WebKit's `decide-policy` signal to intercept navigation actions before they occur.
99 - **Configurable pane border styling**: Added `workspace.styling.inactive_border_width`, `workspace.styling.inactive_border_color`, and `workspace.styling.show_stacked_title_border` config options. Active and inactive borders default to 1px to prevent layout shift, with only color changing on focus. Borders use GTK theme variables by default and support smooth CSS transitions.
10+- **Configurable default search engine**: Added `default_search_engine` config option with explicit URL template format. DuckDuckGo is now the default (was Google). Users can set any search engine with `%s` placeholder (e.g., `default_search_engine = "https://duckduckgo.com/?q=%s"`).
1011
1112 ### Changed
1213 - **Config format**: TOML is now the default format (JSON/YAML still supported). Action bindings inverted to `action→keys` structure for maintainability with O(1) runtime lookup. Added comprehensive validation.
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
7 ### Added7 ### Added
8 - **Middle-click and Ctrl+click link handling**: Links can now be opened in new workspace panes using standard browser gestures (middle-click or Ctrl+left-click). New panes split in the configured direction (default: right) and respect `workspace.popups.placement` configuration. Implemented via WebKit's `decide-policy` signal to intercept navigation actions before they occur.8 - **Middle-click and Ctrl+click link handling**: Links can now be opened in new workspace panes using standard browser gestures (middle-click or Ctrl+left-click). New panes split in the configured direction (default: right) and respect `workspace.popups.placement` configuration. Implemented via WebKit's `decide-policy` signal to intercept navigation actions before they occur.
9 - **Configurable pane border styling**: Added `workspace.styling.inactive_border_width`, `workspace.styling.inactive_border_color`, and `workspace.styling.show_stacked_title_border` config options. Active and inactive borders default to 1px to prevent layout shift, with only color changing on focus. Borders use GTK theme variables by default and support smooth CSS transitions.9 - **Configurable pane border styling**: Added `workspace.styling.inactive_border_width`, `workspace.styling.inactive_border_color`, and `workspace.styling.show_stacked_title_border` config options. Active and inactive borders default to 1px to prevent layout shift, with only color changing on focus. Borders use GTK theme variables by default and support smooth CSS transitions.
10+- **Configurable default search engine**: Added `default_search_engine` config option with explicit URL template format. DuckDuckGo is now the default (was Google). Users can set any search engine with `%s` placeholder (e.g., `default_search_engine = "https://duckduckgo.com/?q=%s"`).
10 11
11 ### Changed12 ### Changed
12 - **Config format**: TOML is now the default format (JSON/YAML still supported). Action bindings inverted to `action→keys` structure for maintainability with O(1) runtime lookup. Added comprehensive validation.13 - **Config format**: TOML is now the default format (JSON/YAML still supported). Action bindings inverted to `action→keys` structure for maintainability with O(1) runtime lookup. Added comprehensive validation.
M docs/CONFIG.md +17 -1
@@ -29,14 +29,30 @@ You can still use JSON or YAML if you prefer - dumber automatically detects the
2929 | `history.retention_period_days` | int | `365` | > 0 | Days to keep history (1 year) |
3030 | `history.cleanup_interval_days` | int | `1` | > 0 | How often to run cleanup |
3131
32-## Search Shortcuts
32+## Search Configuration
3333
3434 | Key | Type | Default | Description |
3535 |-----|------|---------|-------------|
36+| `default_search_engine` | string | `"https://duckduckgo.com/?q=%s"` | Default search engine URL template (must contain `%s` placeholder) |
3637 | `search_shortcuts` | map | See defaults | Map of shortcut aliases to URLs |
3738
39+**Example:**
40+```toml
41+# Default search engine - used when no shortcut is specified
42+default_search_engine = "https://duckduckgo.com/?q=%s"
43+
44+# Alternative search engines:
45+# default_search_engine = "https://www.google.com/search?q=%s"
46+# default_search_engine = "https://www.startpage.com/search?q=%s"
47+# default_search_engine = "https://search.brave.com/search?q=%s"
48+```
49+
3850 **Default shortcuts:**
3951 ```toml
52+[search_shortcuts.ddg]
53+url = "https://duckduckgo.com/?q=%s"
54+description = "DuckDuckGo search"
55+
4056 [search_shortcuts.g]
4157 url = "https://google.com/search?q=%s"
4258 description = "Google Search"
@@ -29,14 +29,30 @@ You can still use JSON or YAML if you prefer - dumber automatically detects the
29 | `history.retention_period_days` | int | `365` | > 0 | Days to keep history (1 year) |29 | `history.retention_period_days` | int | `365` | > 0 | Days to keep history (1 year) |
30 | `history.cleanup_interval_days` | int | `1` | > 0 | How often to run cleanup |30 | `history.cleanup_interval_days` | int | `1` | > 0 | How often to run cleanup |
31 31
32-## Search Shortcuts32+## Search Configuration
33 33
34 | Key | Type | Default | Description |34 | Key | Type | Default | Description |
35 |-----|------|---------|-------------|35 |-----|------|---------|-------------|
36+| `default_search_engine` | string | `"https://duckduckgo.com/?q=%s"` | Default search engine URL template (must contain `%s` placeholder) |
36 | `search_shortcuts` | map | See defaults | Map of shortcut aliases to URLs |37 | `search_shortcuts` | map | See defaults | Map of shortcut aliases to URLs |
37 38
39+**Example:**
40+```toml
41+# Default search engine - used when no shortcut is specified
42+default_search_engine = "https://duckduckgo.com/?q=%s"
43+
44+# Alternative search engines:
45+# default_search_engine = "https://www.google.com/search?q=%s"
46+# default_search_engine = "https://www.startpage.com/search?q=%s"
47+# default_search_engine = "https://search.brave.com/search?q=%s"
48+```
49+
38 **Default shortcuts:**50 **Default shortcuts:**
39 ```toml51 ```toml
52+[search_shortcuts.ddg]
53+url = "https://duckduckgo.com/?q=%s"
54+description = "DuckDuckGo search"
55+
40 [search_shortcuts.g]56 [search_shortcuts.g]
41 url = "https://google.com/search?q=%s"57 url = "https://google.com/search?q=%s"
42 description = "Google Search"58 description = "Google Search"
M internal/config/config.go +4 -1
@@ -26,7 +26,9 @@ type Config struct {
2626 Database DatabaseConfig `mapstructure:"database" yaml:"database"`
2727 History HistoryConfig `mapstructure:"history" yaml:"history"`
2828 SearchShortcuts map[string]SearchShortcut `mapstructure:"search_shortcuts" yaml:"search_shortcuts"`
29- Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`
29+ // DefaultSearchEngine is the URL template for the default search engine (must contain %s placeholder)
30+ DefaultSearchEngine string `mapstructure:"default_search_engine" yaml:"default_search_engine"`
31+ Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`
3032 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`
3133 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`
3234 VideoAcceleration VideoAccelerationConfig `mapstructure:"video_acceleration" yaml:"video_acceleration"`
@@ -608,6 +610,7 @@ func (m *Manager) setDefaults() {
608610
609611 // Search shortcuts defaults
610612 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
613+ m.viper.SetDefault("default_search_engine", defaults.DefaultSearchEngine)
611614
612615 // Dmenu defaults
613616 m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems)
@@ -26,7 +26,9 @@ type Config struct {
26 Database DatabaseConfig `mapstructure:"database" yaml:"database"`26 Database DatabaseConfig `mapstructure:"database" yaml:"database"`
27 History HistoryConfig `mapstructure:"history" yaml:"history"`27 History HistoryConfig `mapstructure:"history" yaml:"history"`
28 SearchShortcuts map[string]SearchShortcut `mapstructure:"search_shortcuts" yaml:"search_shortcuts"`28 SearchShortcuts map[string]SearchShortcut `mapstructure:"search_shortcuts" yaml:"search_shortcuts"`
29- Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`29+ // DefaultSearchEngine is the URL template for the default search engine (must contain %s placeholder)
30+ DefaultSearchEngine string `mapstructure:"default_search_engine" yaml:"default_search_engine"`
31+ Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`
30 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`32 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`
31 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`33 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`
32 VideoAcceleration VideoAccelerationConfig `mapstructure:"video_acceleration" yaml:"video_acceleration"`34 VideoAcceleration VideoAccelerationConfig `mapstructure:"video_acceleration" yaml:"video_acceleration"`
@@ -608,6 +610,7 @@ func (m *Manager) setDefaults() {
608 610
609 // Search shortcuts defaults611 // Search shortcuts defaults
610 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)612 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
613+ m.viper.SetDefault("default_search_engine", defaults.DefaultSearchEngine)
611 614
612 // Dmenu defaults615 // Dmenu defaults
613 m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems)616 m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems)
M internal/config/defaults.go +2 -1
@@ -54,7 +54,8 @@ func DefaultConfig() *Config {
5454 RetentionPeriodDays: defaultRetentionDays, // 1 year
5555 CleanupIntervalDays: 1, // daily cleanup
5656 },
57- SearchShortcuts: GetDefaultSearchShortcuts(),
57+ SearchShortcuts: GetDefaultSearchShortcuts(),
58+ DefaultSearchEngine: "https://duckduckgo.com/?q=%s",
5859 Dmenu: DmenuConfig{
5960 MaxHistoryItems: defaultMaxHistoryItems,
6061 ShowVisitCount: true,
@@ -54,7 +54,8 @@ func DefaultConfig() *Config {
54 RetentionPeriodDays: defaultRetentionDays, // 1 year54 RetentionPeriodDays: defaultRetentionDays, // 1 year
55 CleanupIntervalDays: 1, // daily cleanup55 CleanupIntervalDays: 1, // daily cleanup
56 },56 },
57- SearchShortcuts: GetDefaultSearchShortcuts(),57+ SearchShortcuts: GetDefaultSearchShortcuts(),
58+ DefaultSearchEngine: "https://duckduckgo.com/?q=%s",
58 Dmenu: DmenuConfig{59 Dmenu: DmenuConfig{
59 MaxHistoryItems: defaultMaxHistoryItems,60 MaxHistoryItems: defaultMaxHistoryItems,
60 ShowVisitCount: true,61 ShowVisitCount: true,
M internal/config/validation.go +7 -0
@@ -33,6 +33,13 @@ func validateConfig(config *Config) error {
3333 validationErrors = append(validationErrors, "default_zoom must be between 0.1 and 5.0")
3434 }
3535
36+ // Validate default search engine
37+ if config.DefaultSearchEngine == "" {
38+ validationErrors = append(validationErrors, "default_search_engine cannot be empty")
39+ } else if !strings.Contains(config.DefaultSearchEngine, "%s") {
40+ validationErrors = append(validationErrors, "default_search_engine must contain %s placeholder for the search query")
41+ }
42+
3643 // Validate popup behavior
3744 switch config.Workspace.Popups.Behavior {
3845 case PopupBehaviorSplit, PopupBehaviorStacked, PopupBehaviorTabbed, PopupBehaviorWindowed:
@@ -33,6 +33,13 @@ func validateConfig(config *Config) error {
33 validationErrors = append(validationErrors, "default_zoom must be between 0.1 and 5.0")33 validationErrors = append(validationErrors, "default_zoom must be between 0.1 and 5.0")
34 }34 }
35 35
36+ // Validate default search engine
37+ if config.DefaultSearchEngine == "" {
38+ validationErrors = append(validationErrors, "default_search_engine cannot be empty")
39+ } else if !strings.Contains(config.DefaultSearchEngine, "%s") {
40+ validationErrors = append(validationErrors, "default_search_engine must contain %s placeholder for the search query")
41+ }
42+
36 // Validate popup behavior43 // Validate popup behavior
37 switch config.Workspace.Popups.Behavior {44 switch config.Workspace.Popups.Behavior {
38 case PopupBehaviorSplit, PopupBehaviorStacked, PopupBehaviorTabbed, PopupBehaviorWindowed:45 case PopupBehaviorSplit, PopupBehaviorStacked, PopupBehaviorTabbed, PopupBehaviorWindowed:
M internal/parser/parser.go +28 -20
@@ -18,7 +18,7 @@ func (p *Parser) ParseInput(input string) (*ParseResult, error) {
1818 if input == "" {
1919 return &ParseResult{
2020 Type: InputTypeFallbackSearch,
21- URL: "https://www.google.com/search?q=",
21+ URL: fmt.Sprintf(p.config.DefaultSearchEngine, ""),
2222 Query: input,
2323 Confidence: 0.0,
2424 ProcessingTime: time.Since(startTime),
@@ -29,19 +29,29 @@ func (p *Parser) ParseInput(input string) (*ParseResult, error) {
2929 finalURL, err := p.parseInputUsingCLILogic(input)
3030 if err != nil {
3131 // If parsing fails, fall back to search
32- finalURL = fmt.Sprintf("https://www.google.com/search?q=%s", neturl.QueryEscape(input))
32+ finalURL = fmt.Sprintf(p.config.DefaultSearchEngine, neturl.QueryEscape(input))
3333 }
3434
3535 // Determine result type based on the URL
36- var resultType InputType = InputTypeFallbackSearch
37- confidence := 0.1
36+ var resultType InputType
37+ var confidence float64
3838
39- if strings.Contains(finalURL, "google.com/search") {
39+ // Check if this is a search query result
40+ isSearchResult := strings.Contains(finalURL, neturl.QueryEscape(input))
41+
42+ switch {
43+ case isSearchResult:
4044 resultType = InputTypeFallbackSearch
4145 confidence = 0.1
42- } else if strings.HasPrefix(finalURL, "http://") || strings.HasPrefix(finalURL, "https://") || strings.HasPrefix(finalURL, "dumb://") || strings.HasPrefix(finalURL, "file://") {
46+ case strings.HasPrefix(finalURL, "http://"),
47+ strings.HasPrefix(finalURL, "https://"),
48+ strings.HasPrefix(finalURL, "dumb://"),
49+ strings.HasPrefix(finalURL, "file://"):
4350 resultType = InputTypeDirectURL
4451 confidence = 1.0
52+ default:
53+ resultType = InputTypeFallbackSearch
54+ confidence = 0.1
4555 }
4656
4757 // Check if it was a shortcut
@@ -140,7 +150,7 @@ func (p *Parser) parseInputUsingCLILogic(input string) (string, error) {
140150 // 8. Everything else is a search query
141151 // This includes: single words, multi-word phrases, questions, special characters, etc.
142152 // Single words without TLDs should be treated as search queries, not hostnames
143- return fmt.Sprintf("https://www.google.com/search?q=%s", neturl.QueryEscape(input)), nil
153+ return fmt.Sprintf(p.config.DefaultSearchEngine, neturl.QueryEscape(input)), nil
144154 }
145155
146156 // parseSearchShortcut handles search shortcut inputs.
@@ -256,22 +266,16 @@ func (p *Parser) processShortcut(shortcutKey, query string, shortcut config.Sear
256266 // buildSearchURL builds a search URL using the default search engine.
257267 func (p *Parser) buildSearchURL(searchEngine, query string) string {
258268 q := neturl.QueryEscape(query)
259- // If no specific search engine, use Google as default
269+ // If no specific search engine, use the configured default
260270 if searchEngine == "" {
261- defaultShortcut, exists := p.config.SearchShortcuts["g"]
262- if exists {
263- url := strings.ReplaceAll(defaultShortcut.URL, "{query}", q)
264- url = strings.ReplaceAll(url, "%s", q)
265- return url
266- }
267- // Fallback to Google if no "g" shortcut configured
268- return fmt.Sprintf("https://www.google.com/search?q=%s", q)
271+ return fmt.Sprintf(p.config.DefaultSearchEngine, q)
269272 }
270273
271274 // Use specified search engine
272275 shortcut, exists := p.config.SearchShortcuts[searchEngine]
273276 if !exists {
274- return fmt.Sprintf("https://www.google.com/search?q=%s", q)
277+ // Fallback to default search engine if shortcut not found
278+ return fmt.Sprintf(p.config.DefaultSearchEngine, q)
275279 }
276280
277281 u := strings.ReplaceAll(shortcut.URL, "{query}", q)
@@ -433,9 +437,13 @@ func (p *Parser) ValidateConfig() error {
433437 return fmt.Errorf("no search shortcuts configured")
434438 }
435439
436- // Check for default search shortcut
437- if _, hasGoogle := p.config.SearchShortcuts["g"]; !hasGoogle {
438- // Warning: no default search configured
440+ // Check for default search engine
441+ if p.config.DefaultSearchEngine == "" {
442+ return fmt.Errorf("default search engine is not configured")
443+ }
444+
445+ if !strings.Contains(p.config.DefaultSearchEngine, "%s") {
446+ return fmt.Errorf("default search engine must contain %%s placeholder")
439447 }
440448
441449 return nil
@@ -18,7 +18,7 @@ func (p *Parser) ParseInput(input string) (*ParseResult, error) {
18 if input == "" {18 if input == "" {
19 return &ParseResult{19 return &ParseResult{
20 Type: InputTypeFallbackSearch,20 Type: InputTypeFallbackSearch,
21- URL: "https://www.google.com/search?q=",21+ URL: fmt.Sprintf(p.config.DefaultSearchEngine, ""),
22 Query: input,22 Query: input,
23 Confidence: 0.0,23 Confidence: 0.0,
24 ProcessingTime: time.Since(startTime),24 ProcessingTime: time.Since(startTime),
@@ -29,19 +29,29 @@ func (p *Parser) ParseInput(input string) (*ParseResult, error) {
29 finalURL, err := p.parseInputUsingCLILogic(input)29 finalURL, err := p.parseInputUsingCLILogic(input)
30 if err != nil {30 if err != nil {
31 // If parsing fails, fall back to search31 // If parsing fails, fall back to search
32- finalURL = fmt.Sprintf("https://www.google.com/search?q=%s", neturl.QueryEscape(input))32+ finalURL = fmt.Sprintf(p.config.DefaultSearchEngine, neturl.QueryEscape(input))
33 }33 }
34 34
35 // Determine result type based on the URL35 // Determine result type based on the URL
36- var resultType InputType = InputTypeFallbackSearch36+ var resultType InputType
37- confidence := 0.137+ var confidence float64
38 38
39- if strings.Contains(finalURL, "google.com/search") {39+ // Check if this is a search query result
40+ isSearchResult := strings.Contains(finalURL, neturl.QueryEscape(input))
41+
42+ switch {
43+ case isSearchResult:
40 resultType = InputTypeFallbackSearch44 resultType = InputTypeFallbackSearch
41 confidence = 0.145 confidence = 0.1
42- } else if strings.HasPrefix(finalURL, "http://") || strings.HasPrefix(finalURL, "https://") || strings.HasPrefix(finalURL, "dumb://") || strings.HasPrefix(finalURL, "file://") {46+ case strings.HasPrefix(finalURL, "http://"),
47+ strings.HasPrefix(finalURL, "https://"),
48+ strings.HasPrefix(finalURL, "dumb://"),
49+ strings.HasPrefix(finalURL, "file://"):
43 resultType = InputTypeDirectURL50 resultType = InputTypeDirectURL
44 confidence = 1.051 confidence = 1.0
52+ default:
53+ resultType = InputTypeFallbackSearch
54+ confidence = 0.1
45 }55 }
46 56
47 // Check if it was a shortcut57 // Check if it was a shortcut
@@ -140,7 +150,7 @@ func (p *Parser) parseInputUsingCLILogic(input string) (string, error) {
140 // 8. Everything else is a search query150 // 8. Everything else is a search query
141 // This includes: single words, multi-word phrases, questions, special characters, etc.151 // This includes: single words, multi-word phrases, questions, special characters, etc.
142 // Single words without TLDs should be treated as search queries, not hostnames152 // Single words without TLDs should be treated as search queries, not hostnames
143- return fmt.Sprintf("https://www.google.com/search?q=%s", neturl.QueryEscape(input)), nil153+ return fmt.Sprintf(p.config.DefaultSearchEngine, neturl.QueryEscape(input)), nil
144 }154 }
145 155
146 // parseSearchShortcut handles search shortcut inputs.156 // parseSearchShortcut handles search shortcut inputs.
@@ -256,22 +266,16 @@ func (p *Parser) processShortcut(shortcutKey, query string, shortcut config.Sear
256 // buildSearchURL builds a search URL using the default search engine.266 // buildSearchURL builds a search URL using the default search engine.
257 func (p *Parser) buildSearchURL(searchEngine, query string) string {267 func (p *Parser) buildSearchURL(searchEngine, query string) string {
258 q := neturl.QueryEscape(query)268 q := neturl.QueryEscape(query)
259- // If no specific search engine, use Google as default269+ // If no specific search engine, use the configured default
260 if searchEngine == "" {270 if searchEngine == "" {
261- defaultShortcut, exists := p.config.SearchShortcuts["g"]271+ return fmt.Sprintf(p.config.DefaultSearchEngine, q)
262- if exists {
263- url := strings.ReplaceAll(defaultShortcut.URL, "{query}", q)
264- url = strings.ReplaceAll(url, "%s", q)
265- return url
266- }
267- // Fallback to Google if no "g" shortcut configured
268- return fmt.Sprintf("https://www.google.com/search?q=%s", q)
269 }272 }
270 273
271 // Use specified search engine274 // Use specified search engine
272 shortcut, exists := p.config.SearchShortcuts[searchEngine]275 shortcut, exists := p.config.SearchShortcuts[searchEngine]
273 if !exists {276 if !exists {
274- return fmt.Sprintf("https://www.google.com/search?q=%s", q)277+ // Fallback to default search engine if shortcut not found
278+ return fmt.Sprintf(p.config.DefaultSearchEngine, q)
275 }279 }
276 280
277 u := strings.ReplaceAll(shortcut.URL, "{query}", q)281 u := strings.ReplaceAll(shortcut.URL, "{query}", q)
@@ -433,9 +437,13 @@ func (p *Parser) ValidateConfig() error {
433 return fmt.Errorf("no search shortcuts configured")437 return fmt.Errorf("no search shortcuts configured")
434 }438 }
435 439
436- // Check for default search shortcut440+ // Check for default search engine
437- if _, hasGoogle := p.config.SearchShortcuts["g"]; !hasGoogle {441+ if p.config.DefaultSearchEngine == "" {
438- // Warning: no default search configured442+ return fmt.Errorf("default search engine is not configured")
443+ }
444+
445+ if !strings.Contains(p.config.DefaultSearchEngine, "%s") {
446+ return fmt.Errorf("default search engine must contain %%s placeholder")
439 }447 }
440 448
441 return nil449 return nil

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