refactor block resolve and refresh
This commit is contained in:
@@ -5,7 +5,6 @@ final class PHBlock: ObservableObject, Decodable, Identifiable {
|
|||||||
let id = UUID()
|
let id = UUID()
|
||||||
|
|
||||||
let name: String
|
let name: String
|
||||||
let _command: String?
|
|
||||||
let textName: String?
|
let textName: String?
|
||||||
@Published var text: PHThemeText = .default
|
@Published var text: PHThemeText = .default
|
||||||
let styleName: String?
|
let styleName: String?
|
||||||
@@ -14,8 +13,8 @@ final class PHBlock: ObservableObject, Decodable, Identifiable {
|
|||||||
let centered: Bool?
|
let centered: Bool?
|
||||||
var debug: Bool = false
|
var debug: Bool = false
|
||||||
|
|
||||||
var command: String {
|
nonisolated var command: String {
|
||||||
_command ?? PHPaths.configDirectory.appending(path: "scripts/\(name)").relativePath
|
PHPaths.configDirectory.appending(path: "scripts/\(name)").relativePath
|
||||||
}
|
}
|
||||||
|
|
||||||
var visible: Bool {
|
var visible: Bool {
|
||||||
@@ -44,11 +43,10 @@ final class PHBlock: ObservableObject, Decodable, Identifiable {
|
|||||||
|
|
||||||
/// Guards against stacking concurrent updates during rapid event bursts
|
/// Guards against stacking concurrent updates during rapid event bursts
|
||||||
/// (e.g. dragging the volume slider fires many events in quick succession).
|
/// (e.g. dragging the volume slider fires many events in quick succession).
|
||||||
var updateScheduled = false
|
var resolveScheduled = false
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case name
|
case name
|
||||||
case _command = "command"
|
|
||||||
case textName = "text"
|
case textName = "text"
|
||||||
case styleName = "style"
|
case styleName = "style"
|
||||||
case refresh, events, centered
|
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()
|
stopAutoRefresh()
|
||||||
|
|
||||||
// Immediate refresh so the bar isn't blank until the first trigger fires.
|
// Immediate refresh so the bar isn't blank until the first trigger fires.
|
||||||
scheduleUpdate()
|
resolve()
|
||||||
|
|
||||||
// 1. Periodic refresh.
|
// 1. Periodic refresh.
|
||||||
if let interval = refresh, interval > 0 {
|
if let interval = refresh, interval > 0 {
|
||||||
@@ -28,7 +28,7 @@ extension PHBlock {
|
|||||||
while !Task.isCancelled {
|
while !Task.isCancelled {
|
||||||
try? await Task.sleep(for: .milliseconds(milliseconds))
|
try? await Task.sleep(for: .milliseconds(milliseconds))
|
||||||
if Task.isCancelled { break }
|
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.
|
// system listener only while at least one block subscribes to it.
|
||||||
for event in (events ?? []) {
|
for event in (events ?? []) {
|
||||||
let subscription = registry.subscribe(event) { [weak self] in
|
let subscription = registry.subscribe(event) { [weak self] in
|
||||||
self?.scheduleUpdate()
|
self?.resolve()
|
||||||
}
|
}
|
||||||
subscriptions.append(subscription)
|
subscriptions.append(subscription)
|
||||||
}
|
}
|
||||||
@@ -50,80 +50,4 @@ extension PHBlock {
|
|||||||
subscriptions.forEach { $0.cancel() }
|
subscriptions.forEach { $0.cancel() }
|
||||||
subscriptions.removeAll()
|
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.
|
/// Each block updates concurrently; the view refreshes as results arrive.
|
||||||
func refresh() {
|
func refresh() {
|
||||||
for block in blocks {
|
for block in blocks {
|
||||||
Task { await block.update() }
|
block.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ struct TextView: View {
|
|||||||
.scrollWheel,
|
.scrollWheel,
|
||||||
]) { event in
|
]) { event in
|
||||||
if hovering, let gestureEvent = PHGestureEvent.from(event) {
|
if hovering, let gestureEvent = PHGestureEvent.from(event) {
|
||||||
block.handleGesture(gestureEvent)
|
block.resolve(with: gestureEvent)
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user