forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCursorScreenTracker.swift
More file actions
56 lines (50 loc) · 2.2 KB
/
Copy pathCursorScreenTracker.swift
File metadata and controls
56 lines (50 loc) · 2.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import AppKit
import Foundation
/// The floating bar's ~250 ms cursor poll, armed only while there is a second screen to move to.
///
/// The poll runs on the **main** queue, and it used to be unconditional: on the single-screen Mac
/// most of them are, that was four run-loop wake-ups a second for the life of the process, each one
/// re-reading `NSScreen.screens.count` and returning. Screen parameters changing is the only thing
/// that can change that answer, so the bar re-syncs from that notification instead of asking four
/// times a second.
///
/// Arming is a policy, not a side effect of construction, so `isTracking` states it and a test can
/// drive it through `screenCount` without attaching a display.
@MainActor
final class CursorScreenTracker {
private let screenCount: () -> Int
private var timer: DispatchSourceTimer?
private var onTick: (@MainActor () -> Void)?
init(screenCount: @escaping () -> Int = { NSScreen.screens.count }) {
self.screenCount = screenCount
}
/// Whether the poll is running right now.
var isTracking: Bool { timer != nil }
/// How many timer sources this tracker has ever created.
///
/// The thing that can actually go wrong here is `sync()` stacking a second timer on an already
/// armed tracker, which doubles the wake-up rate. That is a fact about arming, so a test can read
/// it directly instead of counting ticks inside a sleep and inferring the duplicate from a rate.
private(set) var timersArmed = 0
/// Adopts `onTick` as the poll body and arms for the current layout.
func start(onTick: @escaping @MainActor () -> Void) {
self.onTick = onTick
sync()
}
/// Re-decides whether the poll should be running. Idempotent: re-arming an already-armed tracker
/// keeps the existing timer rather than stacking a second one.
func sync() {
guard screenCount() > 1 else {
timer?.cancel()
timer = nil
return
}
guard timer == nil, let onTick else { return }
let source = DispatchSource.makeTimerSource(queue: .main)
source.schedule(deadline: .now(), repeating: .milliseconds(250))
source.setEventHandler { MainActor.assumeIsolated { onTick() } }
source.resume()
timer = source
timersArmed += 1
}
}