// wayBetter tab bridge (waybetter-host `bridge.rs` serves this file). Injected into every browser // tab (top frame only). Tells the shell about SPA URL changes, the favicon and focus and, when the // app enables them, forwards browser shortcuts pressed while the page has focus and link drags. // It can only call `tab_report` / `tab_shortcut` / `tab_link_drag`, which affect this tab's own // address bar, forward a shortcut, or tell the shell a link drag started/entered/ended here. (() => { if (window.top !== window || window.__wbTabBridge) return; window.__wbTabBridge = true; const options = { shortcuts: true, linkDrags: true }; // waybetter-host: bridge::script() options const invoke = (cmd, args) => { const ipc = window.__TAURI_INTERNALS__; if (ipc && typeof ipc.invoke === 'function') ipc.invoke(cmd, args).catch(() => {}); }; let lastUrl = ''; let lastTitle = ''; let lastIcon = ''; const favicon = () => { const link = document.querySelector('link[rel~="icon"][href], link[rel="shortcut icon"][href]'); try { return link ? new URL(link.getAttribute('href'), location.href).href : `${location.origin}/favicon.ico`; } catch { return ''; } }; const report = (extra) => { const url = location.href; const title = document.title || ''; const icon = favicon(); if (!extra && url === lastUrl && title === lastTitle && icon === lastIcon) return; lastUrl = url; lastTitle = title; lastIcon = icon; invoke('tab_report', { report: { url, title, favicon: icon, ...extra } }); }; let queued = false; const schedule = () => { if (!queued) { queued = true; setTimeout(() => { queued = false; report(); }, 50); } }; for (const method of ['pushState', 'replaceState']) { const original = history[method]; history[method] = function (...args) { const result = original.apply(this, args); schedule(); return result; }; } addEventListener('popstate', schedule); addEventListener('hashchange', schedule); addEventListener('focus', () => report({ focused: true })); addEventListener('pointerdown', () => report({ focused: true }), true); const watchHead = () => { if (!document.head) return; new MutationObserver(schedule).observe(document.head, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); schedule(); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', watchHead, { once: true }); else watchHead(); // Power User Mode: a deck column (the X runtime marks ) forwards the deck // keys; bare keys only while nothing editable has focus. const typing = () => { const el = document.activeElement; return !!el && (el.isContentEditable || /^(input|textarea|select)$/i.test(el.tagName)); }; const deckKey = (event) => { const mod = event.ctrlKey || event.metaKey; if (mod && event.shiftKey && !event.altKey && event.code === 'KeyD') return 'deck-toggle'; if (!document.documentElement || !document.documentElement.hasAttribute('data-wb-column')) return null; if (event.ctrlKey && event.altKey && !event.metaKey && !event.shiftKey && event.code === 'KeyN' && !typing()) return 'deck-compose'; if (event.altKey && !mod && !event.shiftKey) { const digit = /^Digit([1-9])$/.exec(event.code); if (digit) return `column-${digit[1]}`; if (event.key === 'PageUp') return 'column-page-prev'; if (event.key === 'PageDown') return 'column-page-next'; } if (!mod && !event.altKey && !typing()) { if (event.key === '[') return 'column-prev'; if (event.key === ']') return 'column-next'; } return null; }; const shortcut = (event) => { const deck = deckKey(event); if (deck) return deck; const mod = event.ctrlKey || event.metaKey; const key = event.key.toLowerCase(); if (mod && !event.altKey) { if (key === 't' && event.shiftKey) return 'reopen-tab'; if (key === 't') return 'new-tab'; if (key === 'w' && event.shiftKey) return 'open-panel'; if (key === 'w') return 'close-tab'; if (key === 'l') return 'focus-address'; if (key === 'k' && !event.shiftKey) return 'command-palette'; if (key === 'tab') return event.shiftKey ? 'prev-tab' : 'next-tab'; if (key === 'r') return event.shiftKey ? 'hard-reload' : 'reload'; // Physical key: layouts like Turkish-Q produce other characters on this key. if (event.code === 'Backslash' || event.code === 'IntlBackslash') return event.shiftKey ? 'split-down' : 'split-right'; // Page zoom is the shell's (`tab_zoom`): this tab only says which key was pressed. if (key === '=' || key === '+' || event.code === 'NumpadAdd') return 'zoom-in'; if (key === '-' || event.code === 'NumpadSubtract') return 'zoom-out'; if (key === '0' || event.code === 'Numpad0') return 'zoom-reset'; } if (key === 'f5') return 'reload'; // The app menu, like a browser's: F10, and Alt+F where Alt is not a typing key (not on macOS). if (key === 'f10' && !mod && !event.altKey && !event.shiftKey) return 'app-menu'; if (event.altKey && !mod && !event.shiftKey && event.code === 'KeyF' && !/Mac/i.test(navigator.platform)) return 'app-menu'; if (event.altKey && !mod && key === 'arrowleft') return 'back'; if (event.altKey && !mod && key === 'arrowright') return 'forward'; return null; }; // Link drags: the shell hides the tab webviews and shows its split/tab drop zones, so a link // dragged out of this page (or into it from another app) can be dropped as a new pane. if (options.linkDrags) { let localDrag = false; let lastEnter = 0; const linkTypes = (types) => { const list = Array.from(types || []); return !list.includes('Files') && (list.includes('text/uri-list') || list.includes('text/x-moz-url')); }; addEventListener('dragstart', (event) => { localDrag = true; // Links, and images inside links (sites like X wrap photos in `/status/.../photo/N` anchors). const target = event.target; const anchor = target && target.closest ? target.closest('a[href]') : null; const url = anchor ? String(anchor.href) : ''; if (/^https?:\/\//i.test(url) && url.length <= 4096) invoke('tab_link_drag', { phase: 'start', url }); }, true); addEventListener('dragend', () => { localDrag = false; invoke('tab_link_drag', { phase: 'end' }); }, true); addEventListener('dragenter', (event) => { if (localDrag || !event.dataTransfer || !linkTypes(event.dataTransfer.types)) return; const now = Date.now(); if (now - lastEnter < 500) return; lastEnter = now; invoke('tab_link_drag', { phase: 'enter' }); }, true); } if (options.shortcuts) addEventListener('keydown', (event) => { const action = shortcut(event); if (!action) return; event.preventDefault(); event.stopImmediatePropagation(); invoke('tab_shortcut', { shortcut: action }); }, true); })();