v0.10.0 0f4e9eb

ucm_js_cgo.go.deprecated 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        // Performance optimization: elements are pre-mounted, just show/hide
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 = 'width:min(90vw, 720px);margin:8vh auto;padding:8px 12px;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%;box-sizing:border-box;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;';
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        // Dynamic style updater for responsive behavior
101        const updateStyles = () => {
102          const vw = window.innerWidth;
103          const isSmall = vw < 640;
104          const isLarge = vw > 1920;
105          
106          if (box) {
107            box.style.width = isSmall ? '95vw' : isLarge ? 'min(90vw, 840px)' : 'min(90vw, 720px)';
108            box.style.padding = isSmall ? '6px 8px' : '8px 12px';
109          }
110          
111          if (input) {
112            input.style.fontSize = isSmall ? '14px' : isLarge ? '17px' : '16px';
113            input.style.padding = isSmall ? '8px 10px' : '10px 12px';
114            input.style.boxSizing = 'border-box';
115          }
116        };
117        updateStyles();
118        window.addEventListener('resize', updateStyles);
119        // Click outside the box closes the overlay
120        root.addEventListener('mousedown', (e)=>{
121          const tgt = e.target;
122          if (tgt && box && !box.contains(tgt)) {
123            e.preventDefault();
124            e.stopPropagation();
125            H.close();
126          }
127        }, true);
128        box.addEventListener('mousedown', (e)=>{ e.stopPropagation(); }, true);
129        // Focus and select input on hover for quick typing
130        input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });
131        root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } H.setFaded(false); } });
132        // Clear fading when user goes back to input typing
133        input.addEventListener('focus', ()=> H.setFaded(false));
134        input.addEventListener('keydown', (e)=>{
135          if (e.key === 'Escape'){ H.close(); }
136          else if (H.mode==='omnibox' && e.key === 'Enter'){
137            const pick = H.suggestions && H.suggestions[H.selectedIndex|0];
138            const v = (pick && pick.url) || input.value || '';
139            if (v) H.post({type:'navigate', url:v});
140            H.toggle(false);
141          } else if (H.mode==='find' && e.key === 'Enter'){
142            e.preventDefault();
143            if (e.shiftKey) {
144              H.jump(-1);
145            } else if (e.altKey) {
146              // Center on current match but keep overlay open for further navigation
147              H.revealSelection();
148            } else {
149              // Default: center and close
150              H.revealSelection();
151              H.close();
152            }
153          } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
154            e.preventDefault(); e.stopPropagation();
155            const n = (H.mode==='find'? H.matches.length : H.suggestions.length);
156            if (n){
157              H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
158              if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
159              H.paintList();
160              H.scrollListToSelection();
161              // Fade overlay while navigating results; also reveal active match in find mode
162              H.setFaded(true);
163              if (H.mode==='find') { H.revealSelection(); }
164            }
165          } else {
166            // Any other key input should restore full opacity while editing
167            H.setFaded(false);
168          }
169        });
170        input.addEventListener('input', ()=> { H.onInput(); H.setFaded(false); });
171        input.addEventListener('mousedown', ()=> H.setFaded(false));
172        input.addEventListener('click', ()=> H.setFaded(false));
173        H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList();
174      },
175      onInput(){
176        const q = H.input.value || '';
177        if (H.mode === 'omnibox'){
178          clearTimeout(H.debounceTimer);
179          H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120);
180        } else {
181          H.find(q);
182        }
183      },
184      paintList(){
185        const list = H.list; if (!list) return; list.textContent = '';
186        
187        // Show/hide separator based on content
188        const hasContent = (H.mode==='omnibox' && H.suggestions.length > 0) || (H.mode==='find' && H.matches.length > 0);
189        list.style.borderTop = hasContent ? '1px solid #333' : 'none';
190        
191        if (H.mode==='omnibox'){
192          H.suggestions.forEach((s, i)=>{
193            const item = document.createElement('div');
194            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;':'');
195            // Favicon chip (wrapper ensures perfect circle sizing)
196            const chip = document.createElement('div');
197            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;';
198            const icon = document.createElement('img');
199            icon.src = s.favicon || '';
200            icon.loading = 'lazy';
201            icon.style.cssText = 'width:16px;height:16px;filter:brightness(1.06) contrast(1.03);image-rendering:-webkit-optimize-contrast;';
202            icon.onerror = ()=>{ chip.style.display='none'; };
203            chip.appendChild(icon);
204            // Text line: Domain | full path (one line, fade at end)
205            let domain = '', path = '';
206            try { const u = new URL(s.url, window.location.href); domain = u.hostname; path = (u.pathname||'') + (u.search||'') + (u.hash||''); } catch(_) { domain = s.url || ''; }
207            const text = document.createElement('div');
208            text.style.cssText = 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;white-space:nowrap;overflow:hidden;';
209            // Apply gradient fade using mask for WebKit
210            text.style.webkitMaskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
211            text.style.maskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
212            const domainEl = document.createElement('span'); domainEl.textContent = domain; domainEl.style.cssText = 'color:#e6e6e6;opacity:.95;';
213            const sep = document.createElement('span'); sep.textContent = ' | '; sep.style.cssText = 'color:#777;';
214            const pathEl = document.createElement('span'); pathEl.textContent = path || '/'; pathEl.style.cssText = 'color:#9ad;';
215            text.appendChild(domainEl); text.appendChild(sep); text.appendChild(pathEl);
216            item.appendChild(chip); item.appendChild(text);
217            item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
218            item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
219            list.appendChild(item);
220          });
221        } else {
222          const total = H.matches.length;
223          const header = document.createElement('div');
224          header.textContent = total ? total + ' matches' : 'No matches';
225          header.style.cssText = 'padding:6px 10px;color:#bbb;font-size:12px;border-bottom:1px solid #2a2a2a;';
226          list.appendChild(header);
227          H.matches.forEach((m, i)=>{
228            const item = document.createElement('div');
229            item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
230            const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
231            item.appendChild(ctx);
232            item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
233            item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });
234            list.appendChild(item);
235          });
236        }
237      },
238      setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); },
239      // FIND MODE IMPLEMENTATION
240      clearHighlights(){
241        try {
242          H.highlightNodes.forEach(({span, text})=>{
243            const p = span.parentNode; if (!p) return; p.replaceChild(text, span); p.normalize();
244          });
245        } catch(_){}
246        H.highlightNodes = [];
247        H.matches = [];
248        H.selectedIndex = -1;
249        H.activeIndex = -1;
250        H.paintList();
251      },
252      find(q){
253        H.clearHighlights();
254        q = (q||'').trim(); if (!q) return;
255        const body = document.body; if (!body) return;
256        const root = document.getElementById('dumber-omnibox-root');
257        const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
258          acceptNode(node){
259            if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
260            let el = node.parentElement; if (!el) return NodeFilter.FILTER_REJECT;
261            if (root && root.contains(el)) return NodeFilter.FILTER_REJECT;
262            const name = el.tagName; if (name==='SCRIPT'||name==='STYLE'||name==='NOSCRIPT') return NodeFilter.FILTER_REJECT;
263            if (getComputedStyle(el).visibility==='hidden' || getComputedStyle(el).display==='none') return NodeFilter.FILTER_REJECT;
264            return NodeFilter.FILTER_ACCEPT;
265          }
266        });
267        const lc = q.toLowerCase();
268        while (walker.nextNode()){
269          const text = walker.currentNode;
270          let s = text.nodeValue; let i = 0;
271          while (s && (i = s.toLowerCase().indexOf(lc)) !== -1) {
272            const before = document.createTextNode(s.slice(0, i));
273            const match = document.createTextNode(s.slice(i, i+q.length));
274            const afterVal = s.slice(i+q.length);
275            const span = document.createElement('span'); span.className = 'dumber-find-highlight'; span.appendChild(match);
276            const parent = text.parentNode; if (!parent) break;
277            parent.insertBefore(before, text);
278            parent.insertBefore(span, text);
279            text.nodeValue = afterVal;
280            H.highlightNodes.push({span, text: match});
281            const leftCtx = before.nodeValue.slice(-30);
282            let rightRaw = afterVal.slice(0, 30);
283            const boundaryIdx = rightRaw.search(/[\.,;:\-]/);
284            if (boundaryIdx !== -1) {
285              rightRaw = rightRaw.slice(0, boundaryIdx+1);
286            }
287            const context = (leftCtx + match.nodeValue + rightRaw).replace(/\s+/g,' ').trim();
288            H.matches.push({el: span, context});
289            if (H.matches.length >= MAX_MATCHES) break;
290            s = afterVal;
291          }
292          if (H.matches.length >= MAX_MATCHES) break;
293        }
294        H.selectedIndex = H.matches.length ? 0 : -1;
295        H.paintList();
296        H.revealSelection();
297      },
298      revealSelection(){
299        const prev = H.matches[H.activeIndex|0];
300        if (prev && prev.el && prev.el.classList) { prev.el.classList.remove('dumber-find-active'); }
301        const m = H.matches[H.selectedIndex|0]; if (!m) return;
302        if (m.el && m.el.classList) { m.el.classList.add('dumber-find-active'); }
303        H.activeIndex = H.selectedIndex|0;
304        try { m.el.scrollIntoView({block:'center', inline:'nearest'}); } catch(_){ m.el.scrollIntoView(); }
305      },
306      jump(delta){
307        const n = H.matches.length; if (!n) return;
308        H.selectedIndex = ((H.selectedIndex||0) + delta) % n; if (H.selectedIndex<0) H.selectedIndex = n-1;
309        H.paintList(); H.revealSelection();
310      }
311    };
312
313    // Keyboard hooks
314    window.addEventListener('keydown', (e)=>{
315      const mod = (e.ctrlKey||e.metaKey);
316      if (mod && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.open('omnibox'); }
317      if (mod && (e.key==='f' || e.key==='F')) { e.preventDefault(); H.open('find'); }
318    }, true);
319
320    // Public API for native
321    window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr);
322    window.__dumber_toggle = ()=> H.toggle();
323    window.__dumber_find_open = (q)=> H.open('find', q||'');
324    window.__dumber_find_close = ()=> H.close();
325    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||''); };
326
327    // Performance optimization: Pre-mount DOM elements immediately for instant show/hide
328    if (document.readyState === 'loading') {
329      document.addEventListener('DOMContentLoaded', ()=> H.mount());
330    } else {
331      H.mount(); // DOM already ready
332    }
333  } catch (e) { /* no-op */ }
334})();`
335}

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