diff options
| author | Tommaso Negri <me@tommasongr.com> | 2026-08-09 23:58:42 +0200 |
|---|---|---|
| committer | Tommaso Negri <me@tommasongr.com> | 2026-08-09 23:58:44 +0200 |
| commit | d59b22e557fa5d69ea6be725a7e5bcd829588225 (patch) | |
| tree | 618e20ef2b648e46eac3d51e14b8e0fcc1faba3c /Sources/phbar/Models | |
| parent | 62ce97dcb63d61c8db739c1b32bd387042ca66a0 (diff) | |
| download | bar-d59b22e557fa5d69ea6be725a7e5bcd829588225.tar.gz bar-d59b22e557fa5d69ea6be725a7e5bcd829588225.zip | |
rename project and add documentation guides
Diffstat (limited to 'Sources/phbar/Models')
| -rw-r--r-- | Sources/phbar/Models/AppDelegate.swift | 47 | ||||
| -rw-r--r-- | Sources/phbar/Models/Block.swift | 107 | ||||
| -rw-r--r-- | Sources/phbar/Models/Cell.swift | 70 | ||||
| -rw-r--r-- | Sources/phbar/Models/HEX.swift | 114 | ||||
| -rw-r--r-- | Sources/phbar/Models/Notifier.swift | 90 | ||||
| -rw-r--r-- | Sources/phbar/Models/Runner.swift | 72 | ||||
| -rw-r--r-- | Sources/phbar/Models/Screen.swift | 38 |
7 files changed, 0 insertions, 538 deletions
diff --git a/Sources/phbar/Models/AppDelegate.swift b/Sources/phbar/Models/AppDelegate.swift deleted file mode 100644 index a7ecb5a..0000000 --- a/Sources/phbar/Models/AppDelegate.swift +++ /dev/null @@ -1,47 +0,0 @@ -import AppKit -import SwiftUI - -@MainActor -final class AppDelegate: NSObject, NSApplicationDelegate { - private var windows: [BarWindow] = [] - private var blocks: [Block] = Config.blocks.map(\.resolved) - private var tokens: [UnsafeMutableRawPointer] = [] - - func applicationDidFinishLaunching(_ notification: Notification) { - for screen in NSScreen.screens { - let frame = Screen.barFrame(screen, position: Config.position, height: Config.height) - let window = BarWindow(contentRect: frame) - window.contentView = NSHostingView(rootView: BarView(blocks: blocks)) - window.orderFrontRegardless() - windows.append(window) - } - - // Register one listener per unique name; a single post fans out to every - // block with that name (see Notifier). - for name in Set(blocks.map(\.name)) { - let token = Notifier.register(name) { [weak self] in - self?.handleRefresh(name) - } - tokens.append(token) - } - - for block in blocks { block.start() } - } - - func applicationWillTerminate(_ notification: Notification) { - for block in blocks { block.stop() } - for token in tokens { Notifier.cancel(token) } - } - - /// Called from a Darwin notification handler. Marked `nonisolated` because - /// libnotify's delivery contract isn't statically visible to Swift, so we - /// hop back to the main actor explicitly. - nonisolated func handleRefresh(_ name: String) { - Task { @MainActor [weak self] in - guard let self else { return } - for block in self.blocks where block.name == name { - block.runScript() - } - } - } -} diff --git a/Sources/phbar/Models/Block.swift b/Sources/phbar/Models/Block.swift deleted file mode 100644 index fe575e4..0000000 --- a/Sources/phbar/Models/Block.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation - -/// A named, self-updating unit of the bar. -/// -/// Each block runs a shell command on an optional timer, -/// splits its stdout into ``Cell`` values, and publishes -/// them for rendering. Blocks are identified by ``name`` -/// so they can be refreshed on demand via Darwin -/// notifications (see ``Notifier``). -@MainActor -final class Block: ObservableObject, Identifiable { - /// Stable identity for SwiftUI diffing. - let id = UUID() - - /// Identifier used for manual updates via Darwin - /// notifications. - /// - /// Multiple blocks can share the same name. - /// In such case, a single notification will refresh - /// all of them. - let name: String - - /// Whether the block is anchored to the left or right - /// side of the bar. - let placement: Config.Block.Placement - - /// The shell command to execute for producing the cells. - /// - /// Can be an inline expression or a path to an executable - /// script. Inherits the environment of the phbar process. - let command: String - - /// Refresh interval in seconds, or `nil` for a one-shot block. - /// - /// A one-shot block can still be manually refreshed via - /// a Darwin notification (see ``Notifier``). - let interval: Double? - - /// The current cells produced by the last command execution. - /// - /// SwiftUI observes this `@Published` property and re-renders - /// the view tree automatically. - @Published var cells: [Cell] = [] - - private var tick: Task<Void, Never>? - - init(name: String, placement: Config.Block.Placement, command: String, interval: Double? = nil) { - self.name = name - self.placement = placement - self.command = command - self.interval = interval - } - - /// Initial paint + interval timer. - func start() { - runScript() - startTick() - } - - /// Re-run the command and update the cells on the main actor. - func runScript(env: [String: String] = [:]) { - let command = self.command - - var env = env - env.updateValue(name, forKey: "BLOCK") - - Task.detached(priority: .utility) { [weak self] in - let lines = Runner.run(command, env: env) - await self?.apply(lines) - } - } - - /// Stop the interval timer. - func stop() { - tick?.cancel() - } - - /// Replace the current cells with freshly parsed lines. - /// - /// Each line becomes a ``Cell``. This is always called - /// on the main actor so SwiftUI sees the update immediately. - @MainActor - private func apply(_ lines: [String]) { - self.cells = lines.map { Cell(rawValue: $0) } - } - - /// Start the interval timer that re-runs the command periodically. - /// - /// Does nothing when ``interval`` is `nil` or ≤ 0. - private func startTick() { - guard let interval, interval > 0 else { return } - tick = Task { [weak self] in - while !Task.isCancelled { - try? await Task.sleep(for: .seconds(interval)) - if Task.isCancelled { break } - self?.runScript() - } - } - } -} - -extension Config.Block { - @MainActor - var resolved: phbar.Block { - .init(name: name, placement: placement, command: command, interval: interval) - } -} diff --git a/Sources/phbar/Models/Cell.swift b/Sources/phbar/Models/Cell.swift deleted file mode 100644 index 0ad8e51..0000000 --- a/Sources/phbar/Models/Cell.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation -import RegexBuilder -import SwiftUI - -/// A thin wrapper around a single text line captured -/// by a block. -/// -/// When a block executes the command it captures stdout -/// and splits each line into a cell. Each cell is then -/// laid out horizontally respecting the order. -/// -/// Besides the underlying text value, a cell holds an -/// highlight level (as Int) that, if true, applies the -/// special background color defined in `Config.swift`. -/// You can set the highlight at runtime by prefixing the -/// text line with the special notation `&!<level>`, where -/// `<level>` is a positive integer that can be used as an -/// index to pick an highlight (count starts from 1). -/// -/// E.g: `echo "&!1 $(date "+%a %d, %H:%M:%S") "` -/// -/// Highlight colors can have multiple use cases, but they -/// were imagined specifically for distinguishing cells -/// within a group. For example when you want to show -/// window manager workspaces and which one is active. -struct Cell: RawRepresentable, Identifiable { - /// Stable identity for SwiftUI diffing. - let id = UUID() - - /// The represented text content. - let rawValue: String - - /// The highlight level of the cell. - let highlight: Int - - /// The background color derived from the highlight level. - /// - /// Returns the `Config.highlights` color at the same index - /// if present, otherwise `.clear` so the underlying bar - /// background shows through. - var background: Color { - guard highlight != 0 && highlight <= Config.highlights.count else { return .clear } - - return Config.highlights[highlight - 1] - } - - init(rawValue: String) { - let pattern = Regex { - Optionally { - "&!" - Capture { OneOrMore(.digit) } - } - Capture { OneOrMore(.any) } - } - - guard let match = rawValue.firstMatch(of: pattern) else { - self.rawValue = rawValue - self.highlight = 0 - return - } - - if let rawHighlight = match.output.1, let highlight = Int(String(rawHighlight)) { - self.highlight = highlight - } else { - self.highlight = 0 - } - - self.rawValue = String(match.output.2) - } -} diff --git a/Sources/phbar/Models/HEX.swift b/Sources/phbar/Models/HEX.swift deleted file mode 100644 index a262630..0000000 --- a/Sources/phbar/Models/HEX.swift +++ /dev/null @@ -1,114 +0,0 @@ -import SwiftUI - -/// A color described by a hexadecimal color code. -/// -/// `HEX` is a lightweight, string-backed representation: the code is stored -/// verbatim and parsed only when converted to a SwiftUI color via `Color(hex:)` -/// or `Color.hex(_:)`, which resolve it in the sRGB color space. -/// -/// Supported formats (the leading `#` is optional): -/// - `#RGB` -/// - `#RGBA` -/// - `#RRGGBB` -/// - `#RRGGBBAA` -/// -/// Single-digit components are expanded by repeating the digit; for example, -/// `#3aF` is read as `#33AAFF`. When present, the trailing digits encode alpha -/// (opacity); otherwise opacity defaults to `1`. -struct HEX { - /// The raw hex code, exactly as supplied. It is not validated or normalized; - /// parsing is performed lazily by `Color.hex(_:)`. - var code: String - - /// Creates a hex color from the given code. - /// - /// - Parameter code: A hex color code. Stored verbatim; see ``HEX`` for the - /// accepted formats. - init(code: String) { - self.code = code - } -} - -extension SwiftUI.Color { - /// Creates a color from a ``HEX`` value. - init(hex: HEX) { - self = .hex(hex.code) - } - - /// Creates a color from a hex code, resolving it in the sRGB color space. - /// - /// Supported formats (the leading `#` is optional): `#RGB`, `#RGBA`, - /// `#RRGGBB`, and `#RRGGBBAA`. Single-digit components are expanded by - /// repeating the digit. When present, the trailing digits encode alpha - /// (opacity); otherwise opacity defaults to `1`. Malformed input resolves - /// to opaque black. - /// - /// - Parameter code: A hex color code. - static func hex(_ code: String) -> SwiftUI.Color { - var sanitized = code.trimmingCharacters(in: .whitespacesAndNewlines) - if sanitized.hasPrefix("#") { - sanitized.removeFirst() - } - - var rgba: UInt64 = 0 - Scanner(string: sanitized).scanHexInt64(&rgba) - - let red: Double - let green: Double - let blue: Double - let opacity: Double - - switch sanitized.count { - case 8: // RRGGBBAA - red = Double((rgba >> 24) & 0xFF) / 255.0 - green = Double((rgba >> 16) & 0xFF) / 255.0 - blue = Double((rgba >> 8) & 0xFF) / 255.0 - opacity = Double(rgba & 0xFF) / 255.0 - case 6: // RRGGBB - red = Double((rgba >> 16) & 0xFF) / 255.0 - green = Double((rgba >> 8) & 0xFF) / 255.0 - blue = Double(rgba & 0xFF) / 255.0 - opacity = 1.0 - case 4: // RGBA - red = Double(((rgba >> 12) & 0xF) * 0x11) / 255.0 - green = Double(((rgba >> 8) & 0xF) * 0x11) / 255.0 - blue = Double(((rgba >> 4) & 0xF) * 0x11) / 255.0 - opacity = Double((rgba & 0xF) * 0x11) / 255.0 - case 3: // RGB - red = Double(((rgba >> 8) & 0xF) * 0x11) / 255.0 - green = Double(((rgba >> 4) & 0xF) * 0x11) / 255.0 - blue = Double((rgba & 0xF) * 0x11) / 255.0 - opacity = 1.0 - default: - red = 0.0 - green = 0.0 - blue = 0.0 - opacity = 1.0 - } - - return Color(.sRGB, red: red, green: green, blue: blue, opacity: opacity) - } -} - -extension SwiftUI.Color.Resolved { - /// The hex code for this resolved color. - /// - /// Components are clamped to `0...1` and quantized to 8 bits. - /// The result is a 6-digit `#RRGGBB` code, or an 8-digit `#RRGGBBAA` code - /// when the color is not fully opaque. - var hex: HEX { - let r = Int((min(max(Double(red), 0), 1) * 255).rounded()) - let g = Int((min(max(Double(green), 0), 1) * 255).rounded()) - let b = Int((min(max(Double(blue), 0), 1) * 255).rounded()) - - let code: String - if opacity < 1 { - let a = Int((min(max(Double(opacity), 0), 1) * 255).rounded()) - code = String(format: "#%02X%02X%02X%02X", r, g, b, a) - } else { - code = String(format: "#%02X%02X%02X", r, g, b) - } - - return HEX(code: code) - } -} diff --git a/Sources/phbar/Models/Notifier.swift b/Sources/phbar/Models/Notifier.swift deleted file mode 100644 index a428c49..0000000 --- a/Sources/phbar/Models/Notifier.swift +++ /dev/null @@ -1,90 +0,0 @@ -import Foundation - -/// Retains the handler for the lifetime of the registration. -/// -/// The raw pointer returned by ``Notifier/register(_:handler:)`` -/// is a retained reference to this box; it is released when -/// the registration is cancelled. -private final class NotifyBox { - let handler: @Sendable () -> Void - init(_ handler: @escaping @Sendable () -> Void) { self.handler = handler } -} - -/// Delivers refresh triggers via Darwin notifications. -/// -/// Wraps CoreFoundation notifications — the macOS-native, -/// signal-like way to trigger behaviour from the shell with -/// no client binary, using `notifyutil`: -/// -/// ``` -/// notifyutil -p <prefix><name> -/// ``` -/// -/// Each category of events uses a different ``prefix`` -/// to avoid collisions. For example, block refresh -/// notifications use ``touchPrefix``: -/// -/// ``` -/// notifyutil -p house.panini.phbar.touch.<name> -/// ``` -/// -/// > Note: `notify.h` isn't part of Swift's Darwin module, -/// > so we go through `CFNotificationCenter`, which talks -/// > to the same subsystem. -/// > -/// > The callback fires on an arbitrary thread; handlers -/// > must hop to the main actor themselves (see ``AppDelegate``). -enum Notifier { - /// Prefix for block refresh notifications. - /// - /// Use `notifyutil -p house.panini.phbar.touch.<name>` to trigger - /// a block update from the shell. - static let touchPrefix = "house.panini.phbar.touch." - - /// Register `handler` to fire whenever the notification is posted. - /// - /// - Parameters: - /// - name: The notification name suffix. - /// - prefix: The full notification name will be `prefix + name`. - /// Defaults to ``touchPrefix``. - /// - handler: Closure to invoke when the notification fires. - /// - /// The returned token is a retained pointer used to keep the handler - /// alive and to cancel the registration later. The handler runs on an - /// arbitrary thread, so hop to the main actor inside it if you touch - /// UI or published state. - @discardableResult - static func register( - _ name: String, - prefix: String = Notifier.touchPrefix, - handler: @escaping @Sendable () -> Void - ) -> UnsafeMutableRawPointer { - let fullName = prefix + name - let ctx = Unmanaged.passRetained(NotifyBox(handler)).toOpaque() - - CFNotificationCenterAddObserver( - CFNotificationCenterGetDarwinNotifyCenter(), - ctx, - { _, observer, _, _, _ in - guard let observer else { return } - let box = Unmanaged<NotifyBox>.fromOpaque(observer).takeUnretainedValue() - box.handler() - }, - fullName as CFString, - nil, - .deliverImmediately - ) - - return ctx - } - - /// Remove and release a registration. - /// - /// Pass the token returned by ``Notifier/register(_:handler:)``. - /// Optional: process exit clears all pending registrations anyway. - static func cancel(_ token: UnsafeMutableRawPointer) { - let center = CFNotificationCenterGetDarwinNotifyCenter() - CFNotificationCenterRemoveEveryObserver(center, token) - Unmanaged<NotifyBox>.fromOpaque(token).release() - } -} diff --git a/Sources/phbar/Models/Runner.swift b/Sources/phbar/Models/Runner.swift deleted file mode 100644 index be4dbff..0000000 --- a/Sources/phbar/Models/Runner.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation - -/// Runs a shell command and returns its stdout as separate lines. -/// -/// Blocking — always call from a background context (e.g. `Task.detached`). -enum Runner { - /// Run a command and returns stdout as an array of separated lines. - /// - Parameters: - /// - command: The command to run. - /// - env: An optional dictionary of strings to merge into - /// the process environment. - /// - /// - Returns: An array of the text lines printed to stdout. - static func run(_ command: String, env: [String: String] = [:]) -> [String] { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/bin/sh") - process.arguments = ["-c", command] - - var environment = ProcessInfo.processInfo.environment - environment.merge(env) { _, new in new } - process.environment = environment - - let stdout = Pipe() - let stderr = Pipe() - process.standardOutput = stdout - process.standardError = stderr - - guard (try? process.run()) != nil else { - return ["ERROR"] - } - - let data = stdout.fileHandleForReading.readDataToEndOfFile() - let errData = stderr.fileHandleForReading.readDataToEndOfFile() - process.waitUntilExit() - - guard process.terminationStatus == 0 else { - write(errData, to: .standardError) - return ["ERROR"] - } - - guard let string = String(data: data, encoding: .utf8) else { - return [""] - } - - return parse(string) - } - - /// Split output into lines. A single trailing newline - /// (from a final `\n`) is dropped; blank lines elsewhere - /// are kept so a script can emit intentional gaps. - static func parse(_ text: String) -> [String] { - var lines = text.components(separatedBy: "\n") - if lines.last == "" { lines.removeLast() } - return lines - } - - /// Write data to the provided handle. - /// - Parameters: - /// - data: The data to write. - /// - handle: The handle to which data should be written. - static func write(_ data: Data, to handle: FileHandle) { - handle.write(data) - } - - /// Write string to the provided handle. - /// - Parameters: - /// - string: The string to write. - /// - handle: The handle to which data should be written. - static func write(_ string: String, to handle: FileHandle) { - write(Data(string.utf8), to: handle) - } -} diff --git a/Sources/phbar/Models/Screen.swift b/Sources/phbar/Models/Screen.swift deleted file mode 100644 index 870d3b6..0000000 --- a/Sources/phbar/Models/Screen.swift +++ /dev/null @@ -1,38 +0,0 @@ -import AppKit - -/// Helpers for multi-screen geometry. -/// -/// Maps a bar configuration (position, height) into concrete -/// screen coordinates so the window can be placed correctly -/// on each display. -enum Screen { - /// Frame of the bar within a screen, in global display coordinates. - /// - /// - Parameters: - /// - screen: The screen to calculate the bar frame for. - /// - position: Whether the bar sits at the top or bottom - /// of the screen. - /// - height: The bar height in points (see ``Config/height``). - /// - /// - Returns: An ``NSRect`` in screen coordinates. The bar spans - /// the full width of the screen and is anchored at the top - /// or bottom edge. - static func barFrame(_ screen: NSScreen, position: Config.Position, height: CGFloat) -> NSRect { - switch position { - case .top: - return NSRect( - x: screen.frame.minX, - y: screen.frame.maxY - height, - width: screen.frame.width, - height: height - ) - case .bottom: - return NSRect( - x: screen.frame.minX, - y: screen.frame.minY, - width: screen.frame.width, - height: height - ) - } - } -} |
