Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 197 additions & 49 deletions Sources/GhosttyConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,36 +103,24 @@ struct GhosttyConfig {
return []
}

func paths(for bundleIdentifier: String) -> [String] {
let directory = appSupport.appendingPathComponent(bundleIdentifier, isDirectory: true)
return [
directory.appendingPathComponent("config", isDirectory: false).path,
directory.appendingPathComponent("config.ghostty", isDirectory: false).path,
]
}

func hasConfig(_ paths: [String]) -> Bool {
paths.contains { path in
guard let attributes = try? fileManager.attributesOfItem(atPath: path),
let type = attributes[.type] as? FileAttributeType,
type == .typeRegular,
let size = attributes[.size] as? NSNumber else {
return false
}
return size.intValue > 0
}
}

let releasePaths = paths(for: cmuxReleaseBundleIdentifier)
let releasePaths = cmuxAppSupportConfigURLs(
currentBundleIdentifier: cmuxReleaseBundleIdentifier,
appSupportDirectory: appSupport,
fileManager: fileManager
).map(\.path)
guard let currentBundleIdentifier, !currentBundleIdentifier.isEmpty else {
return releasePaths
}
if currentBundleIdentifier == cmuxReleaseBundleIdentifier {
return releasePaths
}

let currentPaths = paths(for: currentBundleIdentifier)
if hasConfig(currentPaths) {
let currentPaths = cmuxAppSupportConfigURLs(
currentBundleIdentifier: currentBundleIdentifier,
appSupportDirectory: appSupport,
fileManager: fileManager
).map(\.path)
if !currentPaths.isEmpty {
return currentPaths
}
if SocketControlSettings.isDebugLikeBundleIdentifier(currentBundleIdentifier) {
Expand All @@ -141,6 +129,151 @@ struct GhosttyConfig {
return []
}

private static func canonicalConfigPath(
for url: URL,
fileManager: FileManager = .default
) -> String? {
let path = url.standardizedFileURL.path
guard fileManager.fileExists(atPath: path) else { return nil }
return URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL.path
}

private static func existingNonEmptyConfigURL(
for url: URL,
fileManager: FileManager = .default
) -> URL? {
func hasNonEmptyRegularFileAttributes(at path: String) -> Bool {
guard let attributes = try? fileManager.attributesOfItem(atPath: path),
let type = attributes[.type] as? FileAttributeType,
type == .typeRegular,
let size = attributes[.size] as? NSNumber else {
return false
}
return size.intValue > 0
}

guard let attributes = try? fileManager.attributesOfItem(atPath: url.path),
let type = attributes[.type] as? FileAttributeType else {
return nil
}

if type == .typeRegular {
guard let size = attributes[.size] as? NSNumber, size.intValue > 0 else {
return nil
}
return url
}

guard type == .typeSymbolicLink else {
return nil
}
guard let resolvedPath = canonicalConfigPath(for: url, fileManager: fileManager) else {
return nil
}
return hasNonEmptyRegularFileAttributes(at: resolvedPath) ? url : nil
}

private static func defaultConfigURLs(
homeDirectory: URL,
environment: [String: String] = ProcessInfo.processInfo.environment
) -> [URL] {
let xdgConfigRoot: URL = {
if let raw = environment["XDG_CONFIG_HOME"], !raw.isEmpty {
let normalized: String
if raw == "~" {
normalized = homeDirectory.path
} else if raw.hasPrefix("~/") {
normalized = homeDirectory
.appendingPathComponent(String(raw.dropFirst(2)), isDirectory: true)
.path
} else {
normalized = raw
}
if (normalized as NSString).isAbsolutePath {
return URL(fileURLWithPath: normalized, isDirectory: true)
}
}
return homeDirectory.appendingPathComponent(".config", isDirectory: true)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}()

return [
xdgConfigRoot.appendingPathComponent("ghostty/config", isDirectory: false),
xdgConfigRoot.appendingPathComponent("ghostty/config.ghostty", isDirectory: false),
homeDirectory.appendingPathComponent(
"Library/Application Support/com.mitchellh.ghostty/config",
isDirectory: false
),
homeDirectory.appendingPathComponent(
"Library/Application Support/com.mitchellh.ghostty/config.ghostty",
isDirectory: false
),
]
}

static func cmuxAppSupportConfigURLs(
currentBundleIdentifier: String?,
appSupportDirectory: URL,
fileManager: FileManager = .default
) -> [URL] {
guard let currentBundleIdentifier, !currentBundleIdentifier.isEmpty else { return [] }

func existingConfigURLs(for bundleIdentifier: String) -> [URL] {
let directory = appSupportDirectory.appendingPathComponent(bundleIdentifier, isDirectory: true)
return [
directory.appendingPathComponent("config", isDirectory: false),
directory.appendingPathComponent("config.ghostty", isDirectory: false),
].compactMap { existingNonEmptyConfigURL(for: $0, fileManager: fileManager) }
}

let currentURLs = existingConfigURLs(for: currentBundleIdentifier)
if !currentURLs.isEmpty {
return currentURLs
}
if SocketControlSettings.isDebugLikeBundleIdentifier(currentBundleIdentifier) {
let releaseURLs = existingConfigURLs(for: cmuxReleaseBundleIdentifier)
if !releaseURLs.isEmpty {
return releaseURLs
}
}
return []
}

static func editorConfigURLs(
fileManager: FileManager = .default,
currentBundleIdentifier: String? = Bundle.main.bundleIdentifier,
appSupportDirectory: URL? = nil,
homeDirectory: URL? = nil,
environment: [String: String] = ProcessInfo.processInfo.environment
) -> [URL] {
let resolvedHomeDirectory = homeDirectory ?? fileManager.homeDirectoryForCurrentUser
let resolvedAppSupportDirectory = appSupportDirectory ?? fileManager.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first
let defaultURLs = defaultConfigURLs(
homeDirectory: resolvedHomeDirectory,
environment: environment
)

var urls = defaultURLs.compactMap { existingNonEmptyConfigURL(for: $0, fileManager: fileManager) }

if let resolvedAppSupportDirectory {
urls.append(
contentsOf: Self.cmuxAppSupportConfigURLs(
currentBundleIdentifier: currentBundleIdentifier,
appSupportDirectory: resolvedAppSupportDirectory,
fileManager: fileManager
)
)
}

if !urls.isEmpty {
return urls
}

return [defaultURLs[0]]
}

mutating func resolveSidebarBackground(preferredColorScheme: ColorSchemePreference) {
guard let raw = rawSidebarBackground else { return }

Expand Down Expand Up @@ -189,19 +322,37 @@ struct GhosttyConfig {
}
}

private static func loadFromDisk(preferredColorScheme: ColorSchemePreference) -> GhosttyConfig {
static func loadFromDiskForTests(
preferredColorScheme: ColorSchemePreference,
fileManager: FileManager = .default,
currentBundleIdentifier: String? = Bundle.main.bundleIdentifier,
environment: [String: String] = ProcessInfo.processInfo.environment,
homeDirectory: URL? = nil,
appSupportDirectory: URL? = nil,
bundleResourceURL: URL? = Bundle.main.resourceURL
) -> GhosttyConfig {
var config = GhosttyConfig()

// Match Ghostty's default load order on macOS.
let configPaths = [
"~/.config/ghostty/config",
"~/.config/ghostty/config.ghostty",
"~/Library/Application Support/com.mitchellh.ghostty/config",
"~/Library/Application Support/com.mitchellh.ghostty/config.ghostty",
].map { NSString(string: $0).expandingTildeInPath } + cmuxConfigPaths()
let resolvedHomeDirectory = homeDirectory ?? fileManager.homeDirectoryForCurrentUser
let resolvedAppSupportDirectory = appSupportDirectory ?? fileManager.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first

let configPaths = defaultConfigURLs(
homeDirectory: resolvedHomeDirectory,
environment: environment
).map(\.path) + (
resolvedAppSupportDirectory.map {
cmuxAppSupportConfigURLs(
currentBundleIdentifier: currentBundleIdentifier,
appSupportDirectory: $0,
fileManager: fileManager
).map(\.path)
} ?? []
)

for path in configPaths {
if let contents = readConfigFile(at: path) {
if let contents = readConfigFile(at: path, using: fileManager) {
config.parse(contents)
}
}
Expand All @@ -210,8 +361,8 @@ struct GhosttyConfig {
if let themeName = config.theme {
config.loadTheme(
themeName,
environment: ProcessInfo.processInfo.environment,
bundleResourceURL: Bundle.main.resourceURL,
environment: environment,
bundleResourceURL: bundleResourceURL,
preferredColorScheme: preferredColorScheme
)
}
Expand All @@ -222,6 +373,10 @@ struct GhosttyConfig {
return config
}

private static func loadFromDisk(preferredColorScheme: ColorSchemePreference) -> GhosttyConfig {
loadFromDiskForTests(preferredColorScheme: preferredColorScheme)
}

mutating func parse(_ contents: String) {
let lines = contents.components(separatedBy: .newlines)
for line in lines {
Expand Down Expand Up @@ -522,20 +677,13 @@ struct GhosttyConfig {
return paths
}

private static func readConfigFile(at path: String) -> String? {
let fileManager = FileManager.default
guard fileManager.fileExists(atPath: path) else { return nil }

if let attributes = try? fileManager.attributesOfItem(atPath: path) {
if let type = attributes[.type] as? FileAttributeType, type != .typeRegular {
return nil
}
if let size = attributes[.size] as? NSNumber, size.intValue == 0 {
return nil
}
}

return try? String(contentsOfFile: path, encoding: .utf8)
private static func readConfigFile(at path: String, using fileManager: FileManager = .default) -> String? {
guard let url = existingNonEmptyConfigURL(
for: URL(fileURLWithPath: path),
fileManager: fileManager
) else { return nil }
let resolvedPath = canonicalConfigPath(for: url, fileManager: fileManager) ?? url.path
return try? String(contentsOfFile: resolvedPath, encoding: .utf8)
}
}

Expand Down
Loading