fix(theseus): open local files from the address bar instead of searching for them
A typed or pasted path such as D:\Dev\x\page.html has no dotted host, so the URL-vs-search heuristic handed it to the search engine. Paths (drive, UNC, file://, and absolute/~ on POSIX) now load as file:// URLs before the heuristic runs. Local-file tabs keep their file:// URL in the address bar (normally suppressed because our own home/error pages are file://), show a "Local file" badge, and hide the registry button since no name resolution is involved. A missing file lands on the error page with a matching badge.
This commit is contained in:
parent
a43089782a
commit
e596454446
2 changed files with 42 additions and 3 deletions
|
|
@ -1068,12 +1068,14 @@
|
||||||
const isHttps = typeof d?.url === "string" && d.url.startsWith("https:");
|
const isHttps = typeof d?.url === "string" && d.url.startsWith("https:");
|
||||||
const isHttp = typeof d?.url === "string" && d.url.startsWith("http:") && !isHttps;
|
const isHttp = typeof d?.url === "string" && d.url.startsWith("http:") && !isHttps;
|
||||||
if (!d || d.kind === "home") title = "Theseus";
|
if (!d || d.kind === "home") title = "Theseus";
|
||||||
|
else if (d.kind === "file") title = "Local file";
|
||||||
else if (d.kind === "ok") { cls = "secbadge secure"; title = "Secure · BCDN via " + (d.source || "on-chain") + " — click for details"; }
|
else if (d.kind === "ok") { cls = "secbadge secure"; title = "Secure · BCDN via " + (d.source || "on-chain") + " — click for details"; }
|
||||||
else if (d.kind === "web" && isHttps) { cls = "secbadge secure"; title = "Secure · ICANN (HTTPS) — click for details"; }
|
else if (d.kind === "web" && isHttps) { cls = "secbadge secure"; title = "Secure · ICANN (HTTPS) — click for details"; }
|
||||||
else if (d.kind === "web" && isHttp) { cls = "secbadge insecure"; title = "Not secure · plain HTTP — click for details"; }
|
else if (d.kind === "web" && isHttp) { cls = "secbadge insecure"; title = "Not secure · plain HTTP — click for details"; }
|
||||||
else if (d.kind === "web") title = "Connection · ICANN — click for details";
|
else if (d.kind === "web") title = "Connection · ICANN — click for details";
|
||||||
else if (d.kind === "resolving") title = "Resolving…";
|
else if (d.kind === "resolving") title = "Resolving…";
|
||||||
else if (d.kind === "nxdomain") { cls = "secbadge insecure"; title = "Not registered on BCDN"; }
|
else if (d.kind === "nxdomain") { cls = "secbadge insecure"; title = "Not registered on BCDN"; }
|
||||||
|
else if (d.kind === "error" && d.local) { cls = "secbadge insecure"; title = "Local file could not be opened"; }
|
||||||
else if (d.kind === "error") { cls = "secbadge insecure"; title = "Resolution error"; }
|
else if (d.kind === "error") { cls = "secbadge insecure"; title = "Resolution error"; }
|
||||||
b.className = cls; b.title = title;
|
b.className = cls; b.title = title;
|
||||||
}
|
}
|
||||||
|
|
@ -1082,7 +1084,7 @@
|
||||||
// registry-menu-popup) so it can't be clipped by the chrome view.
|
// registry-menu-popup) so it can't be clipped by the chrome view.
|
||||||
function setReg(d) {
|
function setReg(d) {
|
||||||
const r = $("reg");
|
const r = $("reg");
|
||||||
if (!d || d.kind === "home") { r.hidden = true; r.className = "regbtn"; r.title = "Ariadne's Thread"; return; }
|
if (!d || d.kind === "home" || d.kind === "file" || d.local) { r.hidden = true; r.className = "regbtn"; r.title = "Ariadne's Thread"; return; }
|
||||||
// Keep the last state during "resolving" so a switch reads as a toggle,
|
// Keep the last state during "resolving" so a switch reads as a toggle,
|
||||||
// not a disappear-and-reappear (user complaint 2026-08-02).
|
// not a disappear-and-reappear (user complaint 2026-08-02).
|
||||||
if (d.kind === "resolving") { r.hidden = false; return; }
|
if (d.kind === "resolving") { r.hidden = false; return; }
|
||||||
|
|
|
||||||
41
main.js
41
main.js
|
|
@ -6,6 +6,7 @@
|
||||||
// boundary.
|
// boundary.
|
||||||
const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme, shell, dialog } = require("electron");
|
const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme, shell, dialog } = require("electron");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const url = require("url");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const tls = require("tls");
|
const tls = require("tls");
|
||||||
|
|
@ -195,6 +196,22 @@ function looksLikeUrl(q) {
|
||||||
const host = q.split(/[/?#]/)[0]; // strip path/query/frag
|
const host = q.split(/[/?#]/)[0]; // strip path/query/frag
|
||||||
return host.includes(".") && !host.startsWith(".") && !host.endsWith("."); // dotted host
|
return host.includes(".") && !host.startsWith(".") && !host.endsWith("."); // dotted host
|
||||||
}
|
}
|
||||||
|
// Local files typed or pasted into the address bar: a file:// URL, a Windows
|
||||||
|
// drive path (D:\x\y.html, mixed slashes allowed), a UNC share, or on POSIX
|
||||||
|
// an absolute / ~ path. Returns the file:// URL to load, or null when the
|
||||||
|
// input isn't a local path. Checked before looksLikeUrl — a path has no dotted
|
||||||
|
// host, so the URL heuristic would otherwise hand it to the search engine.
|
||||||
|
function localFileUrl(q) {
|
||||||
|
if (!q) return null;
|
||||||
|
if (/^file:/i.test(q)) { try { return new URL(q).href; } catch { return null; } }
|
||||||
|
const isWin = process.platform === "win32";
|
||||||
|
const winPath = /^[a-z]:[\\/]/i.test(q) || /^\\\\[^\\]/.test(q);
|
||||||
|
const posixPath = !isWin && (q.startsWith("/") || q.startsWith("~/"));
|
||||||
|
if (!winPath && !posixPath) return null;
|
||||||
|
let p = q;
|
||||||
|
if (posixPath && p.startsWith("~/")) p = path.join(app.getPath("home"), p.slice(2));
|
||||||
|
try { return url.pathToFileURL(path.resolve(p)).href; } catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
// ---- persistent user settings (userData/settings.json) ----
|
// ---- persistent user settings (userData/settings.json) ----
|
||||||
const SETTINGS_DEFAULTS = {
|
const SETTINGS_DEFAULTS = {
|
||||||
|
|
@ -2494,7 +2511,9 @@ function refreshTabUrl(tab) {
|
||||||
if (!tab || tab.prov?.kind === "home") return; // home is loadFile → file://; leave t.url = ""
|
if (!tab || tab.prov?.kind === "home") return; // home is loadFile → file://; leave t.url = ""
|
||||||
try {
|
try {
|
||||||
const raw = tab.view.webContents.getURL();
|
const raw = tab.view.webContents.getURL();
|
||||||
if (raw && !raw.startsWith("file:")) tab.url = raw.replace(/^bns:\/\//, "https://");
|
// file: is normally our own surface (home/error pages) and never shown;
|
||||||
|
// a tab the user pointed at a local file is the exception.
|
||||||
|
if (raw && (!raw.startsWith("file:") || tab.prov?.kind === "file")) tab.url = raw.replace(/^bns:\/\//, "https://");
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
function loadHome(id) {
|
function loadHome(id) {
|
||||||
|
|
@ -2547,7 +2566,7 @@ function loadErrorPage(t, id, { url, code, desc }) {
|
||||||
}).toString();
|
}).toString();
|
||||||
t.internalNav = true;
|
t.internalNav = true;
|
||||||
t.title = host ? "Error — " + host : "Load error";
|
t.title = host ? "Error — " + host : "Load error";
|
||||||
t.prov = { host, kind: "error", code, desc };
|
t.prov = { host, kind: "error", code, desc, local: failedUrl.startsWith("file:") };
|
||||||
t.view.webContents.loadFile(path.join(__dirname, "error.html"), { search: q })
|
t.view.webContents.loadFile(path.join(__dirname, "error.html"), { search: q })
|
||||||
.catch((e) => console.warn("error page load failed:", e?.message))
|
.catch((e) => console.warn("error page load failed:", e?.message))
|
||||||
.finally(() => { t.internalNav = false; });
|
.finally(() => { t.internalNav = false; });
|
||||||
|
|
@ -2902,6 +2921,9 @@ async function navigateTab(id, input) {
|
||||||
if (!looksLikeUrl(q)) q = SEARCH(q);
|
if (!looksLikeUrl(q)) q = SEARCH(q);
|
||||||
const raw = q.replace(/^[a-z]+:\/\//i, "");
|
const raw = q.replace(/^[a-z]+:\/\//i, "");
|
||||||
const host = raw.split("/")[0].toLowerCase();
|
const host = raw.split("/")[0].toLowerCase();
|
||||||
|
// Local paths open as files — never BCNR, never a search.
|
||||||
|
const fileUrl = localFileUrl(q);
|
||||||
|
if (fileUrl) return loadLocalFile(t, id, fileUrl);
|
||||||
const rest = raw.slice(host.length) || "/";
|
const rest = raw.slice(host.length) || "/";
|
||||||
// Reflect the target URL immediately so the address bar doesn't keep
|
// Reflect the target URL immediately so the address bar doesn't keep
|
||||||
// showing the previous page's URL for the whole load duration. Without
|
// showing the previous page's URL for the whole load duration. Without
|
||||||
|
|
@ -2933,6 +2955,21 @@ async function navigateTab(id, input) {
|
||||||
// clearnet (https://<host><rest>) so the user isn't stranded when the chain
|
// clearnet (https://<host><rest>) so the user isn't stranded when the chain
|
||||||
// is down or the name isn't registered.
|
// is down or the name isn't registered.
|
||||||
async function loadBns(t, id, host, rest, tld) {
|
async function loadBns(t, id, host, rest, tld) {
|
||||||
|
// Open a local file (file:// URL) in a tab. A missing file surfaces through
|
||||||
|
// did-fail-load → loadErrorPage like any other failed navigation.
|
||||||
|
async function loadLocalFile(t, id, fileUrl) {
|
||||||
|
t.url = fileUrl;
|
||||||
|
t.prov = { host: "", kind: "file" };
|
||||||
|
t.bcnrOffer = null;
|
||||||
|
if (id === activeId) chrome.webContents.send("bcnr-offer", null);
|
||||||
|
setLoading(t, true);
|
||||||
|
t.nav = (t.nav || 0) + 1;
|
||||||
|
if (id === activeId) pushNav(t.prov);
|
||||||
|
emitTabs();
|
||||||
|
try { await t.view.webContents.loadURL(fileUrl); }
|
||||||
|
catch (e) { console.warn("local file load failed:", e?.message); }
|
||||||
|
}
|
||||||
|
|
||||||
const registry = registryOf(tld);
|
const registry = registryOf(tld);
|
||||||
if (id === activeId) pushNav({ host, kind: "resolving", tld, registry });
|
if (id === activeId) pushNav({ host, kind: "resolving", tld, registry });
|
||||||
const fallbackToWeb = async (reason) => {
|
const fallbackToWeb = async (reason) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue