mirror of
https://github.com/acamarata/curtain.git
synced 2026-07-01 11:14:25 +00:00
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.
81 lines
3.7 KiB
Swift
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))
|
|
}
|
|
}
|