diff options
| author | Christian Cleberg <[email protected]> | 2026-05-14 12:41:55 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-05-14 12:41:55 -0500 |
| commit | 0d61e05e98f6f020722ee4d43f024c1da348d2a9 (patch) | |
| tree | b388972b86cf83de380381c52c4390c3beafc69a /HutchSafariExtension/Resources | |
| parent | f28e5cee1643e3f99d7bf90476d1dcf9347229a0 (diff) | |
| download | hutch-0d61e05e98f6f020722ee4d43f024c1da348d2a9.tar.gz hutch-0d61e05e98f6f020722ee4d43f024c1da348d2a9.tar.bz2 hutch-0d61e05e98f6f020722ee4d43f024c1da348d2a9.zip | |
feat: add Safari extension for sourcehut deep linksv3.4.0
Diffstat (limited to 'HutchSafariExtension/Resources')
| -rw-r--r-- | HutchSafariExtension/Resources/background.js | 115 | ||||
| -rw-r--r-- | HutchSafariExtension/Resources/content.js | 183 | ||||
| -rw-r--r-- | HutchSafariExtension/Resources/hutch_light.png | bin | 0 -> 47870 bytes | |||
| -rw-r--r-- | HutchSafariExtension/Resources/icon-128.png | bin | 0 -> 4563 bytes | |||
| -rw-r--r-- | HutchSafariExtension/Resources/icon-16.png | bin | 0 -> 1825 bytes | |||
| -rw-r--r-- | HutchSafariExtension/Resources/icon-32.png | bin | 0 -> 2186 bytes | |||
| -rw-r--r-- | HutchSafariExtension/Resources/icon-48.png | bin | 0 -> 2533 bytes | |||
| -rw-r--r-- | HutchSafariExtension/Resources/manifest.json | 57 |
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 Binary files differnew file mode 100644 index 0000000..cc1ce81 --- /dev/null +++ b/HutchSafariExtension/Resources/hutch_light.png diff --git a/HutchSafariExtension/Resources/icon-128.png b/HutchSafariExtension/Resources/icon-128.png Binary files differnew file mode 100644 index 0000000..cce234a --- /dev/null +++ b/HutchSafariExtension/Resources/icon-128.png diff --git a/HutchSafariExtension/Resources/icon-16.png b/HutchSafariExtension/Resources/icon-16.png Binary files differnew file mode 100644 index 0000000..da0eaaf --- /dev/null +++ b/HutchSafariExtension/Resources/icon-16.png diff --git a/HutchSafariExtension/Resources/icon-32.png b/HutchSafariExtension/Resources/icon-32.png Binary files differnew file mode 100644 index 0000000..5287f7d --- /dev/null +++ b/HutchSafariExtension/Resources/icon-32.png diff --git a/HutchSafariExtension/Resources/icon-48.png b/HutchSafariExtension/Resources/icon-48.png Binary files differnew file mode 100644 index 0000000..9cfb800 --- /dev/null +++ b/HutchSafariExtension/Resources/icon-48.png 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/*" + ] +} |
