v0.4.0 6203a22

ucm_js_cgo.go RawHistoryFolder

1//go:build webkit_cgo
2
3package webkit
4
5// getOmniboxScript returns the injected omnibox/find component script.
6// Kept in a cgo-tagged file to ensure availability when referenced
7// from other cgo files in this package.
8func getOmniboxScript() string {
9    return `(() => {
10  try {
11    if (window.__dumber_omnibox_loaded) return; // idempotent
12    window.__dumber_omnibox_loaded = true;
13
14    const MAX_MATCHES = 2000;
15    const H = {
16      el: null,
17      box: null,
18      input: null,
19      list: null,
20      visible: false,
21      mode: 'omnibox', // 'omnibox' | 'find'
22      suggestions: [],
23      matches: [], // {el, context}
24      selectedIndex: -1,
25      activeIndex: -1,
26      debounceTimer: 0,
27      highlightNodes: [],
28      faded: false,
29      prevOverflow: '',
30      post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} },
31      render(){
32        if (!H.el) H.mount();
33        H.el.style.display = H.visible ? 'block' : 'none';
34        if (!H.visible) return;
35        H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…';
36        H.input.focus();
37      },
38      setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); },
39      open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } },
40      close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); },
41      toggle(v){
42        const newState = (typeof v==='boolean')? v : !H.visible;
43        if (newState && !H.visible) {
44          // Disable page scrolling while overlay is visible
45          H.prevOverflow = document.documentElement.style.overflow;
46          document.documentElement.style.overflow = 'hidden';
47        } else if (!newState && H.visible) {
48          // Restore page scrolling on close
49          document.documentElement.style.overflow = H.prevOverflow || '';
50          H.prevOverflow = '';
51        }
52        H.visible = newState;
53        H.render();
54      },
55      setFaded(v){
56        H.faded = !!v;
57        if (!H.box || !H.input || !H.list) return;
58        if (H.faded) {
59          H.box.style.background = 'rgba(27,27,27,0.25)';
60          H.box.style.backdropFilter = 'blur(2px) saturate(110%)';
61          H.box.style.webkitBackdropFilter = 'blur(2px) saturate(110%)';
62          H.input.style.background = 'rgba(18,18,18,0.35)';
63          H.input.style.color = '#eee';
64        } else {
65          H.box.style.background = '#1b1b1b';
66          H.box.style.backdropFilter = '';
67          H.box.style.webkitBackdropFilter = '';
68          H.input.style.background = '#121212';
69          H.input.style.color = '#eee';
70        }
71      },
72      scrollListToSelection(){
73        if (!H.list) return;
74        let idx = H.selectedIndex|0;
75        if (idx < 0) return;
76        // Account for header in find mode
77        const childIdx = (H.mode==='find' ? 1 : 0) + idx;
78        const el = H.list.children[childIdx];
79        if (el && el.scrollIntoView) {
80          try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); }
81        }
82      },
83      mount(){
84        const root = document.createElement('div');
85        root.id = 'dumber-omnibox-root';
86        root.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:none;';
87        const box = document.createElement('div');
88        box.style.cssText = 'max-width:720px;margin:8vh auto;padding:8px 10px;background:#1b1b1b;color:#eee;border:1px solid #444;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.6);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;';
89        const input = document.createElement('input');
90        input.type = 'text';
91        input.placeholder = 'Type URL or search…';
92        input.style.cssText = 'width:100%;padding:10px 12px;border-radius:6px;border:1px solid #555;background:#121212;color:#eee;font-size:16px;outline:none;';
93        const list = document.createElement('div');
94        list.style.cssText = 'margin-top:8px;max-height:50vh;overflow:auto;border-top:1px solid #333;';
95        const style = document.createElement('style');
96        style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}.dumber-find-active{background:#ff9800 !important;color:#000;box-shadow:0 0 0 1px #b36b00 inset}';
97        document.documentElement.appendChild(style);
98        box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);
99        H.box = box;
100        // Click outside the box closes the overlay
101        root.addEventListener('mousedown', (e)=>{
102          const tgt = e.target;
103          if (tgt && box && !box.contains(tgt)) {
104            e.preventDefault();
105            e.stopPropagation();
106            H.close();
107          }
108        }, true);
109        box.addEventListener('mousedown', (e)=>{ e.stopPropagation(); }, true);
110        // Focus and select input on hover for quick typing
111        input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });
112        root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } H.setFaded(false); } });
113        // Clear fading when user goes back to input typing
114        input.addEventListener('focus', ()=> H.setFaded(false));
115        input.addEventListener('keydown', (e)=>{
116          if (e.key === 'Escape'){ H.close(); }
117          else if (H.mode==='omnibox' && e.key === 'Enter'){
118            const pick = H.suggestions && H.suggestions[H.selectedIndex|0];
119            const v = (pick && pick.url) || input.value || '';
120            if (v) H.post({type:'navigate', url:v});
121            H.toggle(false);
122          } else if (H.mode==='find' && e.key === 'Enter'){
123            e.preventDefault();
124            if (e.shiftKey) {
125              H.jump(-1);
126            } else if (e.altKey) {
127              // Center on current match but keep overlay open for further navigation
128              H.revealSelection();
129            } else {
130              // Default: center and close
131              H.revealSelection();
132              H.close();
133            }
134          } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
135            e.preventDefault(); e.stopPropagation();
136            const n = (H.mode==='find'? H.matches.length : H.suggestions.length);
137            if (n){
138              H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
139              if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
140              H.paintList();
141              H.scrollListToSelection();
142              // Fade overlay while navigating results; also reveal active match in find mode
143              H.setFaded(true);
144              if (H.mode==='find') { H.revealSelection(); }
145            }
146          } else {
147            // Any other key input should restore full opacity while editing
148            H.setFaded(false);
149          }
150        });
151        input.addEventListener('input', ()=> { H.onInput(); H.setFaded(false); });
152        input.addEventListener('mousedown', ()=> H.setFaded(false));
153        input.addEventListener('click', ()=> H.setFaded(false));
154        H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList();
155      },
156      onInput(){
157        const q = H.input.value || '';
158        if (H.mode === 'omnibox'){
159          clearTimeout(H.debounceTimer);
160          H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120);
161        } else {
162          H.find(q);
163        }
164      },
165      paintList(){
166        const list = H.list; if (!list) return; list.textContent = '';
167        if (H.mode==='omnibox'){
168          H.suggestions.forEach((s, i)=>{
169            const item = document.createElement('div');
170            item.style.cssText = 'padding:8px 10px;display:flex;gap:10px;align-items:center;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
171            // Favicon chip (wrapper ensures perfect circle sizing)
172            const chip = document.createElement('div');
173            chip.style.cssText = 'flex:0 0 20px;width:20px;height:20px;border-radius:50%;background:#ccc;border:1px solid rgba(0,0,0,.12);box-shadow:0 1px 2px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;';
174            const icon = document.createElement('img');
175            icon.src = s.favicon || '';
176            icon.width = 16; icon.height = 16; icon.loading = 'lazy';
177            icon.style.cssText = 'width:16px;height:16px;filter:brightness(1.06) contrast(1.03);image-rendering:-webkit-optimize-contrast;';
178            icon.onerror = ()=>{ chip.style.display='none'; };
179            chip.appendChild(icon);
180            // Text line: Domain | full path (one line, fade at end)
181            let domain = '', path = '';
182            try { const u = new URL(s.url, window.location.href); domain = u.hostname; path = (u.pathname||'') + (u.search||'') + (u.hash||''); } catch(_) { domain = s.url || ''; }
183            const text = document.createElement('div');
184            text.style.cssText = 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;white-space:nowrap;overflow:hidden;';
185            // Apply gradient fade using mask for WebKit
186            text.style.webkitMaskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
187            text.style.maskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
188            const domainEl = document.createElement('span'); domainEl.textContent = domain; domainEl.style.cssText = 'color:#e6e6e6;opacity:.95;';
189            const sep = document.createElement('span'); sep.textContent = ' | '; sep.style.cssText = 'color:#777;';
190            const pathEl = document.createElement('span'); pathEl.textContent = path || '/'; pathEl.style.cssText = 'color:#9ad;';
191            text.appendChild(domainEl); text.appendChild(sep); text.appendChild(pathEl);
192            item.appendChild(chip); item.appendChild(text);
193            item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
194            item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
195            list.appendChild(item);
196          });
197        } else {
198          const total = H.matches.length;
199          const header = document.createElement('div');
200          header.textContent = total ? total + ' matches' : 'No matches';
201          header.style.cssText = 'padding:6px 10px;color:#bbb;font-size:12px;border-bottom:1px solid #2a2a2a;';
202          list.appendChild(header);
203          H.matches.forEach((m, i)=>{
204            const item = document.createElement('div');
205            item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
206            const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
207            item.appendChild(ctx);
208            item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
209            item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });
210            list.appendChild(item);
211          });
212        }
213      },
214      setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); },
215      // FIND MODE IMPLEMENTATION
216      clearHighlights(){
217        try {
218          H.highlightNodes.forEach(({span, text})=>{
219            const p = span.parentNode; if (!p) return; p.replaceChild(text, span); p.normalize();
220          });
221        } catch(_){}
222        H.highlightNodes = [];
223        H.matches = [];
224        H.selectedIndex = -1;
225        H.activeIndex = -1;
226        H.paintList();
227      },
228      find(q){
229        H.clearHighlights();
230        q = (q||'').trim(); if (!q) return;
231        const body = document.body; if (!body) return;
232        const root = document.getElementById('dumber-omnibox-root');
233        const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
234          acceptNode(node){
235            if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
236            let el = node.parentElement; if (!el) return NodeFilter.FILTER_REJECT;
237            if (root && root.contains(el)) return NodeFilter.FILTER_REJECT;
238            const name = el.tagName; if (name==='SCRIPT'||name==='STYLE'||name==='NOSCRIPT') return NodeFilter.FILTER_REJECT;
239            if (getComputedStyle(el).visibility==='hidden' || getComputedStyle(el).display==='none') return NodeFilter.FILTER_REJECT;
240            return NodeFilter.FILTER_ACCEPT;
241          }
242        });
243        const lc = q.toLowerCase();
244        while (walker.nextNode()){
245          const text = walker.currentNode;
246          let s = text.nodeValue; let i = 0;
247          while (s && (i = s.toLowerCase().indexOf(lc)) !== -1) {
248            const before = document.createTextNode(s.slice(0, i));
249            const match = document.createTextNode(s.slice(i, i+q.length));
250            const afterVal = s.slice(i+q.length);
251            const span = document.createElement('span'); span.className = 'dumber-find-highlight'; span.appendChild(match);
252            const parent = text.parentNode; if (!parent) break;
253            parent.insertBefore(before, text);
254            parent.insertBefore(span, text);
255            text.nodeValue = afterVal;
256            H.highlightNodes.push({span, text: match});
257            const leftCtx = before.nodeValue.slice(-30);
258            let rightRaw = afterVal.slice(0, 30);
259            const boundaryIdx = rightRaw.search(/[\.,;:\-]/);
260            if (boundaryIdx !== -1) {
261              rightRaw = rightRaw.slice(0, boundaryIdx+1);
262            }
263            const context = (leftCtx + match.nodeValue + rightRaw).replace(/\s+/g,' ').trim();
264            H.matches.push({el: span, context});
265            if (H.matches.length >= MAX_MATCHES) break;
266            s = afterVal;
267          }
268          if (H.matches.length >= MAX_MATCHES) break;
269        }
270        H.selectedIndex = H.matches.length ? 0 : -1;
271        H.paintList();
272        H.revealSelection();
273      },
274      revealSelection(){
275        const prev = H.matches[H.activeIndex|0];
276        if (prev && prev.el && prev.el.classList) { prev.el.classList.remove('dumber-find-active'); }
277        const m = H.matches[H.selectedIndex|0]; if (!m) return;
278        if (m.el && m.el.classList) { m.el.classList.add('dumber-find-active'); }
279        H.activeIndex = H.selectedIndex|0;
280        try { m.el.scrollIntoView({block:'center', inline:'nearest'}); } catch(_){ m.el.scrollIntoView(); }
281      },
282      jump(delta){
283        const n = H.matches.length; if (!n) return;
284        H.selectedIndex = ((H.selectedIndex||0) + delta) % n; if (H.selectedIndex<0) H.selectedIndex = n-1;
285        H.paintList(); H.revealSelection();
286      }
287    };
288
289    // Keyboard hooks
290    window.addEventListener('keydown', (e)=>{
291      const mod = (e.ctrlKey||e.metaKey);
292      if (mod && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.open('omnibox'); }
293      if (mod && (e.key==='f' || e.key==='F')) { e.preventDefault(); H.open('find'); }
294    }, true);
295
296    // Public API for native
297    window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr);
298    window.__dumber_toggle = ()=> H.toggle();
299    window.__dumber_find_open = (q)=> H.open('find', q||'');
300    window.__dumber_find_close = ()=> H.close();
301    window.__dumber_find_query = (q)=> { if (H.mode!=='find') H.setMode('find'); if (!H.visible) H.toggle(true); H.input.value = q||''; H.find(q||''); };
302  } catch (e) { /* no-op */ }
303})();`
304}

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