mirror of
https://github.com/acamarata/curtain.git
synced 2026-07-01 03:04:25 +00:00
Rework Curtain into a proper menu-bar app (Caffeine-style): - SwiftUI settings window: app, on-start, on-idle, on-end, security, displays - Modular ActionSet/ActionRunner — per-event toggles (disconnect/lock/screen-off/deactivate) - Configurable idle timeout; open-at-login via SMAppService; optional menu bar - Settings persisted in UserDefaults (shared with @AppStorage); salted-SHA256 password - Drawn curtains logo (menu-bar template + offscreen-rendered app icon) - Split into single-responsibility modules with comment blocks
76 lines
2.7 KiB
Swift
76 lines
2.7 KiB
Swift
import Cocoa
|
|
|
|
/// Purpose: The brain. Wires the session monitor, curtain, input filter, and the
|
|
/// configurable lifecycle actions together. Owns the connect / idle / end
|
|
/// flow described in the README. Holds no UI.
|
|
/// SPORT: MASTER-COORDINATOR
|
|
final class SessionCoordinator {
|
|
let curtain = CurtainController()
|
|
let input = InputFilter()
|
|
private let monitor = SessionMonitor()
|
|
private lazy var runner = ActionRunner(curtain: curtain, input: input)
|
|
private var tickTimer: Timer?
|
|
|
|
/// Called when the curtain's active state changes (for the menu-bar icon).
|
|
var onStateChange: ((Bool) -> Void)?
|
|
|
|
func start() {
|
|
input.onPhysicalKey = { [weak self] kc, chars in self?.curtain.physicalKey(kc, chars) }
|
|
curtain.onUnlock = { [weak self] in self?.handlePasswordUnlock() }
|
|
|
|
monitor.onConnect = { [weak self] in self?.sessionStarted() }
|
|
monitor.onIdleTimeout = { [weak self] in self?.sessionIdled() }
|
|
monitor.onDisconnect = { [weak self] in self?.sessionEnded() }
|
|
monitor.start()
|
|
|
|
tickTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
|
self?.curtain.tick()
|
|
}
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
private func sessionStarted() {
|
|
guard Settings.onStartActivate else { return }
|
|
runner.activateCover()
|
|
onStateChange?(true)
|
|
}
|
|
|
|
private func sessionIdled() {
|
|
runner.run(Settings.onIdle)
|
|
onStateChange?(curtain.isShown)
|
|
}
|
|
|
|
private func sessionEnded() {
|
|
runner.run(Settings.onEnd)
|
|
onStateChange?(curtain.isShown)
|
|
}
|
|
|
|
/// Host typed the correct password at the desk.
|
|
private func handlePasswordUnlock() {
|
|
runner.deactivateCover()
|
|
onStateChange?(false)
|
|
if Settings.onPasswordDisconnect {
|
|
let alert = NSAlert()
|
|
alert.messageText = "Unlocked at this Mac"
|
|
alert.informativeText = "Disconnect the active remote session?"
|
|
alert.addButton(withTitle: "Disconnect Remote")
|
|
alert.addButton(withTitle: "Keep Connected")
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
if alert.runModal() == .alertFirstButtonReturn { System.endScreenShareSession() }
|
|
}
|
|
}
|
|
|
|
// MARK: - Manual controls (menu bar / settings)
|
|
|
|
func activateNow() { runner.activateCover(); onStateChange?(true) }
|
|
func deactivateNow() { runner.deactivateCover(); onStateChange?(false) }
|
|
var isActive: Bool { curtain.isShown }
|
|
|
|
func testCurtain(seconds: TimeInterval = 10) {
|
|
runner.activateCover(); onStateChange?(true)
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in
|
|
self?.runner.deactivateCover(); self?.onStateChange?(false)
|
|
}
|
|
}
|
|
}
|