diff options
| -rw-r--r-- | Sources/bar/Models/Block.swift | 7 | ||||
| -rw-r--r-- | Sources/bar/Models/Cell.swift | 20 | ||||
| -rw-r--r-- | Sources/bar/Models/Runner.swift | 6 | ||||
| -rw-r--r-- | Tests/barTests/BlockRefreshTests.swift | 29 |
4 files changed, 52 insertions, 10 deletions
diff --git a/Sources/bar/Models/Block.swift b/Sources/bar/Models/Block.swift index a9881f7..6f2b30d 100644 --- a/Sources/bar/Models/Block.swift +++ b/Sources/bar/Models/Block.swift @@ -44,6 +44,7 @@ final class Block: ObservableObject, Identifiable { @Published var cells: [Cell] = [] private var tick: Task<Void, Never>? + private var lastLines: [String]? init(name: String, placement: Config.Block.Placement, command: String, interval: Double? = nil) { self.name = name @@ -81,7 +82,11 @@ final class Block: ObservableObject, Identifiable { /// 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]) { + func apply(_ lines: [String]) { + // A refresh often returns the same text. Avoid parsing and invalidating + // every hosting view when nothing changed (including highlight prefixes). + guard lines != lastLines else { return } + lastLines = lines self.cells = lines.map { Cell(rawValue: $0) } } diff --git a/Sources/bar/Models/Cell.swift b/Sources/bar/Models/Cell.swift index d715fc2..b7911b5 100644 --- a/Sources/bar/Models/Cell.swift +++ b/Sources/bar/Models/Cell.swift @@ -1,5 +1,6 @@ import RegexBuilder import SwiftUI +import Synchronization /// A thin wrapper around a single text line captured /// by a block. @@ -27,6 +28,15 @@ import SwiftUI /// within a group. For example when you want to show /// window manager workspaces and which one is active. struct Cell: RawRepresentable, Identifiable { + /// Build the parser once rather than on every refresh of every cell. + private static let pattern = Mutex(Regex { + Optionally { + "&!" + Capture { OneOrMore(.digit) } + } + Capture { OneOrMore(.any) } + }) + /// A content-derived identifier combining text and highlight. /// /// Identical cells share this value and content changes change it. @@ -41,15 +51,7 @@ struct Cell: RawRepresentable, Identifiable { var background: Color init(rawValue: String) { - let pattern = Regex { - Optionally { - "&!" - Capture { OneOrMore(.digit) } - } - Capture { OneOrMore(.any) } - } - - guard let match = rawValue.firstMatch(of: pattern) else { + guard let match = Self.pattern.withLock({ rawValue.firstMatch(of: $0) }) else { self.rawValue = rawValue self.background = .clear self.id = "\(rawValue):0" diff --git a/Sources/bar/Models/Runner.swift b/Sources/bar/Models/Runner.swift index 1fb88ad..6c90d54 100644 --- a/Sources/bar/Models/Runner.swift +++ b/Sources/bar/Models/Runner.swift @@ -21,6 +21,12 @@ enum Runner { /// nonzero exit, or [""] if successful output cannot be decoded as UTF-8. /// Captured stderr is forwarded only after a nonzero exit. static func run(_ command: String, env: [String: String] = [:]) -> [String] { + // Detached tasks do not run inside AppKit's main event-loop pool. + // Drain temporary Foundation objects at the end of each command. + autoreleasepool { execute(command, env: env) } + } + + private static func execute(_ command: String, env: [String: String]) -> [String] { let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/sh") process.arguments = ["-c", command] diff --git a/Tests/barTests/BlockRefreshTests.swift b/Tests/barTests/BlockRefreshTests.swift new file mode 100644 index 0000000..495bcac --- /dev/null +++ b/Tests/barTests/BlockRefreshTests.swift @@ -0,0 +1,29 @@ +import Combine +import Testing +@testable import bar + +@Test @MainActor func unchangedOutputDoesNotPublish() { + let block = Block(name: "test", placement: .left, command: "") + var publications = 0 + let subscription = block.$cells.dropFirst().sink { _ in publications += 1 } + defer { subscription.cancel() } + + block.apply(["same", "same"]) + block.apply(["same", "same"]) + #expect(publications == 1) + #expect(block.cells.map(\.rawValue) == ["same", "same"]) + + // A highlight-only change must still publish, even with identical text. + block.apply(["&!1 same", "same"]) + #expect(publications == 2) + #expect(block.cells[0].id == " same:1") + block.apply([]) + block.apply([]) + #expect(publications == 3) + #expect(block.cells.isEmpty) +} + +@Test func runnerPreservesOutputAcrossPoolDrain() { + #expect(Runner.run("printf 'first\\n\\nlast\\n'") == ["first", "", "last"]) + #expect(Runner.run("exit 1") == ["ERROR"]) +} |
