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

81 lines
3.7 KiB
Swift

import SwiftUI
import AppKit
import CurtainShared
/// Purpose: Appearance tab cover style, color, message, clock, and reveal trigger.
/// Extracted from PreferencesView to keep every tab file under 500 lines.
/// Inputs: @AppStorage bindings for cover and reveal prefs.
/// Outputs: writes to UserDefaults; no side-effectful closures needed.
/// Constraints: @MainActor (SwiftUI). Color is persisted as "#rrggbb" hex (shared with
/// the headless cover renderer) so a small Color<->hex bridge is included here.
/// SPORT: MASTER-PREFS
struct PrefAppearanceTab: View {
// FIX-5: default literal aligned to registerDefaults (was "solidColor", now "logo")
@AppStorage(Settings.Key.coverStyle) private var coverStyle = "logo"
@AppStorage(Settings.Key.coverColor) private var coverColorHex = "#000000"
@AppStorage(Settings.Key.coverMessage) private var coverMessage = ""
@AppStorage(Settings.Key.coverShowClock) private var coverShowClock = false
@AppStorage(Settings.Key.revealTrigger) private var revealTrigger = "anyKey"
@AppStorage(Settings.Key.revealKeyCombo) private var revealKeyCombo = ""
var body: some View {
Form {
Section("Cover") {
Picker("Cover style", selection: $coverStyle) {
Text("Solid color").tag("solidColor")
Text("Message").tag("message")
Text("Blur").tag("blur")
Text("Lock logo").tag("logo")
Text("Curtain logo").tag("curtainLogo")
Text("Aerial video").tag("aerial")
}
if coverStyle == "aerial" {
Text("Plays a system aerial video on the covered screens (muted, looping). A keypress still brings up the password. Falls back to the logo if no aerial video is installed. Uses more power than a static cover.")
.font(.caption).foregroundStyle(.secondary)
}
ColorPicker("Cover color", selection: colorBinding, supportsOpacity: false)
if coverStyle == "message" {
TextField("Cover message", text: $coverMessage)
}
Toggle("Show a clock on the cover", isOn: $coverShowClock)
}
Section {
Picker("Reveal trigger", selection: $revealTrigger) {
Text("Any key").tag("anyKey")
Text("Key combo").tag("keyCombo")
}
if revealTrigger == "keyCombo" {
TextField("Reveal key combo", text: $revealKeyCombo)
}
} header: {
Text("Reveal")
} footer: {
if revealTrigger == "keyCombo" {
Text("Format: modifiers plus a key, joined by \"+\". For example \"cmd+shift+l\".")
.font(.caption).foregroundStyle(.secondary)
}
}
}
.formStyle(.grouped)
}
// MARK: - Color <-> hex bridge
/// Bridge the stored "#rrggbb" hex string to a SwiftUI Color for the ColorPicker.
private var colorBinding: Binding<Color> {
Binding<Color>(
get: { Self.color(fromHex: coverColorHex) },
set: { coverColorHex = Self.hex(from: $0) }
)
}
private static func color(fromHex hex: String) -> Color {
guard let rgb = HexColor.toRGB(hex) else { return .black }
return Color(red: rgb.r, green: rgb.g, blue: rgb.b)
}
private static func hex(from color: Color) -> String {
let ns = NSColor(color).usingColorSpace(.sRGB) ?? .black
return HexColor.fromRGB(Double(ns.redComponent), Double(ns.greenComponent), Double(ns.blueComponent))
}
}