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}