make events external dylibs

This commit is contained in:
2026-07-11 23:49:25 +02:00
parent 2d6348c901
commit 0ed9e4dbf2
15 changed files with 770 additions and 585 deletions
+45 -41
View File
@@ -189,7 +189,7 @@ private struct TestPayload: Codable, Equatable {
block.stopAutoRefresh()
let count = Int(block.label.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
let count = Int(block.label!.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
// At ~20Hz over 250ms the command should have run more than once.
#expect(count >= 2)
@@ -207,18 +207,20 @@ private struct TestPayload: Codable, Equatable {
let blocks = try PHBlock.load(from: toml)
#expect(blocks[0].events == [.volume, .network, .appearance, .power, .mpd])
#expect(blocks[0].events == [PHEvent("volume"), PHEvent("network"), PHEvent("appearance"), PHEvent("power"), PHEvent("mpd")])
}
@MainActor
@Test func eventRejectsUnknownString() {
#expect(throws: (any Error).self) {
try PHBlock.load(from: """
[[block]]
command = "echo hi"
events = ["totally_made_up"]
""")
}
@Test func eventAcceptsArbitraryStringAsCustom() throws {
// Unknown strings no longer fail decoding: they become custom event names
// resolved at runtime against ~/.config/phbar/events/<name>/.
let blocks = try PHBlock.load(from: """
[[block]]
command = "echo hi"
events = ["totally_made_up"]
""")
#expect(blocks[0].events == [PHEvent("totally_made_up")])
}
@MainActor
@@ -232,20 +234,22 @@ private struct TestPayload: Codable, Equatable {
}
@MainActor
@Test func defaultFactoryReturnsMPDSource() {
let source = PHEventRegistry.defaultFactory(.mpd)
#expect(source is MPDEventSource)
@Test func defaultFactoryReturnsNilWithoutRecognizer() {
// No recognizer installed at a clean directory factory yields nil.
let loader = PHEventLoader(directory: FileManager.default.temporaryDirectory)
let factory: @MainActor @Sendable (PHEvent) -> (any PHEventSource)? = { event in
guard let recognizer = loader.recognizer(for: event.rawValue) else { return nil }
return PHExternalEventSource(recognizer: recognizer)
}
#expect(factory(PHEvent("anything")) == nil)
}
@MainActor
@Test func mpdSourceIsIdempotentStartStop() {
// With MPD absent the source keeps trying to connect; ensure start/stop are
// safe and idempotent without a running daemon.
let source = MPDEventSource(host: "127.0.0.1", port: 1)
source.start(notify: {})
source.start(notify: {}) // second start is a no-op
source.stop()
source.stop() // second stop is a no-op
@Test func eventLoaderReturnsNilForMissingEvent() {
// A clean directory has no event folders, so every name fails to load.
let loader = PHEventLoader(directory: FileManager.default.temporaryDirectory)
let recognizer = loader.recognizer(for: "does-not-exist-\(UUID().uuidString)")
#expect(recognizer == nil)
}
// MARK: - PHEventRegistry
@@ -279,14 +283,14 @@ private final class FakeEventSource: PHEventSource {
#expect(fake.stopCount == 0)
var fired = 0
let s1 = registry.subscribe(.volume) { fired += 1 }
let s1 = registry.subscribe(PHEvent("volume")) { fired += 1 }
#expect(fake.startCount == 1)
#expect(registry.subscriberCount(for: .volume) == 1)
#expect(registry.subscriberCount(for: PHEvent("volume")) == 1)
// A second subscriber must reuse the already-running source.
let s2 = registry.subscribe(.volume) { fired += 1 }
let s2 = registry.subscribe(PHEvent("volume")) { fired += 1 }
#expect(fake.startCount == 1)
#expect(registry.subscriberCount(for: .volume) == 2)
#expect(registry.subscriberCount(for: PHEvent("volume")) == 2)
// One firing fans out to both subscribers.
fake.fire()
@@ -296,7 +300,7 @@ private final class FakeEventSource: PHEventSource {
s1.cancel()
try await Task.sleep(for: .milliseconds(20))
#expect(fake.stopCount == 0)
#expect(registry.subscriberCount(for: .volume) == 1)
#expect(registry.subscriberCount(for: PHEvent("volume")) == 1)
fake.fire()
#expect(fired == 3)
@@ -305,28 +309,26 @@ private final class FakeEventSource: PHEventSource {
s2.cancel()
try await Task.sleep(for: .milliseconds(20))
#expect(fake.stopCount == 1)
#expect(registry.subscriberCount(for: .volume) == 0)
#expect(registry.subscriberCount(for: PHEvent("volume")) == 0)
}
@MainActor
@Test func registryStartsSeparateSourcePerEvent() async throws {
let volumeFake = FakeEventSource()
let networkFake = FakeEventSource()
let factory: @MainActor @Sendable (PHEvent) -> any PHEventSource = { event in
switch event {
case .volume: return volumeFake
default: return networkFake
}
let factory: @MainActor @Sendable (PHEvent) -> (any PHEventSource)? = { event in
if event == PHEvent("volume") { return volumeFake }
return networkFake
}
let registry = PHEventRegistry(factory: factory)
let v = registry.subscribe(.volume) {}
let n = registry.subscribe(.network) {}
let v = registry.subscribe(PHEvent("volume")) {}
let n = registry.subscribe(PHEvent("network")) {}
#expect(volumeFake.startCount == 1)
#expect(networkFake.startCount == 1)
#expect(registry.subscriberCount(for: .volume) == 1)
#expect(registry.subscriberCount(for: .network) == 1)
#expect(registry.subscriberCount(for: PHEvent("volume")) == 1)
#expect(registry.subscriberCount(for: PHEvent("network")) == 1)
v.cancel()
n.cancel()
@@ -354,14 +356,14 @@ private final class FakeEventSource: PHEventSource {
block.startAutoRefresh()
try await Task.sleep(for: .milliseconds(100))
#expect(block.label.trimmingCharacters(in: .whitespacesAndNewlines) == "1")
#expect(block.label!.trimmingCharacters(in: .whitespacesAndNewlines) == "1")
#expect(fake.startCount == 1)
// Firing the event triggers a second refresh.
fake.fire()
try await Task.sleep(for: .milliseconds(100))
#expect(block.label.trimmingCharacters(in: .whitespacesAndNewlines) == "2")
#expect(block.label!.trimmingCharacters(in: .whitespacesAndNewlines) == "2")
// Stopping cancels the subscription and tears the source down.
block.stopAutoRefresh()
@@ -391,7 +393,7 @@ private final class FakeEventSource: PHEventSource {
block.startAutoRefresh()
try await Task.sleep(for: .milliseconds(50))
let afterInitial = Int(block.label.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
let afterInitial = Int(block.label!.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
#expect(afterInitial == 1)
#expect(fake.startCount == 1)
@@ -399,7 +401,7 @@ private final class FakeEventSource: PHEventSource {
fake.fire()
try await Task.sleep(for: .milliseconds(50))
let afterEvent = Int(block.label.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
let afterEvent = Int(block.label!.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
#expect(afterEvent >= 2)
block.stopAutoRefresh()
@@ -418,7 +420,9 @@ private final class FakeEventSource: PHEventSource {
command = "printf b"
""")
let controller = PHController(screen: screen, blocks: blocks)
let config = try PHConfig.load()
let theme = try PHTheme.load("voltage")
let controller = PHController(config: config, screen: screen, theme: theme, blocks: blocks)
controller.refresh()