Ship Theseus 0.3.4 3d6d3d6a (address-picker delete X, Enter picks highlight, URL bar override)

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.
This commit is contained in:
Local Dev 2026-09-02 04:28:54 +02:00
parent 3bf41f92ae
commit c5dd03adc3
5 changed files with 63 additions and 12 deletions

View file

@ -3,6 +3,7 @@ contextBridge.exposeInMainWorld("picker", {
onSuggestions: (cb) => ipcRenderer.on("address-suggest", (_e, d) => cb(d)), onSuggestions: (cb) => ipcRenderer.on("address-suggest", (_e, d) => cb(d)),
onCursor: (cb) => ipcRenderer.on("address-cursor", (_e, dir) => cb(dir)), onCursor: (cb) => ipcRenderer.on("address-cursor", (_e, dir) => cb(dir)),
pick: (url) => ipcRenderer.invoke("address-pick", url), pick: (url) => ipcRenderer.invoke("address-pick", url),
forget: (url) => ipcRenderer.invoke("address-forget", url),
close: () => ipcRenderer.invoke("close-address-picker"), close: () => ipcRenderer.invoke("close-address-picker"),
resize: (h) => ipcRenderer.invoke("address-picker-resize", h), resize: (h) => ipcRenderer.invoke("address-picker-resize", h),
}); });

View file

@ -11,13 +11,17 @@
.item .txt { flex: 1; overflow: hidden; } .item .txt { flex: 1; overflow: hidden; }
.item .url { color: #e7eaf1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; } .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 .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; } .empty { padding: 14px; color: #7f8aa0; font-size: 12.5px; text-align: center; }
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
.menu { background: #ffffff; border-color: rgba(0,0,0,.15); color: #1a1f28; } .menu { background: #ffffff; border-color: rgba(0,0,0,.15); color: #1a1f28; }
.item:hover, .item.on { background: rgba(0,0,0,.05); } .item:hover, .item.on { background: rgba(0,0,0,.05); }
.item { border-bottom-color: rgba(0,0,0,.06); } .item { border-bottom-color: rgba(0,0,0,.06); }
.item .url { color: #1a1f28; } .item .url { color: #1a1f28; }
.item .ttl, .item .ic, .empty { color: #7b8494; } .item .ttl, .item .ic, .item .del, .empty { color: #7b8494; }
} }
</style></head> </style></head>
<body> <body>
@ -31,9 +35,23 @@
const list = $("list"); const list = $("list");
if (!items.length) { list.innerHTML = `<div class="empty">No history yet.</div>`; report(); return; } if (!items.length) { list.innerHTML = `<div class="empty">No history yet.</div>`; report(); return; }
list.innerHTML = items.map((h, i) => list.innerHTML = items.map((h, i) =>
`<div class="item ${i === cursor ? "on" : ""}" data-url="${esc(h.url)}"><span class="ic">🕘</span><span class="txt"><div class="url">${esc(h.url)}</div>${h.title ? `<div class="ttl">${esc(h.title)}</div>` : ""}</span></div>` `<div class="item ${i === cursor ? "on" : ""}" data-url="${esc(h.url)}"><span class="ic">🕘</span><span class="txt"><div class="url">${esc(h.url)}</div>${h.title ? `<div class="ttl">${esc(h.title)}</div>` : ""}</span><span class="del" data-del="${esc(h.url)}" title="Remove from history"></span></div>`
).join(""); ).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(); report();
} }
window.picker.onSuggestions((data) => { items = data.suggestions || []; cursor = -1; paint(); }); window.picker.onSuggestions((data) => { items = data.suggestions || []; cursor = -1; paint(); });

View file

@ -280,19 +280,34 @@
// main hides itself when there are zero matches; assume open otherwise. // main hides itself when there are zero matches; assume open otherwise.
pickerOpen = true; 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); }; const debouncedSuggest = () => { clearTimeout(suggestTimer); suggestTimer = setTimeout(askSuggest, 80); };
$("url").addEventListener("input", debouncedSuggest); $("url").addEventListener("input", () => { pickerHasCursor = false; debouncedSuggest(); });
$("url").addEventListener("focus", debouncedSuggest); $("url").addEventListener("focus", debouncedSuggest);
$("url").addEventListener("blur", () => { $("url").addEventListener("blur", () => {
// Delay: gives a click on the picker time to register before we close. // 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) => { $("url").addEventListener("keydown", (e) => {
if (e.key === "Enter") { T.closeAddressPicker(); pickerOpen = false; goURL(); return; } if (e.key === "Enter") {
if (e.key === "Escape") { T.closeAddressPicker(); pickerOpen = false; return; } 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 (!pickerOpen) return;
if (e.key === "ArrowDown") { e.preventDefault(); T.addressCursor("next"); } if (e.key === "ArrowDown") { e.preventDefault(); pickerHasCursor = true; T.addressCursor("next"); }
else if (e.key === "ArrowUp") { e.preventDefault(); T.addressCursor("prev"); } 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 // 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 // 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 // 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 // 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 // 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) => { T.onAddressPicked && T.onAddressPicked((url) => {
try { $("url").blur(); } catch (e) {} try { $("url").blur(); } catch (e) {}
$("url").value = String(url || ""); $("url").value = String(url || "");
overrideUrlBarUntil = Date.now() + 1500;
}); });
// ---- tabs ---- // ---- tabs ----
@ -578,7 +598,7 @@
const rb = $("reload"); const rb = $("reload");
if (d.loading) { rb.innerHTML = STOP_SVG; rb.title = "Stop"; rb.onclick = () => T.stop(); } 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); } 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 || ""; current.url = d.url || "";
const active = d.tabs.find((t) => t.active); const active = d.tabs.find((t) => t.active);
current.title = active ? active.title : ""; current.title = active ? active.title : "";

12
main.js
View file

@ -2656,6 +2656,18 @@ ipcMain.handle("address-picker-resize", (_e, h) => {
apH = Math.max(40, Math.min(400, Math.round(h) || 60)); apH = Math.max(40, Math.min(400, Math.round(h) || 60));
if (apVisible) positionAddressPicker(); 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) => { ipcMain.handle("address-pick", (_e, url) => {
showAddressPicker(false); showAddressPicker(false);
if (!url) return; if (!url) return;

View file

@ -1,6 +1,6 @@
{ {
"name": "theseus-navigator", "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.", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode", "author": "Silent Mode",
"main": "main.js", "main": "main.js",