summaryrefslogtreecommitdiff
path: root/HutchSafariExtension/Resources/background.js
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-05-14 12:41:55 -0500
committerChristian Cleberg <[email protected]>2026-05-14 12:41:55 -0500
commit0d61e05e98f6f020722ee4d43f024c1da348d2a9 (patch)
treeb388972b86cf83de380381c52c4390c3beafc69a /HutchSafariExtension/Resources/background.js
parentf28e5cee1643e3f99d7bf90476d1dcf9347229a0 (diff)
downloadhutch-3.4.0.tar.gz
hutch-3.4.0.tar.bz2
hutch-3.4.0.zip
feat: add Safari extension for sourcehut deep linksv3.4.0
Diffstat (limited to 'HutchSafariExtension/Resources/background.js')
-rw-r--r--HutchSafariExtension/Resources/background.js115
1 files changed, 115 insertions, 0 deletions
diff --git a/HutchSafariExtension/Resources/background.js b/HutchSafariExtension/Resources/background.js
new file mode 100644
index 0000000..81bf8b6
--- /dev/null
+++ b/HutchSafariExtension/Resources/background.js
@@ -0,0 +1,115 @@
+const supportedHosts = new Set([
+ "git.sr.ht",
+ "hg.sr.ht",
+ "todo.sr.ht",
+ "builds.sr.ht",
+ "lists.sr.ht",
+ "meta.sr.ht",
+ "sr.ht",
+]);
+
+const browserAPI = globalThis.browser ?? globalThis.chrome;
+
+function isOwnerRootPath(path) {
+ const components = path.split("/").filter(Boolean);
+ return components.length === 1 && components[0].startsWith("~");
+}
+
+function normalizedPath(pathname) {
+ return pathname.split("/").filter(Boolean).join("/");
+}
+
+function deepLinkPath(hostname, path) {
+ const components = path.split("/").filter(Boolean);
+ if (
+ hostname === "sr.ht" &&
+ components.length === 2 &&
+ components[0] === "projects" &&
+ components[1].startsWith("~")
+ ) {
+ return components[1];
+ }
+
+ return path;
+}
+
+function deepLinkService(hostname, path) {
+ if (isOwnerRootPath(path)) {
+ return "lookup";
+ }
+
+ switch (hostname) {
+ case "git.sr.ht":
+ return "git";
+ case "hg.sr.ht":
+ return "hg";
+ case "todo.sr.ht":
+ return "todo";
+ case "builds.sr.ht":
+ return "builds";
+ case "lists.sr.ht":
+ return "lists";
+ default:
+ return "lookup";
+ }
+}
+
+function hutchDeepLinkFor(rawURL) {
+ let url;
+ try {
+ url = new URL(rawURL);
+ } catch {
+ return null;
+ }
+
+ if (url.protocol !== "https:" || !supportedHosts.has(url.hostname)) {
+ return null;
+ }
+
+ const path = deepLinkPath(url.hostname, normalizedPath(url.pathname));
+ const service = deepLinkService(url.hostname, path);
+ return `hutch://${service}${path ? `/${path}` : ""}${url.search}${url.hash}`;
+}
+
+function showUnsupportedMessage(tabId) {
+ if (!tabId) {
+ return;
+ }
+
+ const message = "Open in Hutch supports git, hg, todo, builds, lists, meta, and sr.ht pages.";
+
+ if (browserAPI?.scripting?.executeScript) {
+ browserAPI.scripting.executeScript({
+ target: { tabId },
+ func: (text) => {
+ window.alert(text);
+ },
+ args: [message],
+ });
+ return;
+ }
+
+ browserAPI?.tabs?.sendMessage?.(tabId, { type: "hutchUnsupportedPage", message });
+}
+
+function openURL(tabId, url) {
+ if (browserAPI?.tabs?.update && tabId) {
+ browserAPI.tabs.update(tabId, { url });
+ return;
+ }
+
+ if (browserAPI?.tabs?.create) {
+ browserAPI.tabs.create({ url });
+ }
+}
+
+browserAPI.action.onClicked.addListener((tab) => {
+ const hutchURL = tab?.url ? hutchDeepLinkFor(tab.url) : null;
+ if (!hutchURL) {
+ showUnsupportedMessage(tab?.id);
+ return;
+ }
+
+ console.debug("[Hutch] Opening deep link", { sourceURL: tab.url, hutchURL });
+ openURL(tab.id, hutchURL);
+});