66//
77
88import Defaults
9+ import os
910import Scribe
1011import SwiftUI
1112
@@ -25,7 +26,22 @@ final class LoopManager {
2526
2627 private var accessibilityCheckerTask : Task < ( ) , Never > ?
2728
28- private( set) var isLoopActive : Bool = false
29+ private( set) var isLoopActive : Bool = false {
30+ didSet {
31+ let value = isLoopActive
32+ isLoopActiveMirror. withLock { $0 = value }
33+ }
34+ }
35+
36+ private let isLoopActiveMirror = OSAllocatedUnfairLock < Bool > ( initialState: false )
37+ nonisolated var isLoopActiveAtomic : Bool {
38+ isLoopActiveMirror. withLock { $0 }
39+ }
40+
41+ private let hasParentCycleActionMirror = OSAllocatedUnfairLock < Bool > ( initialState: false )
42+ nonisolated var hasParentCycleActionAtomic : Bool {
43+ hasParentCycleActionMirror. withLock { $0 }
44+ }
2945
3046 private lazy var triggerKeyTimeoutTimer = TriggerKeyTimeoutTimer (
3147 closeCallback: { [ weak self] forceClose in
@@ -46,7 +62,7 @@ final class LoopManager {
4662 }
4763 } ,
4864 checkIfLoopOpen: { [ weak self] in
49- self ? . isLoopActive ?? false
65+ self ? . isLoopActiveAtomic ?? false
5066 }
5167 )
5268
@@ -61,7 +77,7 @@ final class LoopManager {
6177 await self ? . closeLoop ( forceClose: forceClose)
6278 }
6379 } ,
64- checkIfLoopOpen: { [ weak self] in self ? . isLoopActive ?? false }
80+ checkIfLoopOpen: { [ weak self] in self ? . isLoopActiveAtomic ?? false }
6581 )
6682
6783 private( set) lazy var mouseInteractionObserver = MouseInteractionObserver (
@@ -81,9 +97,9 @@ final class LoopManager {
8197 }
8298 } ,
8399 canSelectNextCycleitem: { [ weak self] in
84- self ? . resizeContext . parentAction != nil
100+ self ? . hasParentCycleActionAtomic ?? false
85101 } ,
86- checkIfLoopOpen: { [ weak self] in self ? . isLoopActive ?? false }
102+ checkIfLoopOpen: { [ weak self] in self ? . isLoopActiveAtomic ?? false }
87103 )
88104
89105 func start( ) {
@@ -103,6 +119,19 @@ final class LoopManager {
103119 }
104120 }
105121 }
122+
123+ func shutdown( ) {
124+ accessibilityCheckerTask? . cancel ( )
125+ accessibilityCheckerTask = nil
126+
127+ keybindTrigger. stop ( )
128+ middleClickTrigger. stop ( )
129+ mouseInteractionObserver. stop ( )
130+ triggerKeyTimeoutTimer. cancel ( )
131+
132+ isLoopActive = false
133+ hasParentCycleActionMirror. withLock { $0 = false }
134+ }
106135}
107136
108137// MARK: - Opening/Closing Loop
@@ -134,6 +163,9 @@ extension LoopManager {
134163 return
135164 }
136165
166+ isLoopActive = true
167+ hasParentCycleActionMirror. withLock { $0 = false }
168+
137169 log. info ( " Opening Loop with starting action: \( startingAction. description) and target window: \( window? . description ?? " (none) " ) " )
138170
139171 // Refresh accent colors in case user has enabled the wallpaper processor
@@ -163,7 +195,6 @@ extension LoopManager {
163195
164196 indicatorService. openAndUpdate ( context: resizeContext)
165197
166- isLoopActive = true
167198 await changeAction ( startingAction, disableHapticFeedback: true )
168199
169200 triggerKeyTimeoutTimer. start ( )
@@ -175,6 +206,7 @@ extension LoopManager {
175206
176207 indicatorService. closeAll ( )
177208 isLoopActive = false
209+ hasParentCycleActionMirror. withLock { $0 = false }
178210
179211 triggerKeyTimeoutTimer. cancel ( )
180212 mouseInteractionObserver. stop ( )
@@ -312,7 +344,7 @@ extension LoopManager {
312344 if let lastAction = await WindowRecords . shared. getCurrentAction ( for: targetWindow) ,
313345 lastAction. getName ( ) != screenSwitchingCustomActionName,
314346 !lastAction. forceProportionalFrameOnScreenChange {
315- resizeContext . setAction ( to: lastAction, parent: nil )
347+ setResizeAction ( to: lastAction, parent: nil )
316348 } else {
317349 let currentFrame = targetWindow. frame
318350
@@ -327,7 +359,7 @@ extension LoopManager {
327359 height: currentFrame. height / adjustedBounds. height
328360 )
329361
330- resizeContext . setAction (
362+ setResizeAction (
331363 to: . init(
332364 . custom,
333365 keybind: [ ] ,
@@ -344,15 +376,15 @@ extension LoopManager {
344376 )
345377 }
346378 } else {
347- resizeContext . setAction ( to: . init( . center) , parent: nil )
379+ setResizeAction ( to: . init( . center) , parent: nil )
348380 }
349381 }
350382
351383 resizeContext. setScreen ( to: newScreen)
352384 indicatorService. openAndUpdate ( context: resizeContext)
353385
354386 if let parent = newParentAction {
355- resizeContext . setAction ( to: newAction, parent: newParentAction)
387+ setResizeAction ( to: newAction, parent: newParentAction)
356388 await changeAction ( parent, triggeredFromScreenChange: true )
357389 } else {
358390 if !Defaults[ . previewVisibility] {
@@ -377,7 +409,7 @@ extension LoopManager {
377409
378410 if newAction != resizeContext. action || newAction. canRepeat {
379411 let previousActionWasNoOp = resizeContext. action. direction. isNoOp
380- resizeContext . setAction ( to: newAction, parent: newParentAction)
412+ setResizeAction ( to: newAction, parent: newParentAction)
381413 if !Defaults[ . previewVisibility] , !previousActionWasNoOp {
382414 await resizeContext. refreshResolvedState ( )
383415 }
@@ -459,6 +491,11 @@ extension LoopManager {
459491 }
460492 }
461493
494+ private func setResizeAction( to newAction: WindowAction , parent newParentAction: WindowAction ? ) {
495+ resizeContext. setAction ( to: newAction, parent: newParentAction)
496+ hasParentCycleActionMirror. withLock { $0 = newParentAction != nil }
497+ }
498+
462499 /// Resolves the target screen for `screenToResizeOn`.
463500 ///
464501 /// By default, this uses the user's `useScreenWithCursor` setting.
0 commit comments