v0.2.0 759cc92

dom.ts RawHistoryFolder

1type HistoryEntry = { id: number; url: string; title: string };
2type SearchShortcut = { description: string; url: string };
3
4export class DOMRenderer {
5  private historyListElement: HTMLElement | null = null;
6  private shortcutsElement: HTMLElement | null = null;
7
8  constructor() {
9    this.historyListElement = document.getElementById('historyList');
10    this.shortcutsElement = document.getElementById('shortcuts');
11  }
12
13  displayHistory(historyEntries: HistoryEntry[]): void {
14    if (!this.historyListElement) return;
15
16    if (!historyEntries || historyEntries.length === 0) {
17      this.historyListElement.innerHTML = `
18        <div class="empty-state">
19          <h3>No history yet</h3>
20          <p>Use CLI commands to browse websites</p>
21        </div>
22      `;
23      return;
24    }
25
26    this.historyListElement.innerHTML = '';
27    historyEntries.forEach(item => {
28      const historyItem = this.createHistoryItem(item);
29      this.historyListElement!.appendChild(historyItem);
30    });
31  }
32
33  displayShortcuts(shortcutsData: Record<string, SearchShortcut>): void {
34    if (!this.shortcutsElement) return;
35
36    if (!shortcutsData || Object.keys(shortcutsData).length === 0) {
37      this.shortcutsElement.innerHTML = `
38        <div class="empty-state">
39          <h3>No shortcuts configured</h3>
40          <p>Add shortcuts to your config file</p>
41        </div>
42      `;
43      return;
44    }
45
46    this.shortcutsElement.innerHTML = '';
47    Object.entries(shortcutsData).forEach(([key, shortcut]) => {
48      const shortcutEl = this.createShortcutElement(key, shortcut);
49      this.shortcutsElement!.appendChild(shortcutEl);
50    });
51  }
52
53  showLoading(container: 'history' | 'shortcuts'): void {
54    const element = 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 = container === 'history' ? this.historyListElement : this.shortcutsElement;
62    if (element) {
63      element.innerHTML = `
64        <div class="empty-state">
65          <h3>Error</h3>
66          <p>${message}</p>
67        </div>
68      `;
69    }
70  }
71
72  private createHistoryItem(item: HistoryEntry): HTMLElement {
73    const historyItem = document.createElement('div');
74    historyItem.className = 'history-item';
75    const parsed = this.safeParseURL(item.url);
76    const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled');
77    historyItem.innerHTML = `
78      <div class="history-url">${this.escapeHtml(item.url)}</div>
79      <div class="history-title">${this.escapeHtml(title)}</div>
80    `;
81    
82    historyItem.addEventListener('click', () => {
83      this.navigateToUrl(item.url);
84    });
85    
86    return historyItem;
87  }
88
89  private createShortcutElement(key: string, shortcut: SearchShortcut): HTMLElement {
90    const shortcutEl = document.createElement('div');
91    shortcutEl.className = 'shortcut';
92    shortcutEl.innerHTML = `
93      <div class="shortcut-key">${this.escapeHtml(key)}:</div>
94      <div class="shortcut-desc">${this.escapeHtml(shortcut.description)}</div>
95    `;
96    
97    shortcutEl.addEventListener('click', () => {
98      // Navigate to the base URL of the shortcut (remove %s template parameter)
99      const baseUrl = this.extractBaseUrl(shortcut.url);
100      this.navigateToUrl(baseUrl);
101    });
102    
103    return shortcutEl;
104  }
105
106  private async navigateToUrl(url: string): Promise<void> {
107    console.log('Navigating to:', url);
108    window.location.href = url;
109  }
110
111  private extractBaseUrl(templateUrl: string): string {
112    try {
113      const url = new URL(templateUrl);
114      // Remove query parameters that contain %s template
115      const params = new URLSearchParams(url.search);
116      for (const [key, value] of params.entries()) {
117        if (value.includes('%s')) {
118          params.delete(key);
119        }
120      }
121      url.search = params.toString();
122      return url.toString();
123    } catch (error) {
124      // If URL parsing fails, return the original URL
125      console.warn('Failed to parse shortcut URL:', templateUrl);
126      return templateUrl;
127    }
128  }
129
130  private async copyToClipboard(text: string): Promise<void> {
131    try {
132      await navigator.clipboard.writeText(text);
133      console.log('Copied to clipboard:', text);
134    } catch (err) {
135      console.error('Failed to copy:', err);
136    }
137  }
138
139  private escapeHtml(text: string): string {
140    const div = document.createElement('div');
141    div.textContent = text;
142    return div.innerHTML;
143  }
144
145  private safeParseURL(raw: string): { host: string } {
146    try {
147      const u = new URL(raw);
148      return { host: u.host };
149    } catch {
150      return { host: '' };
151    }
152  }
153}

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