Theseus 0.3.121: default-browser toggle + "Open with Theseus" in file menus

New Settings › General › Default handling section (Windows only, hidden on
other platforms until there is a backing path for them). Two toggles:

  * "Make Theseus the default browser" registers Theseus for http and https
    via app.setAsDefaultProtocolClient and lands the user on Windows
    Settings › Default apps so they can confirm the switch — Windows does
    not let an app assert itself as default, so we do what we can and point
    them at the UI that owns the final decision.
  * "Show Open with Theseus in file menus" writes HKCU\Software\Classes
    entries for .html, .htm, .pdf, .svg and .xhtml so Theseus appears in
    the Open-with dropdown for those files. Everything lands under HKCU
    (no admin). A matching RegisteredApplications\Theseus Navigator entry
    makes Theseus visible by name in Settings › Default apps.

Also handles file paths and URL arguments on the command line — this is
what Windows passes when the user picks Theseus from an Open-with menu, or
clicks a link in another app once Theseus is the default. argv[0] is the
exe; the first entry that is a recognised scheme (http/https/file/bns) or
an existing local file wins and opens as a new tab. Second-instance was
already wired to handleLaunch via requestSingleInstanceLock, so a running
Theseus just picks up the file.
This commit is contained in:
Local Dev 2026-10-27 17:28:32 +02:00
parent fa2d2f89da
commit 41eb0acec9
4 changed files with 169 additions and 1 deletions

111
main.js
View file

@ -12164,6 +12164,22 @@ function openInMainTab(url) {
// A second launch: `Theseus.exe --app=<url>` (an installed app's shortcut)
// opens that app's window here; a plain launch fronts the browser window,
// recreating it when only app windows are left.
// A bare URL on the command line, or a local file path, becomes a new tab —
// which is what Windows passes when the user picks Theseus from an "Open
// with" menu, or clicks a link in another app once Theseus is the default
// browser. argv[0] is the exe; the first entry that is a recognised scheme
// or an existing file on disk wins. Returns a URL string or null.
function urlFromArgv(argv) {
for (let i = 1; i < (argv || []).length; i++) {
const a = argv[i];
if (!a || a.startsWith("-")) continue;
if (/^(https?|file|bns):/i.test(a)) return a;
try {
if (fs.existsSync(a) && fs.statSync(a).isFile()) return url.pathToFileURL(a).href;
} catch {}
}
return null;
}
function handleLaunch(argv) {
const appUrl = webapps.appUrlFromArgv(argv);
if (appUrl) { if (webapps.launch(appUrl)) return; openInMainTab(appUrl); return; }
@ -12172,8 +12188,10 @@ function handleLaunch(argv) {
// when the user is actively in another window) but it's also a reasonable
// CLI entry point for `theseus --messages`.
const openMsg = Array.isArray(argv) && argv.some((a) => a === "--messages");
const openUrl = urlFromArgv(argv);
if (win && !win.isDestroyed()) { try { if (win.isMinimized()) win.restore(); win.focus(); } catch {} }
else createWindow();
if (openUrl) setImmediate(() => { try { openInMainTab(openUrl); } catch {} });
if (openMsg) {
// openHermesInSidebarOrWindow is defined further down; defer with setImmediate
// so it runs after createWindow's queue has settled on first launch. Falls
@ -12181,6 +12199,99 @@ function handleLaunch(argv) {
setImmediate(() => { try { openHermesInSidebarOrWindow(); } catch {} });
}
}
// ---- default browser + file-association registration (0.3.121) -----------
// Windows writes everything under HKCU (no admin). On other platforms the
// Settings UI hides these rows; the IPC calls return supported:false.
const DEFAULT_BROWSER_PROTOS = ["http", "https"];
const FILE_ASSOC_EXTS = [".html", ".htm", ".pdf", ".svg", ".xhtml"];
const WIN_PROGID = "TheseusNavigator.html";
function isDefaultProtocolClient() {
try { return DEFAULT_BROWSER_PROTOS.every((p) => app.isDefaultProtocolClient(p)); }
catch { return false; }
}
function setAsDefaultBrowser(on) {
try {
for (const p of DEFAULT_BROWSER_PROTOS) {
if (on) app.setAsDefaultProtocolClient(p);
else app.removeAsDefaultProtocolClient(p);
}
return true;
} catch (e) { console.warn("setAsDefaultBrowser:", e?.message); return false; }
}
function reg(args) {
return new Promise((res) => {
try {
const p = require("child_process").spawn("reg.exe", args, { windowsHide: true, stdio: ["ignore", "ignore", "ignore"] });
p.on("close", (code) => res(code === 0));
p.on("error", () => res(false));
} catch { res(false); }
});
}
const regAdd = (key, name, value, type = "REG_SZ") => {
// reg.exe uses /ve for the key's default (unnamed) value, /v NAME otherwise.
const sel = (name == null || name === "") ? ["/ve"] : ["/v", name];
return reg(["add", key, ...sel, "/t", type, "/d", value, "/f"]);
};
const regDel = (key, name) => reg(["delete", key, "/v", name, "/f"]);
const regDelKey = (key) => reg(["delete", key, "/f"]);
// Writes a progid Theseus owns and lists it under each extension's
// OpenWithProgids. Windows then shows Theseus in the "Open with" dropdown
// for those files, and `theseus "<path>"` is what Windows calls to launch.
async function setFileAssociations(on) {
if (process.platform !== "win32") return false;
const exe = process.execPath;
const openCmd = `"${exe}" "%1"`;
const classes = `HKCU\\Software\\Classes`;
const caps = `HKCU\\Software\\Theseus Navigator\\Capabilities`;
if (on) {
await regAdd(`${classes}\\${WIN_PROGID}`, "", "Theseus HTML Document");
await regAdd(`${classes}\\${WIN_PROGID}\\DefaultIcon`, "", `${exe},0`);
await regAdd(`${classes}\\${WIN_PROGID}\\shell\\open\\command`, "", openCmd);
for (const ext of FILE_ASSOC_EXTS) {
await regAdd(`${classes}\\${ext}\\OpenWithProgids`, WIN_PROGID, "");
}
// RegisteredApplications is what makes Theseus show up by name in
// Windows Settings → Default Apps for each listed extension.
await regAdd(caps, "ApplicationName", "Theseus Navigator");
await regAdd(caps, "ApplicationIcon", `${exe},0`);
await regAdd(caps, "ApplicationDescription", "A browser that follows the thread.");
for (const ext of FILE_ASSOC_EXTS) await regAdd(`${caps}\\FileAssociations`, ext, WIN_PROGID);
for (const p of DEFAULT_BROWSER_PROTOS) await regAdd(`${caps}\\URLAssociations`, p, WIN_PROGID);
await regAdd(`HKCU\\Software\\RegisteredApplications`, "Theseus Navigator", "Software\\Theseus Navigator\\Capabilities");
return true;
}
for (const ext of FILE_ASSOC_EXTS) await regDel(`${classes}\\${ext}\\OpenWithProgids`, WIN_PROGID);
await regDelKey(`${classes}\\${WIN_PROGID}`);
await regDelKey(`HKCU\\Software\\Theseus Navigator`);
await regDel(`HKCU\\Software\\RegisteredApplications`, "Theseus Navigator");
return true;
}
function hasFileAssociations() {
if (process.platform !== "win32") return false;
try {
const out = require("child_process").execSync(
`reg query "HKCU\\Software\\Classes\\${WIN_PROGID}\\shell\\open\\command" /ve`,
{ stdio: ["ignore", "pipe", "ignore"], windowsHide: true }
).toString();
return /\.exe/i.test(out);
} catch { return false; }
}
async function openSystemDefaultApps() {
try {
if (process.platform === "win32") { await shell.openExternal("ms-settings:defaultapps"); return true; }
if (process.platform === "darwin") { await shell.openExternal("x-apple.systempreferences:com.apple.preference.general"); return true; }
return false;
} catch { return false; }
}
ipcMain.handle("default-browser-state", () => ({
supported: process.platform === "win32",
isDefault: isDefaultProtocolClient(),
hasAssocs: hasFileAssociations(),
exe: process.execPath,
}));
ipcMain.handle("default-browser-set", (_e, on) => setAsDefaultBrowser(!!on));
ipcMain.handle("file-assocs-set", (_e, on) => setFileAssociations(!!on));
ipcMain.handle("open-system-defaults", () => openSystemDefaultApps());
if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) {
// Another Theseus owns this profile; it got our argv via second-instance.
app.quit();

View file

@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
"version": "0.3.120",
"version": "0.3.121",
"license": "MPL-2.0",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode",

View file

@ -121,4 +121,11 @@ contextBridge.exposeInMainWorld("cfg", {
resolversReset: () => ipcRenderer.invoke("extra-resolvers-reset"),
// Settings › Sidebar › Reset panel widths (moved from the strip's gear in 0.3.100).
sidebarResetWidths: () => ipcRenderer.invoke("sidebar-reset-widths"),
// Settings › General › Default handling. On Windows, writes HKCU entries
// only — no admin — then opens the Windows Default Apps page so the user
// confirms the switch there.
defaultBrowserState: () => ipcRenderer.invoke("default-browser-state"),
setDefaultBrowser: (on) => ipcRenderer.invoke("default-browser-set", !!on),
setFileAssocs: (on) => ipcRenderer.invoke("file-assocs-set", !!on),
openSystemDefaults: () => ipcRenderer.invoke("open-system-defaults"),
});

View file

@ -400,6 +400,22 @@
<div id="blkSummary" class="pmuted" style="font-size:12.5px"></div>
<div><button id="resetBlocklist" class="btn">Forget "continue anyway" choices</button></div>
</div>
<h2 class="sub" id="defHandlingHead" hidden>Default handling</h2>
<div class="row" id="defaultBrowserRow" hidden>
<div class="txt">
<div class="t">Make Theseus the default browser</div>
<div class="d">Register Theseus for <code>http</code> and <code>https</code> links. Windows still asks you to confirm the switch in <b>Settings &rsaquo; Default apps</b>; the button opens that page.</div>
<div class="acts"><button class="btn small ghost" id="openSysDefaults">Open Default Apps…</button></div>
</div>
<label class="sw"><input type="checkbox" id="defaultBrowser"><span class="track"><span class="knob"></span></span></label>
</div>
<div class="row" id="fileAssocsRow" hidden>
<div class="txt">
<div class="t">Show "Open with Theseus" in file menus</div>
<div class="d">Right-click an <code>.html</code>, <code>.htm</code>, <code>.pdf</code>, <code>.svg</code> or <code>.xhtml</code> file and Theseus appears in the Open-with menu. Theseus opens it as a local-file tab.</div>
</div>
<label class="sw"><input type="checkbox" id="fileAssocs"><span class="track"><span class="knob"></span></span></label>
</div>
<h2 class="sub">About</h2>
<div class="row">
<div class="txt"><div class="t">Theseus Navigator is free software</div>
@ -1251,6 +1267,40 @@
console.error("Updates panel init failed:", e);
}
})();
// General › Default handling. Hidden on platforms without a backing path
// (today: everything other than Windows).
(async () => {
const row = document.getElementById("defaultBrowserRow");
const assocRow = document.getElementById("fileAssocsRow");
const head = document.getElementById("defHandlingHead");
const defBrowser = document.getElementById("defaultBrowser");
const assoc = document.getElementById("fileAssocs");
const openSys = document.getElementById("openSysDefaults");
async function refresh() {
try {
const d = await cfg.defaultBrowserState();
if (!d.supported) return;
head.hidden = false; row.hidden = false; assocRow.hidden = false;
defBrowser.checked = !!d.isDefault;
assoc.checked = !!d.hasAssocs;
} catch {}
}
defBrowser.addEventListener("change", async () => {
await cfg.setDefaultBrowser(defBrowser.checked);
// Windows does not let an app assert itself as default; we land the
// user on the Settings page where that choice lives.
if (defBrowser.checked) await cfg.openSystemDefaults();
setTimeout(refresh, 1000);
});
assoc.addEventListener("change", async () => {
assoc.disabled = true;
try { await cfg.setFileAssocs(assoc.checked); }
finally { assoc.disabled = false; }
setTimeout(refresh, 600);
});
openSys.addEventListener("click", () => cfg.openSystemDefaults());
refresh();
})();
// sidebar navigation
// "naming" is intentionally absent: the Registries section was folded
// into General in 0.3.21. Leaving the id in this list threw a