v0.8.0 0fa6785

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 = container === 'history' ? this.historyListElement : this.shortcutsElement;
54    if (element) {
55      element.innerHTML = '<div class="loading">Loading...</div>';
56    }
57  }
58
59  showError(container: 'history' | 'shortcuts', message: string): void {
60    const element = container === 'history' ? this.historyListElement : this.shortcutsElement;
61    if (element) {
62      element.innerHTML = `
63        <div class="empty-state">
64          <h3>Error</h3>
65          <p>${message}</p>
66        </div>
67      `;
68    }
69  }
70
71  private createHistoryItem(item: History): HTMLElement {
72    const historyItem = document.createElement('div');
73    historyItem.className = 'history-item';
74    const parsed = this.safeParseURL(item.url);
75    const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled');
76
77    const line = document.createElement('div');
78    line.className = 'history-line';
79
80    // Favicon chip (wrapper ensures perfect circle sizing)
81    const iconWrap = document.createElement('div');
82    iconWrap.className = 'history-favicon-chip';
83    const icon = document.createElement('img');
84    icon.className = 'history-favicon-img';
85    icon.width = 16; icon.height = 16;
86    icon.loading = 'lazy';
87    icon.referrerPolicy = 'no-referrer';
88    icon.src = this.buildFaviconURL(item.url);
89    icon.addEventListener('error', () => { iconWrap.style.display = 'none'; });
90    iconWrap.appendChild(icon);
91
92    const titleEl = document.createElement('span');
93    titleEl.className = 'history-title';
94    titleEl.textContent = title;
95
96    const sep1 = document.createElement('span');
97    sep1.className = 'history-sep';
98    sep1.textContent = ' - ';
99
100    const domainEl = document.createElement('span');
101    domainEl.className = 'history-domain';
102    domainEl.textContent = parsed.host || '';
103
104    const sep2 = document.createElement('span');
105    sep2.className = 'history-sep';
106    sep2.textContent = ' - ';
107
108    const urlEl = document.createElement('span');
109    urlEl.className = 'history-url';
110    urlEl.textContent = item.url;
111
112    line.appendChild(iconWrap);
113    line.appendChild(titleEl);
114    line.appendChild(sep1);
115    line.appendChild(domainEl);
116    line.appendChild(sep2);
117    line.appendChild(urlEl);
118
119    historyItem.appendChild(line);
120    
121    historyItem.addEventListener('click', () => {
122      this.navigateToUrl(item.url);
123    });
124    
125    return historyItem;
126  }
127
128  private createShortcutElement(key: string, shortcut: Shortcut): HTMLElement {
129    const shortcutEl = document.createElement('div');
130    shortcutEl.className = 'shortcut';
131    shortcutEl.innerHTML = `
132      <div class="shortcut-key">${this.escapeHtml(key)}:</div>
133      <div class="shortcut-desc">${this.escapeHtml(shortcut.description || 'No description')}</div>
134    `;
135    
136    shortcutEl.addEventListener('click', () => {
137      // Navigate to the base URL of the shortcut (remove %s template parameter)
138      const baseUrl = this.extractBaseUrl(shortcut.url_template);
139      this.navigateToUrl(baseUrl);
140    });
141    
142    return shortcutEl;
143  }
144
145  private async navigateToUrl(url: string): Promise<void> {
146    console.log('Navigating to:', url);
147    window.location.href = url;
148  }
149
150  private extractBaseUrl(templateUrl: string): string {
151    try {
152      // Support both %s and {query} placeholders
153      const candidate = templateUrl.replace(/%s|\{query\}/g, "");
154      const url = new URL(candidate);
155      // Remove empty query params left after placeholder removal
156      const params = new URLSearchParams(url.search);
157      for (const key of Array.from(params.keys())) {
158        const v = params.get(key);
159        if (v === null || v === "") {
160          params.delete(key);
161        }
162      }
163      url.search = params.toString();
164      return url.toString();
165    } catch (error) {
166      console.error('Failed to parse shortcut URL:', templateUrl, error);
167      throw error;
168    }
169  }
170
171  private escapeHtml(text: string): string {
172    const div = document.createElement('div');
173    div.textContent = text;
174    return div.innerHTML;
175  }
176
177  private safeParseURL(raw: string): { host: string } {
178    try {
179      const u = new URL(raw);
180      return { host: u.host };
181    } catch {
182      return { host: '' };
183    }
184  }
185
186  private buildFaviconURL(raw: string): string {
187    try {
188      const u = new URL(raw);
189      const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:';
190      return `${scheme}//${u.host}/favicon.ico`;
191    } catch {
192      return '';
193    }
194  }
195}

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