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",