From 060f80c239ab8f01ddf644acc7ae67a23f3e1b07 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Dec 2025 04:38:18 +0100 Subject: [PATCH] feat: add icon animation setting --- apps/macos/Sources/Clawdis/AppState.swift | 10 ++++++++ apps/macos/Sources/Clawdis/Constants.swift | 1 + .../Sources/Clawdis/GeneralSettings.swift | 5 ++++ apps/macos/Sources/Clawdis/MenuBar.swift | 23 ++++++++++++++++++- 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/macos/Sources/Clawdis/AppState.swift b/apps/macos/Sources/Clawdis/AppState.swift index b6eaee426..0b380a69d 100644 --- a/apps/macos/Sources/Clawdis/AppState.swift +++ b/apps/macos/Sources/Clawdis/AppState.swift @@ -40,6 +40,10 @@ final class AppState: ObservableObject { } } + @Published var iconAnimationsEnabled: Bool { + didSet { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } + } + @Published var showDockIcon: Bool { didSet { UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey) @@ -90,6 +94,12 @@ final class AppState: ObservableObject { self.swabbleEnabled = voiceWakeSupported ? savedVoiceWake : false self.swabbleTriggerWords = UserDefaults.standard .stringArray(forKey: swabbleTriggersKey) ?? defaultVoiceWakeTriggers + if let storedIconAnimations = UserDefaults.standard.object(forKey: iconAnimationsEnabledKey) as? Bool { + self.iconAnimationsEnabled = storedIconAnimations + } else { + self.iconAnimationsEnabled = true + UserDefaults.standard.set(true, forKey: iconAnimationsEnabledKey) + } self.showDockIcon = UserDefaults.standard.bool(forKey: showDockIconKey) self.voiceWakeMicID = UserDefaults.standard.string(forKey: voiceWakeMicKey) ?? "" self.voiceWakeLocaleID = UserDefaults.standard.string(forKey: voiceWakeLocaleKey) ?? Locale.current.identifier diff --git a/apps/macos/Sources/Clawdis/Constants.swift b/apps/macos/Sources/Clawdis/Constants.swift index 89723312a..558c9638a 100644 --- a/apps/macos/Sources/Clawdis/Constants.swift +++ b/apps/macos/Sources/Clawdis/Constants.swift @@ -5,6 +5,7 @@ let launchdLabel = "com.steipete.clawdis" let onboardingVersionKey = "clawdis.onboardingVersion" let currentOnboardingVersion = 2 let pauseDefaultsKey = "clawdis.pauseEnabled" +let iconAnimationsEnabledKey = "clawdis.iconAnimationsEnabled" let swabbleEnabledKey = "clawdis.swabbleEnabled" let swabbleTriggersKey = "clawdis.swabbleTriggers" let showDockIconKey = "clawdis.showDockIcon" diff --git a/apps/macos/Sources/Clawdis/GeneralSettings.swift b/apps/macos/Sources/Clawdis/GeneralSettings.swift index 15ca77c08..a88f52258 100644 --- a/apps/macos/Sources/Clawdis/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdis/GeneralSettings.swift @@ -33,6 +33,11 @@ struct GeneralSettings: View { subtitle: "Keep Clawdis visible in the Dock instead of menu-bar-only mode.", binding: self.$state.showDockIcon) + SettingsToggleRow( + title: "Play menu bar icon animations", + subtitle: "Enable idle blinks and wiggles on the status icon.", + binding: self.$state.iconAnimationsEnabled) + SettingsToggleRow( title: "Enable debug tools", subtitle: "Show the Debug tab with development utilities.", diff --git a/apps/macos/Sources/Clawdis/MenuBar.swift b/apps/macos/Sources/Clawdis/MenuBar.swift index 51a318c43..d5cfa4ef9 100644 --- a/apps/macos/Sources/Clawdis/MenuBar.swift +++ b/apps/macos/Sources/Clawdis/MenuBar.swift @@ -25,7 +25,8 @@ struct ClawdisApp: App { isPaused: self.state.isPaused, isWorking: self.state.isWorking, earBoostActive: self.state.earBoostActive, - relayStatus: self.relayManager.status) + relayStatus: self.relayManager.status, + animationsEnabled: self.state.iconAnimationsEnabled) } .menuBarExtraStyle(.menu) .menuBarExtraAccess(isPresented: self.$isMenuPresented) { item in @@ -137,6 +138,7 @@ private struct CritterStatusLabel: View { var isWorking: Bool var earBoostActive: Bool var relayStatus: RelayProcessManager.Status + var animationsEnabled: Bool @State private var blinkAmount: CGFloat = 0 @State private var nextBlink = Date().addingTimeInterval(Double.random(in: 3.5...8.5)) @@ -165,6 +167,11 @@ private struct CritterStatusLabel: View { .rotationEffect(.degrees(self.wiggleAngle), anchor: .center) .offset(x: self.wiggleOffset) .onReceive(self.ticker) { now in + guard self.animationsEnabled else { + self.resetMotion() + return + } + if now >= self.nextBlink { self.blink() self.nextBlink = now.addingTimeInterval(Double.random(in: 3.5...8.5)) @@ -190,6 +197,13 @@ private struct CritterStatusLabel: View { } } .onChange(of: self.isPaused) { _, _ in self.resetMotion() } + .onChange(of: self.animationsEnabled) { _, enabled in + if enabled { + self.scheduleRandomTimers(from: Date()) + } else { + self.resetMotion() + } + } } } @@ -266,6 +280,13 @@ private struct CritterStatusLabel: View { } } + private func scheduleRandomTimers(from date: Date) { + self.nextBlink = date.addingTimeInterval(Double.random(in: 3.5...8.5)) + self.nextWiggle = date.addingTimeInterval(Double.random(in: 6.5...14)) + self.nextLegWiggle = date.addingTimeInterval(Double.random(in: 5.0...11.0)) + self.nextEarWiggle = date.addingTimeInterval(Double.random(in: 7.0...14.0)) + } + private var relayNeedsAttention: Bool { switch self.relayStatus { case .failed, .stopped: