cleanup delegate
This commit is contained in:
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ extension phbar {
|
|||||||
try MainActor.assumeIsolated {
|
try MainActor.assumeIsolated {
|
||||||
guard !NSScreen.screens.isEmpty else { throw CleanExit.message("No monitor found") }
|
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
|
let app = NSApplication.shared
|
||||||
app.setActivationPolicy(.accessory)
|
app.setActivationPolicy(.accessory)
|
||||||
app.delegate = delegate
|
app.delegate = delegate
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user