diff --git a/Sources/phbar/Commands/PHBar+Start.swift b/Sources/phbar/Commands/PHBar+Start.swift index 13208ad..1115eb9 100644 --- a/Sources/phbar/Commands/PHBar+Start.swift +++ b/Sources/phbar/Commands/PHBar+Start.swift @@ -22,6 +22,8 @@ extension PHBar { mutating func run() throws { let config = try PHConfig.load() + let environment = PHEnvironment.process.merging(config.env) + let theme = try PHTheme.load(config.theme, environment: environment) // NOTE: Make sure NSApp.run() runs in the main thread dispatchPrecondition(condition: .onQueue(.main)) @@ -29,7 +31,7 @@ extension PHBar { try MainActor.assumeIsolated { guard !NSScreen.screens.isEmpty else { throw CleanExit.message("No monitor found") } - let delegate = PHBarDelegate(config: config, debug: debug) + let delegate = PHBarDelegate(config: config, theme: theme, debug: debug) let app = NSApplication.shared app.setActivationPolicy(.accessory) app.delegate = delegate diff --git a/Sources/phbar/Models/PHBarDelegate.swift b/Sources/phbar/Models/PHBarDelegate.swift index d127801..6307afc 100644 --- a/Sources/phbar/Models/PHBarDelegate.swift +++ b/Sources/phbar/Models/PHBarDelegate.swift @@ -19,9 +19,14 @@ final class PHBarDelegate: NSObject, NSApplicationDelegate { /// `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) { + init(config: PHConfig, theme: PHTheme, debug: Bool) { let environment = PHEnvironment.process.merging(config.env) - self.factory = PHBarFactory(config: config, environment: environment, debug: debug) + self.factory = PHBarFactory( + config: config, + theme: theme, + environment: environment, + debug: debug + ) super.init() } diff --git a/Sources/phbar/Models/PHBarDelegate/PHBarDelegate+Reload.swift b/Sources/phbar/Models/PHBarDelegate/PHBarDelegate+Reload.swift index 51934d1..5260857 100644 --- a/Sources/phbar/Models/PHBarDelegate/PHBarDelegate+Reload.swift +++ b/Sources/phbar/Models/PHBarDelegate/PHBarDelegate+Reload.swift @@ -1,19 +1,23 @@ 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. + /// Re-read config and theme from disk and rebuild every bar against them. + /// 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 + let theme: PHTheme do { config = try PHConfig.load() + let environment = PHEnvironment.process.merging(config.env) + theme = try PHTheme.load(config.theme, environment: environment) } catch { stderr("reload failed, could not read config: \(error)") return } factory = PHBarFactory( config: config, + theme: theme, environment: PHEnvironment.process.merging(config.env), debug: factory.debug ) diff --git a/Sources/phbar/Models/PHBarFactory.swift b/Sources/phbar/Models/PHBarFactory.swift index f2dffa5..b43bdda 100644 --- a/Sources/phbar/Models/PHBarFactory.swift +++ b/Sources/phbar/Models/PHBarFactory.swift @@ -1,31 +1,22 @@ import AppKit -/// Builds a `BarController` for a screen by resolving its theme, blocks, and +/// Builds a `BarController` for a screen by resolving its blocks and /// window from the config. -/// -/// Extracted from `AppDelegate` so the exact same resolution drives initial -/// launch, hot-plugged monitors (`syncScreens`), and live reload -/// (`reloadAll` / `reload(screenNamed:)`). Each screen's theme/blocks are -/// resolved independently, so a failure on one screen can be skipped without -/// taking down the others. @MainActor struct PHBarFactory { let config: PHConfig + let theme: PHTheme let environment: PHEnvironment let debug: Bool - /// Resolve theme + blocks for `screen` and build its controller. + /// Resolve blocks for `screen` and build its controller. /// /// - Parameter screen: The screen to build a bar for. Its localized name /// and index in `NSScreen.screens` select any per-monitor config override. - /// - Throws: `PHTheme.load` / `PHBlock.load` errors for the resolved names. + /// - Throws: `PHBlock.load` error for the resolved names. func make(for screen: NSScreen) throws -> BarController { let name = screen.localizedName let index = NSScreen.screens.firstIndex(of: screen) - let theme = try PHTheme.load( - config.theme(screenName: name, screenIndex: index), - environment: environment - ) let blocks = try PHBlock.load(config.blocks(screenName: name, screenIndex: index)) return BarController( config: config, diff --git a/Sources/phbar/Models/PHConfig/PHConfigMonitorOverride.swift b/Sources/phbar/Models/PHConfig/PHConfigMonitorOverride.swift index dc24db8..e01bedb 100644 --- a/Sources/phbar/Models/PHConfig/PHConfigMonitorOverride.swift +++ b/Sources/phbar/Models/PHConfig/PHConfigMonitorOverride.swift @@ -5,12 +5,10 @@ import Foundation /// Either field is optional; an unset field inherits the matching global /// (`theme`, `window`, or `blocks`) so a monitor can override just one of them. struct PHConfigMonitorOverride: Decodable { - let theme: String? let window: String? let blocks: String? - init(theme: String? = nil, window: String? = nil, blocks: String? = nil) { - self.theme = theme + init(window: String? = nil, blocks: String? = nil) { self.window = window self.blocks = blocks } @@ -30,12 +28,6 @@ extension PHConfig { return nil } - /// Theme name for a screen: the override's `theme` if set, otherwise the - /// global `theme`. - func theme(screenName: String, screenIndex: Int?) -> String { - monitorOverride(screenName: screenName, screenIndex: screenIndex)?.theme ?? theme - } - /// Window definition name for a screen: the override's `window` if set, /// otherwise the global `window`. func window(screenName: String, screenIndex: Int?) -> String { diff --git a/Sources/phbar/Models/PHTheme/PHTheme+Loading.swift b/Sources/phbar/Models/PHTheme/PHTheme+Loading.swift index 19184ef..80586bd 100644 --- a/Sources/phbar/Models/PHTheme/PHTheme+Loading.swift +++ b/Sources/phbar/Models/PHTheme/PHTheme+Loading.swift @@ -3,48 +3,39 @@ import TOML extension PHTheme { /// Load theme from the resolved config directory (`PHPaths`/themes). - /// Falls back to the bundled theme when the file is absent or parsing fails. + /// If the file doesn't exist or fails to parse, the execution is interrupted. /// /// - Parameter environment: Used to expand `$VAR` references in `@EnvExpanded` /// fields (colors, fonts); defaults to the process environment. - static func load(_ theme: String?, environment: PHEnvironment = .process) throws -> PHTheme { - guard let theme else { return try loadFromBundle() } - + static func load(_ theme: String, environment: PHEnvironment = .process) throws -> PHTheme { let url = PHPaths.configDirectory.appending(path: "themes").appending(path: "\(theme).toml") if FileManager.default.fileExists(atPath: url.relativePath) { return try load(from: url, environment: environment) } else { - return try loadFromBundle() - } - } + throw PHBar.Error( + """ + Theme file not found. + Make sure to have a file named `\(theme).toml` inside the + directory: `\(url.deletingLastPathComponent().relativePath)` - /// Load the bundled theme. - /// If the file doesn't exist or fails to parse, the execution is interrupted. - static private func loadFromBundle() throws -> PHTheme { - guard - let defaultConfig = Bundle.module.url( - forResource: "theme", - withExtension: "toml" - )?.resolvingSymlinksInPath() - else { - throw PHBar.Error("Failed to load theme file") + Tip: If you've never used phbar before, run the `phbar install` command + to automatically generate the required configuration files. + """ + ) } - return try load(from: defaultConfig, environment: .process) } /// Load theme from a file at URL. /// If the file doesn't exist or fails to parse, the execution is interrupted. static private func load(from url: URL, environment: PHEnvironment) throws -> PHTheme { - do { - let data = try Data(contentsOf: url) - guard let contents = String(data: data, encoding: .utf8), !contents.isEmpty else { - throw PHBar.Error("the file is empty") - } - return try load(from: contents, environment: environment) - } catch { - throw PHBar.Error("Failed to load theme file", underlyingError: error) + guard let data = try? Data(contentsOf: url), + let contents = String(data: data, encoding: .utf8), !contents.isEmpty + else { + throw PHBar.Error("Theme file not readable or empty.") } + + return try load(from: contents, environment: environment) } /// Load theme from a TOML string. @@ -55,7 +46,7 @@ extension PHTheme { decoder.userInfo[PHEnvironment.userInfoKey] = environment return try decoder.decode(PHTheme.self, from: contents) } catch { - throw PHBar.Error("Failed to parse theme file", underlyingError: error) + throw PHBar.Error("Theme file not valid", underlyingError: error) } } }