resolve config directory dynamically following a priority order
This commit is contained in:
@@ -10,20 +10,12 @@ extension PHBlock {
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated static var configDirectory: URL {
|
||||
FileManager.default.homeDirectoryForCurrentUser.appending(path: ".config/phbar")
|
||||
}
|
||||
|
||||
nonisolated static var blocksDirectory: URL {
|
||||
Self.configDirectory.appending(path: "blocks")
|
||||
}
|
||||
|
||||
/// Load a named block set from `<configDirectory>/blocks/<name>.toml`.
|
||||
///
|
||||
/// - Parameter configDirectory: Override the lookup root (used by tests);
|
||||
/// defaults to `~/.config/phbar`.
|
||||
/// defaults to the resolved config directory (see `PHPaths`).
|
||||
static func load(_ blocks: String, in configDirectory: URL? = nil) throws -> [PHBlock] {
|
||||
let base = configDirectory ?? Self.configDirectory
|
||||
let base = configDirectory ?? PHPaths.configDirectory
|
||||
|
||||
let setFile = base.appending(path: "blocks").appending(path: "\(blocks).toml")
|
||||
guard FileManager.default.fileExists(atPath: setFile.relativePath) else {
|
||||
|
||||
@@ -85,10 +85,10 @@ extension PHBlock {
|
||||
|
||||
let lines: [Substring]? = await Task.detached(priority: .userInitiated) {
|
||||
let process = Process()
|
||||
// Commands run with the config root (~/.config/phbar) as their working
|
||||
// directory, regardless of which block set they belong to, so relative
|
||||
// paths in user scripts stay stable.
|
||||
process.currentDirectoryURL = Self.configDirectory
|
||||
// Commands run with the resolved config root (see `PHPaths`) as their
|
||||
// working directory, regardless of which block set they belong to, so
|
||||
// relative paths in user scripts stay stable.
|
||||
process.currentDirectoryURL = PHPaths.configDirectory
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||
process.arguments = ["-c", command]
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ import Foundation
|
||||
import TOML
|
||||
|
||||
extension PHConfig {
|
||||
/// Load configuration from the default path (~/.config/phbar/config.toml).
|
||||
/// If the file doesn't exist or fails to parse, defaults are used (non-fatal).
|
||||
/// Load configuration from the resolved config directory
|
||||
/// (see `PHPaths`). If the file doesn't exist or fails to parse, the
|
||||
/// bundled default is used.
|
||||
static func load() throws -> PHConfig {
|
||||
let url = FileManager.default.homeDirectoryForCurrentUser.appending(
|
||||
path: ".config/phbar/config.toml"
|
||||
)
|
||||
let url = PHPaths.configDirectory.appending(path: "config.toml")
|
||||
|
||||
if FileManager.default.fileExists(atPath: url.relativePath) {
|
||||
return try load(from: url)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
/// Resolves phbar's configuration directory.
|
||||
///
|
||||
/// Searches these locations in priority order and returns the first that
|
||||
/// exists on disk:
|
||||
///
|
||||
/// 1. `$XDG_CONFIG_HOME/phbar` — only when `XDG_CONFIG_HOME` is set to an
|
||||
/// absolute path (a relative/empty value is ignored, per the XDG spec).
|
||||
/// 2. `~/.config/phbar`
|
||||
/// 3. `~/.phbar`
|
||||
///
|
||||
/// If none exist yet (fresh install), the highest-priority candidate is
|
||||
/// returned so loaders and block commands have a stable root to resolve into.
|
||||
enum PHPaths {
|
||||
/// The resolved configuration directory (computed once, cached).
|
||||
static let configDirectory: URL = resolveConfigDirectory()
|
||||
|
||||
/// Ordered candidate directories, priority high → low.
|
||||
///
|
||||
/// - Parameter environment: Override the environment lookup (used by tests);
|
||||
/// defaults to the current process environment.
|
||||
static func configDirectoryCandidates(
|
||||
environment: [String: String] = ProcessInfo.processInfo.environment
|
||||
) -> [URL] {
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||
var candidates: [URL] = []
|
||||
|
||||
if let xdg = environment["XDG_CONFIG_HOME"] {
|
||||
let trimmed = xdg.trimmingCharacters(in: .whitespaces)
|
||||
if trimmed.hasPrefix("/") {
|
||||
candidates.append(URL(fileURLWithPath: trimmed).appending(path: "phbar"))
|
||||
}
|
||||
}
|
||||
candidates.append(home.appending(path: ".config/phbar"))
|
||||
candidates.append(home.appending(path: ".phbar"))
|
||||
return candidates
|
||||
}
|
||||
|
||||
/// First existing candidate, or the highest-priority one if none exist.
|
||||
///
|
||||
/// - Parameter environment: Override the environment lookup (used by tests);
|
||||
/// defaults to the current process environment.
|
||||
static func resolveConfigDirectory(
|
||||
environment: [String: String] = ProcessInfo.processInfo.environment
|
||||
) -> URL {
|
||||
let candidates = configDirectoryCandidates(environment: environment)
|
||||
for candidate in candidates {
|
||||
if FileManager.default.fileExists(atPath: candidate.path) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import Foundation
|
||||
/// Modeled as a `String`-backed value so it decodes straight from the TOML
|
||||
/// config (e.g. `events = ["volume", "network"]`). phbar ships **no compiled
|
||||
/// event sources**: every name resolves at runtime to an external recognizer
|
||||
/// loaded from `~/.config/phbar/events/<name>/` — a `dlopen`'d library whose
|
||||
/// loaded from `<config>/events/<name>/` — a `dlopen`'d library whose
|
||||
/// root object conforms to `PHEventRecognizer`. To add an event, install a
|
||||
/// recognizer folder; no host code change is required.
|
||||
struct PHEvent: Hashable, Sendable {
|
||||
|
||||
@@ -3,8 +3,8 @@ import Foundation
|
||||
import phbarEvents
|
||||
|
||||
/// Loads external event recognizers (shared libraries) from the user's config
|
||||
/// directory: `~/.config/phbar/events/<name>/event.toml` + the referenced
|
||||
/// library.
|
||||
/// directory (see `PHPaths`): `<config>/events/<name>/event.toml` + the
|
||||
/// referenced library.
|
||||
///
|
||||
/// Each event lives in its own folder. The manifest names the `.dylib` and,
|
||||
/// optionally, the factory symbol (default `phbar_event_create`). Libraries are
|
||||
@@ -31,7 +31,7 @@ final class PHEventLoader {
|
||||
}
|
||||
|
||||
static var defaultDirectory: URL {
|
||||
FileManager.default.homeDirectoryForCurrentUser.appending(path: ".config/phbar/events")
|
||||
PHPaths.configDirectory.appending(path: "events")
|
||||
}
|
||||
|
||||
/// Returns the recognizer for `name`, loading and caching it on first access.
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
import TOML
|
||||
|
||||
/// The manifest describing an external event recognizer, read from
|
||||
/// `~/.config/phbar/events/<name>/event.toml`.
|
||||
/// `<config>/events/<name>/event.toml`.
|
||||
///
|
||||
/// ```toml
|
||||
/// library = "event.dylib" # required: shared library file name
|
||||
|
||||
@@ -85,7 +85,7 @@ final class PHEventRegistry {
|
||||
|
||||
/// Resolves an event to its source. phbar ships no compiled sources: every
|
||||
/// name is handed to `PHEventLoader`, which `dlopen`s a user-supplied
|
||||
/// recognizer from `~/.config/phbar/events/<name>/`. Returns `nil` if no
|
||||
/// recognizer from `<config>/events/<name>/`. Returns `nil` if no
|
||||
/// recognizer is installed.
|
||||
static func defaultFactory(_ event: PHEvent) -> (any PHEventSource)? {
|
||||
guard let recognizer = PHEventLoader.shared.recognizer(for: event.rawValue) else { return nil }
|
||||
|
||||
@@ -2,14 +2,12 @@ import Foundation
|
||||
import TOML
|
||||
|
||||
extension PHTheme {
|
||||
/// Load theme from the config directory (~/.config/phbar/themes/).
|
||||
/// If the file doesn't exist or fails to parse, defaults are used (non-fatal).
|
||||
/// Load theme from the resolved config directory (`PHPaths`/themes).
|
||||
/// Falls back to the bundled theme when the file is absent or parsing fails.
|
||||
static func load(_ theme: String?) throws -> PHTheme {
|
||||
guard let theme else { return try loadFromBundle() }
|
||||
|
||||
let url = FileManager.default.homeDirectoryForCurrentUser.appending(
|
||||
path: ".config/phbar/themes/\(theme).toml"
|
||||
)
|
||||
let url = PHPaths.configDirectory.appending(path: "themes").appending(path: "\(theme).toml")
|
||||
|
||||
if FileManager.default.fileExists(atPath: url.relativePath) {
|
||||
return try load(from: url)
|
||||
|
||||
@@ -4,9 +4,9 @@ import Foundation
|
||||
///
|
||||
/// phbar ships a set of built-in recognizers (volume, network, appearance,
|
||||
/// power, mpd) compiled into the host. External recognizers are loaded at
|
||||
/// runtime as shared libraries from `~/.config/phbar/events/<name>/` and conform
|
||||
/// to this same protocol, so the host drives built-in and external recognizers
|
||||
/// through one shape.
|
||||
/// runtime as shared libraries from the events directory
|
||||
/// (`<config>/events/<name>/`) and conform to this same protocol, so the host
|
||||
/// drives built-in and external recognizers through one shape.
|
||||
///
|
||||
/// Conform an `NSObject` subclass and export a zero-argument factory via
|
||||
/// `@_cdecl(PHEventCreateSymbol)`:
|
||||
|
||||
Reference in New Issue
Block a user