Skip to content

Commit 2fa47dc

Browse files
fix: use IOPMAssertion to actually prevent display sleep
CGWarpMouseCursorPosition does not reset the macOS idle timer. Add IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep) held while jiggle is enabled, released when disabled or on quit. Cursor movement kept for visual feedback. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 8e9cda0 commit 2fa47dc

1 file changed

Lines changed: 65 additions & 10 deletions

File tree

Sources/ScreenCursor/main.swift

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Cocoa
22
import Carbon.HIToolbox
33
import ServiceManagement
4+
import IOKit.pwr_mgt
45

56
extension Notification.Name {
67
static let screenCursorSettingsChanged = Notification.Name("ScreenCursor.settingsChanged")
@@ -206,6 +207,7 @@ final class JiggleController {
206207
private var pollTimer: Timer?
207208
private var lastMousePosition: NSPoint = .zero
208209
private var lastActivityAt: Date = Date()
210+
private var assertionID: IOPMAssertionID = IOPMAssertionID(0)
209211

210212
init() {
211213
NotificationCenter.default.addObserver(
@@ -225,6 +227,7 @@ final class JiggleController {
225227
func stop() {
226228
pollTimer?.invalidate()
227229
pollTimer = nil
230+
releaseAssertion()
228231
}
229232

230233
@objc private func settingsChanged() {
@@ -234,13 +237,33 @@ final class JiggleController {
234237
private func applySettings() {
235238
pollTimer?.invalidate()
236239
pollTimer = nil
237-
guard SettingsStore.shared.jiggleEnabled else { return }
240+
guard SettingsStore.shared.jiggleEnabled else {
241+
releaseAssertion()
242+
return
243+
}
244+
createAssertion()
238245
pollTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
239246
self?.checkAndJiggle()
240247
}
241248
RunLoop.main.add(pollTimer!, forMode: .common)
242249
}
243250

251+
private func createAssertion() {
252+
releaseAssertion()
253+
IOPMAssertionCreateWithName(
254+
kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
255+
IOPMAssertionLevel(kIOPMAssertionLevelOn),
256+
"Screen Cursor Jiggle" as CFString,
257+
&assertionID
258+
)
259+
}
260+
261+
private func releaseAssertion() {
262+
guard assertionID != IOPMAssertionID(0) else { return }
263+
IOPMAssertionRelease(assertionID)
264+
assertionID = IOPMAssertionID(0)
265+
}
266+
244267
private func checkAndJiggle() {
245268
let current = NSEvent.mouseLocation
246269
if current != lastMousePosition {
@@ -256,15 +279,47 @@ final class JiggleController {
256279

257280
private func performJiggle() {
258281
guard let origin = CGEvent(source: nil)?.location else { return }
259-
let steps: [(CGPoint, TimeInterval)] = [
260-
(CGPoint(x: origin.x, y: origin.y - 1), 0.0),
261-
(CGPoint(x: origin.x, y: origin.y + 1), 0.1),
262-
(CGPoint(x: origin.x + 1, y: origin.y), 0.2),
263-
(origin, 0.3),
264-
]
265-
for (point, delay) in steps {
266-
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
267-
CGWarpMouseCursorPosition(point)
282+
283+
var displayID = CGMainDisplayID()
284+
var displayCount: UInt32 = 0
285+
CGGetDisplaysWithPoint(origin, 1, &displayID, &displayCount)
286+
let screen = CGDisplayBounds(displayID)
287+
288+
let delta: CGFloat = 150
289+
let right = screen.maxX - origin.x
290+
let left = origin.x - screen.minX
291+
let down = screen.maxY - origin.y
292+
let up = origin.y - screen.minY
293+
294+
let dx: CGFloat
295+
let dy: CGFloat
296+
if right >= left && right >= down && right >= up {
297+
dx = min(delta, right - 5); dy = 0
298+
} else if left >= down && left >= up {
299+
dx = -min(delta, left - 5); dy = 0
300+
} else if down >= up {
301+
dx = 0; dy = min(delta, down - 5)
302+
} else {
303+
dx = 0; dy = -min(delta, up - 5)
304+
}
305+
306+
let steps = 20
307+
let stepDelay = 0.025
308+
let returnOffset = stepDelay * Double(steps) + 0.1
309+
310+
for i in 1...steps {
311+
let t = CGFloat(i) / CGFloat(steps)
312+
let pt = CGPoint(x: origin.x + dx * t, y: origin.y + dy * t)
313+
DispatchQueue.main.asyncAfter(deadline: .now() + stepDelay * Double(i)) {
314+
CGWarpMouseCursorPosition(pt)
315+
CGAssociateMouseAndMouseCursorPosition(1)
316+
}
317+
}
318+
for i in 1...steps {
319+
let t = CGFloat(i) / CGFloat(steps)
320+
let pt = CGPoint(x: origin.x + dx * (1.0 - t), y: origin.y + dy * (1.0 - t))
321+
DispatchQueue.main.asyncAfter(deadline: .now() + returnOffset + stepDelay * Double(i)) {
322+
CGWarpMouseCursorPosition(pt)
268323
CGAssociateMouseAndMouseCursorPosition(1)
269324
}
270325
}

0 commit comments

Comments
 (0)