curtain/Sources/Curtain/System.swift
acamarata 709669e0cb Curtain v1.0.0 — privacy curtain for macOS Screen Sharing
Menu-bar agent that, on a Screen Sharing connection, covers the host
displays and blocks physical keyboard/mouse from the apps while remote
input passes through, then locks the Mac on idle or disconnect.

- netstat-based session detection (debounced)
- CGEventTap input filter (block physical sourceStateID==1, pass remote)
- .none/.readOnly cover windows with on-curtain password box
- SACLockScreenImmediate lock + IOKit display-sleep assertion
- root helper (NOPASSWD) to disconnect the Screen Sharing session
- install/uninstall scripts, app bundle, login agent, CI
2026-06-01 14:10:50 -04:00

86 lines
3.4 KiB
Swift

import Cocoa
import IOKit.pwr_mgt
/// Purpose: Thin wrappers over the macOS system actions Curtain needs.
/// Constraints: every call here was validated against macOS 26 (Sequoia-era).
/// SPORT: MASTER-SYSTEM
enum System {
// MARK: - Reliable screen lock
//
// CGSession -suspend was removed in recent macOS. osascript Ctrl+Cmd+Q needs
// Accessibility and is unreliable from a launchd agent. SACLockScreenImmediate
// (private login.framework symbol) locks immediately with no extra permission.
static func lockScreen() {
let paths = [
"/System/Library/PrivateFrameworks/login.framework/Versions/Current/login",
"/System/Library/PrivateFrameworks/login.framework/login"
]
typealias LockFn = @convention(c) () -> Int32
for p in paths {
if let h = dlopen(p, RTLD_LAZY), let sym = dlsym(h, "SACLockScreenImmediate") {
_ = unsafeBitCast(sym, to: LockFn.self)()
return
}
}
// Fallback (needs Accessibility): the lock-screen shortcut.
let t = Process()
t.launchPath = "/usr/bin/osascript"
t.arguments = ["-e", "tell application \"System Events\" to keystroke \"q\" using {command down, control down}"]
try? t.run()
}
/// Put all displays to sleep (after a lock = a dark, locked Mac).
static func sleepDisplays() {
let t = Process(); t.launchPath = "/usr/bin/pmset"; t.arguments = ["displaysleepnow"]
try? t.run()
}
// MARK: - Prevent display sleep during a session
private static var assertionID: IOPMAssertionID = 0
private static var assertionActive = false
static func preventDisplaySleep() {
guard !assertionActive else { return }
let ok = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoDisplaySleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
"Curtain active" as CFString,
&assertionID)
assertionActive = (ok == kIOReturnSuccess)
}
static func allowDisplaySleep() {
if assertionActive { IOPMAssertionRelease(assertionID); assertionActive = false }
}
// MARK: - End the active Screen Sharing session
//
// Killing the connection processes needs root, so install.sh drops a tiny
// helper at /usr/local/bin/curtain-endsession with a NOPASSWD sudoers rule.
// launchd respawns the listener, so Screen Sharing stays available afterward.
static func endScreenShareSession() {
let helper = "/usr/local/bin/curtain-endsession"
guard FileManager.default.isExecutableFile(atPath: helper) else { return }
let t = Process(); t.launchPath = "/usr/bin/sudo"; t.arguments = ["-n", helper]
try? t.run(); t.waitUntilExit()
}
// MARK: - Displays
static func serial(of screen: NSScreen) -> UInt32 {
let id = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID
return CGDisplaySerialNumber(id)
}
/// A native display can be hidden invisibly (sharingType .none). A DisplayLink
/// display only exists via screen capture, so .none hides it from the capture
/// too it must use .readOnly (visible in the remote view). We identify them
/// by serial because EDID passthrough makes vendor IDs identical.
static func isDisplayLink(_ screen: NSScreen) -> Bool {
Config.shared.displayLinkSerials.contains(serial(of: screen))
}
}