Skip to content

Commit 5f4af9e

Browse files
committed
Fix: Multiuser misbehavior
- supersedes #87 - closes #75 - closes #127 Thanks @alex-f-k for the idea and initial code!
1 parent 285ea48 commit 5f4af9e

File tree

1 file changed

+98
-2
lines changed

1 file changed

+98
-2
lines changed

MiddleClick/Controller.swift

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)