From 78d20d61976b4e501bc8cb1c0787b29615e17b90 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sat, 29 Aug 2026 15:49:30 +0200 Subject: [PATCH] Theseus: link-hover status bar (bottom-left href pill) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firefox / Chrome-style small pill at the bottom-left of the window showing the href when the pointer hovers a link, hidden when the pointer leaves. Fed by webContents.update-target-url on every tab; auto-sizes to its text via an IPC resize channel. Cleared on tab switch so a lingering hover pill from tab A doesn't carry across to tab B. Positioned by layout() so it tracks window resizes. New files link-status.html + link-status-preload.js registered in build.files (GOTCHAS.md rule: missing entries silently omit from the packaged app). Not shipped yet — bundles into the next Theseus release. --- link-status-preload.js | 5 +++++ link-status.html | 22 ++++++++++++++++++++++ main.js | 42 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ 4 files changed, 71 insertions(+) create mode 100644 link-status-preload.js create mode 100644 link-status.html diff --git a/link-status-preload.js b/link-status-preload.js new file mode 100644 index 0000000..c19ecd6 --- /dev/null +++ b/link-status-preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require("electron"); +contextBridge.exposeInMainWorld("linkstatus", { + onUrl: (cb) => ipcRenderer.on("link-status-url", (_e, url) => cb(url)), + resize: (w, h) => ipcRenderer.invoke("link-status-resize", w, h), +}); diff --git a/link-status.html b/link-status.html new file mode 100644 index 0000000..9f82665 --- /dev/null +++ b/link-status.html @@ -0,0 +1,22 @@ + + + + + diff --git a/main.js b/main.js index 3765905..5e15af1 100644 --- a/main.js +++ b/main.js @@ -916,6 +916,12 @@ let apW = 520; let apH = 60; // site has matching credentials. let pwFillPop, pwfVisible = false, pwfPos = { x: 8, y: 90 }; const PWF_W = 280; let pwfH = 80; +// Link-hover status bar — small pill at the bottom-left of the window +// showing the href when the mouse hovers a link (Chrome / Firefox style). +// Hidden when hover leaves. Fed by webContents.update-target-url on every +// tab; the pill auto-sizes to its text. +let linkStatus, linkStatusVisible = false; +let linkStatusW = 100, linkStatusH = 22; // In-memory download list. Not persisted: closing the browser clears history // (the files are still on disk; only the list of "recent downloads" is dropped). const downloads = []; let nextDlId = 1; const dlItems = new Map(); // id -> DownloadItem @@ -951,6 +957,7 @@ function layout() { positionDownloads(); positionAddressPicker(); positionPwFill(); + if (linkStatusVisible) positionLinkStatus(); } function positionPopover() { if (!popover) return; @@ -1013,6 +1020,26 @@ function positionPwFill() { const x = Math.max(6, Math.min(pwfPos.x, width - PWF_W - 6)); pwFillPop.setBounds({ x, y: pwfPos.y, width: PWF_W, height: pwfH }); } +function positionLinkStatus() { + if (!linkStatus || !win) return; + const { width, height } = win.getContentBounds(); + const w = Math.min(Math.max(120, linkStatusW), Math.max(200, width - 20)); + const h = Math.max(20, linkStatusH); + linkStatus.setBounds({ x: 0, y: Math.max(0, height - h), width: w, height: h }); +} +function showLinkStatus(url) { + if (!linkStatus) return; + const s = String(url || ""); + if (!s) { + if (linkStatusVisible) { linkStatus.setVisible(false); linkStatusVisible = false; } + return; + } + positionLinkStatus(); + // Raise the pill above any tab view that was added after it. + try { win.contentView.removeChildView(linkStatus); win.contentView.addChildView(linkStatus); } catch {} + linkStatus.setVisible(true); linkStatusVisible = true; + try { linkStatus.webContents.send("link-status-url", s); } catch {} +} function showPwFill(show, matches) { if (!pwFillPop) return; if (show) { @@ -1134,6 +1161,7 @@ function setActive(id) { activeId = id; if (popVisible) showPopover(false); // don't carry a stale popover across tabs if (epVisible) showEnginePicker(false); + if (linkStatusVisible) showLinkStatus(""); // clear any lingering hover pill for (const t of tabs) t.view.setVisible(t.id === id); const t = activeTab(); if (t?.prov) pushNav(t.prov); @@ -1197,6 +1225,9 @@ function createTab(initial, opts = {}) { wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); }); wc.on("did-start-loading", () => setLoading(tab, true)); wc.on("did-stop-loading", () => setLoading(tab, false)); + // Firefox / Chrome-style bottom-left link preview: fires with the href + // when the pointer enters/leaves an anchor. Empty string = no hover. + wc.on("update-target-url", (_e, url) => { if (tab.id === activeId) showLinkStatus(url); }); wc.on("will-navigate", (e, u) => { try { // Skip our own programmatic loads. fallbackToWeb calls loadURL("https:///") @@ -1328,6 +1359,12 @@ function createWindow() { win.contentView.addChildView(pwFillPop); pwFillPop.webContents.loadFile("pw-fill.html"); pwFillPop.setVisible(false); + // Link-hover status pill (bottom-left of window). + linkStatus = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "link-status-preload.js") } }); + try { linkStatus.setBackgroundColor("#00000000"); } catch {} + win.contentView.addChildView(linkStatus); + linkStatus.webContents.loadFile("link-status.html"); + linkStatus.setVisible(false); chrome.webContents.once("did-finish-load", () => { const saved = settings.restoreSession ? loadSession() : []; if (saved.length) saved.forEach((u) => createTab(u)); else createTab(); @@ -1801,6 +1838,11 @@ ipcMain.handle("toggle-pw-fill", async (_e, rect) => { showPwFill(true, matches); }); ipcMain.handle("close-pw-fill", () => showPwFill(false)); +ipcMain.handle("link-status-resize", (_e, w, h) => { + linkStatusW = Math.max(60, Math.min(2000, Math.round(w) || 100)); + linkStatusH = Math.max(20, Math.min(60, Math.round(h) || 22)); + if (linkStatusVisible) positionLinkStatus(); +}); ipcMain.handle("pw-fill-resize", (_e, h) => { pwfH = Math.max(60, Math.min(300, Math.round(h) || 80)); if (pwfVisible) positionPwFill(); diff --git a/package.json b/package.json index 2d8bd3e..71bb50f 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "pw-fill.html", "pw-fill-preload.js", "home-preload.js", + "link-status.html", + "link-status-preload.js", "collision.html", "collision-preload.js", "messages.html",