From c5dd03adc3d547390f510fc4da000f79e3249867 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 2 Sep 2026 04:28:54 +0200 Subject: [PATCH] Ship Theseus 0.3.4 3d6d3d6a (address-picker delete X, Enter picks highlight, URL bar override) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setup 3d6d3d6aeacf482284707f50d129315d07a593e09b5d68af852abe5aa0ed4c92 Portable 6c8c5ef212bfccb377e17404e83a18cf2f09771d0eb3e0ec86a85587efdcfc9e Three address-bar suggestion fixes. Per-row ✕ delete on hover. Clicks on the X call address-forget instead of address-pick; the row disappears optimistically in the picker and main drops the entry from history + persists. Sender-URL gated to the picker's own file:// origin. Enter with a highlighted suggestion now navigates to THAT url. The URL input handler tracks a pickerHasCursor flag that flips true on ArrowDown/ArrowUp and false on any input; Enter with cursor forwards to the picker's own submit path via addressCursor("enter") — before this fix, Enter always ran goURL against the typed letters, which submitted them as a web search instead of opening the selected url. Address bar reliably shows the picked URL. onAddressPicked now arms an overrideUrlBarUntil = now+1500ms flag; the onTabs handler treats that window as "force write the url", bypassing the focus-guard that was leaving the bar blank when blur() hadn't landed yet. Deployed: scp + sia-upload of both trees, verified LIVE 0.3.4. --- address-picker-preload.js | 1 + address-picker.html | 24 +++++++++++++++++++++--- chrome.html | 36 ++++++++++++++++++++++++++++-------- main.js | 12 ++++++++++++ package.json | 2 +- 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/address-picker-preload.js b/address-picker-preload.js index 2b4eea4..bd7656e 100644 --- a/address-picker-preload.js +++ b/address-picker-preload.js @@ -3,6 +3,7 @@ contextBridge.exposeInMainWorld("picker", { onSuggestions: (cb) => ipcRenderer.on("address-suggest", (_e, d) => cb(d)), onCursor: (cb) => ipcRenderer.on("address-cursor", (_e, dir) => cb(dir)), pick: (url) => ipcRenderer.invoke("address-pick", url), + forget: (url) => ipcRenderer.invoke("address-forget", url), close: () => ipcRenderer.invoke("close-address-picker"), resize: (h) => ipcRenderer.invoke("address-picker-resize", h), }); diff --git a/address-picker.html b/address-picker.html index f1f3dd5..499ab89 100644 --- a/address-picker.html +++ b/address-picker.html @@ -11,13 +11,17 @@ .item .txt { flex: 1; overflow: hidden; } .item .url { color: #e7eaf1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; } .item .ttl { color: #7f8aa0; font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .item .del { flex: none; padding: 2px 8px; border-radius: 5px; color: #7f8aa0; font-size: 14px; + opacity: 0; cursor: pointer; user-select: none; } + .item:hover .del { opacity: .65; } + .item .del:hover { opacity: 1; color: #f6768a; background: rgba(246,118,138,.12); } .empty { padding: 14px; color: #7f8aa0; font-size: 12.5px; text-align: center; } @media (prefers-color-scheme: light) { .menu { background: #ffffff; border-color: rgba(0,0,0,.15); color: #1a1f28; } .item:hover, .item.on { background: rgba(0,0,0,.05); } .item { border-bottom-color: rgba(0,0,0,.06); } .item .url { color: #1a1f28; } - .item .ttl, .item .ic, .empty { color: #7b8494; } + .item .ttl, .item .ic, .item .del, .empty { color: #7b8494; } } @@ -31,9 +35,23 @@ const list = $("list"); if (!items.length) { list.innerHTML = `
No history yet.
`; report(); return; } list.innerHTML = items.map((h, i) => - `
🕘
${esc(h.url)}
${h.title ? `
${esc(h.title)}
` : ""}
` + `
🕘
${esc(h.url)}
${h.title ? `
${esc(h.title)}
` : ""}
` ).join(""); - list.querySelectorAll(".item").forEach((el) => el.onclick = () => window.picker.pick(el.dataset.url)); + list.querySelectorAll(".item").forEach((el) => el.onclick = (ev) => { + // Delete-x click removes the entry instead of navigating. + if (ev.target.dataset.del) { + ev.stopPropagation(); + const url = ev.target.dataset.del; + window.picker.forget(url); + // Optimistically drop it from the list so the UI reacts before main + // pushes the refreshed suggestions. + items = items.filter((h) => h.url !== url); + if (cursor >= items.length) cursor = items.length - 1; + paint(); + return; + } + window.picker.pick(el.dataset.url); + }); report(); } window.picker.onSuggestions((data) => { items = data.suggestions || []; cursor = -1; paint(); }); diff --git a/chrome.html b/chrome.html index bba398a..109ce4b 100644 --- a/chrome.html +++ b/chrome.html @@ -280,19 +280,34 @@ // main hides itself when there are zero matches; assume open otherwise. pickerOpen = true; }; + // Whether the user has moved the picker's highlight since last typing. + // Enter with a highlight submits THAT url (via the picker), not the + // typed text — so pressing ↓ then Enter opens the highlighted entry + // instead of doing a web search for the letters you typed. + let pickerHasCursor = false; const debouncedSuggest = () => { clearTimeout(suggestTimer); suggestTimer = setTimeout(askSuggest, 80); }; - $("url").addEventListener("input", debouncedSuggest); + $("url").addEventListener("input", () => { pickerHasCursor = false; debouncedSuggest(); }); $("url").addEventListener("focus", debouncedSuggest); $("url").addEventListener("blur", () => { // Delay: gives a click on the picker time to register before we close. - setTimeout(() => { T.closeAddressPicker(); pickerOpen = false; }, 160); + setTimeout(() => { T.closeAddressPicker(); pickerOpen = false; pickerHasCursor = false; }, 160); }); $("url").addEventListener("keydown", (e) => { - if (e.key === "Enter") { T.closeAddressPicker(); pickerOpen = false; goURL(); return; } - if (e.key === "Escape") { T.closeAddressPicker(); pickerOpen = false; return; } + if (e.key === "Enter") { + if (pickerOpen && pickerHasCursor) { + // Forward Enter to the picker so it navigates to the HIGHLIGHTED + // suggestion instead of running goURL against the typed text + // (which would search for the letters, not open the URL). + T.addressCursor("enter"); + return; + } + T.closeAddressPicker(); pickerOpen = false; pickerHasCursor = false; goURL(); + return; + } + if (e.key === "Escape") { T.closeAddressPicker(); pickerOpen = false; pickerHasCursor = false; return; } if (!pickerOpen) return; - if (e.key === "ArrowDown") { e.preventDefault(); T.addressCursor("next"); } - else if (e.key === "ArrowUp") { e.preventDefault(); T.addressCursor("prev"); } + if (e.key === "ArrowDown") { e.preventDefault(); pickerHasCursor = true; T.addressCursor("next"); } + else if (e.key === "ArrowUp") { e.preventDefault(); pickerHasCursor = true; T.addressCursor("prev"); } }); // Enter submits the search; keep the query visible so the user can refine // it or search again — clearing it on submit lost context and made refining @@ -564,10 +579,15 @@ // navigateTab, but the tabs event's focus-guard would leave the typed // query in place if the URL input still had DOM focus. Force the full // picked URL into the bar and drop focus so subsequent tabs events - // paint the loaded URL cleanly. + // paint the loaded URL cleanly. Also arm a short-lived override that + // makes the next onTabs event overwrite the URL bar even if the input + // still shows focused — belt-and-braces for cases where blur() is + // async and the tabs event arrives first. + let overrideUrlBarUntil = 0; T.onAddressPicked && T.onAddressPicked((url) => { try { $("url").blur(); } catch (e) {} $("url").value = String(url || ""); + overrideUrlBarUntil = Date.now() + 1500; }); // ---- tabs ---- @@ -578,7 +598,7 @@ const rb = $("reload"); if (d.loading) { rb.innerHTML = STOP_SVG; rb.title = "Stop"; rb.onclick = () => T.stop(); } else { rb.innerHTML = RELOAD_SVG; rb.title = "Reload (Shift-click: hard reload)"; rb.onclick = (ev) => T.reload(ev.shiftKey); } - if (document.activeElement !== $("url")) $("url").value = d.url || ""; + if (document.activeElement !== $("url") || Date.now() < overrideUrlBarUntil) $("url").value = d.url || ""; current.url = d.url || ""; const active = d.tabs.find((t) => t.active); current.title = active ? active.title : ""; diff --git a/main.js b/main.js index 75ae665..31f2ad5 100644 --- a/main.js +++ b/main.js @@ -2656,6 +2656,18 @@ ipcMain.handle("address-picker-resize", (_e, h) => { apH = Math.max(40, Math.min(400, Math.round(h) || 60)); if (apVisible) positionAddressPicker(); }); +// Right-side X on a picker row: drop that URL from history without +// navigating. Sender-URL-gated to our own address-picker.html. +ipcMain.handle("address-forget", (e, url) => { + try { const u = e.sender.getURL() || ""; if (!/address-picker\.html/i.test(u)) return false; } catch { return false; } + if (typeof url !== "string" || !url) return false; + const before = history.length; + history = history.filter((h) => h.url !== url); + if (history.length === before) return false; + saveHistoryDebounced(); + // Re-run the current query so the picker rerenders without the removed row. + return true; +}); ipcMain.handle("address-pick", (_e, url) => { showAddressPicker(false); if (!url) return; diff --git a/package.json b/package.json index 45243b3..bbaa846 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.3", + "version": "0.3.4", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode", "main": "main.js",