summaryrefslogtreecommitdiff
path: root/HutchSafariExtension/Resources
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
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')
-rw-r--r--HutchSafariExtension/Resources/background.js115
-rw-r--r--HutchSafariExtension/Resources/content.js183
-rw-r--r--HutchSafariExtension/Resources/hutch_light.pngbin0 -> 47870 bytes
-rw-r--r--HutchSafariExtension/Resources/icon-128.pngbin0 -> 4563 bytes
-rw-r--r--HutchSafariExtension/Resources/icon-16.pngbin0 -> 1825 bytes
-rw-r--r--HutchSafariExtension/Resources/icon-32.pngbin0 -> 2186 bytes
-rw-r--r--HutchSafariExtension/Resources/icon-48.pngbin0 -> 2533 bytes
-rw-r--r--HutchSafariExtension/Resources/manifest.json57
8 files changed, 355 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);
+});
diff --git a/HutchSafariExtension/Resources/content.js b/HutchSafariExtension/Resources/content.js
new file mode 100644
index 0000000..dba7c5d
--- /dev/null
+++ b/HutchSafariExtension/Resources/content.js
@@ -0,0 +1,183 @@
+const hutchSupportedHosts = new Set([
+ "git.sr.ht",
+ "hg.sr.ht",
+ "todo.sr.ht",
+ "builds.sr.ht",
+ "lists.sr.ht",
+ "meta.sr.ht",
+ "sr.ht",
+]);
+
+const hutchBrowserAPI = globalThis.browser ?? globalThis.chrome;
+const bannerStorageKey = "showOpenInHutchBanner";
+const dismissedStorageKey = `dismissedOpenInHutchBanner:${location.hostname}`;
+
+function hutchIsOwnerRootPath(path) {
+ const components = path.split("/").filter(Boolean);
+ return components.length === 1 && components[0].startsWith("~");
+}
+
+function hutchNormalizedPath(pathname) {
+ return pathname.split("/").filter(Boolean).join("/");
+}
+
+function hutchDeepLinkPath(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 hutchDeepLinkService(hostname, path) {
+ if (hutchIsOwnerRootPath(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 hutchDeepLinkForLocation() {
+ if (location.protocol !== "https:" || !hutchSupportedHosts.has(location.hostname)) {
+ return null;
+ }
+
+ const path = hutchDeepLinkPath(location.hostname, hutchNormalizedPath(location.pathname));
+ const service = hutchDeepLinkService(location.hostname, path);
+ return `hutch://${service}${path ? `/${path}` : ""}${location.search}${location.hash}`;
+}
+
+function storageGet(defaults) {
+ return new Promise((resolve) => {
+ hutchBrowserAPI.storage.local.get(defaults, resolve);
+ });
+}
+
+function storageSet(values) {
+ return new Promise((resolve) => {
+ hutchBrowserAPI.storage.local.set(values, resolve);
+ });
+}
+
+function showNotice(message) {
+ const existing = document.getElementById("hutch-open-notice");
+ existing?.remove();
+
+ const notice = document.createElement("div");
+ notice.id = "hutch-open-notice";
+ notice.textContent = message;
+ notice.style.cssText = [
+ "position:fixed",
+ "left:50%",
+ "bottom:18px",
+ "z-index:2147483647",
+ "transform:translateX(-50%)",
+ "background:#1f2937",
+ "color:white",
+ "font:13px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
+ "padding:8px 12px",
+ "border-radius:8px",
+ "box-shadow:0 8px 24px rgba(0,0,0,.24)",
+ ].join(";");
+
+ document.documentElement.appendChild(notice);
+ setTimeout(() => notice.remove(), 3500);
+}
+
+async function injectBannerIfEnabled() {
+ const hutchURL = hutchDeepLinkForLocation();
+ if (!hutchURL || document.getElementById("hutch-open-banner")) {
+ return;
+ }
+
+ const settings = await storageGet({
+ [bannerStorageKey]: false,
+ [dismissedStorageKey]: false,
+ });
+ if (!settings[bannerStorageKey] || settings[dismissedStorageKey]) {
+ return;
+ }
+
+ const banner = document.createElement("div");
+ banner.id = "hutch-open-banner";
+ banner.style.cssText = [
+ "position:fixed",
+ "right:14px",
+ "bottom:14px",
+ "z-index:2147483647",
+ "display:flex",
+ "align-items:center",
+ "gap:8px",
+ "background:#111827",
+ "color:white",
+ "font:13px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
+ "padding:8px 10px",
+ "border-radius:8px",
+ "box-shadow:0 8px 24px rgba(0,0,0,.22)",
+ ].join(";");
+
+ const button = document.createElement("button");
+ button.type = "button";
+ button.textContent = "Open in Hutch";
+ button.style.cssText = [
+ "appearance:none",
+ "border:0",
+ "background:transparent",
+ "color:inherit",
+ "font:inherit",
+ "font-weight:600",
+ "padding:0",
+ ].join(";");
+ button.addEventListener("click", () => {
+ console.debug("[Hutch] Banner opening deep link", { sourceURL: location.href, hutchURL });
+ location.href = hutchURL;
+ });
+
+ const dismiss = document.createElement("button");
+ dismiss.type = "button";
+ dismiss.textContent = "Close";
+ dismiss.setAttribute("aria-label", "Dismiss Open in Hutch banner");
+ dismiss.style.cssText = [
+ "appearance:none",
+ "border:0",
+ "background:rgba(255,255,255,.14)",
+ "color:inherit",
+ "font:12px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
+ "padding:3px 6px",
+ "border-radius:6px",
+ ].join(";");
+ dismiss.addEventListener("click", async () => {
+ await storageSet({ [dismissedStorageKey]: true });
+ banner.remove();
+ });
+
+ banner.append(button, dismiss);
+ document.documentElement.appendChild(banner);
+}
+
+hutchBrowserAPI.runtime.onMessage.addListener((message) => {
+ if (message?.type === "hutchUnsupportedPage") {
+ showNotice(message.message);
+ }
+});
+
+injectBannerIfEnabled();
diff --git a/HutchSafariExtension/Resources/hutch_light.png b/HutchSafariExtension/Resources/hutch_light.png
new file mode 100644
index 0000000..cc1ce81
--- /dev/null
+++ b/HutchSafariExtension/Resources/hutch_light.png
Binary files differ
diff --git a/HutchSafariExtension/Resources/icon-128.png b/HutchSafariExtension/Resources/icon-128.png
new file mode 100644
index 0000000..cce234a
--- /dev/null
+++ b/HutchSafariExtension/Resources/icon-128.png
Binary files differ
diff --git a/HutchSafariExtension/Resources/icon-16.png b/HutchSafariExtension/Resources/icon-16.png
new file mode 100644
index 0000000..da0eaaf
--- /dev/null
+++ b/HutchSafariExtension/Resources/icon-16.png
Binary files differ
diff --git a/HutchSafariExtension/Resources/icon-32.png b/HutchSafariExtension/Resources/icon-32.png
new file mode 100644
index 0000000..5287f7d
--- /dev/null
+++ b/HutchSafariExtension/Resources/icon-32.png
Binary files differ
diff --git a/HutchSafariExtension/Resources/icon-48.png b/HutchSafariExtension/Resources/icon-48.png
new file mode 100644
index 0000000..9cfb800
--- /dev/null
+++ b/HutchSafariExtension/Resources/icon-48.png
Binary files differ
diff --git a/HutchSafariExtension/Resources/manifest.json b/HutchSafariExtension/Resources/manifest.json
new file mode 100644
index 0000000..7024f6d
--- /dev/null
+++ b/HutchSafariExtension/Resources/manifest.json
@@ -0,0 +1,57 @@
+{
+ "manifest_version": 3,
+ "name": "Open in Hutch",
+ "version": "1.0.0",
+ "description": "Open supported SourceHut pages in Hutch.",
+ "icons": {
+ "16": "icon-16.png",
+ "32": "icon-32.png",
+ "48": "icon-48.png",
+ "128": "icon-128.png"
+ },
+ "action": {
+ "default_title": "Open in Hutch",
+ "default_icon": {
+ "16": "icon-16.png",
+ "32": "icon-32.png",
+ "48": "icon-48.png",
+ "128": "icon-128.png"
+ }
+ },
+ "background": {
+ "scripts": [
+ "background.js"
+ ]
+ },
+ "content_scripts": [
+ {
+ "matches": [
+ "https://git.sr.ht/*",
+ "https://hg.sr.ht/*",
+ "https://todo.sr.ht/*",
+ "https://builds.sr.ht/*",
+ "https://lists.sr.ht/*",
+ "https://meta.sr.ht/*",
+ "https://sr.ht/*"
+ ],
+ "js": [
+ "content.js"
+ ],
+ "run_at": "document_idle"
+ }
+ ],
+ "permissions": [
+ "activeTab",
+ "scripting",
+ "storage"
+ ],
+ "host_permissions": [
+ "https://git.sr.ht/*",
+ "https://hg.sr.ht/*",
+ "https://todo.sr.ht/*",
+ "https://builds.sr.ht/*",
+ "https://lists.sr.ht/*",
+ "https://meta.sr.ht/*",
+ "https://sr.ht/*"
+ ]
+}