v0.2.0 759cc92

ucm_js.go RawHistoryFolder

1//go:build webkit_cgo
2
3package webkit
4
5// ucmOmniboxScript is injected at document-start via WebKit UserContentManager.
6// It renders a minimal overlay with an input and a suggestions list, handles
7// Ctrl+L to toggle, Enter to navigate, and posts search queries to native.
8const ucmOmniboxScript = `(() => {
9  try {
10    if (window.__dumber_omnibox_loaded) return; // idempotent
11    window.__dumber_omnibox_loaded = true;
12
13    const H = {
14      el: null,
15      input: null,
16      list: null,
17      visible: false,
18      suggestions: [],
19      debounceTimer: 0,
20      post(msg){ try { window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumber && window.webkit.messageHandlers.dumber.postMessage(JSON.stringify(msg)); } catch(_){} },
21      render(){
22        if (!H.el) H.mount();
23        H.el.style.display = H.visible ? 'block' : 'none';
24        if (H.visible) H.input.focus();
25      },
26      mount(){
27        const root = document.createElement('div');
28        root.id = 'dumber-omnibox-root';
29        root.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:none;';
30        const box = document.createElement('div');
31        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;';
32        const input = document.createElement('input');
33        input.type = 'text';
34        input.placeholder = 'Type URL or search…';
35        input.style.cssText = 'width:100%;padding:10px 12px;border-radius:6px;border:1px solid #555;background:#121212;color:#eee;font-size:16px;outline:none;';
36        const list = document.createElement('div');
37        list.style.cssText = 'margin-top:8px;max-height:50vh;overflow:auto;border-top:1px solid #333;';
38        box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);
39        input.addEventListener('keydown', (e)=>{
40          if (e.key === 'Escape'){ H.toggle(false); }
41          else if (e.key === 'Enter'){
42            const pick = H.suggestions && H.suggestions[H.selectedIndex|0];
43            const v = (pick && pick.url) || input.value || '';
44            if (v) H.post({type:'navigate', url:v});
45            H.toggle(false);
46          } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
47            e.preventDefault();
48            const n = H.suggestions.length;
49            if (n){
50              H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
51              if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
52              H.paintList();
53            }
54          }
55        });
56        input.addEventListener('input', ()=>{
57          clearTimeout(H.debounceTimer);
58          const q = input.value;
59          H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120);
60        });
61        H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList();
62      },
63      paintList(){
64        const list = H.list; if (!list) return;
65        list.textContent = '';
66        H.suggestions.forEach((s, i)=>{
67          const item = document.createElement('div');
68          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;':'');
69          const url = document.createElement('div'); url.textContent = s.url || ''; url.style.cssText = 'flex:1;color:#9ad;word-break:break-all;';
70          const title = document.createElement('div'); title.textContent = s.title || ''; title.style.cssText = 'flex:1;color:#ccc;opacity:.9;';
71          item.appendChild(title); item.appendChild(url);
72          item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
73          list.appendChild(item);
74        });
75      },
76      toggle(v){ H.visible = (typeof v==='boolean')? v : !H.visible; H.render(); },
77      setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); }
78    };
79    window.addEventListener('keydown', (e)=>{
80      if ((e.ctrlKey||e.metaKey) && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.toggle(true); }
81    }, true);
82    // Allow native side to update suggestions or toggle
83    window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr);
84    window.__dumber_toggle = ()=> H.toggle();
85  } catch (e) { /* no-op */ }
86})();`

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