feat(theseus/find): Ctrl+F opens an in-page find bar

Standard browser Ctrl+F support:

- main.js: Ctrl+F caught in the shared before-input-event handler (same
  place as F12 / reload shortcuts) fires 'find-open' to chrome.
  Two new IPC handlers proxy to activeTab().view.webContents:
    find-in-page(query, {forward, findNext, matchCase})
    find-stop
  Each tab's webContents listens for 'found-in-page' and forwards
  {activeMatchOrdinal, matches, finalUpdate} back to chrome via
  'find-result' — only when it's the active tab so the bar doesn't
  update from a background tab's stale match count.

- preload.js: exposes onFindOpen / findInPage / findStop / onFindResult.

- chrome.html: adds a .findbar strip below the bookmarks bar (part of
  chrome-view height like .tordisc / .bcnrbar, so syncHeight picks it
  up). Input + "N of M" counter + prev/next/close buttons. Enter jumps
  next, Shift+Enter previous, Esc closes. Typing runs findNext:false
  (fresh search); pressing Enter runs findNext:true (walk matches).
  No-match state paints the input border red.
This commit is contained in:
Local Dev 2026-09-08 07:53:48 +02:00
parent b5fc42e4d4
commit cf0951b754
3 changed files with 115 additions and 0 deletions

View file

@ -295,6 +295,18 @@
.logo .gear { font-size: 12px; opacity: .8; }
/* favorites bar — new-tab page only, single compact row */
.bookmarks { display: flex; align-items: center; gap: 4px; padding: 3px 10px 5px; height: 26px; overflow: hidden; }
/* Ctrl+F find-in-page bar. Sits below the address bar (above tabs body),
~32px tall so it's part of the chrome view height like .tordisc / .bcnrbar.
syncHeight in the tabs handler picks it up automatically. */
.findbar { display: flex; align-items: center; gap: 6px; padding: 5px 10px; border-top: 1px solid var(--line); background: var(--surface); }
.findbar input { flex: 1; min-width: 120px; padding: 4px 8px; border-radius: 6px; background: var(--surface2); color: var(--ink);
border: 1px solid var(--line); font: inherit; font-size: 12.5px; outline: none; }
.findbar input:focus { border-color: color-mix(in srgb, var(--acid) 55%, transparent); }
.findbar input.nomatch { border-color: rgba(246,118,138,.55); background: rgba(246,118,138,.10); }
.findbar .findcount { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; min-width: 46px; text-align: right; }
.findbar .findbtn { border: 1px solid var(--line); background: var(--surface2); color: var(--ink); border-radius: 6px;
padding: 3px 8px; font-size: 13px; cursor: pointer; }
.findbar .findbtn:hover { background: var(--surface); border-color: color-mix(in srgb, var(--acid) 35%, transparent); }
.bm { display: flex; align-items: center; gap: 6px; max-width: 180px; padding: 3px 9px; border-radius: 6px;
background: var(--surface2); border: 1px solid var(--hover); color: var(--mut); font-size: 12px; cursor: pointer; white-space: nowrap; }
.bm:hover { background: var(--active); color: var(--ink); }
@ -397,6 +409,13 @@
</div>
<div class="loadbar" id="loadbar"></div>
<div class="bookmarks" id="bookmarks" hidden></div>
<div class="findbar" id="findbar" hidden>
<input id="findInput" type="text" placeholder="Find in page" spellcheck="false" autocomplete="off">
<span class="findcount" id="findCount">–</span>
<button class="findbtn" id="findPrev" title="Previous (Shift+Enter)">↑</button>
<button class="findbtn" id="findNext" title="Next (Enter)">↓</button>
<button class="findbtn" id="findClose" title="Close (Esc)">✕</button>
</div>
<div id="tordisc" class="tordisc" hidden>
Onion mode hides your IP from sites and your ISP (including .bch lookups).
It is <b>not full anonymity</b> — this browser can still be fingerprinted; for that use the Tor Browser.
@ -1303,6 +1322,60 @@
T.getSettings && T.getSettings().then(applyBarSizes);
T.onSettingsUpdate && T.onSettingsUpdate(applyBarSizes);
// ---- Ctrl+F find-in-page bar ----
// Chrome renderer owns the UI; main proxies findInPage / stopFindInPage
// and forwards Chromium's found-in-page events back so we can render
// "N of M". Enter jumps to next, Shift+Enter to previous, Esc closes.
const findbar = $("findbar");
const findInput = $("findInput");
const findCount = $("findCount");
let findQuery = "";
function openFind() {
findbar.hidden = false;
syncHeight();
setTimeout(() => { findInput.focus(); findInput.select(); }, 0);
// Re-highlight the current query if there was one.
if (findInput.value.trim()) T.findInPage && T.findInPage(findInput.value.trim(), { findNext: true });
}
function closeFind() {
findbar.hidden = true;
findQuery = "";
findCount.textContent = "–";
findInput.classList.remove("nomatch");
T.findStop && T.findStop();
syncHeight();
}
function stepFind(forward) {
const q = findInput.value.trim();
if (!q) return;
findQuery = q;
T.findInPage && T.findInPage(q, { forward, findNext: true });
}
T.onFindOpen && T.onFindOpen(openFind);
T.onFindResult && T.onFindResult((r) => {
if (r.matches === 0) {
findCount.textContent = "0";
findInput.classList.add("nomatch");
} else {
findCount.textContent = r.activeMatchOrdinal + " / " + r.matches;
findInput.classList.remove("nomatch");
}
});
findInput.addEventListener("input", () => {
const q = findInput.value.trim();
if (!q) { T.findStop && T.findStop(); findCount.textContent = "–"; findInput.classList.remove("nomatch"); return; }
// findNext: false → new query starts a fresh search from the top.
T.findInPage && T.findInPage(q, { findNext: q === findQuery, forward: true });
findQuery = q;
});
findInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.preventDefault(); stepFind(!e.shiftKey); }
else if (e.key === "Escape") { e.preventDefault(); closeFind(); }
});
$("findPrev").onclick = () => stepFind(false);
$("findNext").onclick = () => stepFind(true);
$("findClose").onclick = closeFind;
// Drag handles: right edge of .urlwrap resizes the URL bar; left edge of
// .searchbox resizes the search box. Persists to settings.urlBarWidthPx /
// searchBoxWidthPx on release, which then wins over the size preset.

35
main.js
View file

@ -2085,6 +2085,12 @@ function createTab(initial, opts = {}) {
const next = (urls && urls[0]) || null;
if (tab.favicon !== next) { tab.favicon = next; emitTabs(); }
});
// Chromium fires found-in-page on every findInPage call + on subsequent
// match walks. Forward to chrome so the find bar shows "N of M".
wc.on("found-in-page", (_e, r) => {
if (tab.id !== activeId) return;
try { chrome?.webContents.send("find-result", { activeMatchOrdinal: r.activeMatchOrdinal, matches: r.matches, finalUpdate: r.finalUpdate }); } catch {}
});
wc.on("did-navigate", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
wc.on("did-start-loading", () => setLoading(tab, true));
@ -2923,6 +2929,27 @@ ipcMain.handle("open-update-download", (_e, url) => {
} catch (e) { console.warn("update download failed:", e?.message); return false; }
});
ipcMain.handle("dismiss-update", () => { updateDismissedThisSession = true; emitUpdateAvailable(); return true; });
// Find-in-page. Chrome renderer's find bar drives this: the bar sends
// `find-in-page` with the query + direction; main proxies to the active
// tab's webContents.findInPage. Chromium raises `found-in-page` on each
// keystroke with the running match count / active match ordinal; we
// forward that back to chrome so the bar can render "3 of 12".
ipcMain.handle("find-in-page", (_e, query, opts = {}) => {
const t = activeTab(); if (!t) return false;
if (typeof query !== "string" || !query) { try { t.view.webContents.stopFindInPage("clearSelection"); } catch {} return false; }
try {
t.view.webContents.findInPage(query, {
forward: opts.forward !== false,
findNext: !!opts.findNext, // false = new query; true = jump next/prev
matchCase: !!opts.matchCase,
});
return true;
} catch { return false; }
});
ipcMain.handle("find-stop", () => {
const t = activeTab(); if (!t) return false;
try { t.view.webContents.stopFindInPage("clearSelection"); return true; } catch { return false; }
});
ipcMain.handle("recheck-update", async () => {
// Manual "Check for updates" also un-dismisses any chip the user closed
// in this session — they're actively asking to see the status, so honour
@ -3992,6 +4019,14 @@ app.on("web-contents-created", (_event, wc) => {
toggleSidebar();
return e.preventDefault();
}
// Find-in-page: Ctrl+F opens the find bar in chrome. Chrome renderer
// owns the UI (input, next/prev, count, close); it drives the active
// tab's webContents.findInPage via IPC (find-in-page / find-stop).
const isF = input.key === "F" || input.key === "f";
if (input.control && !input.shift && !input.alt && isF) {
try { chrome?.webContents.send("find-open"); } catch {}
return e.preventDefault();
}
// DevTools: F12 or Ctrl+Shift+I toggles Chromium DevTools on the active
// TAB (not the chrome/overlay that received the keystroke — a user
// debugging a page wants the page's inspector, always). Detach into

View file

@ -28,6 +28,13 @@ contextBridge.exposeInMainWorld("theseus", {
getSettings: () => ipcRenderer.invoke("settings-get"),
setSetting: (key, val) => ipcRenderer.invoke("settings-set", key, val),
onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)),
// Find-in-page. main.js fires 'find-open' on Ctrl+F; chrome renderer
// owns the bar UI and drives findInPage / stopFindInPage via these
// wrappers. Match count / active ordinal comes back through onFindResult.
onFindOpen: (cb) => ipcRenderer.on("find-open", () => cb()),
findInPage: (query, opts) => ipcRenderer.invoke("find-in-page", query, opts || {}),
findStop: () => ipcRenderer.invoke("find-stop"),
onFindResult: (cb) => ipcRenderer.on("find-result", (_e, d) => cb(d)),
toggleSiteInfo: (rect) => ipcRenderer.invoke("toggle-site-info", rect),
switchToBcnr: () => ipcRenderer.invoke("switch-to-bcnr"),
setChromeHeight: (h) => ipcRenderer.invoke("set-chrome-height", h),