refactor block resolve and refresh
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user