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}