curtain/Sources/CurtainShared/RevealCombo.swift
Aric Camarata 8c19e960d2 Detection root-cause fix + audit batch: netstat path, UDP activator, settings coherence, refactor, docs
Detection: netstat lives at /usr/sbin/netstat, not /usr/bin — the hardcoded wrong
path silently killed the ESTABLISHED-TCP activator (root cause of the failed live
test). Fixed and live-verified. Added peered-UDP activator (5900-5902) for
High-Performance sessions, per-signal transition logging, unconditional error
logging for dead probe helpers, and probe v2 with full CGSession dictionary
diffing. 7 new parser tests (32 total).

Fixes from a full audit + adversarial review: idle source setting honored
(default now Remote session activity), cover scope reduced to a coherent
two-mode model with legacy migration (per-display toggle was inverted in
onlyMarked and dead in all), curtain test no longer schedules a teardown over a
live session, specific-display password box placement gets a real picker,
refuse-to-arm enforced, activation notification posts a real banner, menu
password gate bypassed when the event tap is dead, shared single-decoder aerial
player with stale-task guard and async playability check, password buffer zeroed
on successful unlock and Esc, XPC interruption/invalidation handlers, modern
Accessibility settings URL, launchPath modernized, codesign failures now abort
release.sh, monotonic CFBundleVersion, install.sh temp cleanup, dead
armDisarmHotkey setting removed.

Refactor: Curtain.swift and PreferencesWindow.swift split into focused files
(largest now 479 lines). Wiki, README, and contributing docs updated to match.
Build clean at 0 warnings, 32/32 tests pass.
2026-06-09 20:36:30 -04:00

60 lines
2.9 KiB
Swift

import Foundation
/// Purpose: parse and match a reveal hotkey like "cmd+shift+l" against an incoming
/// physical key-down. Forgiving by design: caps-lock and fn are ignored and
/// only the four real modifiers (cmd/ctrl/option/shift) are compared, so a
/// stray device-dependent bit never blocks a legitimate match.
///
/// This is pure Foundation: modifier flags are matched against documented raw
/// CGEventFlags masks so CurtainShared needs no AppKit/CoreGraphics import.
public enum RevealCombo {
// Documented CGEventFlags raw values (AppKit-free).
private static let maskCommand: UInt64 = 0x100000
private static let maskShift: UInt64 = 0x20000
private static let maskControl: UInt64 = 0x40000
private static let maskAlternate: UInt64 = 0x80000
// Compare only the meaningful modifier bits; drop caps-lock, fn, and device bits.
private static let meaningfulMask: UInt64 =
maskCommand | maskControl | maskAlternate | maskShift
/// Non-character keys we accept by name (keycode), since they carry no usable char.
private static let namedKeycodes: [String: Int] = [
"space": 49, "return": 36, "enter": 76, "tab": 48, "escape": 53, "esc": 53,
"delete": 51, "backspace": 51,
]
/// Returns true when the incoming key-down matches the configured combo.
/// - Parameters:
/// - combo: a string like "cmd+shift+l" (separators: `+`, space, or `-`).
/// - keycode: the physical key code of the event.
/// - chars: the typed character(s), if any.
/// - flagsRawValue: the event's modifier flags as a CGEventFlags raw value.
public static func matches(combo: String, keycode: Int, chars: String?, flagsRawValue: UInt64) -> Bool {
let parts = combo.lowercased()
.split(whereSeparator: { $0 == "+" || $0 == " " || $0 == "-" })
.map(String.init)
.filter { !$0.isEmpty }
guard !parts.isEmpty else { return false }
var required: UInt64 = 0
var finalKey: String?
for p in parts {
switch p {
case "cmd", "command", "": required |= maskCommand
case "ctrl", "control", "": required |= maskControl
case "opt", "option", "alt", "": required |= maskAlternate
case "shift", "": required |= maskShift
default: finalKey = p
}
}
guard let key = finalKey else { return false }
// Modifiers must match exactly within the meaningful set.
guard (flagsRawValue & meaningfulMask) == (required & meaningfulMask) else { return false }
// Match a non-character key by keycode, otherwise by the typed character.
if let kc = namedKeycodes[key] { return kc == keycode }
return chars?.lowercased() == key
}
}