summaryrefslogtreecommitdiff
path: root/Hutch/App/DeepLink.swift
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 /Hutch/App/DeepLink.swift
parentf28e5cee1643e3f99d7bf90476d1dcf9347229a0 (diff)
downloadhutch-0d61e05e98f6f020722ee4d43f024c1da348d2a9.tar.gz
hutch-0d61e05e98f6f020722ee4d43f024c1da348d2a9.tar.bz2
hutch-0d61e05e98f6f020722ee4d43f024c1da348d2a9.zip
feat: add Safari extension for sourcehut deep linksv3.4.0
Diffstat (limited to 'Hutch/App/DeepLink.swift')
-rw-r--r--Hutch/App/DeepLink.swift64
1 files changed, 47 insertions, 17 deletions
diff --git a/Hutch/App/DeepLink.swift b/Hutch/App/DeepLink.swift
index 786891d..574481d 100644
--- a/Hutch/App/DeepLink.swift
+++ b/Hutch/App/DeepLink.swift
@@ -1,15 +1,22 @@
import Foundation
+import os
+
+private let deepLinkParserLogger = Logger(subsystem: "net.cleberg.Hutch", category: "DeepLink")
/// Represents a parsed `hutch://` deep link.
enum DeepLink: Equatable {
case home
case work
- /// hutch://git/<owner>/<repo>
- case repository(owner: String, repo: String)
+ /// hutch://git/<owner>/<repo> or hutch://hg/<owner>/<repo>
+ case repository(service: SRHTService, owner: String, repo: String)
/// hutch://todo/<owner>/<tracker>/<ticketId>
case ticket(owner: String, tracker: String, ticketId: Int)
- /// hutch://builds/<jobId>
+ /// hutch://builds/<jobId> or hutch://builds/<owner>/job/<jobId>
case build(jobId: Int)
+ /// hutch://lists/<owner>/<list>
+ case mailingList(owner: String, list: String)
+ /// hutch://lookup/<owner>
+ case userProfile(owner: String)
/// hutch://builds (tab-level)
case buildsTab
/// hutch://repositories (tab-level)
@@ -26,9 +33,8 @@ enum DeepLink: Equatable {
init?(url: URL) {
guard url.scheme == "hutch" else { return nil }
- // url.host gives the first path component for opaque URLs;
- // use standardized path components from the full string.
- let components = url.pathComponents(fromScheme: "hutch")
+ let components = url.deepLinkPathComponents
+ deepLinkParserLogger.info("DeepLink parser components for \(url.absoluteString, privacy: .public): \(components.joined(separator: ","), privacy: .public)")
switch components.first {
case "home", nil:
@@ -37,10 +43,18 @@ enum DeepLink: Equatable {
case "work", "inbox":
self = .work
- case "git" where components.count >= 3:
+ case let .some(serviceName) where ["git", "hg", "todo", "builds", "lists"].contains(serviceName)
+ && components.count == 2
+ && components[1].hasPrefix("~"):
+ deepLinkParserLogger.info("Treating owner-root service URL as user profile: service=\(serviceName, privacy: .public), owner=\(components[1], privacy: .public)")
+ self = .userProfile(owner: components[1])
+
+ case let .some(serviceName) where ["git", "hg"].contains(serviceName) && components.count >= 3:
+ guard let service = SRHTService(rawValue: serviceName)
+ else { return nil }
let owner = components[1]
let repo = components[2]
- self = .repository(owner: owner, repo: repo)
+ self = .repository(service: service, owner: owner, repo: repo)
case "todo" where components.count >= 4:
let owner = components[1]
@@ -49,12 +63,21 @@ enum DeepLink: Equatable {
self = .ticket(owner: owner, tracker: tracker, ticketId: ticketId)
case "builds" where components.count >= 2:
- guard let jobId = Int(components[1]) else { return nil }
+ let rawJobId: String
+ if components.count >= 4, components[2] == "job" {
+ rawJobId = components[3]
+ } else {
+ rawJobId = components[1]
+ }
+ guard let jobId = Int(rawJobId) else { return nil }
self = .build(jobId: jobId)
case "builds":
self = .buildsTab
+ case "lists" where components.count >= 3:
+ self = .mailingList(owner: components[1], list: components[2])
+
case "repositories":
self = .repositoriesTab
@@ -64,6 +87,9 @@ enum DeepLink: Equatable {
case "status":
self = .systemStatus
+ case "lookup" where components.count >= 2:
+ self = .userProfile(owner: components[1])
+
case "lookup":
self = .lookup
@@ -74,14 +100,18 @@ enum DeepLink: Equatable {
}
private extension URL {
- /// Parse path components from a custom-scheme URL.
- /// For `hutch://git/~user/repo`, returns `["git", "~user", "repo"]`.
- func pathComponents(fromScheme scheme: String) -> [String] {
- // Remove scheme prefix and split by "/"
- var str = absoluteString
- if str.hasPrefix("\(scheme)://") {
- str = String(str.dropFirst("\(scheme)://".count))
+ var deepLinkPathComponents: [String] {
+ guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false) else {
+ return []
+ }
+
+ let pathComponents = components.path
+ .split(separator: "/", omittingEmptySubsequences: true)
+ .map(String.init)
+
+ if let host = components.host, !host.isEmpty {
+ return [host] + pathComponents
}
- return str.split(separator: "/").map(String.init)
+ return pathComponents
}
}