From f4794993095694784bd1171a9596dab96e04feb9 Mon Sep 17 00:00:00 2001 From: Tommaso Negri Date: Mon, 13 Jul 2026 23:05:08 +0200 Subject: [PATCH] refactor block resolve and refresh --- Sources/phbar/Models/PHBlock.swift | 8 +- .../Models/PHBlock/PHBlock+Gesture.swift | 11 ++ .../Models/PHBlock/PHBlock+Refresh.swift | 82 +----------- .../Models/PHBlock/PHBlock+Resolve.swift | 118 ++++++++++++++++++ Sources/phbar/Views/BarController.swift | 2 +- Sources/phbar/Views/TextView.swift | 2 +- 6 files changed, 137 insertions(+), 86 deletions(-) create mode 100644 Sources/phbar/Models/PHBlock/PHBlock+Gesture.swift create mode 100644 Sources/phbar/Models/PHBlock/PHBlock+Resolve.swift diff --git a/Sources/phbar/Models/PHBlock.swift b/Sources/phbar/Models/PHBlock.swift index 3f864c1..eda9ff7 100644 --- a/Sources/phbar/Models/PHBlock.swift +++ b/Sources/phbar/Models/PHBlock.swift @@ -5,7 +5,6 @@ final class PHBlock: ObservableObject, Decodable, Identifiable { let id = UUID() let name: String - let _command: String? let textName: String? @Published var text: PHThemeText = .default let styleName: String? @@ -14,8 +13,8 @@ final class PHBlock: ObservableObject, Decodable, Identifiable { let centered: Bool? var debug: Bool = false - var command: String { - _command ?? PHPaths.configDirectory.appending(path: "scripts/\(name)").relativePath + nonisolated var command: String { + PHPaths.configDirectory.appending(path: "scripts/\(name)").relativePath } var visible: Bool { @@ -44,11 +43,10 @@ final class PHBlock: ObservableObject, Decodable, Identifiable { /// Guards against stacking concurrent updates during rapid event bursts /// (e.g. dragging the volume slider fires many events in quick succession). - var updateScheduled = false + var resolveScheduled = false private enum CodingKeys: String, CodingKey { case name - case _command = "command" case textName = "text" case styleName = "style" case refresh, events, centered diff --git a/Sources/phbar/Models/PHBlock/PHBlock+Gesture.swift b/Sources/phbar/Models/PHBlock/PHBlock+Gesture.swift new file mode 100644 index 0000000..3742c81 --- /dev/null +++ b/Sources/phbar/Models/PHBlock/PHBlock+Gesture.swift @@ -0,0 +1,11 @@ +extension PHBlock { + func resolve(with event: PHGestureEvent) { + var environment = [String: String]() + environment.updateValue(event.type.rawValue, forKey: PHGestureEvent.typeKey) + if let data = event.data { + environment.updateValue(data, forKey: PHGestureEvent.infoKey) + } + + resolve(merging: environment) + } +} diff --git a/Sources/phbar/Models/PHBlock/PHBlock+Refresh.swift b/Sources/phbar/Models/PHBlock/PHBlock+Refresh.swift index 1cbaf7c..84ef25e 100644 --- a/Sources/phbar/Models/PHBlock/PHBlock+Refresh.swift +++ b/Sources/phbar/Models/PHBlock/PHBlock+Refresh.swift @@ -19,7 +19,7 @@ extension PHBlock { stopAutoRefresh() // Immediate refresh so the bar isn't blank until the first trigger fires. - scheduleUpdate() + resolve() // 1. Periodic refresh. if let interval = refresh, interval > 0 { @@ -28,7 +28,7 @@ extension PHBlock { while !Task.isCancelled { try? await Task.sleep(for: .milliseconds(milliseconds)) if Task.isCancelled { break } - await self?.update() + await self?.resolve() } } } @@ -37,7 +37,7 @@ extension PHBlock { // system listener only while at least one block subscribes to it. for event in (events ?? []) { let subscription = registry.subscribe(event) { [weak self] in - self?.scheduleUpdate() + self?.resolve() } subscriptions.append(subscription) } @@ -50,80 +50,4 @@ extension PHBlock { subscriptions.forEach { $0.cancel() } subscriptions.removeAll() } - - /// Recompute the label from `command`. Used by the interval loop, event - /// handlers, and manual updates. - func update() async { - label = await compute() - } - - /// Request an update on the main actor, coalescing rapid bursts into a single - /// recomputation. - private func scheduleUpdate() { - guard !updateScheduled else { return } - updateScheduled = true - Task { [weak self] in - defer { self?.updateScheduled = false } - await self?.update() - } - } - - func handleGesture(_ event: PHGestureEvent) { - Task { label = await compute(with: event) } - } - - /// Runs the provided command and returns its stdout. - /// - /// Runs off the main actor so it can be awaited safely from SwiftUI views - /// without blocking the UI. - /// - /// - Returns: A non-optional String to use as the block label. - func compute(with gestureEvent: PHGestureEvent? = nil) async -> String? { - guard kind == .text else { return nil } - - let command = command - let environment = self.environment - - let lines: [Substring]? = await Task.detached(priority: .userInitiated) { - let process = Process() - // 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] - - // Scripts inherit phbar's process environment with the `[env]` section - // overlaid, so user-defined variables and PATH extensions are visible. - process.environment = environment.variables - if let gestureEvent { - let type = gestureEvent.type.rawValue - process.environment?.updateValue(type, forKey: PHGestureEvent.typeKey) - - if let data = gestureEvent.data { - process.environment?.updateValue(data, forKey: PHGestureEvent.infoKey) - } - } - - // Capture stdout via a pipe (otherwise standardOutput is always nil). - let pipe = Pipe() - process.standardOutput = pipe - - do { - try process.run() - process.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let string = String(data: data, encoding: .utf8) - guard let lines = string?.split(whereSeparator: \.isNewline) else { return nil } - return lines - } catch { - // Silently ignore failures - return nil - } - }.value - - guard let lines else { return nil } - if self.debug { for line in lines { print(line) } } - return lines.last?.description - } } diff --git a/Sources/phbar/Models/PHBlock/PHBlock+Resolve.swift b/Sources/phbar/Models/PHBlock/PHBlock+Resolve.swift new file mode 100644 index 0000000..429736d --- /dev/null +++ b/Sources/phbar/Models/PHBlock/PHBlock+Resolve.swift @@ -0,0 +1,118 @@ +import Foundation + +extension PHBlock { + /// Resolve the label by executing `command`. + /// Used by the interval loop, event handlers, and manual updates. + func resolve(merging environment: [String: String]? = nil) { + guard canResolve() else { return } + + guard !resolveScheduled else { return } + resolveScheduled = true + + Task(name: command, priority: .userInitiated) { [weak self] in + defer { self?.resolveScheduled = false } + + do { + self?.label = try await self?.resolve(merging: environment) + } catch { + self?.printStandardError(error.localizedDescription) + } + } + } + + /// Resolve the label by executing `command`. + /// Used by the interval loop, event handlers, and manual updates. + func resolve(merging environment: [String: String]? = nil) async { + guard canResolve() else { return } + + do { + label = try await resolve(merging: environment) + } catch { + printStandardError(error.localizedDescription) + } + } + + private func resolve(merging environment: [String: String]?) async throws -> String? { + let customEnvironment = environment + + var environment = ProcessInfo.processInfo.environment + if let customEnvironment { + environment.merge(customEnvironment) { (_, new) in new } + } + + return try await resolve(with: environment) + } + + private func resolve(with environment: [String: String]) async throws -> String? { + do { + let (stdout, stderr) = try await resolve(command, with: environment) + printStandardError(stderr) + printStandardOutput(stdout) + guard let lines = stdout?.split(whereSeparator: \.isNewline), + let last = lines.last + else { + return nil + } + return String(last) + } catch { + throw error + } + } + + private func canResolve() -> Bool { + guard kind == .text else { return false } + + return true + } + + private func resolve(_ command: String, with environment: [String: String]) async throws -> ( + stdout: String?, + stderr: String? + ) { + return try await withCheckedThrowingContinuation { continuation in + let process = Process() + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + + process.executableURL = URL(fileURLWithPath: "/bin/zsh") + process.currentDirectoryURL = PHPaths.configDirectory + process.environment = environment + process.arguments = ["-c", command] + process.standardOutput = stdoutPipe + process.standardError = stderrPipe + + do { + try process.run() + process.waitUntilExit() + + let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() + + let stdout = String(data: stdoutData, encoding: .utf8) + let stderr = String(data: stderrData, encoding: .utf8) + + continuation.resume(returning: (stdout, stderr)) + } catch { + continuation.resume(throwing: error) + } + } + } + + /// Print a message to standard output. + /// Silenced unless in debug mode. + /// + /// - Parameter message: the content of the message + private func printStandardOutput(_ message: String?) { + guard debug, let data = message?.data(using: .utf8) else { return } + FileHandle.standardOutput.write(data) + } + + /// Print a message to standard error. + /// Silenced unless in debug mode. + /// + /// - Parameter message: the content of the message + private func printStandardError(_ message: String?) { + guard debug, let data = message?.data(using: .utf8) else { return } + FileHandle.standardError.write(data) + } +} diff --git a/Sources/phbar/Views/BarController.swift b/Sources/phbar/Views/BarController.swift index e22d3f7..b5ecc8a 100644 --- a/Sources/phbar/Views/BarController.swift +++ b/Sources/phbar/Views/BarController.swift @@ -101,7 +101,7 @@ extension BarController { /// Each block updates concurrently; the view refreshes as results arrive. func refresh() { for block in blocks { - Task { await block.update() } + block.resolve() } } } diff --git a/Sources/phbar/Views/TextView.swift b/Sources/phbar/Views/TextView.swift index 03ccac8..cb06267 100644 --- a/Sources/phbar/Views/TextView.swift +++ b/Sources/phbar/Views/TextView.swift @@ -47,7 +47,7 @@ struct TextView: View { .scrollWheel, ]) { event in if hovering, let gestureEvent = PHGestureEvent.from(event) { - block.handleGesture(gestureEvent) + block.resolve(with: gestureEvent) } return event }