Files
clawdbot/apps/macos/Sources/Clawdis/AppState.swift

183 lines
7.1 KiB
Swift

import AppKit
import Foundation
import ServiceManagement
import SwiftUI
@MainActor
final class AppState: ObservableObject {
@Published var isPaused: Bool {
didSet { UserDefaults.standard.set(self.isPaused, forKey: pauseDefaultsKey) }
}
@Published var defaultSound: String {
didSet { UserDefaults.standard.set(self.defaultSound, forKey: "clawdis.defaultSound") }
}
@Published var launchAtLogin: Bool {
didSet { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } }
}
@Published var onboardingSeen: Bool {
didSet { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") }
}
@Published var debugPaneEnabled: Bool {
didSet { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") }
}
@Published var swabbleEnabled: Bool {
didSet { UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey) }
}
@Published var swabbleTriggerWords: [String] {
didSet {
let cleaned = self.swabbleTriggerWords.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
UserDefaults.standard.set(cleaned, forKey: swabbleTriggersKey)
if cleaned.count != self.swabbleTriggerWords.count {
self.swabbleTriggerWords = cleaned
}
}
}
@Published var showDockIcon: Bool {
didSet {
UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey)
AppActivationPolicy.apply(showDockIcon: self.showDockIcon)
}
}
@Published var voiceWakeMicID: String {
didSet { UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey) }
}
@Published var voiceWakeLocaleID: String {
didSet { UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey) }
}
@Published var voiceWakeAdditionalLocaleIDs: [String] {
didSet { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) }
}
@Published var voiceWakeForwardEnabled: Bool {
didSet { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) }
}
@Published var voiceWakeForwardTarget: String {
didSet { UserDefaults.standard.set(self.voiceWakeForwardTarget, forKey: voiceWakeForwardTargetKey) }
}
@Published var voiceWakeForwardIdentity: String {
didSet { UserDefaults.standard.set(self.voiceWakeForwardIdentity, forKey: voiceWakeForwardIdentityKey) }
}
@Published var voiceWakeForwardCommand: String {
didSet { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) }
}
@Published var isWorking: Bool = false
@Published var earBoostActive: Bool = false
private var earBoostTask: Task<Void, Never>?
init() {
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
self.defaultSound = UserDefaults.standard.string(forKey: "clawdis.defaultSound") ?? ""
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)
self.swabbleEnabled = voiceWakeSupported ? savedVoiceWake : false
self.swabbleTriggerWords = UserDefaults.standard
.stringArray(forKey: swabbleTriggersKey) ?? defaultVoiceWakeTriggers
self.showDockIcon = UserDefaults.standard.bool(forKey: showDockIconKey)
self.voiceWakeMicID = UserDefaults.standard.string(forKey: voiceWakeMicKey) ?? ""
self.voiceWakeLocaleID = UserDefaults.standard.string(forKey: voiceWakeLocaleKey) ?? Locale.current.identifier
self.voiceWakeAdditionalLocaleIDs = UserDefaults.standard
.stringArray(forKey: voiceWakeAdditionalLocalesKey) ?? []
self.voiceWakeForwardEnabled = UserDefaults.standard.bool(forKey: voiceWakeForwardEnabledKey)
let legacyTarget = Self.legacyTargetString()
self.voiceWakeForwardTarget = UserDefaults.standard
.string(forKey: voiceWakeForwardTargetKey) ?? legacyTarget
self.voiceWakeForwardIdentity = UserDefaults.standard.string(forKey: voiceWakeForwardIdentityKey) ?? ""
self.voiceWakeForwardCommand = UserDefaults.standard
.string(forKey: voiceWakeForwardCommandKey) ?? defaultVoiceWakeForwardCommand
if self.swabbleEnabled && !PermissionManager.voiceWakePermissionsGranted() {
self.swabbleEnabled = false
}
}
func triggerVoiceEars(ttl: TimeInterval = 5) {
self.earBoostTask?.cancel()
self.earBoostActive = true
self.earBoostTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(ttl * 1_000_000_000))
await MainActor.run { [weak self] in self?.earBoostActive = false }
}
}
func setVoiceWakeEnabled(_ enabled: Bool) async {
guard voiceWakeSupported else {
self.swabbleEnabled = false
return
}
if !enabled {
self.swabbleEnabled = false
return
}
if PermissionManager.voiceWakePermissionsGranted() {
self.swabbleEnabled = true
return
}
let granted = await PermissionManager.ensureVoiceWakePermissions(interactive: true)
self.swabbleEnabled = granted
}
func setWorking(_ working: Bool) {
self.isWorking = working
}
}
@MainActor
enum AppStateStore {
static let shared = AppState()
static var isPausedFlag: Bool { UserDefaults.standard.bool(forKey: pauseDefaultsKey) }
static var defaultSound: String { UserDefaults.standard.string(forKey: "clawdis.defaultSound") ?? "" }
static func updateLaunchAtLogin(enabled: Bool) {
LaunchAgentManager.set(enabled: enabled, bundlePath: Bundle.main.bundlePath)
}
}
extension AppState {
var voiceWakeForwardConfig: VoiceWakeForwardConfig {
VoiceWakeForwardConfig(
enabled: self.voiceWakeForwardEnabled,
target: self.voiceWakeForwardTarget,
identityPath: self.voiceWakeForwardIdentity,
commandTemplate: self.voiceWakeForwardCommand,
timeout: defaultVoiceWakeForwardTimeout)
}
private static func legacyTargetString() -> String {
let host = UserDefaults.standard.string(forKey: voiceWakeForwardHostKey) ?? ""
let user = UserDefaults.standard.string(forKey: voiceWakeForwardUserKey) ?? ""
let savedPort = UserDefaults.standard.integer(forKey: voiceWakeForwardPortKey)
let port = savedPort == 0 ? defaultVoiceWakeForwardPort : savedPort
let userPrefix = user.isEmpty ? "" : "\(user)@"
let portSuffix = host.isEmpty ? "" : ":\(port)"
return "\(userPrefix)\(host)\(portSuffix)"
}
}
@MainActor
enum AppActivationPolicy {
static func apply(showDockIcon: Bool) {
NSApp.setActivationPolicy(showDockIcon ? .regular : .accessory)
}
}