feat(search): add bang shortcut syntax for search engines
- commit
- 8de40706bad5ef26c00258586efb9fdcfaea84e8
- author
- bnema · · 2025-12-18 22:07 UTC
- parent
- d6a17f6
- signature
- unsigned
8 files · +372 -27
M README.md +2 -2
| @@ -19,7 +19,7 @@ A dumb browser that works like your favorite terminal multiplexer. | |||
| 19 | 19 | - **GPU rendering**: Hardware video acceleration with automatic VA-API/VDPAU detection. | |
| 20 | 20 | - **Built-in ad blocking**: UBlock-based network filtering (cosmetic filtering in progress). | |
| 21 | 21 | - **Launcher integration**: dmenu-style with rofi/fuzzel support, shows favicons and history. | |
| 22 | - | - **Search shortcuts**: Quick search via aliases (e.g., `g:golang` for Google). | |
| 22 | + | - **Search shortcuts**: Quick search via bangs (e.g., `!g golang` for Google, `!gi cats` for Google Images). | |
| 23 | 23 | - **Customizable themes**: Light and dark palettes with semantic color tokens. | |
| 24 | 24 | - **Persistent storage**: SQLite for history, zoom levels, and settings. | |
| 25 | 25 | - **Live configuration**: Single config file with hot reload when possible. | |
| @@ -207,7 +207,7 @@ sudo apt install va-driver-all | |||
| 207 | 207 | - `dumber browse https://example.com` | |
| 208 | 208 | - `dumber browse example.com` # scheme auto‑added | |
| 209 | 209 | - `dumber browse dumb://home` # built-in homepage with stats | |
| 210 | - | - `dumber browse g:golang` # Google search via shortcut | |
| 210 | + | - `dumber browse "!g golang"` # Google search via bang shortcut | |
| 211 | 211 | - Show version information: | |
| 212 | 212 | - `dumber version` # display version, commit, and build date | |
| 213 | 213 | - Launcher integration (dmenu‑style examples with favicon support): | |
| @@ -19,7 +19,7 @@ A dumb browser that works like your favorite terminal multiplexer. | |||||
| 19 | - **GPU rendering**: Hardware video acceleration with automatic VA-API/VDPAU detection. | 19 | - **GPU rendering**: Hardware video acceleration with automatic VA-API/VDPAU detection. | ||
| 20 | - **Built-in ad blocking**: UBlock-based network filtering (cosmetic filtering in progress). | 20 | - **Built-in ad blocking**: UBlock-based network filtering (cosmetic filtering in progress). | ||
| 21 | - **Launcher integration**: dmenu-style with rofi/fuzzel support, shows favicons and history. | 21 | - **Launcher integration**: dmenu-style with rofi/fuzzel support, shows favicons and history. | ||
| 22 | - | - **Search shortcuts**: Quick search via aliases (e.g., `g:golang` for Google). | 22 | + | - **Search shortcuts**: Quick search via bangs (e.g., `!g golang` for Google, `!gi cats` for Google Images). |
| 23 | - **Customizable themes**: Light and dark palettes with semantic color tokens. | 23 | - **Customizable themes**: Light and dark palettes with semantic color tokens. | ||
| 24 | - **Persistent storage**: SQLite for history, zoom levels, and settings. | 24 | - **Persistent storage**: SQLite for history, zoom levels, and settings. | ||
| 25 | - **Live configuration**: Single config file with hot reload when possible. | 25 | - **Live configuration**: Single config file with hot reload when possible. | ||
| @@ -207,7 +207,7 @@ sudo apt install va-driver-all | |||||
| 207 | - `dumber browse https://example.com` | 207 | - `dumber browse https://example.com` | ||
| 208 | - `dumber browse example.com` # scheme auto‑added | 208 | - `dumber browse example.com` # scheme auto‑added | ||
| 209 | - `dumber browse dumb://home` # built-in homepage with stats | 209 | - `dumber browse dumb://home` # built-in homepage with stats | ||
| 210 | - | - `dumber browse g:golang` # Google search via shortcut | 210 | + | - `dumber browse "!g golang"` # Google search via bang shortcut |
| 211 | - Show version information: | 211 | - Show version information: | ||
| 212 | - `dumber version` # display version, commit, and build date | 212 | - `dumber version` # display version, commit, and build date | ||
| 213 | - Launcher integration (dmenu‑style examples with favicon support): | 213 | - Launcher integration (dmenu‑style examples with favicon support): | ||
M docs/CONFIG.md +12 -1
| @@ -47,8 +47,15 @@ default_search_engine = "https://duckduckgo.com/?q=%s" | |||
| 47 | 47 | # default_search_engine = "https://search.brave.com/search?q=%s" | |
| 48 | 48 | ``` | |
| 49 | 49 | ||
| 50 | - | **Default shortcuts:** | |
| 50 | + | **Default shortcuts (usage: `!shortcut query`):** | |
| 51 | 51 | ```toml | |
| 52 | + | # Examples: | |
| 53 | + | # !ddg golang → DuckDuckGo search for "golang" | |
| 54 | + | # !g rust tutorial → Google search for "rust tutorial" | |
| 55 | + | # !gi cats → Google Images search for "cats" | |
| 56 | + | # !gh opencode → GitHub search for "opencode" | |
| 57 | + | # !yt music video → YouTube search for "music video" | |
| 58 | + | ||
| 52 | 59 | [search_shortcuts.ddg] | |
| 53 | 60 | url = "https://duckduckgo.com/?q=%s" | |
| 54 | 61 | description = "DuckDuckGo search" | |
| @@ -57,6 +64,10 @@ description = "DuckDuckGo search" | |||
| 57 | 64 | url = "https://google.com/search?q=%s" | |
| 58 | 65 | description = "Google Search" | |
| 59 | 66 | ||
| 67 | + | [search_shortcuts.gi] | |
| 68 | + | url = "https://google.com/search?tbm=isch&q=%s" | |
| 69 | + | description = "Google Images" | |
| 70 | + | ||
| 60 | 71 | [search_shortcuts.gh] | |
| 61 | 72 | url = "https://github.com/search?q=%s" | |
| 62 | 73 | description = "GitHub" | |
| @@ -47,8 +47,15 @@ default_search_engine = "https://duckduckgo.com/?q=%s" | |||||
| 47 | # default_search_engine = "https://search.brave.com/search?q=%s" | 47 | # default_search_engine = "https://search.brave.com/search?q=%s" | ||
| 48 | ``` | 48 | ``` | ||
| 49 | 49 | ||||
| 50 | - | **Default shortcuts:** | 50 | + | **Default shortcuts (usage: `!shortcut query`):** |
| 51 | ```toml | 51 | ```toml | ||
| 52 | + | # Examples: | |||
| 53 | + | # !ddg golang → DuckDuckGo search for "golang" | |||
| 54 | + | # !g rust tutorial → Google search for "rust tutorial" | |||
| 55 | + | # !gi cats → Google Images search for "cats" | |||
| 56 | + | # !gh opencode → GitHub search for "opencode" | |||
| 57 | + | # !yt music video → YouTube search for "music video" | |||
| 58 | + | ||||
| 52 | [search_shortcuts.ddg] | 59 | [search_shortcuts.ddg] | ||
| 53 | url = "https://duckduckgo.com/?q=%s" | 60 | url = "https://duckduckgo.com/?q=%s" | ||
| 54 | description = "DuckDuckGo search" | 61 | description = "DuckDuckGo search" | ||
| @@ -57,6 +64,10 @@ description = "DuckDuckGo search" | |||||
| 57 | url = "https://google.com/search?q=%s" | 64 | url = "https://google.com/search?q=%s" | ||
| 58 | description = "Google Search" | 65 | description = "Google Search" | ||
| 59 | 66 | ||||
| 67 | + | [search_shortcuts.gi] | |||
| 68 | + | url = "https://google.com/search?tbm=isch&q=%s" | |||
| 69 | + | description = "Google Images" | |||
| 70 | + | ||||
| 60 | [search_shortcuts.gh] | 71 | [search_shortcuts.gh] | ||
| 61 | url = "https://github.com/search?q=%s" | 72 | url = "https://github.com/search?q=%s" | ||
| 62 | description = "GitHub" | 73 | description = "GitHub" | ||
M internal/cli/cmd/dmenu.go +9 -2
| @@ -159,7 +159,8 @@ func runDmenuSelect() error { | |||
| 159 | 159 | } | |
| 160 | 160 | ||
| 161 | 161 | // parseSelection extracts the URL from a dmenu selection. | |
| 162 | - | // Selection is the URL (stripped of scheme) as displayed by runDmenuPipe. | |
| 162 | + | // Selection is the URL (stripped of scheme) as displayed by runDmenuPipe, | |
| 163 | + | // or a bang shortcut like "!g query" for search. | |
| 163 | 164 | func parseSelection(selection string) string { | |
| 164 | 165 | selection = strings.TrimSpace(selection) | |
| 165 | 166 | ||
| @@ -173,7 +174,13 @@ func parseSelection(selection string) string { | |||
| 173 | 174 | return "" | |
| 174 | 175 | } | |
| 175 | 176 | ||
| 176 | - | // Add https:// scheme if not present (display was stripped of scheme) | |
| 177 | + | // Use BuildSearchURL to handle bang shortcuts, URLs, and search queries | |
| 178 | + | app := GetApp() | |
| 179 | + | if app != nil { | |
| 180 | + | return domainurl.BuildSearchURL(selection, app.Config.ShortcutURLs(), app.Config.DefaultSearchEngine) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | // Fallback: add https:// scheme if no app context | |
| 177 | 184 | if !strings.Contains(selection, "://") { | |
| 178 | 185 | selection = "https://" + selection | |
| 179 | 186 | } | |
| @@ -159,7 +159,8 @@ func runDmenuSelect() error { | |||||
| 159 | } | 159 | } | ||
| 160 | 160 | ||||
| 161 | // parseSelection extracts the URL from a dmenu selection. | 161 | // parseSelection extracts the URL from a dmenu selection. | ||
| 162 | - | // Selection is the URL (stripped of scheme) as displayed by runDmenuPipe. | 162 | + | // Selection is the URL (stripped of scheme) as displayed by runDmenuPipe, |
| 163 | + | // or a bang shortcut like "!g query" for search. | |||
| 163 | func parseSelection(selection string) string { | 164 | func parseSelection(selection string) string { | ||
| 164 | selection = strings.TrimSpace(selection) | 165 | selection = strings.TrimSpace(selection) | ||
| 165 | 166 | ||||
| @@ -173,7 +174,13 @@ func parseSelection(selection string) string { | |||||
| 173 | return "" | 174 | return "" | ||
| 174 | } | 175 | } | ||
| 175 | 176 | ||||
| 176 | - | // Add https:// scheme if not present (display was stripped of scheme) | 177 | + | // Use BuildSearchURL to handle bang shortcuts, URLs, and search queries |
| 178 | + | app := GetApp() | |||
| 179 | + | if app != nil { | |||
| 180 | + | return domainurl.BuildSearchURL(selection, app.Config.ShortcutURLs(), app.Config.DefaultSearchEngine) | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | // Fallback: add https:// scheme if no app context | |||
| 177 | if !strings.Contains(selection, "://") { | 184 | if !strings.Contains(selection, "://") { | ||
| 178 | selection = "https://" + selection | 185 | selection = "https://" + selection | ||
| 179 | } | 186 | } | ||
A internal/domain/url/search.go +72 -0
| @@ -0,0 +1,72 @@ | |||
| 1 | + | package url | |
| 2 | + | ||
| 3 | + | import "strings" | |
| 4 | + | ||
| 5 | + | // ParseBangShortcut extracts a bang shortcut from input. | |
| 6 | + | // Input must start with "!" followed by shortcut key and a space. | |
| 7 | + | // Returns (shortcutKey, query, found). | |
| 8 | + | // | |
| 9 | + | // Examples: | |
| 10 | + | // | |
| 11 | + | // "!g golang" → ("g", "golang", true) | |
| 12 | + | // "!ddg test" → ("ddg", "test", true) | |
| 13 | + | // "!gh repo name" → ("gh", "repo name", true) | |
| 14 | + | // "!g" → ("", "", false) - no query | |
| 15 | + | // "plain text" → ("", "", false) | |
| 16 | + | // "test !g" → ("", "", false) - bang not at start | |
| 17 | + | func ParseBangShortcut(input string) (shortcut, query string, found bool) { | |
| 18 | + | if !strings.HasPrefix(input, "!") { | |
| 19 | + | return "", "", false | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | // Find the space separating shortcut from query | |
| 23 | + | spaceIdx := strings.Index(input, " ") | |
| 24 | + | if spaceIdx == -1 || spaceIdx == 1 { | |
| 25 | + | // No space found or empty shortcut ("! query") | |
| 26 | + | return "", "", false | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | shortcut = input[1:spaceIdx] | |
| 30 | + | query = strings.TrimSpace(input[spaceIdx+1:]) | |
| 31 | + | ||
| 32 | + | if query == "" { | |
| 33 | + | return "", "", false | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | return shortcut, query, true | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // BuildSearchURL constructs a URL from user input, handling bang shortcuts. | |
| 40 | + | // It checks for bang shortcuts first, then URL-like input, then falls back to default search. | |
| 41 | + | // | |
| 42 | + | // Parameters: | |
| 43 | + | // - input: user input (e.g., "!g golang", "example.com", "search query") | |
| 44 | + | // - shortcutURLs: map of shortcut keys to URL templates (e.g., {"g": "https://google.com/search?q=%s"}) | |
| 45 | + | // - defaultSearch: default search engine URL template (e.g., "https://duckduckgo.com/?q=%s") | |
| 46 | + | // | |
| 47 | + | // Returns the resolved URL. | |
| 48 | + | func BuildSearchURL(input string, shortcutURLs map[string]string, defaultSearch string) string { | |
| 49 | + | if input == "" { | |
| 50 | + | return "" | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // Check for bang shortcut (e.g., "!g query") | |
| 54 | + | if shortcutKey, query, found := ParseBangShortcut(input); found { | |
| 55 | + | if urlTemplate, ok := shortcutURLs[shortcutKey]; ok { | |
| 56 | + | return strings.Replace(urlTemplate, "%s", query, 1) | |
| 57 | + | } | |
| 58 | + | // Unknown bang falls through to default search with original input | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // Check if it looks like a URL | |
| 62 | + | if LooksLikeURL(input) { | |
| 63 | + | return Normalize(input) | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Use default search | |
| 67 | + | if defaultSearch != "" { | |
| 68 | + | return strings.Replace(defaultSearch, "%s", input, 1) | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | return input | |
| 72 | + | } | |
| @@ -0,0 +1,72 @@ | |||||
| 1 | + | package url | |||
| 2 | + | ||||
| 3 | + | import "strings" | |||
| 4 | + | ||||
| 5 | + | // ParseBangShortcut extracts a bang shortcut from input. | |||
| 6 | + | // Input must start with "!" followed by shortcut key and a space. | |||
| 7 | + | // Returns (shortcutKey, query, found). | |||
| 8 | + | // | |||
| 9 | + | // Examples: | |||
| 10 | + | // | |||
| 11 | + | // "!g golang" → ("g", "golang", true) | |||
| 12 | + | // "!ddg test" → ("ddg", "test", true) | |||
| 13 | + | // "!gh repo name" → ("gh", "repo name", true) | |||
| 14 | + | // "!g" → ("", "", false) - no query | |||
| 15 | + | // "plain text" → ("", "", false) | |||
| 16 | + | // "test !g" → ("", "", false) - bang not at start | |||
| 17 | + | func ParseBangShortcut(input string) (shortcut, query string, found bool) { | |||
| 18 | + | if !strings.HasPrefix(input, "!") { | |||
| 19 | + | return "", "", false | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | // Find the space separating shortcut from query | |||
| 23 | + | spaceIdx := strings.Index(input, " ") | |||
| 24 | + | if spaceIdx == -1 || spaceIdx == 1 { | |||
| 25 | + | // No space found or empty shortcut ("! query") | |||
| 26 | + | return "", "", false | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | shortcut = input[1:spaceIdx] | |||
| 30 | + | query = strings.TrimSpace(input[spaceIdx+1:]) | |||
| 31 | + | ||||
| 32 | + | if query == "" { | |||
| 33 | + | return "", "", false | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | return shortcut, query, true | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // BuildSearchURL constructs a URL from user input, handling bang shortcuts. | |||
| 40 | + | // It checks for bang shortcuts first, then URL-like input, then falls back to default search. | |||
| 41 | + | // | |||
| 42 | + | // Parameters: | |||
| 43 | + | // - input: user input (e.g., "!g golang", "example.com", "search query") | |||
| 44 | + | // - shortcutURLs: map of shortcut keys to URL templates (e.g., {"g": "https://google.com/search?q=%s"}) | |||
| 45 | + | // - defaultSearch: default search engine URL template (e.g., "https://duckduckgo.com/?q=%s") | |||
| 46 | + | // | |||
| 47 | + | // Returns the resolved URL. | |||
| 48 | + | func BuildSearchURL(input string, shortcutURLs map[string]string, defaultSearch string) string { | |||
| 49 | + | if input == "" { | |||
| 50 | + | return "" | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // Check for bang shortcut (e.g., "!g query") | |||
| 54 | + | if shortcutKey, query, found := ParseBangShortcut(input); found { | |||
| 55 | + | if urlTemplate, ok := shortcutURLs[shortcutKey]; ok { | |||
| 56 | + | return strings.Replace(urlTemplate, "%s", query, 1) | |||
| 57 | + | } | |||
| 58 | + | // Unknown bang falls through to default search with original input | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // Check if it looks like a URL | |||
| 62 | + | if LooksLikeURL(input) { | |||
| 63 | + | return Normalize(input) | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // Use default search | |||
| 67 | + | if defaultSearch != "" { | |||
| 68 | + | return strings.Replace(defaultSearch, "%s", input, 1) | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | return input | |||
| 72 | + | } | |||
A internal/domain/url/search_test.go +255 -0
| @@ -0,0 +1,255 @@ | |||
| 1 | + | package url | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | // Test shortcuts for BuildSearchURL tests | |
| 6 | + | var testShortcuts = map[string]string{ | |
| 7 | + | "g": "https://google.com/search?q=%s", | |
| 8 | + | "ddg": "https://duckduckgo.com/?q=%s", | |
| 9 | + | "gh": "https://github.com/search?q=%s", | |
| 10 | + | "gi": "https://google.com/search?tbm=isch&q=%s", | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | const testDefaultSearch = "https://duckduckgo.com/?q=%s" | |
| 14 | + | ||
| 15 | + | func TestBuildSearchURL(t *testing.T) { | |
| 16 | + | tests := []struct { | |
| 17 | + | name string | |
| 18 | + | input string | |
| 19 | + | shortcuts map[string]string | |
| 20 | + | defaultSearch string | |
| 21 | + | want string | |
| 22 | + | }{ | |
| 23 | + | { | |
| 24 | + | name: "bang shortcut google", | |
| 25 | + | input: "!g golang", | |
| 26 | + | shortcuts: testShortcuts, | |
| 27 | + | defaultSearch: testDefaultSearch, | |
| 28 | + | want: "https://google.com/search?q=golang", | |
| 29 | + | }, | |
| 30 | + | { | |
| 31 | + | name: "bang shortcut duckduckgo multi-word", | |
| 32 | + | input: "!ddg rust async await", | |
| 33 | + | shortcuts: testShortcuts, | |
| 34 | + | defaultSearch: testDefaultSearch, | |
| 35 | + | want: "https://duckduckgo.com/?q=rust async await", | |
| 36 | + | }, | |
| 37 | + | { | |
| 38 | + | name: "bang shortcut google images", | |
| 39 | + | input: "!gi cats", | |
| 40 | + | shortcuts: testShortcuts, | |
| 41 | + | defaultSearch: testDefaultSearch, | |
| 42 | + | want: "https://google.com/search?tbm=isch&q=cats", | |
| 43 | + | }, | |
| 44 | + | { | |
| 45 | + | name: "unknown bang falls back to default search", | |
| 46 | + | input: "!unknown test query", | |
| 47 | + | shortcuts: testShortcuts, | |
| 48 | + | defaultSearch: testDefaultSearch, | |
| 49 | + | want: "https://duckduckgo.com/?q=!unknown test query", | |
| 50 | + | }, | |
| 51 | + | { | |
| 52 | + | name: "url-like input gets normalized", | |
| 53 | + | input: "example.com", | |
| 54 | + | shortcuts: testShortcuts, | |
| 55 | + | defaultSearch: testDefaultSearch, | |
| 56 | + | want: "https://example.com", | |
| 57 | + | }, | |
| 58 | + | { | |
| 59 | + | name: "url with path", | |
| 60 | + | input: "github.com/user/repo", | |
| 61 | + | shortcuts: testShortcuts, | |
| 62 | + | defaultSearch: testDefaultSearch, | |
| 63 | + | want: "https://github.com/user/repo", | |
| 64 | + | }, | |
| 65 | + | { | |
| 66 | + | name: "url with scheme unchanged", | |
| 67 | + | input: "https://example.com", | |
| 68 | + | shortcuts: testShortcuts, | |
| 69 | + | defaultSearch: testDefaultSearch, | |
| 70 | + | want: "https://example.com", | |
| 71 | + | }, | |
| 72 | + | { | |
| 73 | + | name: "plain search query uses default", | |
| 74 | + | input: "how to parse json", | |
| 75 | + | shortcuts: testShortcuts, | |
| 76 | + | defaultSearch: testDefaultSearch, | |
| 77 | + | want: "https://duckduckgo.com/?q=how to parse json", | |
| 78 | + | }, | |
| 79 | + | { | |
| 80 | + | name: "empty input returns empty", | |
| 81 | + | input: "", | |
| 82 | + | shortcuts: testShortcuts, | |
| 83 | + | defaultSearch: testDefaultSearch, | |
| 84 | + | want: "", | |
| 85 | + | }, | |
| 86 | + | { | |
| 87 | + | name: "no shortcuts uses default for bang", | |
| 88 | + | input: "!g test", | |
| 89 | + | shortcuts: nil, | |
| 90 | + | defaultSearch: testDefaultSearch, | |
| 91 | + | want: "https://duckduckgo.com/?q=!g test", | |
| 92 | + | }, | |
| 93 | + | { | |
| 94 | + | name: "no default search returns input for plain query", | |
| 95 | + | input: "plain query", | |
| 96 | + | shortcuts: testShortcuts, | |
| 97 | + | defaultSearch: "", | |
| 98 | + | want: "plain query", | |
| 99 | + | }, | |
| 100 | + | { | |
| 101 | + | name: "bang without query treated as search", | |
| 102 | + | input: "!g", | |
| 103 | + | shortcuts: testShortcuts, | |
| 104 | + | defaultSearch: testDefaultSearch, | |
| 105 | + | want: "https://duckduckgo.com/?q=!g", | |
| 106 | + | }, | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | for _, tt := range tests { | |
| 110 | + | t.Run(tt.name, func(t *testing.T) { | |
| 111 | + | got := BuildSearchURL(tt.input, tt.shortcuts, tt.defaultSearch) | |
| 112 | + | if got != tt.want { | |
| 113 | + | t.Errorf("BuildSearchURL() = %q, want %q", got, tt.want) | |
| 114 | + | } | |
| 115 | + | }) | |
| 116 | + | } | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func TestParseBangShortcut(t *testing.T) { | |
| 120 | + | tests := []struct { | |
| 121 | + | name string | |
| 122 | + | input string | |
| 123 | + | wantShortcut string | |
| 124 | + | wantQuery string | |
| 125 | + | wantFound bool | |
| 126 | + | }{ | |
| 127 | + | { | |
| 128 | + | name: "simple google search", | |
| 129 | + | input: "!g golang", | |
| 130 | + | wantShortcut: "g", | |
| 131 | + | wantQuery: "golang", | |
| 132 | + | wantFound: true, | |
| 133 | + | }, | |
| 134 | + | { | |
| 135 | + | name: "duckduckgo multi-word query", | |
| 136 | + | input: "!ddg rust async await", | |
| 137 | + | wantShortcut: "ddg", | |
| 138 | + | wantQuery: "rust async await", | |
| 139 | + | wantFound: true, | |
| 140 | + | }, | |
| 141 | + | { | |
| 142 | + | name: "github search", | |
| 143 | + | input: "!gh opencode cli", | |
| 144 | + | wantShortcut: "gh", | |
| 145 | + | wantQuery: "opencode cli", | |
| 146 | + | wantFound: true, | |
| 147 | + | }, | |
| 148 | + | { | |
| 149 | + | name: "google images", | |
| 150 | + | input: "!gi cats", | |
| 151 | + | wantShortcut: "gi", | |
| 152 | + | wantQuery: "cats", | |
| 153 | + | wantFound: true, | |
| 154 | + | }, | |
| 155 | + | { | |
| 156 | + | name: "youtube search", | |
| 157 | + | input: "!yt music video", | |
| 158 | + | wantShortcut: "yt", | |
| 159 | + | wantQuery: "music video", | |
| 160 | + | wantFound: true, | |
| 161 | + | }, | |
| 162 | + | { | |
| 163 | + | name: "no query after shortcut", | |
| 164 | + | input: "!g", | |
| 165 | + | wantShortcut: "", | |
| 166 | + | wantQuery: "", | |
| 167 | + | wantFound: false, | |
| 168 | + | }, | |
| 169 | + | { | |
| 170 | + | name: "empty query after space", | |
| 171 | + | input: "!g ", | |
| 172 | + | wantShortcut: "", | |
| 173 | + | wantQuery: "", | |
| 174 | + | wantFound: false, | |
| 175 | + | }, | |
| 176 | + | { | |
| 177 | + | name: "multiple spaces only", | |
| 178 | + | input: "!g ", | |
| 179 | + | wantShortcut: "", | |
| 180 | + | wantQuery: "", | |
| 181 | + | wantFound: false, | |
| 182 | + | }, | |
| 183 | + | { | |
| 184 | + | name: "empty shortcut", | |
| 185 | + | input: "! query", | |
| 186 | + | wantShortcut: "", | |
| 187 | + | wantQuery: "", | |
| 188 | + | wantFound: false, | |
| 189 | + | }, | |
| 190 | + | { | |
| 191 | + | name: "plain text no bang", | |
| 192 | + | input: "plain text", | |
| 193 | + | wantShortcut: "", | |
| 194 | + | wantQuery: "", | |
| 195 | + | wantFound: false, | |
| 196 | + | }, | |
| 197 | + | { | |
| 198 | + | name: "bang at end not supported", | |
| 199 | + | input: "query !g", | |
| 200 | + | wantShortcut: "", | |
| 201 | + | wantQuery: "", | |
| 202 | + | wantFound: false, | |
| 203 | + | }, | |
| 204 | + | { | |
| 205 | + | name: "url-like input", | |
| 206 | + | input: "example.com", | |
| 207 | + | wantShortcut: "", | |
| 208 | + | wantQuery: "", | |
| 209 | + | wantFound: false, | |
| 210 | + | }, | |
| 211 | + | { | |
| 212 | + | name: "empty input", | |
| 213 | + | input: "", | |
| 214 | + | wantShortcut: "", | |
| 215 | + | wantQuery: "", | |
| 216 | + | wantFound: false, | |
| 217 | + | }, | |
| 218 | + | { | |
| 219 | + | name: "just exclamation mark", | |
| 220 | + | input: "!", | |
| 221 | + | wantShortcut: "", | |
| 222 | + | wantQuery: "", | |
| 223 | + | wantFound: false, | |
| 224 | + | }, | |
| 225 | + | { | |
| 226 | + | name: "query with extra spaces trimmed", | |
| 227 | + | input: "!g lots of spaces ", | |
| 228 | + | wantShortcut: "g", | |
| 229 | + | wantQuery: "lots of spaces", | |
| 230 | + | wantFound: true, | |
| 231 | + | }, | |
| 232 | + | { | |
| 233 | + | name: "long shortcut key", | |
| 234 | + | input: "!stackoverflow how to parse json", | |
| 235 | + | wantShortcut: "stackoverflow", | |
| 236 | + | wantQuery: "how to parse json", | |
| 237 | + | wantFound: true, | |
| 238 | + | }, | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | for _, tt := range tests { | |
| 242 | + | t.Run(tt.name, func(t *testing.T) { | |
| 243 | + | gotShortcut, gotQuery, gotFound := ParseBangShortcut(tt.input) | |
| 244 | + | if gotShortcut != tt.wantShortcut { | |
| 245 | + | t.Errorf("ParseBangShortcut() shortcut = %q, want %q", gotShortcut, tt.wantShortcut) | |
| 246 | + | } | |
| 247 | + | if gotQuery != tt.wantQuery { | |
| 248 | + | t.Errorf("ParseBangShortcut() query = %q, want %q", gotQuery, tt.wantQuery) | |
| 249 | + | } | |
| 250 | + | if gotFound != tt.wantFound { | |
| 251 | + | t.Errorf("ParseBangShortcut() found = %v, want %v", gotFound, tt.wantFound) | |
| 252 | + | } | |
| 253 | + | }) | |
| 254 | + | } | |
| 255 | + | } | |
| @@ -0,0 +1,255 @@ | |||||
| 1 | + | package url | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | // Test shortcuts for BuildSearchURL tests | |||
| 6 | + | var testShortcuts = map[string]string{ | |||
| 7 | + | "g": "https://google.com/search?q=%s", | |||
| 8 | + | "ddg": "https://duckduckgo.com/?q=%s", | |||
| 9 | + | "gh": "https://github.com/search?q=%s", | |||
| 10 | + | "gi": "https://google.com/search?tbm=isch&q=%s", | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | const testDefaultSearch = "https://duckduckgo.com/?q=%s" | |||
| 14 | + | ||||
| 15 | + | func TestBuildSearchURL(t *testing.T) { | |||
| 16 | + | tests := []struct { | |||
| 17 | + | name string | |||
| 18 | + | input string | |||
| 19 | + | shortcuts map[string]string | |||
| 20 | + | defaultSearch string | |||
| 21 | + | want string | |||
| 22 | + | }{ | |||
| 23 | + | { | |||
| 24 | + | name: "bang shortcut google", | |||
| 25 | + | input: "!g golang", | |||
| 26 | + | shortcuts: testShortcuts, | |||
| 27 | + | defaultSearch: testDefaultSearch, | |||
| 28 | + | want: "https://google.com/search?q=golang", | |||
| 29 | + | }, | |||
| 30 | + | { | |||
| 31 | + | name: "bang shortcut duckduckgo multi-word", | |||
| 32 | + | input: "!ddg rust async await", | |||
| 33 | + | shortcuts: testShortcuts, | |||
| 34 | + | defaultSearch: testDefaultSearch, | |||
| 35 | + | want: "https://duckduckgo.com/?q=rust async await", | |||
| 36 | + | }, | |||
| 37 | + | { | |||
| 38 | + | name: "bang shortcut google images", | |||
| 39 | + | input: "!gi cats", | |||
| 40 | + | shortcuts: testShortcuts, | |||
| 41 | + | defaultSearch: testDefaultSearch, | |||
| 42 | + | want: "https://google.com/search?tbm=isch&q=cats", | |||
| 43 | + | }, | |||
| 44 | + | { | |||
| 45 | + | name: "unknown bang falls back to default search", | |||
| 46 | + | input: "!unknown test query", | |||
| 47 | + | shortcuts: testShortcuts, | |||
| 48 | + | defaultSearch: testDefaultSearch, | |||
| 49 | + | want: "https://duckduckgo.com/?q=!unknown test query", | |||
| 50 | + | }, | |||
| 51 | + | { | |||
| 52 | + | name: "url-like input gets normalized", | |||
| 53 | + | input: "example.com", | |||
| 54 | + | shortcuts: testShortcuts, | |||
| 55 | + | defaultSearch: testDefaultSearch, | |||
| 56 | + | want: "https://example.com", | |||
| 57 | + | }, | |||
| 58 | + | { | |||
| 59 | + | name: "url with path", | |||
| 60 | + | input: "github.com/user/repo", | |||
| 61 | + | shortcuts: testShortcuts, | |||
| 62 | + | defaultSearch: testDefaultSearch, | |||
| 63 | + | want: "https://github.com/user/repo", | |||
| 64 | + | }, | |||
| 65 | + | { | |||
| 66 | + | name: "url with scheme unchanged", | |||
| 67 | + | input: "https://example.com", | |||
| 68 | + | shortcuts: testShortcuts, | |||
| 69 | + | defaultSearch: testDefaultSearch, | |||
| 70 | + | want: "https://example.com", | |||
| 71 | + | }, | |||
| 72 | + | { | |||
| 73 | + | name: "plain search query uses default", | |||
| 74 | + | input: "how to parse json", | |||
| 75 | + | shortcuts: testShortcuts, | |||
| 76 | + | defaultSearch: testDefaultSearch, | |||
| 77 | + | want: "https://duckduckgo.com/?q=how to parse json", | |||
| 78 | + | }, | |||
| 79 | + | { | |||
| 80 | + | name: "empty input returns empty", | |||
| 81 | + | input: "", | |||
| 82 | + | shortcuts: testShortcuts, | |||
| 83 | + | defaultSearch: testDefaultSearch, | |||
| 84 | + | want: "", | |||
| 85 | + | }, | |||
| 86 | + | { | |||
| 87 | + | name: "no shortcuts uses default for bang", | |||
| 88 | + | input: "!g test", | |||
| 89 | + | shortcuts: nil, | |||
| 90 | + | defaultSearch: testDefaultSearch, | |||
| 91 | + | want: "https://duckduckgo.com/?q=!g test", | |||
| 92 | + | }, | |||
| 93 | + | { | |||
| 94 | + | name: "no default search returns input for plain query", | |||
| 95 | + | input: "plain query", | |||
| 96 | + | shortcuts: testShortcuts, | |||
| 97 | + | defaultSearch: "", | |||
| 98 | + | want: "plain query", | |||
| 99 | + | }, | |||
| 100 | + | { | |||
| 101 | + | name: "bang without query treated as search", | |||
| 102 | + | input: "!g", | |||
| 103 | + | shortcuts: testShortcuts, | |||
| 104 | + | defaultSearch: testDefaultSearch, | |||
| 105 | + | want: "https://duckduckgo.com/?q=!g", | |||
| 106 | + | }, | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | for _, tt := range tests { | |||
| 110 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 111 | + | got := BuildSearchURL(tt.input, tt.shortcuts, tt.defaultSearch) | |||
| 112 | + | if got != tt.want { | |||
| 113 | + | t.Errorf("BuildSearchURL() = %q, want %q", got, tt.want) | |||
| 114 | + | } | |||
| 115 | + | }) | |||
| 116 | + | } | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | func TestParseBangShortcut(t *testing.T) { | |||
| 120 | + | tests := []struct { | |||
| 121 | + | name string | |||
| 122 | + | input string | |||
| 123 | + | wantShortcut string | |||
| 124 | + | wantQuery string | |||
| 125 | + | wantFound bool | |||
| 126 | + | }{ | |||
| 127 | + | { | |||
| 128 | + | name: "simple google search", | |||
| 129 | + | input: "!g golang", | |||
| 130 | + | wantShortcut: "g", | |||
| 131 | + | wantQuery: "golang", | |||
| 132 | + | wantFound: true, | |||
| 133 | + | }, | |||
| 134 | + | { | |||
| 135 | + | name: "duckduckgo multi-word query", | |||
| 136 | + | input: "!ddg rust async await", | |||
| 137 | + | wantShortcut: "ddg", | |||
| 138 | + | wantQuery: "rust async await", | |||
| 139 | + | wantFound: true, | |||
| 140 | + | }, | |||
| 141 | + | { | |||
| 142 | + | name: "github search", | |||
| 143 | + | input: "!gh opencode cli", | |||
| 144 | + | wantShortcut: "gh", | |||
| 145 | + | wantQuery: "opencode cli", | |||
| 146 | + | wantFound: true, | |||
| 147 | + | }, | |||
| 148 | + | { | |||
| 149 | + | name: "google images", | |||
| 150 | + | input: "!gi cats", | |||
| 151 | + | wantShortcut: "gi", | |||
| 152 | + | wantQuery: "cats", | |||
| 153 | + | wantFound: true, | |||
| 154 | + | }, | |||
| 155 | + | { | |||
| 156 | + | name: "youtube search", | |||
| 157 | + | input: "!yt music video", | |||
| 158 | + | wantShortcut: "yt", | |||
| 159 | + | wantQuery: "music video", | |||
| 160 | + | wantFound: true, | |||
| 161 | + | }, | |||
| 162 | + | { | |||
| 163 | + | name: "no query after shortcut", | |||
| 164 | + | input: "!g", | |||
| 165 | + | wantShortcut: "", | |||
| 166 | + | wantQuery: "", | |||
| 167 | + | wantFound: false, | |||
| 168 | + | }, | |||
| 169 | + | { | |||
| 170 | + | name: "empty query after space", | |||
| 171 | + | input: "!g ", | |||
| 172 | + | wantShortcut: "", | |||
| 173 | + | wantQuery: "", | |||
| 174 | + | wantFound: false, | |||
| 175 | + | }, | |||
| 176 | + | { | |||
| 177 | + | name: "multiple spaces only", | |||
| 178 | + | input: "!g ", | |||
| 179 | + | wantShortcut: "", | |||
| 180 | + | wantQuery: "", | |||
| 181 | + | wantFound: false, | |||
| 182 | + | }, | |||
| 183 | + | { | |||
| 184 | + | name: "empty shortcut", | |||
| 185 | + | input: "! query", | |||
| 186 | + | wantShortcut: "", | |||
| 187 | + | wantQuery: "", | |||
| 188 | + | wantFound: false, | |||
| 189 | + | }, | |||
| 190 | + | { | |||
| 191 | + | name: "plain text no bang", | |||
| 192 | + | input: "plain text", | |||
| 193 | + | wantShortcut: "", | |||
| 194 | + | wantQuery: "", | |||
| 195 | + | wantFound: false, | |||
| 196 | + | }, | |||
| 197 | + | { | |||
| 198 | + | name: "bang at end not supported", | |||
| 199 | + | input: "query !g", | |||
| 200 | + | wantShortcut: "", | |||
| 201 | + | wantQuery: "", | |||
| 202 | + | wantFound: false, | |||
| 203 | + | }, | |||
| 204 | + | { | |||
| 205 | + | name: "url-like input", | |||
| 206 | + | input: "example.com", | |||
| 207 | + | wantShortcut: "", | |||
| 208 | + | wantQuery: "", | |||
| 209 | + | wantFound: false, | |||
| 210 | + | }, | |||
| 211 | + | { | |||
| 212 | + | name: "empty input", | |||
| 213 | + | input: "", | |||
| 214 | + | wantShortcut: "", | |||
| 215 | + | wantQuery: "", | |||
| 216 | + | wantFound: false, | |||
| 217 | + | }, | |||
| 218 | + | { | |||
| 219 | + | name: "just exclamation mark", | |||
| 220 | + | input: "!", | |||
| 221 | + | wantShortcut: "", | |||
| 222 | + | wantQuery: "", | |||
| 223 | + | wantFound: false, | |||
| 224 | + | }, | |||
| 225 | + | { | |||
| 226 | + | name: "query with extra spaces trimmed", | |||
| 227 | + | input: "!g lots of spaces ", | |||
| 228 | + | wantShortcut: "g", | |||
| 229 | + | wantQuery: "lots of spaces", | |||
| 230 | + | wantFound: true, | |||
| 231 | + | }, | |||
| 232 | + | { | |||
| 233 | + | name: "long shortcut key", | |||
| 234 | + | input: "!stackoverflow how to parse json", | |||
| 235 | + | wantShortcut: "stackoverflow", | |||
| 236 | + | wantQuery: "how to parse json", | |||
| 237 | + | wantFound: true, | |||
| 238 | + | }, | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | for _, tt := range tests { | |||
| 242 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 243 | + | gotShortcut, gotQuery, gotFound := ParseBangShortcut(tt.input) | |||
| 244 | + | if gotShortcut != tt.wantShortcut { | |||
| 245 | + | t.Errorf("ParseBangShortcut() shortcut = %q, want %q", gotShortcut, tt.wantShortcut) | |||
| 246 | + | } | |||
| 247 | + | if gotQuery != tt.wantQuery { | |||
| 248 | + | t.Errorf("ParseBangShortcut() query = %q, want %q", gotQuery, tt.wantQuery) | |||
| 249 | + | } | |||
| 250 | + | if gotFound != tt.wantFound { | |||
| 251 | + | t.Errorf("ParseBangShortcut() found = %v, want %v", gotFound, tt.wantFound) | |||
| 252 | + | } | |||
| 253 | + | }) | |||
| 254 | + | } | |||
| 255 | + | } | |||
M internal/infrastructure/config/defaults.go +4 -0
| @@ -205,6 +205,10 @@ func GetDefaultSearchShortcuts() map[string]SearchShortcut { | |||
| 205 | 205 | URL: "https://www.google.com/search?q=%s", | |
| 206 | 206 | Description: "Google search", | |
| 207 | 207 | }, | |
| 208 | + | "gi": { | |
| 209 | + | URL: "https://www.google.com/search?tbm=isch&q=%s", | |
| 210 | + | Description: "Google Images search", | |
| 211 | + | }, | |
| 208 | 212 | "gh": { | |
| 209 | 213 | URL: "https://github.com/search?q=%s", | |
| 210 | 214 | Description: "GitHub search", | |
| @@ -205,6 +205,10 @@ func GetDefaultSearchShortcuts() map[string]SearchShortcut { | |||||
| 205 | URL: "https://www.google.com/search?q=%s", | 205 | URL: "https://www.google.com/search?q=%s", | ||
| 206 | Description: "Google search", | 206 | Description: "Google search", | ||
| 207 | }, | 207 | }, | ||
| 208 | + | "gi": { | |||
| 209 | + | URL: "https://www.google.com/search?tbm=isch&q=%s", | |||
| 210 | + | Description: "Google Images search", | |||
| 211 | + | }, | |||
| 208 | "gh": { | 212 | "gh": { | ||
| 209 | URL: "https://github.com/search?q=%s", | 213 | URL: "https://github.com/search?q=%s", | ||
| 210 | Description: "GitHub search", | 214 | Description: "GitHub search", | ||
M internal/infrastructure/config/schema.go +10 -0
| @@ -80,6 +80,16 @@ type SearchShortcut struct { | |||
| 80 | 80 | Description string `mapstructure:"description" toml:"description" yaml:"description" json:"description"` | |
| 81 | 81 | } | |
| 82 | 82 | ||
| 83 | + | // ShortcutURLs returns a simple map of shortcut keys to URL templates. | |
| 84 | + | // This is useful for passing to url.BuildSearchURL. | |
| 85 | + | func (c *Config) ShortcutURLs() map[string]string { | |
| 86 | + | result := make(map[string]string, len(c.SearchShortcuts)) | |
| 87 | + | for key, shortcut := range c.SearchShortcuts { | |
| 88 | + | result[key] = shortcut.URL | |
| 89 | + | } | |
| 90 | + | return result | |
| 91 | + | } | |
| 92 | + | ||
| 83 | 93 | // DmenuConfig holds dmenu/rofi integration configuration. | |
| 84 | 94 | type DmenuConfig struct { | |
| 85 | 95 | MaxHistoryItems int `mapstructure:"max_history_items" yaml:"max_history_items" toml:"max_history_items"` | |
| @@ -80,6 +80,16 @@ type SearchShortcut struct { | |||||
| 80 | Description string `mapstructure:"description" toml:"description" yaml:"description" json:"description"` | 80 | Description string `mapstructure:"description" toml:"description" yaml:"description" json:"description"` | ||
| 81 | } | 81 | } | ||
| 82 | 82 | ||||
| 83 | + | // ShortcutURLs returns a simple map of shortcut keys to URL templates. | |||
| 84 | + | // This is useful for passing to url.BuildSearchURL. | |||
| 85 | + | func (c *Config) ShortcutURLs() map[string]string { | |||
| 86 | + | result := make(map[string]string, len(c.SearchShortcuts)) | |||
| 87 | + | for key, shortcut := range c.SearchShortcuts { | |||
| 88 | + | result[key] = shortcut.URL | |||
| 89 | + | } | |||
| 90 | + | return result | |||
| 91 | + | } | |||
| 92 | + | ||||
| 83 | // DmenuConfig holds dmenu/rofi integration configuration. | 93 | // DmenuConfig holds dmenu/rofi integration configuration. | ||
| 84 | type DmenuConfig struct { | 94 | type DmenuConfig struct { | ||
| 85 | MaxHistoryItems int `mapstructure:"max_history_items" yaml:"max_history_items" toml:"max_history_items"` | 95 | MaxHistoryItems int `mapstructure:"max_history_items" yaml:"max_history_items" toml:"max_history_items"` | ||
M internal/ui/component/omnibox.go +8 -22
| @@ -1122,30 +1122,16 @@ func (o *Omnibox) yankSelectedURL() { | |||
| 1122 | 1122 | ||
| 1123 | 1123 | // buildURL constructs a URL from input, handling search shortcuts. | |
| 1124 | 1124 | func (o *Omnibox) buildURL(input string) string { | |
| 1125 | - | if input == "" { | |
| 1126 | - | return "" | |
| 1127 | - | } | |
| 1128 | - | ||
| 1129 | - | // Check for search shortcut (e.g., "g:query") | |
| 1130 | - | if colonIdx := strings.Index(input, ":"); colonIdx > 0 && colonIdx < 10 { | |
| 1131 | - | prefix := input[:colonIdx] | |
| 1132 | - | if shortcut, ok := o.shortcuts[prefix]; ok { | |
| 1133 | - | query := strings.TrimSpace(input[colonIdx+1:]) | |
| 1134 | - | return strings.Replace(shortcut.URL, "%s", query, 1) | |
| 1135 | - | } | |
| 1136 | - | } | |
| 1137 | - | ||
| 1138 | - | // Check if it looks like a URL - use shared URL normalization | |
| 1139 | - | if url.LooksLikeURL(input) { | |
| 1140 | - | return url.Normalize(input) | |
| 1141 | - | } | |
| 1125 | + | return url.BuildSearchURL(input, o.shortcutURLs(), o.defaultSearch) | |
| 1126 | + | } | |
| 1142 | 1127 | ||
| 1143 | - | // Use default search | |
| 1144 | - | if o.defaultSearch != "" { | |
| 1145 | - | return strings.Replace(o.defaultSearch, "%s", input, 1) | |
| 1128 | + | // shortcutURLs returns a map of shortcut keys to URL templates. | |
| 1129 | + | func (o *Omnibox) shortcutURLs() map[string]string { | |
| 1130 | + | result := make(map[string]string, len(o.shortcuts)) | |
| 1131 | + | for key, shortcut := range o.shortcuts { | |
| 1132 | + | result[key] = shortcut.URL | |
| 1146 | 1133 | } | |
| 1147 | - | ||
| 1148 | - | return input | |
| 1134 | + | return result | |
| 1149 | 1135 | } | |
| 1150 | 1136 | ||
| 1151 | 1137 | // toggleViewMode switches between history and favorites. | |
| @@ -1122,30 +1122,16 @@ func (o *Omnibox) yankSelectedURL() { | |||||
| 1122 | 1122 | ||||
| 1123 | // buildURL constructs a URL from input, handling search shortcuts. | 1123 | // buildURL constructs a URL from input, handling search shortcuts. | ||
| 1124 | func (o *Omnibox) buildURL(input string) string { | 1124 | func (o *Omnibox) buildURL(input string) string { | ||
| 1125 | - | if input == "" { | 1125 | + | return url.BuildSearchURL(input, o.shortcutURLs(), o.defaultSearch) |
| 1126 | - | return "" | 1126 | + | } |
| 1127 | - | } | |||
| 1128 | - | ||||
| 1129 | - | // Check for search shortcut (e.g., "g:query") | |||
| 1130 | - | if colonIdx := strings.Index(input, ":"); colonIdx > 0 && colonIdx < 10 { | |||
| 1131 | - | prefix := input[:colonIdx] | |||
| 1132 | - | if shortcut, ok := o.shortcuts[prefix]; ok { | |||
| 1133 | - | query := strings.TrimSpace(input[colonIdx+1:]) | |||
| 1134 | - | return strings.Replace(shortcut.URL, "%s", query, 1) | |||
| 1135 | - | } | |||
| 1136 | - | } | |||
| 1137 | - | ||||
| 1138 | - | // Check if it looks like a URL - use shared URL normalization | |||
| 1139 | - | if url.LooksLikeURL(input) { | |||
| 1140 | - | return url.Normalize(input) | |||
| 1141 | - | } | |||
| 1142 | 1127 | ||||
| 1143 | - | // Use default search | 1128 | + | // shortcutURLs returns a map of shortcut keys to URL templates. |
| 1144 | - | if o.defaultSearch != "" { | 1129 | + | func (o *Omnibox) shortcutURLs() map[string]string { |
| 1145 | - | return strings.Replace(o.defaultSearch, "%s", input, 1) | 1130 | + | result := make(map[string]string, len(o.shortcuts)) |
| 1131 | + | for key, shortcut := range o.shortcuts { | |||
| 1132 | + | result[key] = shortcut.URL | |||
| 1146 | } | 1133 | } | ||
| 1147 | - | 1134 | + | return result | |
| 1148 | - | return input | |||
| 1149 | } | 1135 | } | ||
| 1150 | 1136 | ||||
| 1151 | // toggleViewMode switches between history and favorites. | 1137 | // toggleViewMode switches between history and favorites. | ||