v0.23.2 bfd5eb4

dom.ts RawHistoryFolder

1import type { History, Shortcut } from "../types/generated.js";
2
3export class DOMRenderer {
4  private historyListElement: HTMLElement | null = null;
5  private shortcutsElement: HTMLElement | null = null;
6
7  constructor() {
8    this.historyListElement = document.getElementById("historyList");
9    this.shortcutsElement = document.getElementById("shortcuts");
10  }
11
12  displayHistory(historyEntries: History[]): void {
13    if (!this.historyListElement) return;
14
15    if (!historyEntries || historyEntries.length === 0) {
16      this.historyListElement.innerHTML = `
17        <div class="empty-state">
18          <h3>No history yet</h3>
19          <p>Use CLI commands to browse websites</p>
20        </div>
21      `;
22      return;
23    }
24
25    this.historyListElement.innerHTML = "";
26    historyEntries.forEach((item) => {
27      const historyItem = this.createHistoryItem(item);
28      this.historyListElement!.appendChild(historyItem);
29    });
30  }
31
32  displayShortcuts(shortcutsData: Record<string, Shortcut>): void {
33    if (!this.shortcutsElement) return;
34
35    if (!shortcutsData || Object.keys(shortcutsData).length === 0) {
36      this.shortcutsElement.innerHTML = `
37        <div class="empty-state">
38          <h3>No shortcuts configured</h3>
39          <p>Add shortcuts to your config file</p>
40        </div>
41      `;
42      return;
43    }
44
45    this.shortcutsElement.innerHTML = "";
46    Object.entries(shortcutsData).forEach(([key, shortcut]) => {
47      const shortcutEl = this.createShortcutElement(key, shortcut);
48      this.shortcutsElement!.appendChild(shortcutEl);
49    });
50  }
51
52  showLoading(container: "history" | "shortcuts"): void {
53    const element =
54      container === "history" ? this.historyListElement : this.shortcutsElement;
55    if (element) {
56      element.innerHTML = '<div class="loading">Loading...</div>';
57    }
58  }
59
60  showError(container: "history" | "shortcuts", message: string): void {
61    const element =
62      container === "history" ? this.historyListElement : this.shortcutsElement;
63    if (element) {
64      element.innerHTML = `
65        <div class="empty-state">
66          <h3>Error</h3>
67          <p>${message}</p>
68        </div>
69      `;
70    }
71  }
72
73  private createHistoryItem(item: History): HTMLElement {
74    const historyItem = document.createElement("div");
75    historyItem.className = "history-item";
76    const parsed = this.safeParseURL(item.url);
77    const title =
78      item.title && item.title.trim() !== ""
79        ? item.title
80        : parsed.host || "Untitled";
81
82    const line = document.createElement("div");
83    line.className = "history-line";
84
85    // Favicon chip (wrapper ensures perfect circle sizing)
86    const iconWrap = document.createElement("div");
87    iconWrap.className = "history-favicon-chip";
88    const icon = document.createElement("img");
89    icon.className = "history-favicon-img";
90    icon.width = 16;
91    icon.height = 16;
92    icon.loading = "lazy";
93    icon.referrerPolicy = "no-referrer";
94    icon.src = this.buildFaviconURL(item.url);
95    icon.addEventListener("error", () => {
96      iconWrap.style.display = "none";
97    });
98    iconWrap.appendChild(icon);
99
100    const titleEl = document.createElement("span");
101    titleEl.className = "history-title";
102    titleEl.textContent = title;
103
104    const sep1 = document.createElement("span");
105    sep1.className = "history-sep";
106    sep1.textContent = " - ";
107
108    const domainEl = document.createElement("span");
109    domainEl.className = "history-domain";
110    domainEl.textContent = parsed.host || "";
111
112    const sep2 = document.createElement("span");
113    sep2.className = "history-sep";
114    sep2.textContent = " - ";
115
116    const urlEl = document.createElement("span");
117    urlEl.className = "history-url";
118    urlEl.textContent = item.url;
119
120    line.appendChild(iconWrap);
121    line.appendChild(titleEl);
122    line.appendChild(sep1);
123    line.appendChild(domainEl);
124    line.appendChild(sep2);
125    line.appendChild(urlEl);
126
127    historyItem.appendChild(line);
128
129    historyItem.addEventListener("click", () => {
130      this.navigateToUrl(item.url);
131    });
132
133    return historyItem;
134  }
135
136  private createShortcutElement(key: string, shortcut: Shortcut): HTMLElement {
137    const shortcutEl = document.createElement("div");
138    shortcutEl.className = "shortcut";
139    shortcutEl.innerHTML = `
140      <div class="shortcut-key">${this.escapeHtml(key)}:</div>
141      <div class="shortcut-desc">${this.escapeHtml(shortcut.description || "No description")}</div>
142    `;
143
144    shortcutEl.addEventListener("click", () => {
145      // Navigate to the base URL of the shortcut (remove %s template parameter)
146      const baseUrl = this.extractBaseUrl(shortcut.url_template);
147      this.navigateToUrl(baseUrl);
148    });
149
150    return shortcutEl;
151  }
152
153  private async navigateToUrl(url: string): Promise<void> {
154    console.log("Navigating to:", url);
155    window.location.href = url;
156  }
157
158  private extractBaseUrl(templateUrl: string): string {
159    try {
160      // Support both %s and {query} placeholders
161      const candidate = templateUrl.replace(/%s|\{query\}/g, "");
162      const url = new URL(candidate);
163      // Remove empty query params left after placeholder removal
164      const params = new URLSearchParams(url.search);
165      for (const key of Array.from(params.keys())) {
166        const v = params.get(key);
167        if (v === null || v === "") {
168          params.delete(key);
169        }
170      }
171      url.search = params.toString();
172      return url.toString();
173    } catch (error) {
174      console.error("Failed to parse shortcut URL:", templateUrl, error);
175      throw error;
176    }
177  }
178
179  private escapeHtml(text: string): string {
180    const div = document.createElement("div");
181    div.textContent = text;
182    return div.innerHTML;
183  }
184
185  private safeParseURL(raw: string): { host: string } {
186    try {
187      const u = new URL(raw);
188      return { host: u.host };
189    } catch {
190      return { host: "" };
191    }
192  }
193
194  private buildFaviconURL(raw: string): string {
195    try {
196      const u = new URL(raw);
197      const scheme = u.protocol && u.protocol !== ":" ? u.protocol : "https:";
198      return `${scheme}//${u.host}/favicon.ico`;
199    } catch {
200      return "";
201    }
202  }
203}

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