cleanup delegate

This commit is contained in:
2026-07-12 19:57:33 +02:00
parent 3c5222d800
commit 33c4cbe26e
6 changed files with 185 additions and 181 deletions
-180
View File
@@ -1,180 +0,0 @@
import AppKit
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
/// One window per attached screen the single source of truth. The matching
/// controllers are reached through `windows[i].barController`.
var windows: [BarWindow] = []
/// Convenience accessor over `windows`.
var controllers: [BarController] { windows.map(\.barController) }
/// Resolves a controller for any screen from the current config. Swapped by
/// `reloadAll()` so a live reload re-reads theme/blocks from disk.
private(set) var factory: PHBarFactory
private var observers: [IPCObserver] = []
private var screenObserver: NSObjectProtocol?
/// `config` is captured here (not re-read per screen) so all bars share one
/// resolved config + environment for their lifetime. Live reload swaps the
/// whole factory via `reloadAll()`.
init(config: PHConfig, debug: Bool) {
let environment = PHEnvironment.process.merging(config.env)
self.factory = PHBarFactory(config: config, environment: environment, debug: debug)
super.init()
}
func applicationDidFinishLaunching(_ notification: Notification) {
syncScreens()
// React to monitor connect/disconnect and resolution changes. macOS
// delivers a single app-level notification for any of these; `syncScreens`
// diffs so unchanged bars are left untouched.
screenObserver = NotificationCenter.default.addObserver(
forName: NSApplication.didChangeScreenParametersNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in self?.syncScreens() }
}
// Listen for refresh notifications from `phbar refresh`.
let token = IPC.observe(.refresh) { [weak self] _ in
Task { @MainActor in self?.refresh() }
}
observers.append(token)
}
func refresh() {
for controller in controllers {
controller.refresh()
}
}
func applicationWillTerminate(_ notification: Notification) {
for controller in controllers {
controller.stopAutoRefresh()
}
observers.removeAll()
if let screenObserver {
NotificationCenter.default.removeObserver(screenObserver)
self.screenObserver = nil
}
}
// MARK: - Screen sync
/// Reconcile live bars against `screens`: tear down bars whose screen is
/// gone, recompute frames for screens whose geometry may have changed, and
/// add bars for newly attached screens.
///
/// A screen whose theme or blocks fail to load is skipped (with a stderr
/// message) rather than aborting the rest.
///
/// - Parameter screens: Defaults to `NSScreen.screens`; injected for tests.
func syncScreens(screens: [NSScreen] = NSScreen.screens) {
let plan = PHScreenSync.diff(
current: screens,
occupied: controllers.map(\.screen)
)
for screen in plan.removed {
removeBar(for: screen)
}
for screen in plan.kept {
window(for: screen)?.recomputeFrame()
}
for screen in plan.added {
addBar(for: screen)
}
}
// MARK: - Reload (entry points for a future `phbar reload`)
/// Re-read config from disk and rebuild every bar against it. Screens are
/// kept; only theme/blocks/window are re-resolved. This is the global path
/// for picking up config edits at runtime.
func reloadAll() {
let config: PHConfig
do {
config = try PHConfig.load()
} catch {
stderr("reload failed, could not read config: \(error)")
return
}
factory = PHBarFactory(
config: config,
environment: PHEnvironment.process.merging(config.env),
debug: factory.debug
)
rebuildAllBars()
}
/// Rebuild a single monitor's bar against the current factory. Per-monitor
/// path for picking up config edits at runtime. Returns `false` if no bar
/// currently runs on `name` (or its reload failed to load).
@discardableResult
func reload(screenNamed name: String) -> Bool {
guard let screen = window(named: name)?.barController.screen else { return false }
removeBar(for: screen)
return addBar(for: screen)
}
/// Tear down every bar and rebuild against the current factory, preserving
/// the set of screens.
private func rebuildAllBars() {
let screens = controllers.map(\.screen)
for screen in screens {
removeBar(for: screen)
}
for screen in screens {
addBar(for: screen)
}
}
// MARK: - Bar lifecycle
/// Build, show, and start a bar for `screen`. Returns `false` (and prints to
/// stderr) if the screen's theme or blocks fail to load, so one bad screen
/// can't take down the others.
@discardableResult
private func addBar(for screen: NSScreen) -> Bool {
let controller: BarController
do {
controller = try factory.make(for: screen)
} catch {
stderr("skipping screen \(screen.localizedName): \(error)")
return false
}
let window = BarWindow(controller: controller)
window.orderFront(nil)
windows.append(window)
controller.startAutoRefresh()
return true
}
/// Stop refresh, hide, and drop the bar running on `screen` (if any).
private func removeBar(for screen: NSScreen) {
guard let index = windows.firstIndex(where: { $0.barController.screen === screen }) else { return }
windows[index].barController.stopAutoRefresh()
windows[index].orderOut(nil)
windows.remove(at: index)
}
/// The window currently running on `screen`, if any.
private func window(for screen: NSScreen) -> BarWindow? {
windows.first { $0.barController.screen === screen }
}
/// The window currently running on the monitor named `name`, if any.
private func window(named name: String) -> BarWindow? {
windows.first { $0.barController.screen.localizedName == name }
}
// MARK: - Helpers
private func stderr(_ message: String) {
FileHandle.standardError.write(Data("phbar: \(message)\n".utf8))
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ extension phbar {
try MainActor.assumeIsolated {
guard !NSScreen.screens.isEmpty else { throw CleanExit.message("No monitor found") }
let delegate = AppDelegate(config: config, debug: debug)
let delegate = PHBarDelegate(config: config, debug: debug)
let app = NSApplication.shared
app.setActivationPolicy(.accessory)
app.delegate = delegate
+69
View File
@@ -0,0 +1,69 @@
import AppKit
@MainActor
final class PHBarDelegate: NSObject, NSApplicationDelegate {
/// One window per attached screen the single source of truth. The matching
/// controllers are reached through `windows[i].barController`.
var windows: [BarWindow] = []
/// Convenience accessor over `windows`.
var controllers: [BarController] { windows.map(\.barController) }
/// Resolves a controller for any screen from the current config. Swapped by
/// `reloadAll()` so a live reload re-reads theme/blocks from disk.
var factory: PHBarFactory
private var observers: [IPCObserver] = []
private var screenObserver: NSObjectProtocol?
/// `config` is captured here (not re-read per screen) so all bars share one
/// resolved config + environment for their lifetime. Live reload swaps the
/// whole factory via `reloadAll()`.
init(config: PHConfig, debug: Bool) {
let environment = PHEnvironment.process.merging(config.env)
self.factory = PHBarFactory(config: config, environment: environment, debug: debug)
super.init()
}
func applicationDidFinishLaunching(_ notification: Notification) {
syncScreens()
// React to monitor connect/disconnect and resolution changes. macOS
// delivers a single app-level notification for any of these; `syncScreens`
// diffs so unchanged bars are left untouched.
screenObserver = NotificationCenter.default.addObserver(
forName: NSApplication.didChangeScreenParametersNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in self?.syncScreens() }
}
// Listen for refresh notifications from `phbar refresh`.
let token = IPC.observe(.refresh) { [weak self] _ in
Task { @MainActor in self?.refresh() }
}
observers.append(token)
}
func refresh() {
for controller in controllers {
controller.refresh()
}
}
func applicationWillTerminate(_ notification: Notification) {
for controller in controllers {
controller.stopAutoRefresh()
}
observers.removeAll()
if let screenObserver {
NotificationCenter.default.removeObserver(screenObserver)
self.screenObserver = nil
}
}
func stderr(_ message: String) {
FileHandle.standardError.write(Data("phbar: \(message)\n".utf8))
}
}
@@ -0,0 +1,43 @@
import AppKit
extension PHBarDelegate {
/// Build, show, and start a bar for `screen`. Returns `false` (and prints to
/// stderr) if the screen's theme or blocks fail to load, so one bad screen
/// can't take down the others.
@discardableResult
func addBar(for screen: NSScreen) -> Bool {
let controller: BarController
do {
controller = try factory.make(for: screen)
} catch {
stderr("skipping screen \(screen.localizedName): \(error)")
return false
}
let window = BarWindow(controller: controller)
window.orderFront(nil)
windows.append(window)
controller.startAutoRefresh()
return true
}
/// Stop refresh, hide, and drop the bar running on `screen` (if any).
func removeBar(for screen: NSScreen) {
guard let index = windows.firstIndex(where: { $0.barController.screen === screen }) else {
return
}
windows[index].barController.stopAutoRefresh()
windows[index].orderOut(nil)
windows.remove(at: index)
}
/// The window currently running on `screen`, if any.
func window(for screen: NSScreen) -> BarWindow? {
windows.first { $0.barController.screen === screen }
}
/// The window currently running on the monitor named `name`, if any.
func window(named name: String) -> BarWindow? {
windows.first { $0.barController.screen.localizedName == name }
}
}
@@ -0,0 +1,44 @@
extension PHBarDelegate {
// MARK: - Reload (entry points for a future `phbar reload`)
/// Re-read config from disk and rebuild every bar against it. Screens are
/// kept; only theme/blocks/window are re-resolved. This is the global path
/// for picking up config edits at runtime.
func reloadAll() {
let config: PHConfig
do {
config = try PHConfig.load()
} catch {
stderr("reload failed, could not read config: \(error)")
return
}
factory = PHBarFactory(
config: config,
environment: PHEnvironment.process.merging(config.env),
debug: factory.debug
)
rebuildAllBars()
}
/// Rebuild a single monitor's bar against the current factory. Per-monitor
/// path for picking up config edits at runtime. Returns `false` if no bar
/// currently runs on `name` (or its reload failed to load).
@discardableResult
func reload(screenNamed name: String) -> Bool {
guard let screen = window(named: name)?.barController.screen else { return false }
removeBar(for: screen)
return addBar(for: screen)
}
/// Tear down every bar and rebuild against the current factory, preserving
/// the set of screens.
private func rebuildAllBars() {
let screens = controllers.map(\.screen)
for screen in screens {
removeBar(for: screen)
}
for screen in screens {
addBar(for: screen)
}
}
}
@@ -0,0 +1,28 @@
import AppKit
extension PHBarDelegate {
/// Reconcile live bars against `screens`: tear down bars whose screen is
/// gone, recompute frames for screens whose geometry may have changed, and
/// add bars for newly attached screens.
///
/// A screen whose theme or blocks fail to load is skipped (with a stderr
/// message) rather than aborting the rest.
///
/// - Parameter screens: Defaults to `NSScreen.screens`; injected for tests.
func syncScreens(screens: [NSScreen] = NSScreen.screens) {
let plan = PHScreenSync.diff(
current: screens,
occupied: controllers.map(\.screen)
)
for screen in plan.removed {
removeBar(for: screen)
}
for screen in plan.kept {
window(for: screen)?.recomputeFrame()
}
for screen in plan.added {
addBar(for: screen)
}
}
}