Add URL scheme support for external trigger integration

Register snippingtool:// URL scheme so external tools (Logitech Options,
Shortcuts, Stream Deck) can trigger specific capture modes directly.
Adds AppDelegate for reliable URL handling in MenuBarExtra-only apps.

Supported URLs:
  snippingtool://fullscreen
  snippingtool://window
  snippingtool://region

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-01 16:03:25 +02:00
co-authored by Claude
parent 1c2b52fa30
commit ecbae7cd21
4 changed files with 50 additions and 2 deletions
+29
View File
@@ -92,6 +92,35 @@ final class AppCoordinator: ObservableObject {
} }
} }
// MARK: - External triggers (URL scheme)
/// Handles `snippingtool://<mode>` 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 // MARK: - Capture
func capture(mode: CaptureMode) { func capture(mode: CaptureMode) {
+15
View File
@@ -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) }
}
}
+2 -2
View File
@@ -2,12 +2,12 @@ import SwiftUI
@main @main
struct SnippingToolApp: App { struct SnippingToolApp: App {
@StateObject private var coordinator = AppCoordinator() @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene { var body: some Scene {
MenuBarExtra("Snipping Tool", systemImage: "camera.viewfinder") { MenuBarExtra("Snipping Tool", systemImage: "camera.viewfinder") {
MenuBarContentView() MenuBarContentView()
.environmentObject(coordinator) .environmentObject(appDelegate.coordinator)
} }
.menuBarExtraStyle(.menu) .menuBarExtraStyle(.menu)
} }
+4
View File
@@ -18,6 +18,10 @@ targets:
CFBundleName: Snipping Tool CFBundleName: Snipping Tool
LSUIElement: true LSUIElement: true
NSHumanReadableCopyright: "Copyright © 2026" NSHumanReadableCopyright: "Copyright © 2026"
CFBundleURLTypes:
- CFBundleURLName: it.wende.snippingtool
CFBundleURLSchemes:
- snippingtool
settings: settings:
base: base:
PRODUCT_BUNDLE_IDENTIFIER: it.wende.snippingtool PRODUCT_BUNDLE_IDENTIFIER: it.wende.snippingtool