v0.12.1 1453d21

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          favicon_url: null,
58        },
59        {
60          id: 2,
61          url: "https://go.dev",
62          title: "The Go Programming Language",
63          visit_count: 2,
64          last_visited: new Date().toISOString(),
65          created_at: new Date().toISOString(),
66          favicon_url: null,
67        },
68        {
69          id: 3,
70          url: "https://developer.mozilla.org",
71          title: "MDN Web Docs",
72          visit_count: 1,
73          last_visited: new Date().toISOString(),
74          created_at: new Date().toISOString(),
75          favicon_url: null,
76        },
77      ];
78    }
79  }
80
81  async loadShortcuts(): Promise<void> {
82    return new Promise((resolve, reject) => {
83      // Set up response handler
84      window.__dumber_search_shortcuts = (data: unknown) => {
85        // Type guard
86        if (typeof data !== "object" || data === null) {
87          reject(new Error("Invalid shortcuts data"));
88          return;
89        }
90        const dataObj = data as Record<string, unknown>;
91        // Normalize field casing from backend (supports URL/Description and url/description)
92        const normalized: Record<string, SearchShortcut> = {};
93        for (const [key, value] of Object.entries(dataObj)) {
94          const v = value as Record<string, unknown>;
95          normalized[key] = {
96            id: 0,
97            shortcut: key,
98            url_template: (v.url ?? v.URL ?? "") as string,
99            description: (v.description ?? v.Description ?? "") as string,
100            created_at: null,
101          };
102        }
103        this.shortcuts = normalized;
104        resolve();
105      };
106
107      window.__dumber_search_shortcuts_error = (error: string) => {
108        console.error("Failed to load shortcuts:", error);
109        reject(new Error(error));
110      };
111
112      // Send message to Go backend
113      const bridge = window.webkit?.messageHandlers?.dumber;
114      if (bridge && typeof bridge.postMessage === "function") {
115        bridge.postMessage(
116          JSON.stringify({
117            type: "get_search_shortcuts",
118          }),
119        );
120      } else {
121        reject(new Error("WebKit message handler not available"));
122      }
123    });
124  }
125
126  getHistory(): HistoryEntry[] {
127    return this.history;
128  }
129
130  getShortcuts(): Record<string, SearchShortcut> {
131    return this.shortcuts;
132  }
133
134  async copyToClipboard(text: string): Promise<void> {
135    try {
136      await navigator.clipboard.writeText(text);
137      console.log("Copied to clipboard:", text);
138    } catch (err) {
139      console.error("Failed to copy:", err);
140    }
141  }
142}

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