Compare commits
3 Commits
2d047d69b6
...
34fc5a4c0d
| Author | SHA1 | Date | |
|---|---|---|---|
|
34fc5a4c0d
|
|||
|
289cc6eb61
|
|||
|
d8d5e38d33
|
@@ -0,0 +1,52 @@
|
|||||||
|
import Foundation
|
||||||
|
import TOML
|
||||||
|
|
||||||
|
extension PHBlock {
|
||||||
|
private struct Wrapper: Decodable {
|
||||||
|
let blocks: [PHBlock]
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case blocks = "block"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated static var configFile: URL {
|
||||||
|
FileManager.default.homeDirectoryForCurrentUser.appending(path: ".config/phbar/blocks.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load blocks from the default path (~/.config/phbar/blocks.toml).
|
||||||
|
/// If the file doesn't exist or fails to parse, defaults are used (non-fatal).
|
||||||
|
static func load() throws -> [PHBlock] {
|
||||||
|
if FileManager.default.fileExists(atPath: Self.configFile.relativePath) {
|
||||||
|
return try load(from: Self.configFile)
|
||||||
|
} else {
|
||||||
|
throw phbar.Error("Failed to load blocks file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load theme from a file at URL.
|
||||||
|
/// If the file doesn't exist or fails to parse, the execution is interrupted.
|
||||||
|
static func load(from url: URL) throws -> [PHBlock] {
|
||||||
|
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)
|
||||||
|
} catch {
|
||||||
|
throw phbar.Error("Failed to load blocks file", underlyingError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load theme from a TOML string.
|
||||||
|
/// If the content fails to parse, the execution is interrupted.
|
||||||
|
static func load(from contents: String) throws -> [PHBlock] {
|
||||||
|
do {
|
||||||
|
let decoder = TOMLDecoder()
|
||||||
|
let wrapper = try decoder.decode(PHBlock.Wrapper.self, from: contents)
|
||||||
|
return wrapper.blocks
|
||||||
|
} catch {
|
||||||
|
throw phbar.Error("Failed to parse blocks file", underlyingError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-117
@@ -1,69 +1,4 @@
|
|||||||
import ArgumentParser
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
import TOML
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
final class PHBlock: ObservableObject, Decodable, Identifiable {
|
|
||||||
let id = UUID()
|
|
||||||
let command: String
|
|
||||||
let name: String?
|
|
||||||
let styleName: String?
|
|
||||||
@Published var style: PHThemeStyle = .default
|
|
||||||
let refresh: Double?
|
|
||||||
let centered: Bool?
|
|
||||||
var debug: Bool = false
|
|
||||||
|
|
||||||
var visible: Bool {
|
|
||||||
label != nil && !label!.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
/// System events that should trigger a refresh (e.g. `["volume", "network"]`).
|
|
||||||
/// `nil` when omitted from config.
|
|
||||||
let events: [PHEvent]?
|
|
||||||
|
|
||||||
/// Event source registry used to subscribe to system events. Defaults to the
|
|
||||||
/// shared instance; inject a custom one for testing.
|
|
||||||
var registry: PHEventRegistry = .shared
|
|
||||||
|
|
||||||
@Published private(set) var label: String?
|
|
||||||
|
|
||||||
/// The repeating interval task, if any.
|
|
||||||
private var intervalTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
/// Active event subscriptions, torn down in `stopAutoRefresh`.
|
|
||||||
private var subscriptions: [PHEventSubscription] = []
|
|
||||||
|
|
||||||
/// Guards against stacking concurrent updates during rapid event bursts
|
|
||||||
/// (e.g. dragging the volume slider fires many events in quick succession).
|
|
||||||
private var updateScheduled = false
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case command, name
|
|
||||||
case styleName = "style"
|
|
||||||
case refresh, events, centered
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
intervalTask?.cancel()
|
|
||||||
subscriptions.forEach { $0.cancel() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kind
|
|
||||||
|
|
||||||
extension PHBlock {
|
|
||||||
enum Kind: String {
|
|
||||||
case text, space
|
|
||||||
}
|
|
||||||
|
|
||||||
var kind: Kind {
|
|
||||||
if command.starts(with: "_space") { return .space }
|
|
||||||
return .text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh
|
|
||||||
|
|
||||||
extension PHBlock {
|
extension PHBlock {
|
||||||
/// Start keeping the label up to date.
|
/// Start keeping the label up to date.
|
||||||
@@ -187,55 +122,3 @@ extension PHBlock {
|
|||||||
return lines.last?.description
|
return lines.last?.description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading
|
|
||||||
|
|
||||||
extension PHBlock {
|
|
||||||
private struct Wrapper: Decodable {
|
|
||||||
let blocks: [PHBlock]
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case blocks = "block"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private nonisolated static var configFile: URL {
|
|
||||||
FileManager.default.homeDirectoryForCurrentUser.appending(path: ".config/phbar/blocks.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load blocks from the default path (~/.config/phbar/blocks.toml).
|
|
||||||
/// If the file doesn't exist or fails to parse, defaults are used (non-fatal).
|
|
||||||
static func load() throws -> [PHBlock] {
|
|
||||||
if FileManager.default.fileExists(atPath: Self.configFile.relativePath) {
|
|
||||||
return try load(from: Self.configFile)
|
|
||||||
} else {
|
|
||||||
throw phbar.Error("Failed to load blocks file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load theme from a file at URL.
|
|
||||||
/// If the file doesn't exist or fails to parse, the execution is interrupted.
|
|
||||||
static func load(from url: URL) throws -> [PHBlock] {
|
|
||||||
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)
|
|
||||||
} catch {
|
|
||||||
throw phbar.Error("Failed to load blocks file", underlyingError: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load theme from a TOML string.
|
|
||||||
/// If the content fails to parse, the execution is interrupted.
|
|
||||||
static func load(from contents: String) throws -> [PHBlock] {
|
|
||||||
do {
|
|
||||||
let decoder = TOMLDecoder()
|
|
||||||
let wrapper = try decoder.decode(PHBlock.Wrapper.self, from: contents)
|
|
||||||
return wrapper.blocks
|
|
||||||
} catch {
|
|
||||||
throw phbar.Error("Failed to parse blocks file", underlyingError: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class PHBlock: ObservableObject, Decodable, Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
|
||||||
|
let command: String
|
||||||
|
let name: String?
|
||||||
|
let styleName: String?
|
||||||
|
@Published var style: PHThemeStyle = .default
|
||||||
|
let refresh: Double?
|
||||||
|
let centered: Bool?
|
||||||
|
var debug: Bool = false
|
||||||
|
|
||||||
|
var visible: Bool {
|
||||||
|
label != nil && !label!.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System events that should trigger a refresh (e.g. `["volume", "network"]`).
|
||||||
|
/// `nil` when omitted from config.
|
||||||
|
let events: [PHEvent]?
|
||||||
|
|
||||||
|
/// Event source registry used to subscribe to system events. Defaults to the
|
||||||
|
/// shared instance; inject a custom one for testing.
|
||||||
|
var registry: PHEventRegistry = .shared
|
||||||
|
|
||||||
|
@Published var label: String?
|
||||||
|
|
||||||
|
/// The repeating interval task, if any.
|
||||||
|
var intervalTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
/// Active event subscriptions, torn down in `stopAutoRefresh`.
|
||||||
|
var subscriptions: [PHEventSubscription] = []
|
||||||
|
|
||||||
|
/// Guards against stacking concurrent updates during rapid event bursts
|
||||||
|
/// (e.g. dragging the volume slider fires many events in quick succession).
|
||||||
|
var updateScheduled = false
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case command, name
|
||||||
|
case styleName = "style"
|
||||||
|
case refresh, events, centered
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
intervalTask?.cancel()
|
||||||
|
subscriptions.forEach { $0.cancel() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
enum PHBlockKind: String {
|
||||||
|
case text, space
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PHBlock {
|
||||||
|
var kind: PHBlockKind {
|
||||||
|
if command.starts(with: "_space") { return .space }
|
||||||
|
return .text
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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).
|
||||||
|
static func load() throws -> PHConfig {
|
||||||
|
let url = FileManager.default.homeDirectoryForCurrentUser.appending(
|
||||||
|
path: ".config/phbar/config.toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: url.relativePath) {
|
||||||
|
return try load(from: url)
|
||||||
|
} else {
|
||||||
|
return try loadFromBundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the bundled configuration.
|
||||||
|
/// If the file doesn't exist or fails to parse, the execution is interrupted.
|
||||||
|
static private func loadFromBundle() throws -> PHConfig {
|
||||||
|
guard
|
||||||
|
let defaultConfig = Bundle.module.url(
|
||||||
|
forResource: "config",
|
||||||
|
withExtension: "toml"
|
||||||
|
)?.resolvingSymlinksInPath()
|
||||||
|
else {
|
||||||
|
throw phbar.Error("Failed to load config file")
|
||||||
|
}
|
||||||
|
return try load(from: defaultConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode configuration 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) throws -> PHConfig {
|
||||||
|
guard let data = try? Data(contentsOf: url),
|
||||||
|
let contents = String(data: data, encoding: .utf8)
|
||||||
|
else {
|
||||||
|
throw phbar.Error("Failed to load config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try load(from: contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode configuration from a TOML string.
|
||||||
|
/// If the content fails to parse, the execution is interrupted.
|
||||||
|
static private func load(from contents: String) throws -> PHConfig {
|
||||||
|
do {
|
||||||
|
let decoder = TOMLDecoder()
|
||||||
|
let configFile = try decoder.decode(PHConfig.self, from: contents)
|
||||||
|
return configFile
|
||||||
|
} catch {
|
||||||
|
throw phbar.Error("Failed to parse config file", underlyingError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,62 +1,3 @@
|
|||||||
import Foundation
|
|
||||||
import TOML
|
|
||||||
|
|
||||||
struct PHConfig: Decodable {
|
struct PHConfig: Decodable {
|
||||||
let text: PHConfigText
|
let text: PHConfigText
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading
|
|
||||||
|
|
||||||
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).
|
|
||||||
static func load() throws -> PHConfig {
|
|
||||||
let url = FileManager.default.homeDirectoryForCurrentUser.appending(
|
|
||||||
path: ".config/phbar/config.toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: url.relativePath) {
|
|
||||||
return try load(from: url)
|
|
||||||
} else {
|
|
||||||
return try loadFromBundle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the bundled configuration.
|
|
||||||
/// If the file doesn't exist or fails to parse, the execution is interrupted.
|
|
||||||
static private func loadFromBundle() throws -> PHConfig {
|
|
||||||
guard
|
|
||||||
let defaultConfig = Bundle.module.url(
|
|
||||||
forResource: "config",
|
|
||||||
withExtension: "toml"
|
|
||||||
)?.resolvingSymlinksInPath()
|
|
||||||
else {
|
|
||||||
throw phbar.Error("Failed to load config file")
|
|
||||||
}
|
|
||||||
return try load(from: defaultConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode configuration 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) throws -> PHConfig {
|
|
||||||
guard let data = try? Data(contentsOf: url),
|
|
||||||
let contents = String(data: data, encoding: .utf8)
|
|
||||||
else {
|
|
||||||
throw phbar.Error("Failed to load config file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return try load(from: contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode configuration from a TOML string.
|
|
||||||
/// If the content fails to parse, the execution is interrupted.
|
|
||||||
static private func load(from contents: String) throws -> PHConfig {
|
|
||||||
do {
|
|
||||||
let decoder = TOMLDecoder()
|
|
||||||
let configFile = try decoder.decode(PHConfig.self, from: contents)
|
|
||||||
return configFile
|
|
||||||
} catch {
|
|
||||||
throw phbar.Error("Failed to parse config file", underlyingError: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ struct PHConfigText: Decodable {
|
|||||||
let style: PHConfigTextStyle
|
let style: PHConfigTextStyle
|
||||||
let offset: Double?
|
let offset: Double?
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case fontFamily = "font"
|
case fontFamily = "font"
|
||||||
case size, weight, style, offset
|
case size, weight, style, offset
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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).
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: url.relativePath) {
|
||||||
|
return try load(from: url)
|
||||||
|
} else {
|
||||||
|
return try loadFromBundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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")
|
||||||
|
}
|
||||||
|
return try load(from: defaultConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) 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)
|
||||||
|
} catch {
|
||||||
|
throw phbar.Error("Failed to load theme file", underlyingError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load theme from a TOML string.
|
||||||
|
/// If the content fails to parse, the execution is interrupted.
|
||||||
|
static private func load(from contents: String) throws -> PHTheme {
|
||||||
|
do {
|
||||||
|
let decoder = TOMLDecoder()
|
||||||
|
return try decoder.decode(PHTheme.self, from: contents)
|
||||||
|
} catch {
|
||||||
|
throw phbar.Error("Failed to parse theme file", underlyingError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +1,9 @@
|
|||||||
import Foundation
|
|
||||||
import TOML
|
|
||||||
|
|
||||||
struct PHTheme: Decodable {
|
struct PHTheme: Decodable {
|
||||||
let window: PHThemeWindow
|
let window: PHThemeWindow
|
||||||
let styles: [PHThemeStyle]
|
let styles: [PHThemeStyle]
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case window
|
case window
|
||||||
case styles = "style"
|
case styles = "style"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading
|
|
||||||
|
|
||||||
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).
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: url.relativePath) {
|
|
||||||
return try load(from: url)
|
|
||||||
} else {
|
|
||||||
return try loadFromBundle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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")
|
|
||||||
}
|
|
||||||
return try load(from: defaultConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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) 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)
|
|
||||||
} catch {
|
|
||||||
throw phbar.Error("Failed to load theme file", underlyingError: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load theme from a TOML string.
|
|
||||||
/// If the content fails to parse, the execution is interrupted.
|
|
||||||
static private func load(from contents: String) throws -> PHTheme {
|
|
||||||
do {
|
|
||||||
let decoder = TOMLDecoder()
|
|
||||||
return try decoder.decode(PHTheme.self, from: contents)
|
|
||||||
} catch {
|
|
||||||
throw phbar.Error("Failed to parse theme file", underlyingError: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ struct PHThemeWindow: Decodable {
|
|||||||
let hasShadow: Bool
|
let hasShadow: Bool
|
||||||
let blur: Double
|
let blur: Double
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case hasShadow = "shadow"
|
case hasShadow = "shadow"
|
||||||
case blur
|
case blur
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user