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:
parent
fa2d2f89da
commit
41eb0acec9
4 changed files with 169 additions and 1 deletions
111
main.js
111
main.js
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 › 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue