v0.9.0 636c44c

app.ts RawHistoryFolder

1import type { HistoryEntry, SearchShortcut } from "../types/generated.js";
2
3export class AppService {
4  private history: HistoryEntry[] = [];
5  private shortcuts: Record<string, SearchShortcut> = {};
6
7  constructor() {}
8
9  async initialize(): Promise<void> {
10    try {
11      await Promise.all([this.loadHistory(), this.loadShortcuts()]);
12    } catch (error) {
13      console.error("Failed to initialize app:", error);
14    }
15  }
16
17  async loadHistory(): Promise<void> {
18    try {
19      // Use message bridge instead of fetch
20      return new Promise((resolve, reject) => {
21        // Set up response handler
22        window.__dumber_history_recent = (data: HistoryEntry[]) => {
23          this.history = Array.isArray(data) ? data : [];
24          resolve();
25        };
26
27        window.__dumber_history_error = (error: string) => {
28          console.error("Failed to load history:", error);
29          reject(new Error(error));
30        };
31
32        // Send message to Go backend
33        const bridge = window.webkit?.messageHandlers?.dumber;
34        if (bridge && typeof bridge.postMessage === "function") {
35          bridge.postMessage(
36            JSON.stringify({
37              type: "history_recent",
38              limit: 50,
39              offset: 0,
40            }),
41          );
42        } else {
43          reject(new Error("WebKit message handler not available"));
44        }
45      });
46    } catch (error) {
47      console.error("Failed to load history:", error);
48      // Mock history for development/fallback
49      this.history = [
50        {
51          id: 1,
52          url: "https://github.com/wailsapp/wails",
53          title: "Wails Framework",
54          visit_count: 3,
55          last_visited: new Date().toISOString(),
56          created_at: new Date().toISOString(),
57        },
58        {
59          id: 2,
60          url: "https://go.dev",
61          title: "The Go Programming Language",
62          visit_count: 2,
63          last_visited: new Date().toISOString(),
64          created_at: new Date().toISOString(),
65        },
66        {
67          id: 3,
68          url: "https://developer.mozilla.org",
69          title: "MDN Web Docs",
70          visit_count: 1,
71          last_visited: new Date().toISOString(),
72          created_at: new Date().toISOString(),
73        },
74      ];
75    }
76  }
77
78  async loadShortcuts(): Promise<void> {
79    try {
80      const cfg = await fetch("/api/config").then((r) => r.json());
81      const raw = cfg?.search_shortcuts || {};
82      // Normalize field casing from backend (supports URL/Description and url/description)
83      const normalized: Record<string, SearchShortcut> = {};
84      for (const [key, value] of Object.entries(raw)) {
85        const v = value as Record<string, unknown>;
86        normalized[key] = {
87          id: 0,
88          shortcut: key,
89          url_template: (v.url ?? v.URL ?? "") as string,
90          description: (v.description ?? v.Description ?? "") as string,
91          created_at: null,
92        };
93      }
94      this.shortcuts = normalized;
95    } catch (error) {
96      console.error("Failed to load shortcuts:", error);
97      // Mock shortcuts for development/fallback
98      this.shortcuts = {
99        g: {
100          id: 0,
101          shortcut: "g",
102          url_template: "https://google.com/search?q={query}",
103          description: "Google Search",
104          created_at: null,
105        },
106        gh: {
107          id: 0,
108          shortcut: "gh",
109          url_template: "https://github.com/search?q={query}",
110          description: "GitHub Search",
111          created_at: null,
112        },
113        so: {
114          id: 0,
115          shortcut: "so",
116          url_template: "https://stackoverflow.com/search?q={query}",
117          description: "Stack Overflow",
118          created_at: null,
119        },
120        w: {
121          id: 0,
122          shortcut: "w",
123          url_template:
124            "https://en.wikipedia.org/wiki/Special:Search?search={query}",
125          description: "Wikipedia",
126          created_at: null,
127        },
128      };
129    }
130  }
131
132  getHistory(): HistoryEntry[] {
133    return this.history;
134  }
135
136  getShortcuts(): Record<string, SearchShortcut> {
137    return this.shortcuts;
138  }
139
140  async copyToClipboard(text: string): Promise<void> {
141    try {
142      await navigator.clipboard.writeText(text);
143      console.log("Copied to clipboard:", text);
144    } catch (err) {
145      console.error("Failed to copy:", err);
146    }
147  }
148}

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