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) {
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)
}

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'
// 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");

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

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

View File

@ -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_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_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) {

View File

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

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
\\
\\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* setf ghostty
\\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/*,*.ghostty setf ghostty
\\
;
pub const ftplugin =