native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180import Foundationimport GhosttyKitimport Testing
@testable import Prowl
struct GhosttyConfigSourceTests { @Test func blankPathSelectsGhosttyDefaultFiles() { for path: String? in [nil, "", " \n"] { #expect(GhosttyConfigSource(dedicatedPath: path) == .ghosttyDefault) } }
@Test func dedicatedPathIsTrimmedAndTildeExpanded() { let home = FileManager.default.homeDirectoryForCurrentUser.path(percentEncoded: false) let source = GhosttyConfigSource(dedicatedPath: " ~/prowl/ghostty.conf ") guard case .file(let path) = source else { Issue.record("Expected a dedicated file, got \(source)") return } #expect(path.hasPrefix(home.hasSuffix("/") ? home : home + "/")) #expect(path.hasSuffix("/prowl/ghostty.conf")) #expect(GhosttyConfigSource.normalizedPath(" /tmp/a.conf ") == "/tmp/a.conf") }
@Test func dedicatedFileIsTheEditableAndOnlyRootFile() { let source = GhosttyConfigSource.file(path: "/tmp/prowl-ghostty.conf") #expect(source.editableFilePath == "/tmp/prowl-ghostty.conf") #expect(source.userConfigFileURLs == [URL(fileURLWithPath: "/tmp/prowl-ghostty.conf")]) }
@Test func sharedSourceListsGhosttyDefaultFilesInLoadOrder() { let names = GhosttyConfigSource.ghosttyDefault.userConfigFileURLs.map { "\($0.deletingLastPathComponent().lastPathComponent)/\($0.lastPathComponent)" } #expect( names == [ "ghostty/config", "ghostty/config.ghostty", "com.mitchellh.ghostty/config", "com.mitchellh.ghostty/config.ghostty", ]) }
@Test func dedicatedFileReplacesTheSharedConfig() throws { let directory = try Self.makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } let file = directory.appending(path: "prowl.ghostty") try "font-size = 23\n".write(to: file, atomically: true, encoding: .utf8)
let config = try #require( GhosttyRuntime.makeConfig(source: .file(path: file.path(percentEncoded: false)), overrideFileURLs: []) ) defer { ghostty_config_free(config) } #expect(Self.fontSize(config) == 23) }
@Test func dedicatedFileFollowsItsIncludes() throws { let directory = try Self.makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } let included = directory.appending(path: "fonts.ghostty") try "font-size = 19\n".write(to: included, atomically: true, encoding: .utf8) let file = directory.appending(path: "prowl.ghostty") try "config-file = fonts.ghostty\n".write(to: file, atomically: true, encoding: .utf8)
let config = try #require( GhosttyRuntime.makeConfig(source: .file(path: file.path(percentEncoded: false)), overrideFileURLs: []) ) defer { ghostty_config_free(config) } #expect(Self.fontSize(config) == 19) }
/// A missing dedicated file loads nothing; the result matches an empty file, /// so no shared Ghostty setting leaks in. @Test func missingDedicatedFileLoadsOnlyBuiltInDefaults() throws { let directory = try Self.makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } let empty = directory.appending(path: "empty.ghostty") try "".write(to: empty, atomically: true, encoding: .utf8) let missing = directory.appending(path: "missing.ghostty")
let emptyConfig = try #require( GhosttyRuntime.makeConfig(source: .file(path: empty.path(percentEncoded: false)), overrideFileURLs: []) ) defer { ghostty_config_free(emptyConfig) } let missingConfig = try #require( GhosttyRuntime.makeConfig(source: .file(path: missing.path(percentEncoded: false)), overrideFileURLs: []) ) defer { ghostty_config_free(missingConfig) } #expect(Self.fontSize(missingConfig) == Self.fontSize(emptyConfig)) #expect(!FileManager.default.fileExists(atPath: missing.path(percentEncoded: false))) }
@Test func overrideFilesWinOverTheDedicatedFile() throws { let directory = try Self.makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } let file = directory.appending(path: "prowl.ghostty") try "font-size = 23\n".write(to: file, atomically: true, encoding: .utf8) let override = directory.appending(path: "override.conf") try "font-size = 15\n".write(to: override, atomically: true, encoding: .utf8)
let config = try #require( GhosttyRuntime.makeConfig(source: .file(path: file.path(percentEncoded: false)), overrideFileURLs: [override]) ) defer { ghostty_config_free(config) } #expect(Self.fontSize(config) == 15) }
@Test func dedicatedFileSnapshotReadsBackgroundAndRawTheme() throws { let directory = try Self.makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } let file = directory.appending(path: "prowl.ghostty") try "background = #fafafa\n".write(to: file, atomically: true, encoding: .utf8)
let source = GhosttyConfigSource.file(path: file.path(percentEncoded: false)) let snapshot = try #require(GhosttyRuntime.userConfigSnapshot(loading: source)) #expect(snapshot == GhosttyUserConfigSnapshot(themeMode: .none, backgroundTone: .light))
try "background = #101010\ntheme = light:A,dark:B\n".write(to: file, atomically: true, encoding: .utf8) let dual = try #require(GhosttyRuntime.userConfigSnapshot(loading: source)) #expect(dual.themeMode == .dual) #expect(dual.backgroundTone == .dark) }
/// A light/dark pair set in an include is the user's explicit choice, so the /// theme fallback must not treat the config as having no theme. @Test func dedicatedFileSnapshotFollowsIncludesForTheTheme() throws { let directory = try Self.makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } try "theme = light:My Light,dark:My Dark\n".write( to: directory.appending(path: "themes.ghostty"), atomically: true, encoding: .utf8) let file = directory.appending(path: "prowl.ghostty") try "background = #101010\nconfig-file = themes.ghostty\n".write(to: file, atomically: true, encoding: .utf8)
let source = GhosttyConfigSource.file(path: file.path(percentEncoded: false)) let snapshot = try #require(GhosttyRuntime.userConfigSnapshot(loading: source)) #expect(snapshot.themeMode == .dual) }
/// A blank `config-file` drops the include, so its light/dark pair never applies. @Test func clearedIncludeThemeIsNotTheUsersTheme() throws { let directory = try Self.makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } try "theme = light:My Light,dark:My Dark\n".write( to: directory.appending(path: "themes.ghostty"), atomically: true, encoding: .utf8) let file = directory.appending(path: "prowl.ghostty") try "config-file = themes.ghostty\nconfig-file =\n".write(to: file, atomically: true, encoding: .utf8)
let source = GhosttyConfigSource.file(path: file.path(percentEncoded: false)) #expect(GhosttyRuntime.rawUserThemeMode(source: source) == nil) let snapshot = try #require(GhosttyRuntime.userConfigSnapshot(loading: source)) #expect(snapshot.themeMode == .none) }
/// Ghostty loads includes after the file that names them, so an included theme wins. @Test func includedThemeWinsOverTheRootTheme() throws { let directory = try Self.makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } try "theme = Single\n".write( to: directory.appending(path: "themes.ghostty"), atomically: true, encoding: .utf8) let file = directory.appending(path: "prowl.ghostty") try "config-file = themes.ghostty\ntheme = light:A,dark:B\n".write(to: file, atomically: true, encoding: .utf8)
let source = GhosttyConfigSource.file(path: file.path(percentEncoded: false)) #expect(GhosttyRuntime.rawUserThemeMode(source: source) == .single) }
private static func makeDirectory() throws -> URL { let directory = FileManager.default.temporaryDirectory .appending(path: "GhosttyConfigSourceTests-\(UUID().uuidString)", directoryHint: .isDirectory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) return directory }
private static func fontSize(_ config: ghostty_config_t) -> Float { var value: Float = 0 let key = "font-size" _ = ghostty_config_get(config, &value, key, UInt(key.lengthOfBytes(using: .utf8))) return value }}