diff --git a/macos/Sources/Features/Settings/SettingsView.swift b/macos/Sources/Features/Settings/SettingsView.swift index 82d24181a..6b0a2c46c 100644 --- a/macos/Sources/Features/Settings/SettingsView.swift +++ b/macos/Sources/Features/Settings/SettingsView.swift @@ -14,7 +14,7 @@ struct SettingsView: View { VStack(alignment: .leading) { Text("Coming Soon. 🚧").font(.title) Text("You can't configure settings in the GUI yet. To modify settings, " + - "edit the file at $HOME/.config/ghostty/config and restart Ghostty.") + "edit the file at $HOME/.config/ghostty/config.ghostty and restart Ghostty.") .multilineTextAlignment(.leading) .lineLimit(nil) } diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 7880a98a0..b80aef97e 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -225,7 +225,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { // 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes' // directory. The syntax then needs to be mapped to the correct language in // the config file within the '~.config/bat' directory - // (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config"). + // (ex: --map-syntax "/Users/user/.config/ghostty/config.ghostty:Ghostty Config"). { const run = b.addRunArtifact(build_data_exe); run.addArg("+sublime"); diff --git a/src/build/mdgen/ghostty_1_footer.md b/src/build/mdgen/ghostty_1_footer.md index f8e502b45..88aa16273 100644 --- a/src/build/mdgen/ghostty_1_footer.md +++ b/src/build/mdgen/ghostty_1_footer.md @@ -1,15 +1,15 @@ # FILES -_\$XDG_CONFIG_HOME/ghostty/config_ +_\$XDG_CONFIG_HOME/ghostty/config.ghostty_ : Location of the default configuration file. -_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_ +_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_ : **On macOS**, location of the default configuration file. This location takes precedence over the XDG environment locations. -_\$LOCALAPPDATA/ghostty/config_ +_\$LOCALAPPDATA/ghostty/config.ghostty_ : **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched for configuration files. diff --git a/src/build/mdgen/ghostty_5_footer.md b/src/build/mdgen/ghostty_5_footer.md index 380d83a53..d2cf024d1 100644 --- a/src/build/mdgen/ghostty_5_footer.md +++ b/src/build/mdgen/ghostty_5_footer.md @@ -1,15 +1,15 @@ # FILES -_\$XDG_CONFIG_HOME/ghostty/config_ +_\$XDG_CONFIG_HOME/ghostty/config.ghostty_ : Location of the default configuration file. -_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_ +_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_ : **On macOS**, location of the default configuration file. This location takes precedence over the XDG environment locations. -_\$LOCALAPPDATA/ghostty/config_ +_\$LOCALAPPDATA/ghostty/config.ghostty_ : **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched for configuration files. diff --git a/src/build/mdgen/ghostty_5_header.md b/src/build/mdgen/ghostty_5_header.md index 078133861..b9d4cb751 100644 --- a/src/build/mdgen/ghostty_5_header.md +++ b/src/build/mdgen/ghostty_5_header.md @@ -8,11 +8,11 @@ To configure Ghostty, you must use a configuration file. GUI-based configuration is on the roadmap but not yet supported. The configuration file must be placed -at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to `~/.config/ghostty/config` +at `$XDG_CONFIG_HOME/ghostty/config.ghostty`, which defaults to `~/.config/ghostty/config.ghostty` if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). **If you are using macOS, the configuration file can also be placed at -`$HOME/Library/Application Support/com.mitchellh.ghostty/config`.** This is the +`$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty`.** This is the default configuration location for macOS. It will be searched before any of the XDG environment locations listed above. diff --git a/src/cli/edit_config.zig b/src/cli/edit_config.zig index f103ca4a0..37f961a44 100644 --- a/src/cli/edit_config.zig +++ b/src/cli/edit_config.zig @@ -30,9 +30,9 @@ pub const Options = struct { /// this yet. /// /// The filepath opened is the default user-specific configuration -/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config`. +/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config.ghostty`. /// On macOS, this may also be located at -/// `~/Library/Application Support/com.mitchellh.ghostty/config`. +/// `~/Library/Application Support/com.mitchellh.ghostty/config.ghostty`. /// On macOS, whichever path exists and is non-empty will be prioritized, /// prioritizing the Application Support directory if neither are /// non-empty. @@ -73,7 +73,7 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 { defer config.deinit(); // Find the preferred path. - const path = try Config.preferredDefaultFilePath(alloc); + const path = try configpkg.preferredDefaultFilePath(alloc); defer alloc.free(path); // We don't currently support Windows because we use the exec syscall. diff --git a/src/config.zig b/src/config.zig index a596eb5e6..4abd319a6 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); +const file_load = @import("config/file_load.zig"); const formatter = @import("config/formatter.zig"); pub const Config = @import("config/Config.zig"); pub const conditional = @import("config/conditional.zig"); @@ -12,6 +13,7 @@ pub const ConditionalState = conditional.State; pub const FileFormatter = formatter.FileFormatter; pub const entryFormatter = formatter.entryFormatter; pub const formatEntry = formatter.formatEntry; +pub const preferredDefaultFilePath = file_load.preferredDefaultFilePath; // Field types pub const BoldColor = Config.BoldColor; diff --git a/src/config/Config.zig b/src/config/Config.zig index 7d8c7d9ff..aabefcd8f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -24,6 +24,7 @@ const cli = @import("../cli.zig"); const conditional = @import("conditional.zig"); const Conditional = conditional.Conditional; +const file_load = @import("file_load.zig"); const formatterpkg = @import("formatter.zig"); const themepkg = @import("theme.zig"); const url = @import("url.zig"); @@ -2071,7 +2072,7 @@ keybind: Keybinds = .{}, /// When this is true, the default configuration file paths will be loaded. /// The default configuration file paths are currently only the XDG -/// config path ($XDG_CONFIG_HOME/ghostty/config). +/// config path ($XDG_CONFIG_HOME/ghostty/config.ghostty). /// /// If this is false, the default configuration paths will not be loaded. /// This is targeted directly at using Ghostty from the CLI in a way @@ -3397,7 +3398,7 @@ pub fn loadIter( /// `path` must be resolved and absolute. pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { assert(std.fs.path.isAbsolute(path)); - var file = openFile(path) catch |err| switch (err) { + var file = file_load.open(path) catch |err| switch (err) { error.NotAFile => { log.warn( "config-file {s}: not reading because it is not a file", @@ -3461,31 +3462,60 @@ fn writeConfigTemplate(path: []const u8) !void { } /// Load configurations from the default configuration files. The default -/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. +/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config.ghostty`. /// -/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` +/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/` /// is also loaded. +/// +/// The legacy `config` file (without extension) is first loaded, +/// then `config.ghostty`. pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { // Load XDG first - const xdg_path = try defaultXdgPath(alloc); + const legacy_xdg_path = try file_load.legacyDefaultXdgPath(alloc); + defer alloc.free(legacy_xdg_path); + const xdg_path = try file_load.defaultXdgPath(alloc); defer alloc.free(xdg_path); - const xdg_action = self.loadOptionalFile(alloc, xdg_path); + const xdg_loaded: bool = xdg_loaded: { + const legacy_xdg_action = self.loadOptionalFile(alloc, legacy_xdg_path); + const xdg_action = self.loadOptionalFile(alloc, xdg_path); + if (xdg_action != .not_found and legacy_xdg_action != .not_found) { + log.warn("both config files `{s}` and `{s}` exist.", .{ legacy_xdg_path, xdg_path }); + log.warn("loading them both in that order", .{}); + break :xdg_loaded true; + } + + break :xdg_loaded xdg_action != .not_found or + legacy_xdg_action != .not_found; + }; // On macOS load the app support directory as well if (comptime builtin.os.tag == .macos) { - const app_support_path = try defaultAppSupportPath(alloc); + const legacy_app_support_path = try file_load.legacyDefaultAppSupportPath(alloc); + defer alloc.free(legacy_app_support_path); + const app_support_path = try file_load.preferredAppSupportPath(alloc); defer alloc.free(app_support_path); - const app_support_action = self.loadOptionalFile(alloc, app_support_path); + const app_support_loaded: bool = loaded: { + const legacy_app_support_action = self.loadOptionalFile(alloc, legacy_app_support_path); + const app_support_action = self.loadOptionalFile(alloc, app_support_path); + if (app_support_action != .not_found and legacy_app_support_action != .not_found) { + log.warn("both config files `{s}` and `{s}` exist.", .{ legacy_app_support_path, app_support_path }); + log.warn("loading them both in that order", .{}); + break :loaded true; + } + + break :loaded app_support_action != .not_found or + legacy_app_support_action != .not_found; + }; // If both files are not found, then we create a template file. // For macOS, we only create the template file in the app support - if (app_support_action == .not_found and xdg_action == .not_found) { + if (app_support_loaded and xdg_loaded) { writeConfigTemplate(app_support_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; } } else { - if (xdg_action == .not_found) { + if (xdg_loaded) { writeConfigTemplate(xdg_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; @@ -3493,102 +3523,6 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { } } -/// Default path for the XDG home configuration file. Returned value -/// must be freed by the caller. -fn defaultXdgPath(alloc: Allocator) ![]const u8 { - return try internal_os.xdg.config( - alloc, - .{ .subdir = "ghostty/config" }, - ); -} - -/// Default path for the macOS Application Support configuration file. -/// Returned value must be freed by the caller. -fn defaultAppSupportPath(alloc: Allocator) ![]const u8 { - return try internal_os.macos.appSupportDir(alloc, "config"); -} - -/// Returns the path to the preferred default configuration file. -/// This is the file where users should place their configuration. -/// -/// This doesn't create or populate the file with any default -/// contents; downstream callers must handle this. -/// -/// The returned value must be freed by the caller. -pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 { - switch (builtin.os.tag) { - .macos => { - // macOS prefers the Application Support directory - // if it exists. - const app_support_path = try defaultAppSupportPath(alloc); - if (openFile(app_support_path)) |f| { - f.close(); - return app_support_path; - } else |_| {} - - // Try the XDG path if it exists - const xdg_path = try defaultXdgPath(alloc); - if (openFile(xdg_path)) |f| { - f.close(); - alloc.free(app_support_path); - return xdg_path; - } else |_| {} - defer alloc.free(xdg_path); - - // Neither exist, use app support - return app_support_path; - }, - - // All other platforms use XDG only - else => return try defaultXdgPath(alloc), - } -} - -const OpenFileError = error{ - FileNotFound, - FileIsEmpty, - FileOpenFailed, - NotAFile, -}; - -/// Opens the file at the given path and returns the file handle -/// if it exists and is non-empty. This also constrains the possible -/// errors to a smaller set that we can explicitly handle. -fn openFile(path: []const u8) OpenFileError!std.fs.File { - assert(std.fs.path.isAbsolute(path)); - - var file = std.fs.openFileAbsolute( - path, - .{}, - ) catch |err| switch (err) { - error.FileNotFound => return OpenFileError.FileNotFound, - else => { - log.warn("unexpected file open error path={s} err={}", .{ - path, - err, - }); - return OpenFileError.FileOpenFailed; - }, - }; - errdefer file.close(); - - const stat = file.stat() catch |err| { - log.warn("error getting file stat path={s} err={}", .{ - path, - err, - }); - return OpenFileError.FileOpenFailed; - }; - switch (stat.kind) { - .file => {}, - else => return OpenFileError.NotAFile, - } - - if (stat.size == 0) return OpenFileError.FileIsEmpty; - - return file; -} - /// Load and parse the CLI args. pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { switch (builtin.os.tag) { diff --git a/src/config/edit.zig b/src/config/edit.zig index 07bb7ee5a..6087106e7 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -4,6 +4,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const internal_os = @import("../os/main.zig"); +const file_load = @import("file_load.zig"); /// The path to the configuration that should be opened for editing. /// @@ -89,20 +90,16 @@ fn configPath(alloc_arena: Allocator) ![]const u8 { /// Returns a const list of possible paths the main config file could be /// in for the current OS. fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 { - var paths: std.ArrayList([]const u8) = try .initCapacity(alloc_arena, 2); + var paths: std.ArrayList([]const u8) = try .initCapacity(alloc_arena, 4); errdefer paths.deinit(alloc_arena); if (comptime builtin.os.tag == .macos) { - paths.appendAssumeCapacity(try internal_os.macos.appSupportDir( - alloc_arena, - "config", - )); + paths.appendAssumeCapacity(try file_load.defaultAppSupportPath(alloc_arena)); + paths.appendAssumeCapacity(try file_load.legacyDefaultAppSupportPath(alloc_arena)); } - paths.appendAssumeCapacity(try internal_os.xdg.config( - alloc_arena, - .{ .subdir = "ghostty/config" }, - )); + paths.appendAssumeCapacity(try file_load.defaultXdgPath(alloc_arena)); + paths.appendAssumeCapacity(try file_load.legacyDefaultXdgPath(alloc_arena)); return paths.items; } diff --git a/src/config/file_load.zig b/src/config/file_load.zig new file mode 100644 index 000000000..8dbefeea8 --- /dev/null +++ b/src/config/file_load.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const internal_os = @import("../os/main.zig"); + +const log = std.log.scoped(.config); + +/// Default path for the XDG home configuration file. Returned value +/// must be freed by the caller. +pub fn defaultXdgPath(alloc: Allocator) ![]const u8 { + return try internal_os.xdg.config( + alloc, + .{ .subdir = "ghostty/config.ghostty" }, + ); +} + +/// Ghostty <1.3.0 default path for the XDG home configuration file. +/// Returned value must be freed by the caller. +pub fn legacyDefaultXdgPath(alloc: Allocator) ![]const u8 { + return try internal_os.xdg.config( + alloc, + .{ .subdir = "ghostty/config" }, + ); +} + +/// Preferred default path for the XDG home configuration file. +/// Returned value must be freed by the caller. +pub fn preferredXdgPath(alloc: Allocator) ![]const u8 { + // If the XDG path exists, use that. + const xdg_path = try defaultXdgPath(alloc); + if (open(xdg_path)) |f| { + f.close(); + return xdg_path; + } else |_| {} + + // Try the legacy path + errdefer alloc.free(xdg_path); + const legacy_xdg_path = try legacyDefaultXdgPath(alloc); + if (open(legacy_xdg_path)) |f| { + f.close(); + alloc.free(xdg_path); + return legacy_xdg_path; + } else |_| {} + + // Legacy path and XDG path both don't exist. Return the + // new one. + alloc.free(legacy_xdg_path); + return xdg_path; +} + +/// Default path for the macOS Application Support configuration file. +/// Returned value must be freed by the caller. +pub fn defaultAppSupportPath(alloc: Allocator) ![]const u8 { + return try internal_os.macos.appSupportDir(alloc, "config.ghostty"); +} + +/// Ghostty <1.3.0 default path for the macOS Application Support +/// configuration file. Returned value must be freed by the caller. +pub fn legacyDefaultAppSupportPath(alloc: Allocator) ![]const u8 { + return try internal_os.macos.appSupportDir(alloc, "config"); +} + +/// Preferred default path for the macOS Application Support configuration file. +/// Returned value must be freed by the caller. +pub fn preferredAppSupportPath(alloc: Allocator) ![]const u8 { + // If the app support path exists, use that. + const app_support_path = try defaultAppSupportPath(alloc); + if (open(app_support_path)) |f| { + f.close(); + return app_support_path; + } else |_| {} + + // Try the legacy path + errdefer alloc.free(app_support_path); + const legacy_app_support_path = try legacyDefaultAppSupportPath(alloc); + if (open(legacy_app_support_path)) |f| { + f.close(); + alloc.free(app_support_path); + return legacy_app_support_path; + } else |_| {} + + // Legacy path and app support path both don't exist. Return the + // new one. + alloc.free(legacy_app_support_path); + return app_support_path; +} + +/// Returns the path to the preferred default configuration file. +/// This is the file where users should place their configuration. +/// +/// This doesn't create or populate the file with any default +/// contents; downstream callers must handle this. +/// +/// The returned value must be freed by the caller. +pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 { + switch (builtin.os.tag) { + .macos => { + // macOS prefers the Application Support directory + // if it exists. + const app_support_path = try preferredAppSupportPath(alloc); + const app_support_file = open(app_support_path) catch { + // Try the XDG path if it exists + const xdg_path = try preferredXdgPath(alloc); + const xdg_file = open(xdg_path) catch { + // If neither file exists, use app support + alloc.free(xdg_path); + return app_support_path; + }; + xdg_file.close(); + alloc.free(app_support_path); + return xdg_path; + }; + app_support_file.close(); + return app_support_path; + }, + + // All other platforms use XDG only + else => return try preferredXdgPath(alloc), + } +} + +const OpenFileError = error{ + FileNotFound, + FileIsEmpty, + FileOpenFailed, + NotAFile, +}; + +/// Opens the file at the given path and returns the file handle +/// if it exists and is non-empty. This also constrains the possible +/// errors to a smaller set that we can explicitly handle. +pub fn open(path: []const u8) OpenFileError!std.fs.File { + assert(std.fs.path.isAbsolute(path)); + + var file = std.fs.openFileAbsolute( + path, + .{}, + ) catch |err| switch (err) { + error.FileNotFound => return OpenFileError.FileNotFound, + else => { + log.warn("unexpected file open error path={s} err={}", .{ + path, + err, + }); + return OpenFileError.FileOpenFailed; + }, + }; + errdefer file.close(); + + const stat = file.stat() catch |err| { + log.warn("error getting file stat path={s} err={}", .{ + path, + err, + }); + return OpenFileError.FileOpenFailed; + }; + switch (stat.kind) { + .file => {}, + else => return OpenFileError.NotAFile, + } + + if (stat.size == 0) return OpenFileError.FileIsEmpty; + + return file; +} diff --git a/src/extra/vim.zig b/src/extra/vim.zig index 2c0192d03..9140b83f8 100644 --- a/src/extra/vim.zig +++ b/src/extra/vim.zig @@ -10,7 +10,7 @@ pub const ftdetect = \\" \\" THIS FILE IS AUTO-GENERATED \\ - \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* setf ghostty + \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/*,*.ghostty setf ghostty \\ ; pub const ftplugin =