From 53f8b297c94cf6b0de1b77d74afa0eb85b5eddc5 Mon Sep 17 00:00:00 2001 From: delli Date: Thu, 8 Jan 2026 16:51:12 +0100 Subject: [PATCH 1/3] just 2 small features --- DynamicIsland/ContentView.swift | 4 + .../DynamicIslandViewCoordinator.swift | 6 +- DynamicIsland/Localizable.xcstrings | 3 + .../Live activities/InlineHUD.swift | 20 ++++- .../LockScreenLiveActivityOverlay.swift | 14 ++- .../components/Settings/SettingsView.swift | 5 ++ DynamicIsland/managers/CapsLockManager.swift | 85 +++++++++++++++++++ .../LockScreenLiveActivityWindowManager.swift | 2 +- DynamicIsland/models/Constants.swift | 4 + 9 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 DynamicIsland/managers/CapsLockManager.swift diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 206c6a2..bbf2eaf 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -28,6 +28,7 @@ struct ContentView: View { @ObservedObject var privacyManager = PrivacyIndicatorManager.shared @ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared @ObservedObject var lockScreenManager = LockScreenManager.shared + @ObservedObject var capsLockManager = CapsLockManager.shared @State private var downloadManager = DownloadManager.shared @Default(.enableStatsFeature) var enableStatsFeature @@ -485,6 +486,9 @@ struct ContentView: View { } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && (coordinator.sneakPeek.type != .volume || vm.notchState == .closed) { InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress) .transition(.opacity) + } else if vm.notchState == .closed && capsLockManager.isCapsLockActive && Defaults[.enableCapsLockIndicator] && !vm.hideOnClosed && !lockScreenManager.isLocked { + InlineHUD(type: .constant(.capsLock), value: .constant(1.0), icon: .constant(""), hoverAnimation: $isHovering, gestureProgress: $gestureProgress) + .transition(.opacity) } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed && !lockScreenManager.isLocked { MusicLiveActivity() } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .timer) && vm.notchState == .closed && timerManager.isTimerActive && coordinator.timerLiveActivityEnabled && !vm.hideOnClosed { diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 2bc22e6..66aec7e 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -24,6 +24,7 @@ enum SneakContentType { case bluetoothAudio case privacy case lockScreen + case capsLock } struct sneakPeek { @@ -190,7 +191,7 @@ class DynamicIslandViewCoordinator: ObservableObject { return } DispatchQueue.main.async { - withAnimation(.smooth) { + withAnimation(.smooth(duration: 0.3)) { self.sneakPeek.show = status self.sneakPeek.type = type self.sneakPeek.value = value @@ -205,6 +206,9 @@ class DynamicIslandViewCoordinator: ObservableObject { // Helper function to manage sneakPeek timer using Swift Concurrency private func scheduleSneakPeekHide(after duration: TimeInterval) { sneakPeekTask?.cancel() + + // Don't schedule auto-hide if duration is infinite (for persistent indicators like Caps Lock) + guard duration.isFinite else { return } sneakPeekTask = Task { [weak self] in try? await Task.sleep(for: .seconds(duration)) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 459d989..abf9094 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -4622,6 +4622,9 @@ } } } + }, + "Caps Lock" : { + }, "Center" : { "extractionState" : "stale", diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index fed58ea..4e4a2ec 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -128,6 +128,12 @@ struct InlineHUD: View { .symbolRenderingMode(.hierarchical) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) + case .capsLock: + Image(systemName: "capslock.fill") + .symbolRenderingMode(.hierarchical) + .contentTransition(.interpolate) + .frame(width: 20, height: 15, alignment: .center) + .foregroundStyle(Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white) default: EmptyView() } @@ -147,7 +153,7 @@ struct InlineHUD: View { frameWidth: infoWidth ) } - } else { + } else if type != .capsLock { Text(Type2Name(type)) .font(.subheadline) .fontWeight(.medium) @@ -180,6 +186,16 @@ struct InlineHUD: View { .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) .contentTransition(.interpolate) + } else if (type == .capsLock) { + Text("Caps Lock") + .foregroundStyle(Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white) + .font(.subheadline) + .fontWeight(.medium) + .lineLimit(1) + .allowsTightening(true) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity, alignment: .trailing) + .contentTransition(.interpolate) } else if (type == .bluetoothAudio) { if hasBatteryLevel { let indicatorSpacing: CGFloat = { @@ -374,6 +390,8 @@ struct InlineHUD: View { return "Mic" case .bluetoothAudio: return BluetoothAudioManager.shared.lastConnectedDevice?.name ?? "Bluetooth" + case .capsLock: + return "Caps Lock" default: return "" } diff --git a/DynamicIsland/components/LockScreen/LockScreenLiveActivityOverlay.swift b/DynamicIsland/components/LockScreen/LockScreenLiveActivityOverlay.swift index 7b0b796..50aba39 100644 --- a/DynamicIsland/components/LockScreen/LockScreenLiveActivityOverlay.swift +++ b/DynamicIsland/components/LockScreen/LockScreenLiveActivityOverlay.swift @@ -26,6 +26,8 @@ struct LockScreenLiveActivityOverlay: View { Self.collapsedScale(for: notchSize) } + @State private var isHovering: Bool = false + var body: some View { HStack(spacing: 0) { Color.clear @@ -52,8 +54,18 @@ struct LockScreenLiveActivityOverlay: View { ) ) .frame(width: totalWidth, height: notchSize.height) - .scaleEffect(x: max(model.scale, collapsedScale), y: 1, anchor: .center) + .scaleEffect(x: max(model.scale, collapsedScale) * (isHovering ? 1.03 : 1.0), + y: 1 * (isHovering ? 1.03 : 1.0), + anchor: .center) .opacity(model.opacity) + .onHover { hovering in + withAnimation(.smooth(duration: 0.2)) { + isHovering = hovering + } + if hovering { + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .default) + } + } } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 59de231..51b7f39 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1735,6 +1735,11 @@ private struct DevicesSettingsView: View { Section { Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections) .settingsHighlight(id: highlightID("Show Bluetooth device connections")) + Defaults.Toggle("Show Caps Lock Indicator", key: .enableCapsLockIndicator) + .settingsHighlight(id: highlightID("Show Caps Lock Indicator")) + Defaults.Toggle("Use green color for Caps Lock text", key: .capsLockIndicatorUseGreenColor) + .disabled(!Defaults[.enableCapsLockIndicator]) + .settingsHighlight(id: highlightID("Use green color for Caps Lock text")) Defaults.Toggle("Use circular battery indicator", key: .useCircularBluetoothBatteryIndicator) .settingsHighlight(id: highlightID("Use circular battery indicator")) Defaults.Toggle("Show battery percentage text in HUD", key: .showBluetoothBatteryPercentageText) diff --git a/DynamicIsland/managers/CapsLockManager.swift b/DynamicIsland/managers/CapsLockManager.swift new file mode 100644 index 0000000..49d5c9e --- /dev/null +++ b/DynamicIsland/managers/CapsLockManager.swift @@ -0,0 +1,85 @@ +// +// CapsLockManager.swift +// DynamicIsland +// +// Created for Caps Lock indicator feature +// Monitors Caps Lock state and integrates with Dynamic Island +// + +import Foundation +import Combine +import AppKit +import Defaults + +@MainActor +class CapsLockManager: ObservableObject { + static let shared = CapsLockManager() + + @Published var isCapsLockActive: Bool = false + + private var localEventMonitor: Any? + private var globalEventMonitor: Any? + private let coordinator = DynamicIslandViewCoordinator.shared + + private init() { + // Get initial state + isCapsLockActive = NSEvent.modifierFlags.contains(.capsLock) + + // Monitor flag changes when app is focused + localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in + self?.handleFlagsChanged(event) + return event + } + + // Monitor flag changes globally (even when app is not focused) + globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in + Task { @MainActor in + self?.handleFlagsChanged(event) + } + } + + print("CapsLockManager: ✅ Initialized with Caps Lock \(isCapsLockActive ? "ON" : "OFF")") + } + + deinit { + if let monitor = localEventMonitor { + NSEvent.removeMonitor(monitor) + } + if let monitor = globalEventMonitor { + NSEvent.removeMonitor(monitor) + } + } + + private func handleFlagsChanged(_ event: NSEvent) { + let newState = event.modifierFlags.contains(.capsLock) + + guard newState != isCapsLockActive else { return } + + isCapsLockActive = newState + + print("CapsLockManager: Caps Lock \(newState ? "ACTIVATED" : "DEACTIVATED")") + + // Only show/hide if feature is enabled + guard Defaults[.enableCapsLockIndicator] else { return } + + if newState { + // Show inline indicator + coordinator.toggleSneakPeek( + status: true, + type: .capsLock, + duration: .infinity, // Stay visible until deactivated + value: 1.0, + icon: "" + ) + } else { + // Hide indicator + coordinator.toggleSneakPeek( + status: false, + type: .capsLock, + duration: 0, + value: 0, + icon: "" + ) + } + } +} diff --git a/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift b/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift index 4cc22a2..dda84e4 100644 --- a/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift +++ b/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift @@ -68,7 +68,7 @@ class LockScreenLiveActivityWindowManager { window.isOpaque = false window.backgroundColor = .clear window.isReleasedWhenClosed = false - window.ignoresMouseEvents = true + window.ignoresMouseEvents = false window.hasShadow = false window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 4f00e16..718a97c 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -738,6 +738,10 @@ extension Defaults.Keys { static let enableLockScreenLiveActivity = Key("enableLockScreenLiveActivity", default: true) static let enableLockSounds = Key("enableLockSounds", default: true) + // MARK: Caps Lock Indicator + static let enableCapsLockIndicator = Key("enableCapsLockIndicator", default: true) + static let capsLockIndicatorUseGreenColor = Key("capsLockIndicatorUseGreenColor", default: false) + // MARK: ImageService static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCacheV1", default: false) From 168dc27af549e134a2818bcbe6ba8669be4b7ab3 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 8 Jan 2026 23:16:34 +0530 Subject: [PATCH 2/3] Fix: Caps Lock View --- DynamicIsland/ContentView.swift | 8 +- DynamicIsland/Localizable.xcstrings | 6 ++ .../Live activities/InlineHUD.swift | 95 +++++++++++++------ .../components/Settings/SettingsView.swift | 22 ++++- DynamicIsland/managers/CapsLockManager.swift | 6 +- DynamicIsland/models/Constants.swift | 1 + 6 files changed, 100 insertions(+), 38 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index bbf2eaf..ebcc50e 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -485,10 +485,14 @@ struct ContentView: View { .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && (coordinator.sneakPeek.type != .volume || vm.notchState == .closed) { InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress) - .transition(.opacity) + .transition( + coordinator.sneakPeek.type == .capsLock + ? .move(edge: .trailing).combined(with: .opacity) + : .opacity + ) } else if vm.notchState == .closed && capsLockManager.isCapsLockActive && Defaults[.enableCapsLockIndicator] && !vm.hideOnClosed && !lockScreenManager.isLocked { InlineHUD(type: .constant(.capsLock), value: .constant(1.0), icon: .constant(""), hoverAnimation: $isHovering, gestureProgress: $gestureProgress) - .transition(.opacity) + .transition(.move(edge: .trailing).combined(with: .opacity)) } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed && !lockScreenManager.isLocked { MusicLiveActivity() } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .timer) && vm.notchState == .closed && timerManager.isTimerActive && coordinator.timerLiveActivityEnabled && !vm.hideOnClosed { diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index abf9094..84fc92e 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -2362,6 +2362,9 @@ } } } + }, + "Adds a notch HUD when Caps Lock is enabled, with optional label and tint controls." : { + }, "Adds the AirPlay/route picker button back to the customizable controls palette." : { @@ -4625,6 +4628,9 @@ }, "Caps Lock" : { + }, + "Caps Lock Indicator" : { + }, "Center" : { "extractionState" : "stale", diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index 4e4a2ec..217b465 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -25,6 +25,7 @@ struct InlineHUD: View { @Default(.showBluetoothBatteryPercentageText) var showBluetoothBatteryPercentageText @Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee @Default(.enableMinimalisticUI) var enableMinimalisticUI + @Default(.showCapsLockLabel) var showCapsLockLabel @ObservedObject var bluetoothManager = BluetoothAudioManager.shared @State private var displayName: String = "" @@ -32,55 +33,87 @@ struct InlineHUD: View { var body: some View { let useCircularIndicator = useCircularBluetoothBatteryIndicator let hasBatteryLevel = value > 0 + let capsLockAccentColor: Color = Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white let baseInfoWidth: CGFloat = { - guard type == .bluetoothAudio else { return 100 } - if showBluetoothDeviceNameMarquee { - return enableMinimalisticUI ? 128 : 140 + if type == .bluetoothAudio { + if showBluetoothDeviceNameMarquee { + return enableMinimalisticUI ? 128 : 140 + } + return enableMinimalisticUI ? 64 : 72 } - return enableMinimalisticUI ? 64 : 72 + + if type == .capsLock && !showCapsLockLabel { + return enableMinimalisticUI ? 56 : 64 + } + + return 100 }() let infoWidth: CGFloat = { var width = baseInfoWidth + gestureProgress / 2 if !hoverAnimation { width -= 8 } let minimum: CGFloat = { - guard type == .bluetoothAudio else { return 88 } - if showBluetoothDeviceNameMarquee { - return enableMinimalisticUI ? 112 : 120 + if type == .bluetoothAudio { + if showBluetoothDeviceNameMarquee { + return enableMinimalisticUI ? 112 : 120 + } + return enableMinimalisticUI ? 56 : 68 } - return enableMinimalisticUI ? 56 : 68 + + if type == .capsLock && !showCapsLockLabel { + return enableMinimalisticUI ? 44 : 52 + } + + return 88 }() return max(width, minimum) }() let baseTrailingWidth: CGFloat = { - guard type == .bluetoothAudio else { return 100 } - if !hasBatteryLevel { - return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 104 : 118) : (enableMinimalisticUI ? 74 : 88) + if type == .bluetoothAudio { + if !hasBatteryLevel { + return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 104 : 118) : (enableMinimalisticUI ? 74 : 88) + } + + if useCircularIndicator { + return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 108 : 120) : (enableMinimalisticUI ? 72 : 84) + } + + return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 118 : 136) : (enableMinimalisticUI ? 92 : 108) } - if useCircularIndicator { - return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 108 : 120) : (enableMinimalisticUI ? 72 : 84) + if type == .capsLock { + if showCapsLockLabel { + return enableMinimalisticUI ? 84 : 96 + } + return 0 } - return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 118 : 136) : (enableMinimalisticUI ? 92 : 108) + return 100 }() let trailingWidth: CGFloat = { var width = baseTrailingWidth + gestureProgress / 2 if !hoverAnimation { width -= 8 } let minimum: CGFloat = { - guard type == .bluetoothAudio else { return 90 } - if !hasBatteryLevel { - return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 96 : 110) : (enableMinimalisticUI ? 62 : 88) + if type == .bluetoothAudio { + if !hasBatteryLevel { + return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 96 : 110) : (enableMinimalisticUI ? 62 : 88) + } + + if useCircularIndicator { + return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 92 : 110) : (enableMinimalisticUI ? 56 : 72) + } + + return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 104 : 120) : (enableMinimalisticUI ? 72 : 90) } - if useCircularIndicator { - return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 92 : 110) : (enableMinimalisticUI ? 56 : 72) + if type == .capsLock { + return showCapsLockLabel ? (enableMinimalisticUI ? 68 : 80) : 0 } - return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 104 : 120) : (enableMinimalisticUI ? 72 : 90) + return 90 }() return max(width, minimum) }() @@ -133,7 +166,7 @@ struct InlineHUD: View { .symbolRenderingMode(.hierarchical) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) - .foregroundStyle(Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white) + .foregroundStyle(capsLockAccentColor) default: EmptyView() } @@ -187,15 +220,17 @@ struct InlineHUD: View { .frame(maxWidth: .infinity, alignment: .trailing) .contentTransition(.interpolate) } else if (type == .capsLock) { - Text("Caps Lock") - .foregroundStyle(Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white) - .font(.subheadline) - .fontWeight(.medium) - .lineLimit(1) - .allowsTightening(true) - .multilineTextAlignment(.trailing) - .frame(maxWidth: .infinity, alignment: .trailing) - .contentTransition(.interpolate) + if showCapsLockLabel { + Text("Caps Lock") + .foregroundStyle(capsLockAccentColor) + .font(.subheadline) + .fontWeight(.medium) + .lineLimit(1) + .allowsTightening(true) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity, alignment: .trailing) + .contentTransition(.interpolate) + } } else if (type == .bluetoothAudio) { if hasBatteryLevel { let indicatorSpacing: CGFloat = { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 51b7f39..34d406b 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1735,11 +1735,6 @@ private struct DevicesSettingsView: View { Section { Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections) .settingsHighlight(id: highlightID("Show Bluetooth device connections")) - Defaults.Toggle("Show Caps Lock Indicator", key: .enableCapsLockIndicator) - .settingsHighlight(id: highlightID("Show Caps Lock Indicator")) - Defaults.Toggle("Use green color for Caps Lock text", key: .capsLockIndicatorUseGreenColor) - .disabled(!Defaults[.enableCapsLockIndicator]) - .settingsHighlight(id: highlightID("Use green color for Caps Lock text")) Defaults.Toggle("Use circular battery indicator", key: .useCircularBluetoothBatteryIndicator) .settingsHighlight(id: highlightID("Use circular battery indicator")) Defaults.Toggle("Show battery percentage text in HUD", key: .showBluetoothBatteryPercentageText) @@ -2644,6 +2639,23 @@ struct LiveActivitiesSettings: View { Text("Listens for Focus session changes via distributed notifications") } + Section { + Defaults.Toggle("Show Caps Lock Indicator", key: .enableCapsLockIndicator) + .settingsHighlight(id: highlightID("Show Caps Lock Indicator")) + + Defaults.Toggle("Show Caps Lock label", key: .showCapsLockLabel) + .disabled(!Defaults[.enableCapsLockIndicator]) + .settingsHighlight(id: highlightID("Show Caps Lock label")) + + Defaults.Toggle("Use green color for Caps Lock text", key: .capsLockIndicatorUseGreenColor) + .disabled(!Defaults[.enableCapsLockIndicator]) + .settingsHighlight(id: highlightID("Use green color for Caps Lock text")) + } header: { + Text("Caps Lock Indicator") + } footer: { + Text("Adds a notch HUD when Caps Lock is enabled, with optional label and tint controls.") + } + Section { Defaults.Toggle("Enable Camera Detection", key: .enableCameraDetection) .settingsHighlight(id: highlightID("Enable Camera Detection")) diff --git a/DynamicIsland/managers/CapsLockManager.swift b/DynamicIsland/managers/CapsLockManager.swift index 49d5c9e..489d037 100644 --- a/DynamicIsland/managers/CapsLockManager.swift +++ b/DynamicIsland/managers/CapsLockManager.swift @@ -10,6 +10,7 @@ import Foundation import Combine import AppKit import Defaults +import SwiftUI @MainActor class CapsLockManager: ObservableObject { @@ -20,6 +21,7 @@ class CapsLockManager: ObservableObject { private var localEventMonitor: Any? private var globalEventMonitor: Any? private let coordinator = DynamicIslandViewCoordinator.shared + private let capsLockAnimation = Animation.spring(response: 0.32, dampingFraction: 0.85) private init() { // Get initial state @@ -55,7 +57,9 @@ class CapsLockManager: ObservableObject { guard newState != isCapsLockActive else { return } - isCapsLockActive = newState + withAnimation(capsLockAnimation) { + isCapsLockActive = newState + } print("CapsLockManager: Caps Lock \(newState ? "ACTIVATED" : "DEACTIVATED")") diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 718a97c..b54873d 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -741,6 +741,7 @@ extension Defaults.Keys { // MARK: Caps Lock Indicator static let enableCapsLockIndicator = Key("enableCapsLockIndicator", default: true) static let capsLockIndicatorUseGreenColor = Key("capsLockIndicatorUseGreenColor", default: false) + static let showCapsLockLabel = Key("showCapsLockLabel", default: true) // MARK: ImageService static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCacheV1", default: false) From 776144064fa7b84fe46a6a1ba8156d462f6a0b48 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 8 Jan 2026 23:22:49 +0530 Subject: [PATCH 3/3] Update caps lock settings --- DynamicIsland/DynamicIslandApp.swift | 1 + DynamicIsland/Localizable.xcstrings | 3 ++ .../Live activities/InlineHUD.swift | 3 +- .../components/Settings/SettingsView.swift | 12 ++++-- DynamicIsland/models/Constants.swift | 42 ++++++++++++++++++- 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 59981a7..0640878 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -345,6 +345,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { Defaults.Keys.migrateProgressBarStyle() Defaults.Keys.migrateMusicAuxControls() Defaults.Keys.migrateMusicControlSlots() + Defaults.Keys.migrateCapsLockTintMode() // Initialize idle animations (load bundled + built-in face) idleAnimationManager.initializeDefaultAnimations() diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 84fc92e..0f8860d 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -4628,6 +4628,9 @@ }, "Caps Lock" : { + }, + "Caps Lock color" : { + }, "Caps Lock Indicator" : { diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index 217b465..310ec35 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -26,6 +26,7 @@ struct InlineHUD: View { @Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee @Default(.enableMinimalisticUI) var enableMinimalisticUI @Default(.showCapsLockLabel) var showCapsLockLabel + @Default(.capsLockIndicatorTintMode) var capsLockTintMode @ObservedObject var bluetoothManager = BluetoothAudioManager.shared @State private var displayName: String = "" @@ -33,7 +34,7 @@ struct InlineHUD: View { var body: some View { let useCircularIndicator = useCircularBluetoothBatteryIndicator let hasBatteryLevel = value > 0 - let capsLockAccentColor: Color = Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white + let capsLockAccentColor = capsLockTintMode.color let baseInfoWidth: CGFloat = { if type == .bluetoothAudio { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 34d406b..546eefd 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2538,6 +2538,7 @@ struct LiveActivitiesSettings: View { @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection @Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection @Default(.focusIndicatorNonPersistent) var focusIndicatorNonPersistent + @Default(.capsLockIndicatorTintMode) var capsLockTintMode private func highlightID(_ title: String) -> String { SettingsTab.liveActivities.highlightID(for: title) @@ -2647,9 +2648,14 @@ struct LiveActivitiesSettings: View { .disabled(!Defaults[.enableCapsLockIndicator]) .settingsHighlight(id: highlightID("Show Caps Lock label")) - Defaults.Toggle("Use green color for Caps Lock text", key: .capsLockIndicatorUseGreenColor) - .disabled(!Defaults[.enableCapsLockIndicator]) - .settingsHighlight(id: highlightID("Use green color for Caps Lock text")) + Picker("Caps Lock color", selection: $capsLockTintMode) { + ForEach(CapsLockIndicatorTintMode.allCases) { mode in + Text(mode.displayName).tag(mode) + } + } + .pickerStyle(.segmented) + .disabled(!Defaults[.enableCapsLockIndicator]) + .settingsHighlight(id: highlightID("Caps Lock color")) } header: { Text("Caps Lock Indicator") } footer: { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index b54873d..39e655d 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -191,6 +191,36 @@ enum SneakPeekStyle: String, CaseIterable, Identifiable, Defaults.Serializable { var id: String { self.rawValue } } +enum CapsLockIndicatorTintMode: String, CaseIterable, Identifiable, Defaults.Serializable { + case green + case accent + case white + + var id: String { rawValue } + + var displayName: String { + switch self { + case .green: + return "Green" + case .accent: + return "Accent" + case .white: + return "White" + } + } + + var color: Color { + switch self { + case .green: + return .green + case .accent: + return .accentColor + case .white: + return .white + } + } +} + enum ProgressBarStyle: String, CaseIterable, Identifiable, Defaults.Serializable { case hierarchical = "Hierarchical" case gradient = "Gradient" @@ -740,7 +770,9 @@ extension Defaults.Keys { // MARK: Caps Lock Indicator static let enableCapsLockIndicator = Key("enableCapsLockIndicator", default: true) - static let capsLockIndicatorUseGreenColor = Key("capsLockIndicatorUseGreenColor", default: false) + static let capsLockIndicatorUseGreenColor = Key("capsLockIndicatorUseGreenColor", default: false) // Legacy toggle + static let capsLockIndicatorTintMode = Key("capsLockIndicatorTintMode", default: .white) + static let didMigrateCapsLockTintMode = Key("didMigrateCapsLockTintMode", default: false) static let showCapsLockLabel = Key("showCapsLockLabel", default: true) // MARK: ImageService @@ -793,6 +825,14 @@ extension Defaults.Keys { normalizeMusicAuxControls() } + static func migrateCapsLockTintMode() { + guard Defaults[.didMigrateCapsLockTintMode] == false else { return } + + let legacyGreen = Defaults[.capsLockIndicatorUseGreenColor] + Defaults[.capsLockIndicatorTintMode] = legacyGreen ? .green : .white + Defaults[.didMigrateCapsLockTintMode] = true + } + static func migrateMusicControlSlots() { guard Defaults[.didMigrateMusicControlSlots] == false else { return }