curtain/Sources/Curtain/PrefIdleEndTab.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

79 lines
4 KiB
Swift

import SwiftUI
/// Purpose: Idle & End tab idle-timeout block and on-disconnect block.
/// Extracted from PreferencesView to keep every tab file under 500 lines.
/// Inputs: @AppStorage bindings for idle and end prefs.
/// Outputs: writes to UserDefaults; no side-effectful closures needed.
/// Constraints: @MainActor (SwiftUI). Idle source labels updated to be self-explanatory
/// about what "session input" vs "HID" means for the remote operator.
/// SPORT: MASTER-PREFS
struct PrefIdleEndTab: View {
@AppStorage(Settings.Key.idleEnabled) private var idleEnabled = true
@AppStorage(Settings.Key.idleMinutes) private var idleMinutes = 30
// FIX-5: default literal aligned to registerDefaults (was "hidIdle", now "sessionInput")
@AppStorage(Settings.Key.idleSource) private var idleSource = "sessionInput"
@AppStorage(Settings.Key.onIdleDisconnect) private var idleDisconnect = true
@AppStorage(Settings.Key.onIdleLock) private var idleLock = true
@AppStorage(Settings.Key.onIdleScreenOff) private var idleScreenOff = true
@AppStorage(Settings.Key.onIdleDeactivate) private var idleDeactivate = true
@AppStorage(Settings.Key.onEndLock) private var endLock = true
@AppStorage(Settings.Key.onEndScreenOff) private var endScreenOff = true
@AppStorage(Settings.Key.onEndDeactivate) private var endDeactivate = true
var body: some View {
Form {
Section {
Toggle("Act after the session is idle", isOn: $idleEnabled)
if idleEnabled {
Stepper("After \(idleMinutes) minutes of inactivity:", value: $idleMinutes, in: 1...240)
Picker("Inactivity is measured by", selection: $idleSource) {
// Tag values are unchanged; only the human-readable labels are updated
// to make it obvious that "session input" tracks the remote operator.
Text("Remote session activity (recommended)").tag("sessionInput")
Text("This Mac's physical input only").tag("hidIdle")
}
Toggle("Disconnect the remote session", isOn: $idleDisconnect)
Toggle("Lock the Mac", isOn: $idleLock)
Toggle("Turn off the displays", isOn: $idleScreenOff)
Toggle("Deactivate the curtain", isOn: $idleDeactivate)
}
} header: {
Text("On idle")
} footer: {
if idleEnabled {
VStack(alignment: .leading, spacing: 4) {
if idleMinutes <= 2 && idleDisconnect {
warn("A very short idle timeout with disconnect-on-idle can cut a session during a brief pause.")
}
if !idleDeactivate && idleScreenOff {
warn("Screens go dark on idle but the curtain stays up. The desk shows nothing until you deactivate.")
}
}
}
}
Section {
Toggle("Lock the Mac", isOn: $endLock)
Toggle("Turn off the displays", isOn: $endScreenOff)
Toggle("Deactivate the curtain", isOn: $endDeactivate)
} header: {
Text("When the remote session disconnects")
} footer: {
if !endDeactivate && !endLock {
warn("On disconnect the Mac is neither locked nor uncovered. It is left covered but unlocked (\"dead but unlocked\").")
}
}
}
.formStyle(.grouped)
}
/// Small inline warning row: orange triangle + caption. Used in section footers
/// to flag dangerous setting combinations without alarming the layout.
private func warn(_ text: String) -> some View {
HStack(alignment: .top, spacing: 6) {
Image(systemName: "exclamationmark.triangle")
Text(text)
}
.font(.caption)
.foregroundStyle(.orange)
}
}