@@ -13,11 +13,20 @@ import AppKit
1313
1414 private static let immediateRestart = false
1515
16+ /// Feature flag to enable/disable session handling for Fast User Switching.
17+ private static let enableSessionHandling = decideOnEnableSessionHandling ( )
18+
1619 func start( ) {
1720 log. info ( " Starting listeners... " )
1821
1922 TouchHandler . shared. registerTouchCallback ( )
2023 observeWakeNotification ( )
24+ if Self . enableSessionHandling {
25+ log. info ( " Session handling enabled - will monitor Fast User Switching " )
26+ setupSessionHandling ( )
27+ } else {
28+ log. info ( " Session handling disabled - macOS 15 handles session switching correctly " )
29+ }
2130 multitouchManager. setupMultitouchListener ( )
2231 setupDisplayReconfigurationCallback ( )
2332
@@ -35,6 +44,10 @@ import AppKit
3544
3645 /// Schedule listeners to be restarted. If a restart is pending, discard its delay and use the most recently requested delay.
3746 func scheduleRestart( _ delay: TimeInterval , reason: String ) {
47+ if Self . enableSessionHandling && !isUserSessionActive {
48+ restartLog. info ( " \( reason) , but user session is inactive - skipping restart " )
49+ return
50+ }
3851 restartLog. info ( " \( reason) , restarting in \( delay) " )
3952 restartTimer? . invalidate ( )
4053 restartTimer = Timer . scheduledTimer (
@@ -49,8 +62,13 @@ import AppKit
4962 func restartListeners( ) {
5063 log. info ( " Restarting now... " )
5164 stopUnstableListeners ( )
52- startUnstableListeners ( )
53- log. info ( " Restart success. " )
65+ if !Self. enableSessionHandling || isUserSessionActive {
66+ startUnstableListeners ( )
67+ log. info ( " Restart success. " )
68+ } else {
69+ // This logic should never be reached — just a safeguard.
70+ log. info ( " Restart completed - listeners remain stopped due to inactive session " )
71+ }
5472 }
5573
5674 private func startUnstableListeners( ) {
@@ -103,3 +121,81 @@ fileprivate extension CGDisplayChangeSummaryFlags {
103121 flags. contains ( where: contains)
104122 }
105123}
124+
125+ // MARK: - Session Handling for Fast User Switching
126+
127+ fileprivate extension Controller {
128+ /// Session state tracking variables (using static storage for simplicity)
129+ private static var _userSessionActive = true
130+ private static var _lastSessionChangeTime : Date = . distantPast
131+
132+ /// Public accessor for session state (used by scheduleRestart and restartListeners)
133+ var isUserSessionActive : Bool { Self . _userSessionActive }
134+
135+ /// Enable for macOS versions that have the multitouch session switching bug.
136+ /// Disable for macOS 15.0+ (Sequoia) where Apple fixed the issue.
137+ ///
138+ /// This is actually not confirmed, but I can't reproduce issue #127 on macOS 15.7.
139+ private static func decideOnEnableSessionHandling( ) -> Bool {
140+ let osVersion = ProcessInfo . processInfo. operatingSystemVersion
141+ if osVersion. majorVersion < 15 {
142+ return true
143+ }
144+ return false
145+ }
146+
147+ /// Initialize session handling - call this from start() when feature is enabled
148+ func setupSessionHandling( ) {
149+ Self . _userSessionActive = true
150+ observeSessionNotifications ( )
151+ }
152+
153+ private func observeSessionNotifications( ) {
154+ NSWorkspace . shared. notificationCenter. addObserver (
155+ self ,
156+ selector: #selector( receiveSessionResignActiveNote) ,
157+ name: NSWorkspace . sessionDidResignActiveNotification,
158+ object: nil
159+ )
160+ NSWorkspace . shared. notificationCenter. addObserver (
161+ self ,
162+ selector: #selector( receiveSessionBecomeActiveNote) ,
163+ name: NSWorkspace . sessionDidBecomeActiveNotification,
164+ object: nil
165+ )
166+ }
167+
168+ @objc private func receiveSessionResignActiveNote( _ note: Notification ) {
169+ let now = Date ( )
170+ guard now. timeIntervalSince ( Self . _lastSessionChangeTime) > 0.5 else {
171+ log. info ( " Ignoring session resign - too soon after last change " )
172+ return
173+ }
174+ Self . _lastSessionChangeTime = now
175+
176+ log. info ( " User session resigned active, stopping listeners " )
177+ Self . _userSessionActive = false
178+ restartTimer? . invalidate ( )
179+ restartTimer = nil
180+
181+ DispatchQueue . main. async {
182+ self . stopUnstableListeners ( )
183+ }
184+ }
185+
186+ @objc private func receiveSessionBecomeActiveNote( _ note: Notification ) {
187+ let now = Date ( )
188+ guard now. timeIntervalSince ( Self . _lastSessionChangeTime) > 0.5 else {
189+ log. info ( " Ignoring session become active - too soon after last change " )
190+ return
191+ }
192+ Self . _lastSessionChangeTime = now
193+
194+ log. info ( " User session became active, starting listeners " )
195+ Self . _userSessionActive = true
196+
197+ DispatchQueue . main. async {
198+ self . startUnstableListeners ( )
199+ }
200+ }
201+ }
0 commit comments