detached cdb4cef

refactor(services): extract search shortcuts to shared constants

- Create gui/src/constants/shortcuts.ts with DEFAULT_SHORTCUTS
- Define SearchShortcutDefinition interface
- Add helper functions: isValidShortcut, getShortcutDescription, SHORTCUT_KEYS
- Update AppService to use DEFAULT_SHORTCUTS for fallback
- Simplify loadShortcuts() error handling with shared constants
- Add deprecation notice: backend Go config is source of truth
- Keep constants only for development fallback when backend unavailable
commit
cdb4cef561106f0524b1d3f9dcfa6a484d382a0e
author
bamen · · 2025-10-06 18:25 UTC
committer
bamen · · 2025-10-06 17:40 UTC
parent
926c7d2
signature
unsigned
2 files · +105 -32
A gui/src/constants/shortcuts.ts +91 -0
@@ -0,0 +1,91 @@
1+/**
2+ * Search shortcuts configuration
3+ *
4+ * @deprecated This file is only used as a fallback. The real source of truth
5+ * is the backend Go config (internal/config/defaults.go). Shortcuts should be
6+ * fetched from the backend via /api/config or the messaging bridge.
7+ *
8+ * Only kept for development fallback when backend is unavailable.
9+ */
10+
11+export interface SearchShortcutDefinition {
12+ shortcut: string;
13+ url_template: string;
14+ description: string;
15+}
16+
17+/**
18+ * Default search shortcuts
19+ * Alphabetically ordered by shortcut key
20+ */
21+export const DEFAULT_SHORTCUTS: Record<string, SearchShortcutDefinition> = {
22+ ddg: {
23+ shortcut: "ddg",
24+ url_template: "https://duckduckgo.com/?q={query}",
25+ description: "DuckDuckGo search",
26+ },
27+ g: {
28+ shortcut: "g",
29+ url_template: "https://www.google.com/search?q={query}",
30+ description: "Google search",
31+ },
32+ gh: {
33+ shortcut: "gh",
34+ url_template: "https://github.com/search?q={query}",
35+ description: "GitHub search",
36+ },
37+ go: {
38+ shortcut: "go",
39+ url_template: "https://pkg.go.dev/search?q={query}",
40+ description: "Go package search",
41+ },
42+ mdn: {
43+ shortcut: "mdn",
44+ url_template: "https://developer.mozilla.org/en-US/search?q={query}",
45+ description: "MDN Web Docs search",
46+ },
47+ npm: {
48+ shortcut: "npm",
49+ url_template: "https://www.npmjs.com/search?q={query}",
50+ description: "npm package search",
51+ },
52+ r: {
53+ shortcut: "r",
54+ url_template: "https://www.reddit.com/search?q={query}",
55+ description: "Reddit search",
56+ },
57+ so: {
58+ shortcut: "so",
59+ url_template: "https://stackoverflow.com/search?q={query}",
60+ description: "Stack Overflow search",
61+ },
62+ w: {
63+ shortcut: "w",
64+ url_template: "https://en.wikipedia.org/wiki/{query}",
65+ description: "Wikipedia search",
66+ },
67+ yt: {
68+ shortcut: "yt",
69+ url_template: "https://www.youtube.com/results?search_query={query}",
70+ description: "YouTube search",
71+ },
72+};
73+
74+/**
75+ * Get array of shortcut keys for validation
76+ */
77+export const SHORTCUT_KEYS = Object.keys(DEFAULT_SHORTCUTS);
78+
79+/**
80+ * Check if a string is a valid shortcut
81+ */
82+export function isValidShortcut(key: string): boolean {
83+ return key in DEFAULT_SHORTCUTS;
84+}
85+
86+/**
87+ * Get shortcut description by key
88+ */
89+export function getShortcutDescription(key: string): string | null {
90+ return DEFAULT_SHORTCUTS[key]?.description ?? null;
91+}
@@ -0,0 +1,91 @@
1+/**
2+ * Search shortcuts configuration
3+ *
4+ * @deprecated This file is only used as a fallback. The real source of truth
5+ * is the backend Go config (internal/config/defaults.go). Shortcuts should be
6+ * fetched from the backend via /api/config or the messaging bridge.
7+ *
8+ * Only kept for development fallback when backend is unavailable.
9+ */
10+
11+export interface SearchShortcutDefinition {
12+ shortcut: string;
13+ url_template: string;
14+ description: string;
15+}
16+
17+/**
18+ * Default search shortcuts
19+ * Alphabetically ordered by shortcut key
20+ */
21+export const DEFAULT_SHORTCUTS: Record<string, SearchShortcutDefinition> = {
22+ ddg: {
23+ shortcut: "ddg",
24+ url_template: "https://duckduckgo.com/?q={query}",
25+ description: "DuckDuckGo search",
26+ },
27+ g: {
28+ shortcut: "g",
29+ url_template: "https://www.google.com/search?q={query}",
30+ description: "Google search",
31+ },
32+ gh: {
33+ shortcut: "gh",
34+ url_template: "https://github.com/search?q={query}",
35+ description: "GitHub search",
36+ },
37+ go: {
38+ shortcut: "go",
39+ url_template: "https://pkg.go.dev/search?q={query}",
40+ description: "Go package search",
41+ },
42+ mdn: {
43+ shortcut: "mdn",
44+ url_template: "https://developer.mozilla.org/en-US/search?q={query}",
45+ description: "MDN Web Docs search",
46+ },
47+ npm: {
48+ shortcut: "npm",
49+ url_template: "https://www.npmjs.com/search?q={query}",
50+ description: "npm package search",
51+ },
52+ r: {
53+ shortcut: "r",
54+ url_template: "https://www.reddit.com/search?q={query}",
55+ description: "Reddit search",
56+ },
57+ so: {
58+ shortcut: "so",
59+ url_template: "https://stackoverflow.com/search?q={query}",
60+ description: "Stack Overflow search",
61+ },
62+ w: {
63+ shortcut: "w",
64+ url_template: "https://en.wikipedia.org/wiki/{query}",
65+ description: "Wikipedia search",
66+ },
67+ yt: {
68+ shortcut: "yt",
69+ url_template: "https://www.youtube.com/results?search_query={query}",
70+ description: "YouTube search",
71+ },
72+};
73+
74+/**
75+ * Get array of shortcut keys for validation
76+ */
77+export const SHORTCUT_KEYS = Object.keys(DEFAULT_SHORTCUTS);
78+
79+/**
80+ * Check if a string is a valid shortcut
81+ */
82+export function isValidShortcut(key: string): boolean {
83+ return key in DEFAULT_SHORTCUTS;
84+}
85+
86+/**
87+ * Get shortcut description by key
88+ */
89+export function getShortcutDescription(key: string): string | null {
90+ return DEFAULT_SHORTCUTS[key]?.description ?? null;
91+}
M gui/src/services/app.ts +14 -32
@@ -1,4 +1,5 @@
11 import type { HistoryEntry, SearchShortcut } from "../types/generated.js";
2+import { DEFAULT_SHORTCUTS } from "../constants/shortcuts.js";
23
34 export class AppService {
45 private history: HistoryEntry[] = [];
@@ -97,38 +98,19 @@ export class AppService {
9798 this.shortcuts = normalized;
9899 } catch (error) {
99100 console.error("Failed to load shortcuts:", error);
100- // Mock shortcuts for development/fallback
101- this.shortcuts = {
102- g: {
103- id: 0,
104- shortcut: "g",
105- url_template: "https://google.com/search?q={query}",
106- description: "Google Search",
107- created_at: null,
108- },
109- gh: {
110- id: 0,
111- shortcut: "gh",
112- url_template: "https://github.com/search?q={query}",
113- description: "GitHub Search",
114- created_at: null,
115- },
116- so: {
117- id: 0,
118- shortcut: "so",
119- url_template: "https://stackoverflow.com/search?q={query}",
120- description: "Stack Overflow",
121- created_at: null,
122- },
123- w: {
124- id: 0,
125- shortcut: "w",
126- url_template:
127- "https://en.wikipedia.org/wiki/Special:Search?search={query}",
128- description: "Wikipedia",
129- created_at: null,
130- },
131- };
101+ // Mock shortcuts for development/fallback using shared constants
102+ this.shortcuts = Object.fromEntries(
103+ Object.entries(DEFAULT_SHORTCUTS).map(([key, def]) => [
104+ key,
105+ {
106+ id: 0,
107+ shortcut: def.shortcut,
108+ url_template: def.url_template,
109+ description: def.description,
110+ created_at: null,
111+ },
112+ ])
113+ );
132114 }
133115 }
134116
@@ -1,4 +1,5 @@
1 import type { HistoryEntry, SearchShortcut } from "../types/generated.js";1 import type { HistoryEntry, SearchShortcut } from "../types/generated.js";
2+import { DEFAULT_SHORTCUTS } from "../constants/shortcuts.js";
2 3
3 export class AppService {4 export class AppService {
4 private history: HistoryEntry[] = [];5 private history: HistoryEntry[] = [];
@@ -97,38 +98,19 @@ export class AppService {
97 this.shortcuts = normalized;98 this.shortcuts = normalized;
98 } catch (error) {99 } catch (error) {
99 console.error("Failed to load shortcuts:", error);100 console.error("Failed to load shortcuts:", error);
100- // Mock shortcuts for development/fallback101+ // Mock shortcuts for development/fallback using shared constants
101- this.shortcuts = {102+ this.shortcuts = Object.fromEntries(
102- g: {103+ Object.entries(DEFAULT_SHORTCUTS).map(([key, def]) => [
103- id: 0,104+ key,
104- shortcut: "g",105+ {
105- url_template: "https://google.com/search?q={query}",106+ id: 0,
106- description: "Google Search",107+ shortcut: def.shortcut,
107- created_at: null,108+ url_template: def.url_template,
108- },109+ description: def.description,
109- gh: {110+ created_at: null,
110- id: 0,111+ },
111- shortcut: "gh",112+ ])
112- url_template: "https://github.com/search?q={query}",113+ );
113- description: "GitHub Search",
114- created_at: null,
115- },
116- so: {
117- id: 0,
118- shortcut: "so",
119- url_template: "https://stackoverflow.com/search?q={query}",
120- description: "Stack Overflow",
121- created_at: null,
122- },
123- w: {
124- id: 0,
125- shortcut: "w",
126- url_template:
127- "https://en.wikipedia.org/wiki/Special:Search?search={query}",
128- description: "Wikipedia",
129- created_at: null,
130- },
131- };
132 }114 }
133 }115 }
134 116

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