make bar and blocks monitor scoped
This commit is contained in:
@@ -110,7 +110,7 @@ private struct TestPayload: Codable, Equatable {
|
||||
#expect(blocks[0].command == "echo hello")
|
||||
#expect(blocks[0].name == "greeting")
|
||||
#expect(blocks[0].refresh == 5.0)
|
||||
#expect(blocks[0].label == "")
|
||||
#expect(blocks[0].label == nil)
|
||||
#expect(blocks[1].name == nil)
|
||||
#expect(blocks[1].refresh == nil)
|
||||
}
|
||||
@@ -134,6 +134,26 @@ private struct TestPayload: Codable, Equatable {
|
||||
#expect(output == "panini")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func computeRunsInConfigDirectory() async throws {
|
||||
// Block commands execute with the config root (~/.config/phbar) as their
|
||||
// working directory, so a block from any set resolves relative paths the
|
||||
// same way. Guards against the config dir being absent in sandboxed CIs.
|
||||
guard FileManager.default.fileExists(atPath: PHBlock.configDirectory.path) else { return }
|
||||
|
||||
let blocks = try PHBlock.load(from: """
|
||||
[[block]]
|
||||
command = "pwd"
|
||||
""")
|
||||
|
||||
let output = await blocks[0].compute()
|
||||
|
||||
// Resolve symlinks on both sides: `~/.config/phbar` is commonly a
|
||||
// symlink into a dotfiles repo, and `pwd` reports the physical path.
|
||||
let expected = PHBlock.configDirectory.resolvingSymlinksInPath().path
|
||||
#expect(output == expected)
|
||||
}
|
||||
|
||||
// MARK: - PHBlock Refresh
|
||||
|
||||
@MainActor
|
||||
@@ -143,7 +163,7 @@ private struct TestPayload: Codable, Equatable {
|
||||
command = "printf panini"
|
||||
""")[0]
|
||||
|
||||
#expect(block.label == "")
|
||||
#expect(block.label == nil)
|
||||
|
||||
await block.update()
|
||||
|
||||
@@ -422,7 +442,7 @@ private final class FakeEventSource: PHEventSource {
|
||||
|
||||
let config = try PHConfig.load()
|
||||
let theme = try PHTheme.load("voltage")
|
||||
let controller = BarController(config: config, screen: screen, theme: theme, blocks: blocks)
|
||||
let controller = BarController(config: config, screen: screen, theme: theme, blocks: blocks, debug: false)
|
||||
|
||||
controller.refresh()
|
||||
|
||||
@@ -433,3 +453,206 @@ private final class FakeEventSource: PHEventSource {
|
||||
#expect(blocks[1].label == "b")
|
||||
}
|
||||
|
||||
// MARK: - PHThemeDimension
|
||||
|
||||
private struct DimensionWrapper: Decodable {
|
||||
let v: PHThemeDimension
|
||||
}
|
||||
|
||||
@Test func dimensionDecodesPoints() throws {
|
||||
let decoder = JSONDecoder()
|
||||
#expect(try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":400}"#.utf8)).v == .points(400))
|
||||
#expect(try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":400.5}"#.utf8)).v == .points(400.5))
|
||||
}
|
||||
|
||||
@Test func dimensionDecodesPercentage() throws {
|
||||
let decoder = JSONDecoder()
|
||||
#expect(try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":"100%"}"#.utf8)).v == .percentage(1.0))
|
||||
#expect(try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":"50%"}"#.utf8)).v == .percentage(0.5))
|
||||
}
|
||||
|
||||
@Test func dimensionRejectsGarbage() {
|
||||
let decoder = JSONDecoder()
|
||||
#expect(throws: (any Error).self) {
|
||||
try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":"wat"}"#.utf8))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func dimensionResolvesAgainstLength() {
|
||||
#expect(PHThemeDimension.points(250).resolve(against: 1000) == 250)
|
||||
#expect(PHThemeDimension.percentage(0.25).resolve(against: 1000) == 250)
|
||||
}
|
||||
|
||||
// MARK: - PHThemeAnchor geometry
|
||||
|
||||
@Test func anchorPlacesAtTopEdge() {
|
||||
let screen = CGRect(x: 0, y: 0, width: 1000, height: 800)
|
||||
let size = CGSize(width: 1000, height: 30)
|
||||
let origin = PHThemeAnchor.top.origin(in: screen, size: size, margin: .init())
|
||||
#expect(origin == CGPoint(x: 0, y: 770))
|
||||
}
|
||||
|
||||
@Test func anchorRespectsTopMargin() {
|
||||
let screen = CGRect(x: 0, y: 0, width: 1000, height: 800)
|
||||
let size = CGSize(width: 1000, height: 30)
|
||||
let origin = PHThemeAnchor.top.origin(in: screen, size: size, margin: .init(top: 10))
|
||||
#expect(origin == CGPoint(x: 0, y: 760))
|
||||
}
|
||||
|
||||
@Test func anchorPlacesBottomLeadingCorner() {
|
||||
let screen = CGRect(x: 0, y: 0, width: 1000, height: 800)
|
||||
let size = CGSize(width: 200, height: 40)
|
||||
let origin = PHThemeAnchor.bottomLeading.origin(
|
||||
in: screen, size: size, margin: .init(bottom: 8, leading: 12)
|
||||
)
|
||||
#expect(origin == CGPoint(x: 12, y: 8))
|
||||
}
|
||||
|
||||
@Test func anchorTrailingCentersVertically() {
|
||||
let screen = CGRect(x: 0, y: 0, width: 1000, height: 800)
|
||||
let size = CGSize(width: 200, height: 40)
|
||||
let origin = PHThemeAnchor.trailing.origin(
|
||||
in: screen, size: size, margin: .init(trailing: 20)
|
||||
)
|
||||
#expect(origin == CGPoint(x: 780, y: 380))
|
||||
}
|
||||
|
||||
@Test func anchorCentersOnBothAxesIgnoringMargin() {
|
||||
let screen = CGRect(x: 0, y: 0, width: 1000, height: 800)
|
||||
let size = CGSize(width: 200, height: 40)
|
||||
let origin = PHThemeAnchor.center.origin(in: screen, size: size, margin: .init(top: 999))
|
||||
#expect(origin == CGPoint(x: 400, y: 380))
|
||||
}
|
||||
|
||||
// MARK: - Monitor → window/blocks resolver
|
||||
|
||||
@Test func windowNameFallsBackToGlobal() {
|
||||
let config = PHConfig(window: "default", blocks: nil, env: nil, monitors: nil)
|
||||
#expect(config.windowName(screenName: "Built-in", screenIndex: 0) == "default")
|
||||
}
|
||||
|
||||
@Test func windowNameMatchesByIndex() {
|
||||
let config = PHConfig(window: "default", blocks: nil, env: nil, monitors: ["1": .init(window: "bottom", blocks: nil)])
|
||||
#expect(config.windowName(screenName: "Whatever", screenIndex: 1) == "bottom")
|
||||
}
|
||||
|
||||
@Test func windowNameMatchesByName() {
|
||||
let config = PHConfig(window: "default", blocks: nil, env: nil, monitors: ["DELL U2723QE": .init(window: "clock", blocks: nil)])
|
||||
#expect(config.windowName(screenName: "DELL U2723QE", screenIndex: 0) == "clock")
|
||||
}
|
||||
|
||||
@Test func monitorOverridePrefersNameOverIndex() {
|
||||
let config = PHConfig(
|
||||
window: "default", blocks: nil, env: nil,
|
||||
monitors: ["0": .init(window: "byIndex", blocks: nil), "DELL": .init(window: "byName", blocks: nil)]
|
||||
)
|
||||
#expect(config.windowName(screenName: "DELL", screenIndex: 0) == "byName")
|
||||
}
|
||||
|
||||
@Test func windowInheritsGlobalWhenOverrideOmitsIt() {
|
||||
// Override sets blocks only → window falls back to the global.
|
||||
let config = PHConfig(window: "top", blocks: nil, env: nil, monitors: ["1": .init(window: nil, blocks: "laptop")])
|
||||
#expect(config.windowName(screenName: "S", screenIndex: 1) == "top")
|
||||
#expect(config.blocksName(screenName: "S", screenIndex: 1) == "laptop")
|
||||
}
|
||||
|
||||
@Test func blocksNameDefaultsToDefault() {
|
||||
let config = PHConfig(window: "default", blocks: nil, env: nil, monitors: nil)
|
||||
#expect(config.blocksName(screenName: "S", screenIndex: 0) == "default")
|
||||
}
|
||||
|
||||
@Test func blocksNameUsesGlobalWhenNoOverride() {
|
||||
let config = PHConfig(window: "default", blocks: "main", env: nil, monitors: nil)
|
||||
#expect(config.blocksName(screenName: "S", screenIndex: 0) == "main")
|
||||
}
|
||||
|
||||
@Test func blocksOverrideBeatsGlobal() {
|
||||
let config = PHConfig(window: "default", blocks: "main", env: nil, monitors: ["1": .init(window: nil, blocks: "alt")])
|
||||
#expect(config.blocksName(screenName: "S", screenIndex: 1) == "alt")
|
||||
}
|
||||
|
||||
@Test func windowAndBlocksResolveIndependently() {
|
||||
let config = PHConfig(
|
||||
window: "top", blocks: "main", env: nil,
|
||||
monitors: [
|
||||
"DELL": .init(window: "clock", blocks: nil), // inherits blocks "main"
|
||||
"1": .init(window: nil, blocks: "laptop"), // inherits window "top"
|
||||
]
|
||||
)
|
||||
#expect(config.windowName(screenName: "DELL", screenIndex: 0) == "clock")
|
||||
#expect(config.blocksName(screenName: "DELL", screenIndex: 0) == "main")
|
||||
#expect(config.windowName(screenName: "S", screenIndex: 1) == "top")
|
||||
#expect(config.blocksName(screenName: "S", screenIndex: 1) == "laptop")
|
||||
}
|
||||
|
||||
// MARK: - PHBlock set loading
|
||||
|
||||
private func makeBlocksConfigDir() throws -> URL {
|
||||
let dir = FileManager.default.temporaryDirectory.appending(path: "phbar_blocks_\(UUID().uuidString)")
|
||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
return dir
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func loadSetReadsNamedFile() throws {
|
||||
let dir = try makeBlocksConfigDir()
|
||||
defer { try? FileManager.default.removeItem(at: dir) }
|
||||
|
||||
let blocksDir = dir.appending(path: "blocks")
|
||||
try FileManager.default.createDirectory(at: blocksDir, withIntermediateDirectories: true)
|
||||
try Data("""
|
||||
[[block]]
|
||||
command = "echo hi"
|
||||
name = "greeting"
|
||||
""".utf8).write(to: blocksDir.appending(path: "laptop.toml"))
|
||||
|
||||
let blocks = try PHBlock.load("laptop", in: dir)
|
||||
|
||||
#expect(blocks.count == 1)
|
||||
#expect(blocks[0].name == "greeting")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func loadSetThrowsForMissingSet() {
|
||||
let dir = FileManager.default.temporaryDirectory.appending(path: "phbar_missing_\(UUID().uuidString)")
|
||||
#expect(throws: (any Error).self) {
|
||||
try PHBlock.load("nope", in: dir)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BarWindow.computeFrame
|
||||
|
||||
@MainActor
|
||||
@Test func computeFrameDefaultsToFullScreenTopBar() throws {
|
||||
let screen = try #require(NSScreen.main)
|
||||
let theme = try PHTheme.load("voltage")
|
||||
let config = PHConfig(window: "default", blocks: nil, env: nil, monitors: nil)
|
||||
let blocks = try PHBlock.load(from: "[[block]]\ncommand = \"echo x\"")
|
||||
let controller = BarController(config: config, screen: screen, theme: theme, blocks: blocks, debug: false)
|
||||
|
||||
let frame = BarWindow.computeFrame(from: controller)
|
||||
|
||||
#expect(frame.width == screen.frame.width)
|
||||
#expect(frame.minX == screen.frame.minX)
|
||||
#expect(frame.minY == screen.frame.maxY - 30)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func computeFrameUsesAbsoluteOriginOverAnchor() throws {
|
||||
let screen = try #require(NSScreen.main)
|
||||
let theme = PHTheme(
|
||||
windows: [PHThemeWindow(name: "abs", anchor: .bottom, origin: PHThemePoint(x: 100, y: 200))],
|
||||
texts: nil,
|
||||
styles: nil
|
||||
)
|
||||
let config = PHConfig(window: "abs", blocks: nil, env: nil, monitors: nil)
|
||||
let blocks = try PHBlock.load(from: "[[block]]\ncommand = \"echo x\"")
|
||||
let controller = BarController(config: config, screen: screen, theme: theme, blocks: blocks, debug: false)
|
||||
|
||||
let frame = BarWindow.computeFrame(from: controller)
|
||||
|
||||
#expect(frame.origin == CGPoint(x: 100, y: 200))
|
||||
#expect(frame.height == 30)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user