mirror of
https://github.com/acamarata/curtain.git
synced 2026-06-30 18:54: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.
79 lines
3.1 KiB
Swift
79 lines
3.1 KiB
Swift
import Cocoa
|
|
|
|
/// Purpose: Optional menu-bar presence (the curtains glyph) with quick actions.
|
|
/// Reflects active + armed state and routes actions to the coordinator.
|
|
/// SPORT: MASTER-MENUBAR
|
|
@MainActor
|
|
final class MenuBarController: NSObject {
|
|
private var statusItem: NSStatusItem?
|
|
private var armedItem: NSMenuItem?
|
|
private weak var coordinator: SessionCoordinator?
|
|
var onOpenSettings: (() -> Void)?
|
|
var onOpenSetup: (() -> Void)?
|
|
var onQuit: (() -> Void)?
|
|
|
|
init(coordinator: SessionCoordinator) { self.coordinator = coordinator; super.init() }
|
|
|
|
func show() {
|
|
guard statusItem == nil else { return }
|
|
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
item.button?.image = CurtainIcon.menuBarImage()
|
|
let menu = NSMenu()
|
|
add(menu, "Open Curtain Settings…", #selector(openSettings))
|
|
add(menu, "Setup…", #selector(openSetup))
|
|
menu.addItem(.separator())
|
|
armedItem = add(menu, "Armed", #selector(toggleArmed))
|
|
add(menu, "Activate Now", #selector(activate))
|
|
add(menu, "Deactivate", #selector(deactivate))
|
|
add(menu, "Test Curtain (10s)", #selector(test))
|
|
menu.addItem(.separator())
|
|
add(menu, "Quit Curtain", #selector(quit), key: "q")
|
|
item.menu = menu
|
|
statusItem = item
|
|
reflect(active: coordinator?.isActive ?? false)
|
|
reflect(armed: coordinator?.isArmed ?? false)
|
|
}
|
|
|
|
func hide() {
|
|
if let i = statusItem { NSStatusBar.system.removeStatusItem(i) }
|
|
statusItem = nil
|
|
armedItem = nil
|
|
}
|
|
|
|
/// Update the icon to reflect active/idle. Active = highlighted (non-template).
|
|
func reflect(active: Bool) {
|
|
guard let button = statusItem?.button else { return }
|
|
let img = CurtainIcon.menuBarImage()
|
|
img.isTemplate = !active // active = tinted/filled, idle = template (adapts)
|
|
button.image = img
|
|
button.contentTintColor = active ? NSColor.systemRed : nil
|
|
}
|
|
|
|
/// Update the Armed menu item state and the icon tooltip.
|
|
func reflect(armed: Bool) {
|
|
armedItem?.state = armed ? .on : .off
|
|
statusItem?.button?.toolTip = armed ? "Armed" : "Disarmed"
|
|
}
|
|
|
|
// MARK: - Actions
|
|
@discardableResult
|
|
private func add(_ menu: NSMenu, _ title: String, _ sel: Selector, key: String = "") -> NSMenuItem {
|
|
let item = NSMenuItem(title: title, action: sel, keyEquivalent: key)
|
|
item.target = self; menu.addItem(item); return item
|
|
}
|
|
|
|
@objc private func openSettings() { onOpenSettings?() }
|
|
@objc private func openSetup() { onOpenSetup?() }
|
|
@objc private func toggleArmed() {
|
|
guard let coordinator else { return }
|
|
coordinator.setArmed(!coordinator.isArmed)
|
|
reflect(armed: coordinator.isArmed)
|
|
}
|
|
@objc private func activate() { coordinator?.activateNow() }
|
|
@objc private func deactivate() {
|
|
// If gated, the coordinator already presented the password box; nothing else to do.
|
|
_ = coordinator?.requestDeactivateFromMenu()
|
|
}
|
|
@objc private func test() { coordinator?.testCurtain(seconds: 10) }
|
|
@objc private func quit() { onQuit?() }
|
|
}
|