Theseus: Firefox-style security panel + search-engine picker

- Security popover redesigned to resemble Firefox's site-info panel:
  lock/shield hero, "Connection secure" status, host, plain-language
  subtitle, and a details grid; the overlay view auto-sizes to content.
- Search-engine picker returned to the toolbar with an "Add / edit engines…"
  entry that opens Settings; added Ecosia, Mojeek, Presearch.
- Custom search engines: add (name + URL template with %s) / remove in
  Settings > General; used by address-bar search.
- Address-bar placeholder: "Ask a search engine or enter web address".
This commit is contained in:
Local Dev 2026-07-30 20:58:45 +02:00
parent b14aebeb85
commit 01429990ea
7 changed files with 170 additions and 48 deletions

View file

@ -50,6 +50,11 @@
.reg.icann { background: rgba(139,152,169,.14); color: #9aa6b6; border-color: #ffffff1a; } .reg.icann { background: rgba(139,152,169,.14); color: #9aa6b6; border-color: #ffffff1a; }
.star { border: none; background: transparent; cursor: pointer; color: #8b98a9; font-size: 15px; padding: 2px 4px; border-radius: 6px; } .star { border: none; background: transparent; cursor: pointer; color: #8b98a9; font-size: 15px; padding: 2px 4px; border-radius: 6px; }
.star:hover { background: #ffffff14; color: #fff; } .star.on { color: #ffd23d; } .star:hover { background: #ffffff14; color: #fff; } .star.on { color: #ffd23d; }
/* compact search-engine picker */
.engine { display: flex; align-items: center; gap: 4px; background: #1b2330; border: 1px solid #ffffff22;
border-radius: 8px; padding: 0 4px 0 8px; }
.engine .mag { color: #8b98a9; font-size: 12px; }
.engine select { background: transparent; color: #c5cdda; border: none; font-size: 12px; padding: 7px 2px; outline: none; cursor: pointer; }
.tor { padding: 7px 10px; border-radius: 8px; border: 1px solid #ffffff22; background: #2b2f3a; color: #fff; cursor: pointer; white-space: nowrap; font-size: 12.5px; } .tor { padding: 7px 10px; border-radius: 8px; border: 1px solid #ffffff22; background: #2b2f3a; color: #fff; cursor: pointer; white-space: nowrap; font-size: 12.5px; }
.tor.connecting { background: #7a5c14; } .tor.on { background: #6b3fa0; } .tor.connecting { background: #7a5c14; } .tor.on { background: #6b3fa0; }
.logo { display: flex; align-items: center; gap: 6px; font-weight: 700; color: #d6ff3d; white-space: nowrap; .logo { display: flex; align-items: center; gap: 6px; font-weight: 700; color: #d6ff3d; white-space: nowrap;
@ -90,10 +95,13 @@
<button class="secbadge" id="secbadge" title="Site information"> <button class="secbadge" id="secbadge" title="Site information">
<svg viewBox="0 0 16 16" aria-hidden="true"><path class="shk" d="M5 7 V5.3 a3 3 0 0 1 6 0 V7"/><rect class="bdy" x="3.6" y="6.9" width="8.8" height="6" rx="1.2"/></svg> <svg viewBox="0 0 16 16" aria-hidden="true"><path class="shk" d="M5 7 V5.3 a3 3 0 0 1 6 0 V7"/><rect class="bdy" x="3.6" y="6.9" width="8.8" height="6" rx="1.2"/></svg>
</button> </button>
<input id="url" placeholder="Search or enter a .bch / .p2p name or web address" spellcheck="false"> <input id="url" placeholder="Ask a search engine or enter web address" spellcheck="false">
<span class="reg" id="reg" hidden></span> <span class="reg" id="reg" hidden></span>
<button class="star" id="star" title="Save this page"></button> <button class="star" id="star" title="Save this page"></button>
</div> </div>
<span class="engine" title="Search engine (type a search in the address bar)">
<span class="mag">🔍</span><select id="engine"></select>
</span>
<button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button> <button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button>
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear"></span></button> <button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear"></span></button>
</div> </div>
@ -126,6 +134,23 @@
$("logo").onclick = () => T.openSettings(); $("logo").onclick = () => T.openSettings();
$("secbadge").onclick = () => { const r = $("secbadge").getBoundingClientRect(); T.toggleSiteInfo({ x: Math.round(r.left), y: Math.round(r.bottom + 4) }); }; $("secbadge").onclick = () => { const r = $("secbadge").getBoundingClientRect(); T.toggleSiteInfo({ x: Math.round(r.left), y: Math.round(r.bottom + 4) }); };
// ---- search-engine picker (with an "Add / edit…" entry that opens Settings) ----
let engineCurrent = "duckduckgo";
function fillEngines(d) {
engineCurrent = d.current;
const sel = $("engine");
sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${e.name.replace(/</g,"&lt;")}</option>`).join("")
+ `<option disabled>──────────</option><option value="__manage"> Add / edit engines…</option>`;
sel.value = d.current;
}
$("engine").onchange = () => {
const v = $("engine").value;
if (v === "__manage") { $("engine").value = engineCurrent; T.openSettings(); return; }
engineCurrent = v; T.setSearchEngine(v);
};
T.getSearchEngines().then(fillEngines);
T.onEngines && T.onEngines(fillEngines);
// ---- favorites bar (new-tab page only) ---- // ---- favorites bar (new-tab page only) ----
let current = { url: "", title: "" }, bookmarks = []; let current = { url: "", title: "" }, bookmarks = [];
function renderBookmarks() { function renderBookmarks() {

54
main.js
View file

@ -21,7 +21,8 @@ const RES_DIR = app.isPackaged ? process.resourcesPath : __dirname;
const RESOLVER = app.isPackaged const RESOLVER = app.isPackaged
? path.join(RES_DIR, "resolver-web.mjs") ? path.join(RES_DIR, "resolver-web.mjs")
: path.join(__dirname, "..", "Argus", "src", "lib", "resolver-web.js"); : path.join(__dirname, "..", "Argus", "src", "lib", "resolver-web.js");
// Search engines the user can pick from (default persisted in settings.searchEngine). // Built-in search engines. Users can also add their own (settings.customEngines,
// each { id, name, url } where the url contains "%s" for the query).
const SEARCH_ENGINES = { const SEARCH_ENGINES = {
duckduckgo: { name: "DuckDuckGo", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) }, duckduckgo: { name: "DuckDuckGo", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) },
google: { name: "Google", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) }, google: { name: "Google", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) },
@ -29,8 +30,22 @@ const SEARCH_ENGINES = {
yandex: { name: "Yandex", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) }, yandex: { name: "Yandex", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) },
brave: { name: "Brave", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) }, brave: { name: "Brave", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) },
startpage: { name: "Startpage", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) }, startpage: { name: "Startpage", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) },
ecosia: { name: "Ecosia", url: (q) => "https://www.ecosia.org/search?q=" + encodeURIComponent(q) },
mojeek: { name: "Mojeek", url: (q) => "https://www.mojeek.com/search?q=" + encodeURIComponent(q) },
presearch: { name: "Presearch", url: (q) => "https://presearch.com/search?q=" + encodeURIComponent(q) },
}; };
const SEARCH = (q) => (SEARCH_ENGINES[settings.searchEngine] || SEARCH_ENGINES.duckduckgo).url(q); // Built-in + user-added, as flat metadata for the pickers.
function allEngines() {
const list = Object.entries(SEARCH_ENGINES).map(([id, e]) => ({ id, name: e.name, builtin: true }));
for (const c of settings.customEngines || []) list.push({ id: c.id, name: c.name, builtin: false });
return list;
}
function engineUrl(id, q) {
if (SEARCH_ENGINES[id]) return SEARCH_ENGINES[id].url(q);
const c = (settings.customEngines || []).find((e) => e.id === id);
return c ? c.url.replace(/%s/g, encodeURIComponent(q)) : SEARCH_ENGINES.duckduckgo.url(q);
}
const SEARCH = (q) => engineUrl(settings.searchEngine, q);
// Public content relay (secret-free): serves s3/ip/h/u without shipping keys. // Public content relay (secret-free): serves s3/ip/h/u without shipping keys.
const GATEWAY = "https://navigate.st"; const GATEWAY = "https://navigate.st";
@ -41,7 +56,7 @@ const GATEWAY = "https://navigate.st";
// DUAL — real ICANN TLDs we ALSO offer on BCNR. These never hijack the real // DUAL — real ICANN TLDs we ALSO offer on BCNR. These never hijack the real
// web: the ICANN site loads normally, and if a BCNR name also exists // web: the ICANN site loads normally, and if a BCNR name also exists
// we surface a passive "also on BCNR" switch (see navigateTab). // we surface a passive "also on BCNR" switch (see navigateTab).
const BNS_NATIVE_TLDS = new Set(["bch", "p2p", "bit", "nav"]); const BNS_NATIVE_TLDS = new Set(["bch", "p2p", "bit", "nav", "x"]);
const BNS_DUAL_TLDS = new Set(["de", "dev", "ltd"]); const BNS_DUAL_TLDS = new Set(["de", "dev", "ltd"]);
const REGISTRY = "BCNR"; // user-facing registry label (Bitcoin Cash Name Registry) const REGISTRY = "BCNR"; // user-facing registry label (Bitcoin Cash Name Registry)
const tldOf = (host) => { const tldOf = (host) => {
@ -79,7 +94,8 @@ const SETTINGS_DEFAULTS = {
timezoneMode: "show", timezoneValue: "Europe/Berlin", // IANA zone for manual timezoneMode: "show", timezoneValue: "Europe/Berlin", // IANA zone for manual
languageMode: "show", languageValue: "en-US", // locale for manual languageMode: "show", languageValue: "en-US", // locale for manual
locationMode: "hide", locationLat: "40.7128", locationLon: "-74.0060", // coords for manual (default: deny) locationMode: "hide", locationLat: "40.7128", locationLon: "-74.0060", // coords for manual (default: deny)
searchEngine: "duckduckgo",// default search engine (see SEARCH_ENGINES) searchEngine: "duckduckgo",// default search engine (built-in id or a custom id)
customEngines: [], // user-added: [{ id, name, url-with-%s }]
}; };
// Auto decoys used by "spoof" mode (plausible but not the user's real values). // Auto decoys used by "spoof" mode (plausible but not the user's real values).
const SPOOF = { tz: "America/New_York", lang: "en-US", lat: 40.7128, lon: -74.0060 }; const SPOOF = { tz: "America/New_York", lang: "en-US", lat: 40.7128, lon: -74.0060 };
@ -98,6 +114,7 @@ const bookmarksFile = () => path.join(app.getPath("userData"), "bookmarks.json")
function loadBookmarks() { try { if (fs.existsSync(bookmarksFile())) bookmarks = JSON.parse(fs.readFileSync(bookmarksFile(), "utf8")); } catch (e) { console.error("bookmarks load failed:", e.message); } } function loadBookmarks() { try { if (fs.existsSync(bookmarksFile())) bookmarks = JSON.parse(fs.readFileSync(bookmarksFile(), "utf8")); } catch (e) { console.error("bookmarks load failed:", e.message); } }
function saveBookmarks() { try { fs.writeFileSync(bookmarksFile(), JSON.stringify(bookmarks, null, 2)); } catch (e) { console.error("bookmarks save failed:", e.message); } } function saveBookmarks() { try { fs.writeFileSync(bookmarksFile(), JSON.stringify(bookmarks, null, 2)); } catch (e) { console.error("bookmarks save failed:", e.message); } }
function emitBookmarks() { try { chrome?.webContents.send("bookmarks", bookmarks); } catch {} } function emitBookmarks() { try { chrome?.webContents.send("bookmarks", bookmarks); } catch {} }
function emitEngines() { try { chrome?.webContents.send("engines", { engines: allEngines(), current: settings.searchEngine }); } catch {} }
// WebRTC IP-handling policy: protect by default (independent of Tor), strongest under Tor. // WebRTC IP-handling policy: protect by default (independent of Tor), strongest under Tor.
function webrtcPolicy() { function webrtcPolicy() {
if (!settings.webrtcProtect) return "default"; if (!settings.webrtcProtect) return "default";
@ -402,7 +419,7 @@ let CHROME_H = 84; // grows when an extra bar (Tor notice / BCNR offer) is sh
// Site-info popover: a floating overlay VIEW on top of the page content, so it // Site-info popover: a floating overlay VIEW on top of the page content, so it
// never pushes the page down. Positioned under the address-bar badge on demand. // never pushes the page down. Positioned under the address-bar badge on demand.
let popover, popVisible = false, popPos = { x: 8, y: 90 }; let popover, popVisible = false, popPos = { x: 8, y: 90 };
const POP_W = 360, POP_H = 200; const POP_W = 360; let popH = 210; // popH is updated to fit the popover's content
const tabs = []; // { id, view, title, url, prov } const tabs = []; // { id, view, title, url, prov }
let activeId = null, tabSeq = 0; let activeId = null, tabSeq = 0;
const tabById = (id) => tabs.find((t) => t.id === id); const tabById = (id) => tabs.find((t) => t.id === id);
@ -427,7 +444,7 @@ function positionPopover() {
if (!popover) return; if (!popover) return;
const { width } = win.getContentBounds(); const { width } = win.getContentBounds();
const x = Math.max(6, Math.min(popPos.x, width - POP_W - 6)); const x = Math.max(6, Math.min(popPos.x, width - POP_W - 6));
popover.setBounds({ x, y: popPos.y, width: POP_W, height: POP_H }); popover.setBounds({ x, y: popPos.y, width: POP_W, height: popH });
} }
function showPopover(show) { function showPopover(show) {
if (!popover) return; if (!popover) return;
@ -639,14 +656,29 @@ ipcMain.handle("toggle-site-info", (_e, rect) => {
showPopover(true); showPopover(true);
}); });
ipcMain.handle("close-site-info", () => showPopover(false)); ipcMain.handle("close-site-info", () => showPopover(false));
ipcMain.handle("search-engines", () => ({ ipcMain.handle("popover-resize", (_e, h) => { popH = Math.max(90, Math.min(380, Math.round(h) || 210)); if (popVisible) positionPopover(); });
engines: Object.entries(SEARCH_ENGINES).map(([id, e]) => ({ id, name: e.name })), ipcMain.handle("search-engines", () => ({ engines: allEngines(), current: settings.searchEngine }));
current: settings.searchEngine,
}));
ipcMain.handle("set-search-engine", (_e, id) => { ipcMain.handle("set-search-engine", (_e, id) => {
if (SEARCH_ENGINES[id]) { settings.searchEngine = id; saveSettings(); } if (allEngines().some((e) => e.id === id)) { settings.searchEngine = id; saveSettings(); emitEngines(); }
return settings.searchEngine; return settings.searchEngine;
}); });
ipcMain.handle("add-engine", (_e, eng) => {
// A custom engine needs a name and a URL template containing "%s".
if (eng && eng.name && eng.url && String(eng.url).includes("%s")) {
const id = "custom-" + Date.now().toString(36);
settings.customEngines = [...(settings.customEngines || []),
{ id, name: String(eng.name).slice(0, 40), url: String(eng.url).slice(0, 400) }];
settings.searchEngine = id; // select the one just added
saveSettings(); emitEngines();
}
return { engines: allEngines(), current: settings.searchEngine };
});
ipcMain.handle("remove-engine", (_e, id) => {
settings.customEngines = (settings.customEngines || []).filter((e) => e.id !== id);
if (settings.searchEngine === id) settings.searchEngine = "duckduckgo";
saveSettings(); emitEngines();
return { engines: allEngines(), current: settings.searchEngine };
});
ipcMain.handle("bookmarks-get", () => bookmarks); ipcMain.handle("bookmarks-get", () => bookmarks);
ipcMain.handle("bookmark-add", (_e, bm) => { ipcMain.handle("bookmark-add", (_e, bm) => {
if (bm && bm.url && !bookmarks.some((b) => b.url === bm.url)) { if (bm && bm.url && !bookmarks.some((b) => b.url === bm.url)) {

View file

@ -2,4 +2,5 @@ const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("pop", { contextBridge.exposeInMainWorld("pop", {
onData: (cb) => ipcRenderer.on("site-info", (_e, d) => cb(d)), onData: (cb) => ipcRenderer.on("site-info", (_e, d) => cb(d)),
close: () => ipcRenderer.invoke("close-site-info"), close: () => ipcRenderer.invoke("close-site-info"),
resize: (h) => ipcRenderer.invoke("popover-resize", h),
}); });

View file

@ -2,46 +2,72 @@
<html><head><meta charset="utf-8"> <html><head><meta charset="utf-8">
<style> <style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; } :root { color-scheme: dark; font-family: system-ui, sans-serif; }
html, body { margin: 0; height: 100%; background: transparent; } html, body { margin: 0; background: transparent; }
.card { height: 100%; display: flex; flex-direction: column; background: #141b28; .card { display: flex; flex-direction: column; background: #1c222c;
border: 1px solid #ffffff24; border-radius: 10px; box-shadow: 0 12px 34px #000b; overflow: hidden; } border: 1px solid #ffffff26; border-radius: 8px; box-shadow: 0 12px 34px #000c; overflow: hidden; color: #e7eaf1; }
.head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; .close { position: absolute; top: 6px; right: 8px; border: none; background: transparent; color: #8b98a9;
border-bottom: 1px solid #ffffff12; font-size: 12px; color: #b9c2d0; } cursor: pointer; font-size: 13px; border-radius: 5px; padding: 1px 6px; z-index: 2; }
.head .x { border: none; background: transparent; color: #8b98a9; cursor: pointer; font-size: 13px; border-radius: 5px; padding: 1px 6px; } .close:hover { background: #ffffff14; color: #fff; }
.head .x:hover { background: #ffffff14; color: #fff; } /* Firefox-style hero: big lock/shield + status + host */
.body { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; padding: 11px 14px; font-size: 12px; overflow-y: auto; } .hero { display: flex; gap: 12px; padding: 15px 16px 13px; align-items: flex-start; }
.k { color: #7f8aa0; } .v { color: #e7eaf1; } .hero .ico { flex: none; width: 26px; height: 26px; display: grid; place-items: center; }
.v code { font-family: ui-monospace, monospace; color: #bfeae4; word-break: break-all; } .hero .ico svg { width: 22px; height: 22px; fill: none; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.dotv { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #5e6678; margin-right: 6px; } .hero.secure .ico svg { stroke: #4fd1a5; } .hero.secure .ico .bdy { fill: #4fd1a5; stroke: none; }
.dotv.chain { background: #d6ff3d; } .dotv.sia { background: #b39ddb; } .hero.warn .ico svg { stroke: #f6ad55; }
.dotv.server { background: #4fd1a5; } .dotv.web { background: #8b93a7; } .dotv.err { background: #f6768a; } .hero.neutral .ico svg { stroke: #9aa6b6; } .hero.neutral .ico .bdy { fill: #9aa6b6; stroke: none; }
.hero .status { font-weight: 600; font-size: 13.5px; }
.hero .host { color: #b9c2d0; font-size: 12px; margin-top: 2px; font-family: ui-monospace, monospace; word-break: break-all; }
.hero .sub { color: #8b98a9; font-size: 12px; margin-top: 7px; line-height: 1.5; }
.details { border-top: 1px solid #ffffff12; padding: 11px 16px 13px; display: grid; grid-template-columns: auto 1fr; gap: 7px 14px; font-size: 12px; }
.details .k { color: #7f8aa0; } .details .v { color: #e7eaf1; }
.details .v code { font-family: ui-monospace, monospace; color: #bfeae4; word-break: break-all; }
</style></head> </style></head>
<body> <body>
<div class="card"> <div class="card">
<div class="head"><span>Site information</span><button class="x" id="x" title="Close"></button></div> <button class="close" id="x" title="Close"></button>
<div class="body" id="body"></div> <div class="hero" id="hero">
<div class="ico" id="ico"></div>
<div><div class="status" id="status"></div><div class="host" id="host"></div><div class="sub" id="sub"></div></div>
</div>
<div class="details" id="details"></div>
</div> </div>
<script> <script>
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
$("x").onclick = () => window.pop.close(); $("x").onclick = () => window.pop.close();
const esc = (s) => String(s || "").replace(/</g, "&lt;"); const esc = (s) => String(s || "").replace(/</g, "&lt;");
const row = (k, v) => `<span class="k">${k}</span><span class="v">${v}</span>`; const LOCK = `<svg viewBox="0 0 24 24"><path class="shk" fill="none" d="M8 10 V7.5 a4 4 0 0 1 8 0 V10"/><rect class="bdy" x="5.5" y="9.8" width="13" height="10" rx="2"/></svg>`;
const WARN = `<svg viewBox="0 0 24 24"><path d="M12 3 L22 20 H2 Z"/><path d="M12 9 V14 M12 17 v.4"/></svg>`;
const GLOBE = `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12 h18 M12 3 a14 14 0 0 1 0 18 a14 14 0 0 1 0 -18"/></svg>`;
window.pop.onData((d) => { window.pop.onData((d) => {
let html = ""; const hero = $("hero");
if (!d || d.kind === "home") html = row("Page", "Theseus home"); let cls = "hero secure", ico = LOCK, status = "Connection secure", sub = "", details = "";
else if (d.kind === "web") html = row("Site", `<span class="dotv web"></span><code>${esc(d.host)}</code>`) + row("Resolved via", "ICANN DNS (web)") + row("Registry", "ICANN"); const row = (k, v) => `<span class="k">${k}</span><span class="v">${v}</span>`;
else if (d.kind === "resolving") html = row("Site", `<code>${esc(d.host)}</code>`) + row("Status", "resolving on the BCH chain…"); $("host").textContent = (d && d.host) || "";
else if (d.kind === "nxdomain") html = row("Name", `<span class="dotv err"></span><code>${esc(d.host)}</code>`) + row("Status", "not registered (NXDOMAIN)");
else if (d.kind === "error") html = row("Status", `<span class="dotv err"></span>${esc(d.error || "resolution failed")}`); if (!d || d.kind === "home") {
else { cls = "hero neutral"; ico = GLOBE; status = "Theseus"; $("host").textContent = "";
const cls = (d.source || "").includes("chain") ? "chain" : (d.source || "").includes("Sia") ? "sia" : (d.source || "").includes("server") ? "server" : "web"; sub = "The decentralized-web navigator. It resolves BCNR names straight from the Bitcoin Cash chain.";
html = row("Site", `<span class="dotv ${cls}"></span><code>${esc(d.host)}</code>`) + } else if (d.kind === "resolving") {
row("Served from", esc(d.source) || "chain") + cls = "hero neutral"; ico = GLOBE; status = "Resolving…"; sub = "Looking this name up on the Bitcoin Cash chain.";
row("Registry", (d.registry || "BCNR") + (d.tld ? " · ." + d.tld : "")) + } else if (d.kind === "nxdomain") {
(d.records && d.records.length ? row("Records", `<code>${esc(d.records.join(", "))}</code>`) : "") + cls = "hero warn"; ico = WARN; status = "Not registered"; sub = "This name is not registered on the BCNR chain.";
(d.category ? row("Certificate", `<code>${esc(String(d.category).slice(0, 24))}…</code>`) : ""); } else if (d.kind === "error") {
cls = "hero warn"; ico = WARN; status = "Resolution error"; sub = esc(d.error || "The name could not be resolved.");
} else if (d.kind === "web") {
cls = "hero neutral"; ico = LOCK; status = "Connection"; sub = "A standard web page, resolved through ICANN DNS.";
details = row("Registry", "ICANN");
} else { // ok — a resolved BCNR name
status = "Connection secure";
sub = "Served over an encrypted connection from the <b>decentralized web</b> — no host, no registrar, no certificate authority.";
details = row("Served from", esc(d.source) || "chain")
+ row("Registry", (d.registry || "BCNR") + (d.tld ? " · ." + d.tld : ""))
+ (d.records && d.records.length ? row("Records", `<code>${esc(d.records.join(", "))}</code>`) : "")
+ (d.category ? row("Certificate", `<code>${esc(String(d.category).slice(0, 20))}…</code>`) : "");
} }
$("body").innerHTML = html; hero.className = cls; $("ico").innerHTML = ico; $("status").textContent = status; $("sub").innerHTML = sub;
$("details").innerHTML = details; $("details").style.display = details ? "grid" : "none";
requestAnimationFrame(() => { try { window.pop.resize(document.querySelector(".card").offsetHeight); } catch (e) {} });
}); });
</script> </script>
</body></html> </body></html>

View file

@ -17,6 +17,9 @@ contextBridge.exposeInMainWorld("theseus", {
setChromeHeight: (h) => ipcRenderer.invoke("set-chrome-height", h), setChromeHeight: (h) => ipcRenderer.invoke("set-chrome-height", h),
getSearchEngines: () => ipcRenderer.invoke("search-engines"), getSearchEngines: () => ipcRenderer.invoke("search-engines"),
setSearchEngine: (id) => ipcRenderer.invoke("set-search-engine", id), setSearchEngine: (id) => ipcRenderer.invoke("set-search-engine", id),
addEngine: (eng) => ipcRenderer.invoke("add-engine", eng),
removeEngine: (id) => ipcRenderer.invoke("remove-engine", id),
onEngines: (cb) => ipcRenderer.on("engines", (_e, d) => cb(d)),
getBookmarks: () => ipcRenderer.invoke("bookmarks-get"), getBookmarks: () => ipcRenderer.invoke("bookmarks-get"),
addBookmark: (bm) => ipcRenderer.invoke("bookmark-add", bm), addBookmark: (bm) => ipcRenderer.invoke("bookmark-add", bm),
removeBookmark: (url) => ipcRenderer.invoke("bookmark-remove", url), removeBookmark: (url) => ipcRenderer.invoke("bookmark-remove", url),

View file

@ -3,4 +3,6 @@ contextBridge.exposeInMainWorld("cfg", {
get: () => ipcRenderer.invoke("settings-get"), get: () => ipcRenderer.invoke("settings-get"),
set: (key, value) => ipcRenderer.invoke("settings-set", key, value), set: (key, value) => ipcRenderer.invoke("settings-set", key, value),
engines: () => ipcRenderer.invoke("search-engines"), engines: () => ipcRenderer.invoke("search-engines"),
addEngine: (eng) => ipcRenderer.invoke("add-engine", eng),
removeEngine: (id) => ipcRenderer.invoke("remove-engine", id),
}); });

View file

@ -28,6 +28,15 @@
select,.ctl input{background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;outline:none} select,.ctl input{background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;outline:none}
select:focus,.ctl input:focus{border-color:#4b7bec} select:focus,.ctl input:focus{border-color:#4b7bec}
.ctl input{width:150px} .ctl .coords input{width:92px} .ctl input{width:150px} .ctl .coords input{width:92px}
code{font-family:ui-monospace,monospace;font-size:12px;background:#0e131b;border:1px solid var(--line);border-radius:5px;padding:1px 5px;color:#bfeae4}
.addeng{display:flex;gap:6px} .addeng input{flex:1;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:13px;outline:none}
.addeng input:focus{border-color:#4b7bec}
.btn{border:1px solid #d6ff3d55;background:rgba(214,255,61,.12);color:#eaffb0;border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer}
.btn:hover{background:rgba(214,255,61,.22)}
.ceng{display:flex;align-items:center;gap:8px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px}
.ceng .cn{font-weight:600} .ceng .cu{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.ceng .cx{cursor:pointer;color:#8b98a9;border:none;background:transparent;font-size:13px} .ceng .cx:hover{color:#f6768a}
.cempty{color:var(--dim);font-size:12.5px}
/* toggle */ /* toggle */
.sw{position:relative;width:46px;height:26px;flex:none;cursor:pointer} .sw{position:relative;width:46px;height:26px;flex:none;cursor:pointer}
.sw input{opacity:0;width:0;height:0} .sw input{opacity:0;width:0;height:0}
@ -55,6 +64,16 @@
<div class="txt"><div class="t">Search engine</div><div class="d">Used when you type a search into the address bar.</div></div> <div class="txt"><div class="t">Search engine</div><div class="d">Used when you type a search into the address bar.</div></div>
<div class="ctl"><select id="searchEngine"></select></div> <div class="ctl"><select id="searchEngine"></select></div>
</div> </div>
<div class="row" style="flex-direction:column;align-items:stretch;gap:12px">
<div class="txt"><div class="t">Add a search engine</div>
<div class="d">Name it, and give a URL with <code>%s</code> where the query goes — e.g. <code>https://searx.be/search?q=%s</code></div></div>
<div id="customList"></div>
<div class="addeng">
<input id="engName" placeholder="Name (e.g. SearXNG)">
<input id="engUrl" placeholder="https://example.com/search?q=%s">
<button id="engAdd" class="btn">Add</button>
</div>
</div>
<div class="row"> <div class="row">
<div class="txt"><div class="t">Reopen tabs on launch</div><div class="d">Restore the tabs from your last session when Theseus starts.</div></div> <div class="txt"><div class="t">Reopen tabs on launch</div><div class="d">Restore the tabs from your last session when Theseus starts.</div></div>
<label class="sw"><input type="checkbox" id="restoreSession"><span class="track"><span class="knob"></span></span></label> <label class="sw"><input type="checkbox" id="restoreSession"><span class="track"><span class="knob"></span></span></label>
@ -133,13 +152,27 @@
el.checked = !!s[k]; el.checked = !!s[k];
el.addEventListener("change", () => C.set(k, el.checked)); el.addEventListener("change", () => C.set(k, el.checked));
} }
// search engine list // search engines: default picker + custom-engine list + add form
C.engines().then((d) => { const sel = document.getElementById("searchEngine");
const sel = document.getElementById("searchEngine"); const esc = (s) => String(s || "").replace(/</g, "&lt;");
sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${e.name}</option>`).join(""); function renderEngines(d) {
sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${esc(e.name)}</option>`).join("");
sel.value = d.current; sel.value = d.current;
sel.onchange = () => C.set("searchEngine", sel.value); const custom = d.engines.filter((e) => !e.builtin);
}); const list = document.getElementById("customList");
list.innerHTML = custom.length
? custom.map((e) => `<div class="ceng"><span class="cn">${esc(e.name)}</span><span class="cu">${esc(e.id)}</span><button class="cx" data-id="${e.id}" title="Remove"></button></div>`).join("")
: `<div class="cempty">No custom engines yet.</div>`;
list.querySelectorAll(".cx").forEach((b) => b.onclick = () => C.removeEngine(b.dataset.id).then(renderEngines));
}
sel.onchange = () => C.set("searchEngine", sel.value);
document.getElementById("engAdd").onclick = () => {
const name = document.getElementById("engName").value.trim();
const url = document.getElementById("engUrl").value.trim();
if (!name || !url.includes("%s")) { alert("Enter a name and a URL containing %s (where the query goes)."); return; }
C.addEngine({ name, url }).then((d) => { document.getElementById("engName").value = ""; document.getElementById("engUrl").value = ""; renderEngines(d); });
};
C.engines().then(renderEngines);
// anti-fingerprinting mode selectors, with value field(s) shown on "manual" // anti-fingerprinting mode selectors, with value field(s) shown on "manual"
const bind = (mode, showValIf, apply) => { const bind = (mode, showValIf, apply) => {
const m = document.getElementById(mode); const m = document.getElementById(mode);