Add `.ghostty` extension to `config` (#8885)

Resolves #8689

For various reason, ghostty wants to have a unique file extension for
the config files. The name was settled on `config.ghostty`. This will
help with tooling. See #8438 (original discussion) for more details.

This PR introduces the preferred default of `.ghostty` while still
supporting the previous `config` file. If both files exist, a warning
log is sent.

The docs / website will need to be updated to reflect this change. 

> [!NOTE]
> Only tested on macOS 26.0.

---------

Co-authored-by: Mitchell Hashimoto <m@mitchellh.com>
pull/9170/head
Joshie 2025-10-12 16:48:06 -04:00 committed by GitHub
parent 37b3c27020
commit cbeb6890c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 228 additions and 129 deletions

View File

@ -14,7 +14,7 @@ struct SettingsView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Coming Soon. 🚧").font(.title) Text("Coming Soon. 🚧").font(.title)
Text("You can't configure settings in the GUI yet. To modify settings, " + 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) .multilineTextAlignment(.leading)
.lineLimit(nil) .lineLimit(nil)
} }

View File

@ -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' // '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 // directory. The syntax then needs to be mapped to the correct language in
// the config file within the '~.config/bat' directory // 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); const run = b.addRunArtifact(build_data_exe);
run.addArg("+sublime"); run.addArg("+sublime");

View File

@ -1,15 +1,15 @@
# FILES # FILES
_\$XDG_CONFIG_HOME/ghostty/config_ _\$XDG_CONFIG_HOME/ghostty/config.ghostty_
: Location of the default configuration file. : 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 : **On macOS**, location of the default configuration file. This location takes
precedence over the XDG environment locations. 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 : **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
for configuration files. for configuration files.

View File

@ -1,15 +1,15 @@
# FILES # FILES
_\$XDG_CONFIG_HOME/ghostty/config_ _\$XDG_CONFIG_HOME/ghostty/config.ghostty_
: Location of the default configuration file. : 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 : **On macOS**, location of the default configuration file. This location takes
precedence over the XDG environment locations. 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 : **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
for configuration files. for configuration files.

View File

@ -8,11 +8,11 @@
To configure Ghostty, you must use a configuration file. GUI-based configuration 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 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 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 **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 default configuration location for macOS. It will be searched before any of the
XDG environment locations listed above. XDG environment locations listed above.

View File

@ -30,9 +30,9 @@ pub const Options = struct {
/// this yet. /// this yet.
/// ///
/// The filepath opened is the default user-specific configuration /// 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 /// 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, /// On macOS, whichever path exists and is non-empty will be prioritized,
/// prioritizing the Application Support directory if neither are /// prioritizing the Application Support directory if neither are
/// non-empty. /// non-empty.
@ -73,7 +73,7 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 {
defer config.deinit(); defer config.deinit();
// Find the preferred path. // Find the preferred path.
const path = try Config.preferredDefaultFilePath(alloc); const path = try configpkg.preferredDefaultFilePath(alloc);
defer alloc.free(path); defer alloc.free(path);
// We don't currently support Windows because we use the exec syscall. // We don't currently support Windows because we use the exec syscall.

View File

@ -1,5 +1,6 @@
const builtin = @import("builtin"); const builtin = @import("builtin");
const file_load = @import("config/file_load.zig");
const formatter = @import("config/formatter.zig"); const formatter = @import("config/formatter.zig");
pub const Config = @import("config/Config.zig"); pub const Config = @import("config/Config.zig");
pub const conditional = @import("config/conditional.zig"); pub const conditional = @import("config/conditional.zig");
@ -12,6 +13,7 @@ pub const ConditionalState = conditional.State;
pub const FileFormatter = formatter.FileFormatter; pub const FileFormatter = formatter.FileFormatter;
pub const entryFormatter = formatter.entryFormatter; pub const entryFormatter = formatter.entryFormatter;
pub const formatEntry = formatter.formatEntry; pub const formatEntry = formatter.formatEntry;
pub const preferredDefaultFilePath = file_load.preferredDefaultFilePath;
// Field types // Field types
pub const BoldColor = Config.BoldColor; pub const BoldColor = Config.BoldColor;

View File

@ -24,6 +24,7 @@ const cli = @import("../cli.zig");
const conditional = @import("conditional.zig"); const conditional = @import("conditional.zig");
const Conditional = conditional.Conditional; const Conditional = conditional.Conditional;
const file_load = @import("file_load.zig");
const formatterpkg = @import("formatter.zig"); const formatterpkg = @import("formatter.zig");
const themepkg = @import("theme.zig"); const themepkg = @import("theme.zig");
const url = @import("url.zig"); const url = @import("url.zig");
@ -2071,7 +2072,7 @@ keybind: Keybinds = .{},
/// When this is true, the default configuration file paths will be loaded. /// When this is true, the default configuration file paths will be loaded.
/// The default configuration file paths are currently only the XDG /// 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. /// 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 /// 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. /// `path` must be resolved and absolute.
pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
assert(std.fs.path.isAbsolute(path)); 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 => { error.NotAFile => {
log.warn( log.warn(
"config-file {s}: not reading because it is not a file", "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 /// 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. /// is also loaded.
///
/// The legacy `config` file (without extension) is first loaded,
/// then `config.ghostty`.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
// Load XDG first // 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); 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 // On macOS load the app support directory as well
if (comptime builtin.os.tag == .macos) { 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); 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. // If both files are not found, then we create a template file.
// For macOS, we only create the template file in the app support // 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| { writeConfigTemplate(app_support_path) catch |err| {
log.warn("error creating template config file err={}", .{err}); log.warn("error creating template config file err={}", .{err});
}; };
} }
} else { } else {
if (xdg_action == .not_found) { if (xdg_loaded) {
writeConfigTemplate(xdg_path) catch |err| { writeConfigTemplate(xdg_path) catch |err| {
log.warn("error creating template config file err={}", .{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. /// Load and parse the CLI args.
pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
switch (builtin.os.tag) { switch (builtin.os.tag) {

View File

@ -4,6 +4,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const internal_os = @import("../os/main.zig"); 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. /// 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 /// Returns a const list of possible paths the main config file could be
/// in for the current OS. /// in for the current OS.
fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 { 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); errdefer paths.deinit(alloc_arena);
if (comptime builtin.os.tag == .macos) { if (comptime builtin.os.tag == .macos) {
paths.appendAssumeCapacity(try internal_os.macos.appSupportDir( paths.appendAssumeCapacity(try file_load.defaultAppSupportPath(alloc_arena));
alloc_arena, paths.appendAssumeCapacity(try file_load.legacyDefaultAppSupportPath(alloc_arena));
"config",
));
} }
paths.appendAssumeCapacity(try internal_os.xdg.config( paths.appendAssumeCapacity(try file_load.defaultXdgPath(alloc_arena));
alloc_arena, paths.appendAssumeCapacity(try file_load.legacyDefaultXdgPath(alloc_arena));
.{ .subdir = "ghostty/config" },
));
return paths.items; return paths.items;
} }

166
src/config/file_load.zig Normal file
View File

@ -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;
}

View File

@ -10,7 +10,7 @@ pub const ftdetect =
\\" \\"
\\" THIS FILE IS AUTO-GENERATED \\" 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 = pub const ftplugin =