diff --git a/Sources/AppCoordinator.swift b/Sources/AppCoordinator.swift index 2a26bfd..bac4c3c 100644 --- a/Sources/AppCoordinator.swift +++ b/Sources/AppCoordinator.swift @@ -92,6 +92,35 @@ final class AppCoordinator: ObservableObject { } } + // MARK: - External triggers (URL scheme) + + /// Handles `snippingtool://` URLs so external tools (Logitech + /// Options, Shortcuts, Stream Deck, `open` from the command line, …) + /// can trigger a specific capture mode instead of just launching the app. + /// Accepts the mode either as the host (`snippingtool://region`) or as + /// a query item (`snippingtool://capture?mode=region`). + func handle(url: URL) { + guard url.scheme?.caseInsensitiveCompare("snippingtool") == .orderedSame else { return } + + let modeString = url.host?.lowercased() + ?? URLComponents(url: url, resolvingAgainstBaseURL: false)? + .queryItems? + .first(where: { $0.name.caseInsensitiveCompare("mode") == .orderedSame })? + .value? + .lowercased() + + switch modeString { + case "fullscreen", "full", "full-screen", "screen": + capture(mode: .fullScreen) + case "window": + capture(mode: .window) + case "region", "area", "selection": + capture(mode: .region) + default: + NSLog("SnippingTool: unrecognized URL trigger \(url.absoluteString)") + } + } + // MARK: - Capture func capture(mode: CaptureMode) { diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift new file mode 100644 index 0000000..f744d48 --- /dev/null +++ b/Sources/AppDelegate.swift @@ -0,0 +1,15 @@ +import AppKit + +/// MenuBarExtra-only apps (no WindowGroup) don't reliably receive SwiftUI's +/// `.onOpenURL` scene events — SwiftUI logs "Cannot use Scene methods for +/// URL... without using SwiftUI Lifecycle" and the handler never fires. +/// The classic AppKit delegate callback works regardless, so URL scheme +/// triggers (from Logitech Options, Shortcuts, Stream Deck, `open`, …) are +/// routed through here instead. +final class AppDelegate: NSObject, NSApplicationDelegate { + let coordinator = AppCoordinator() + + func application(_ application: NSApplication, open urls: [URL]) { + urls.forEach { coordinator.handle(url: $0) } + } +} diff --git a/Sources/SnippingToolApp.swift b/Sources/SnippingToolApp.swift index 21f8265..0394d74 100644 --- a/Sources/SnippingToolApp.swift +++ b/Sources/SnippingToolApp.swift @@ -2,12 +2,12 @@ import SwiftUI @main struct SnippingToolApp: App { - @StateObject private var coordinator = AppCoordinator() + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate var body: some Scene { MenuBarExtra("Snipping Tool", systemImage: "camera.viewfinder") { MenuBarContentView() - .environmentObject(coordinator) + .environmentObject(appDelegate.coordinator) } .menuBarExtraStyle(.menu) } diff --git a/project.yml b/project.yml index b0ce3dc..0c331b0 100644 --- a/project.yml +++ b/project.yml @@ -18,6 +18,10 @@ targets: CFBundleName: Snipping Tool LSUIElement: true NSHumanReadableCopyright: "Copyright © 2026" + CFBundleURLTypes: + - CFBundleURLName: it.wende.snippingtool + CFBundleURLSchemes: + - snippingtool settings: base: PRODUCT_BUNDLE_IDENTIFIER: it.wende.snippingtool