From 699cb92e868414aa3e3c2f8fadf7b495cc092c3f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Dec 2025 01:09:49 +0000 Subject: [PATCH] Mac: let launch checkbox toggle launchd agent --- apps/macos/Sources/Clawdis/AppState.swift | 8 +-- apps/macos/Sources/Clawdis/Utilities.swift | 70 ++++++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/apps/macos/Sources/Clawdis/AppState.swift b/apps/macos/Sources/Clawdis/AppState.swift index a49f61055..bdd70c622 100644 --- a/apps/macos/Sources/Clawdis/AppState.swift +++ b/apps/macos/Sources/Clawdis/AppState.swift @@ -83,7 +83,7 @@ final class AppState: ObservableObject { init() { self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey) self.defaultSound = UserDefaults.standard.string(forKey: "clawdis.defaultSound") ?? "" - self.launchAtLogin = SMAppService.mainApp.status == .enabled + self.launchAtLogin = LaunchAgentManager.status() self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen") self.debugPaneEnabled = UserDefaults.standard.bool(forKey: "clawdis.debugPaneEnabled") let savedVoiceWake = UserDefaults.standard.bool(forKey: swabbleEnabledKey) @@ -125,11 +125,7 @@ enum AppStateStore { static var defaultSound: String { UserDefaults.standard.string(forKey: "clawdis.defaultSound") ?? "" } static func updateLaunchAtLogin(enabled: Bool) { - if enabled { - try? SMAppService.mainApp.register() - } else { - try? SMAppService.mainApp.unregister() - } + LaunchAgentManager.set(enabled: enabled, bundlePath: Bundle.main.bundlePath) } } diff --git a/apps/macos/Sources/Clawdis/Utilities.swift b/apps/macos/Sources/Clawdis/Utilities.swift index f3f5c78c4..117b02483 100644 --- a/apps/macos/Sources/Clawdis/Utilities.swift +++ b/apps/macos/Sources/Clawdis/Utilities.swift @@ -20,6 +20,76 @@ enum LaunchdManager { } } +enum LaunchAgentManager { + private static var plistURL: URL { + FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/LaunchAgents/com.steipete.clawdis.plist") + } + + static func status() -> Bool { + guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false } + let result = self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"]) + return result == 0 + } + + static func set(enabled: Bool, bundlePath: String) { + if enabled { + self.writePlist(bundlePath: bundlePath) + _ = self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"]) + _ = self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path]) + _ = self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"]) + } else { + _ = self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"]) + try? FileManager.default.removeItem(at: self.plistURL) + } + } + + private static func writePlist(bundlePath: String) { + let plist = """ + + + + + Label + com.steipete.clawdis + ProgramArguments + + \(bundlePath)/Contents/MacOS/Clawdis + + WorkingDirectory + \(FileManager.default.homeDirectoryForCurrentUser.path) + RunAtLoad + + KeepAlive + + MachServices + + com.steipete.clawdis.xpc + + + StandardOutPath + /tmp/clawdis.log + StandardErrorPath + /tmp/clawdis.log + + + """ + try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8) + } + + @discardableResult + private static func runLaunchctl(_ args: [String]) -> Int32 { + let process = Process() + process.launchPath = "/bin/launchctl" + process.arguments = args + process.standardOutput = Pipe() + process.standardError = Pipe() + try? process.run() + process.waitUntilExit() + return process.terminationStatus + } +} + @MainActor enum CLIInstaller { static func install(statusHandler: @escaping @Sendable (String) async -> Void) async {