diff --git a/src/build/mdgen/ghostty_1_footer.md b/src/build/mdgen/ghostty_1_footer.md index 86a8a8098..0ec61563d 100644 --- a/src/build/mdgen/ghostty_1_footer.md +++ b/src/build/mdgen/ghostty_1_footer.md @@ -2,7 +2,11 @@ _\$XDG_CONFIG_HOME/ghostty/config_ -: Location of the default configuration file. +: Location of the default user configuration file. + +_\$XDG_CONFIG_DIRS/ghostty/config_ + +: Location of the default system configuration files. _\$LOCALAPPDATA/ghostty/config_ @@ -23,6 +27,10 @@ for configuration files. : Default location for configuration files. +**XDG_CONFIG_DIRS** + +: Colon separated list of paths to load configuration files. + **LOCALAPPDATA** : **WINDOWS ONLY:** alternate location to search for configuration files. diff --git a/src/build/mdgen/ghostty_5_footer.md b/src/build/mdgen/ghostty_5_footer.md index 0c893dd07..339f33d69 100644 --- a/src/build/mdgen/ghostty_5_footer.md +++ b/src/build/mdgen/ghostty_5_footer.md @@ -2,7 +2,11 @@ _\$XDG_CONFIG_HOME/ghostty/config_ -: Location of the default configuration file. +: Location of the default user configuration file. + +_\$XDG_CONFIG_DIRS/ghostty/config_ + +: Location of the default system configuration files. _\$LOCALAPPDATA/ghostty/config_ @@ -15,6 +19,10 @@ for configuration files. : Default location for configuration files. +**XDG_CONFIG_DIRS** + +: Colon separated list of paths to load configuration files. + **LOCALAPPDATA** : **WINDOWS ONLY:** alternate location to search for configuration files. diff --git a/src/config/Config.zig b/src/config/Config.zig index 5a47cb1b3..d7464e85c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1548,8 +1548,10 @@ keybind: Keybinds = .{}, @"config-file": RepeatablePath = .{}, /// 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). +/// The default configuration files are at ./ghostty/config, +/// in each of the colon seperated directories in $XDG_CONFIG_DIRS, and, +/// the xdg user config directory $XDG_CONFIG_HOME +/// (/etc/xdg and ~/.config if these environment variables are not set). /// /// 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 @@ -3074,20 +3076,32 @@ fn writeConfigTemplate(path: []const u8) !void { ); } -/// Load configurations from the default configuration files. The default -/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. +/// Load configurations from the default configuration files. +/// The default system configuration files are at `$XDG_CONFIG_DIRS/ghostty/config`. +/// The default user configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. /// /// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` /// is also loaded. pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { - // Load XDG first - const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" }); - defer alloc.free(xdg_path); - const xdg_action = self.loadOptionalFile(alloc, xdg_path); + const config_dir = "ghostty"; + const config_file = "config"; + const config_subdir = try std.fs.path.join(alloc, &[_][]const u8{ + config_dir, + config_file, + }); + defer alloc.free(config_subdir); + var it = internal_os.xdg.Dir.config.iter(alloc, .{ .subdir = config_subdir }); + var xdg_action: ?OptionalFileAction = null; + var xdg_path: ?[]const u8 = null; + while (try it.next()) |dir| { + defer alloc.free(dir); + xdg_path = dir; + xdg_action = self.loadOptionalFile(alloc, dir); + } // On macOS load the app support directory as well if (comptime builtin.os.tag == .macos) { - const app_support_path = try internal_os.macos.appSupportDir(alloc, "config"); + const app_support_path = try internal_os.macos.appSupportDir(alloc, config_file); defer alloc.free(app_support_path); const app_support_action = self.loadOptionalFile(alloc, app_support_path); @@ -3100,9 +3114,10 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { } } else { if (xdg_action == .not_found) { - writeConfigTemplate(xdg_path) catch |err| { - log.warn("error creating template config file err={}", .{err}); - }; + if (xdg_path) |p| + writeConfigTemplate(p) catch |err| { + log.warn("error creating template config file err={}", .{err}); + }; } } } diff --git a/src/config/edit.zig b/src/config/edit.zig index 871a1a755..e1b993bbc 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -98,9 +98,11 @@ fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 { )); } - paths.appendAssumeCapacity(try internal_os.xdg.config( + const subdir = try std.fs.path.join(alloc_arena, &[_][]const u8{ "ghostty", "config" }); + defer alloc_arena.free(subdir); + paths.appendAssumeCapacity(try internal_os.xdg.UserDir.config.path( alloc_arena, - .{ .subdir = "ghostty/config" }, + .{ .subdir = subdir }, )); return paths.items; diff --git a/src/config/theme.zig b/src/config/theme.zig index 2d206e1f6..ec34c3d30 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -31,7 +31,7 @@ pub const Location = enum { "ghostty", "themes", }) catch return error.OutOfMemory; - break :user internal_os.xdg.config( + break :user internal_os.xdg.UserDir.config.path( arena_alloc, .{ .subdir = subdir }, ) catch |err| { diff --git a/src/crash/dir.zig b/src/crash/dir.zig index e5a8f8a0c..e8201c439 100644 --- a/src/crash/dir.zig +++ b/src/crash/dir.zig @@ -6,7 +6,9 @@ const internal_os = @import("../os/main.zig"); /// Returns a Dir for the default directory. The Dir.path field must be /// freed with the given allocator. pub fn defaultDir(alloc: Allocator) !Dir { - const crash_dir = try internal_os.xdg.state(alloc, .{ .subdir = "ghostty/crash" }); + const subdir = try std.fs.path.join(alloc, &[_][]const u8{ "ghostty", "crash" }); + defer alloc.free(subdir); + const crash_dir = try internal_os.xdg.UserDir.state.path(alloc, .{ .subdir = subdir }); errdefer alloc.free(crash_dir); return .{ .path = crash_dir }; } diff --git a/src/os/xdg.zig b/src/os/xdg.zig index 1383679fe..a013662e0 100644 --- a/src/os/xdg.zig +++ b/src/os/xdg.zig @@ -19,33 +19,6 @@ pub const Options = struct { home: ?[]const u8 = null, }; -/// Get the XDG user config directory. The returned value is allocated. -pub fn config(alloc: Allocator, opts: Options) ![]u8 { - return try dir(alloc, opts, .{ - .env = "XDG_CONFIG_HOME", - .windows_env = "LOCALAPPDATA", - .default_subdir = ".config", - }); -} - -/// Get the XDG cache directory. The returned value is allocated. -pub fn cache(alloc: Allocator, opts: Options) ![]u8 { - return try dir(alloc, opts, .{ - .env = "XDG_CACHE_HOME", - .windows_env = "LOCALAPPDATA", - .default_subdir = ".cache", - }); -} - -/// Get the XDG state directory. The returned value is allocated. -pub fn state(alloc: Allocator, opts: Options) ![]u8 { - return try dir(alloc, opts, .{ - .env = "XDG_STATE_HOME", - .windows_env = "LOCALAPPDATA", - .default_subdir = ".local/state", - }); -} - const InternalOptions = struct { env: []const u8, windows_env: []const u8, @@ -115,6 +88,43 @@ fn dir( return error.NoHomeDir; } +/// XDG user directories for program config, data, cache, or, state +pub const UserDir = enum { + config, + data, + cache, + state, + + pub fn path( + self: UserDir, + alloc: Allocator, + opts: Options, + ) ![]u8 { + const internal_opts: InternalOptions = switch (self) { + .config => .{ + .env = "XDG_CONFIG_HOME", + .windows_env = "LOCALAPPDATA", + .default_subdir = ".config", + }, + .data => .{ + .env = "XDG_DATA_HOME", + .windows_env = "LOCALAPPDATA", // unclear what to use + .default_subdir = ".local/share", + }, + .cache => .{ + .env = "XDG_CACHE_HOME", + .windows_env = "LOCALAPPDATA", // also unclear + .default_subdir = ".cache", + }, + .state => .{ + .env = "XDG_STATE_HOME", + .windows_env = "LOCALAPPDATA", // again ... + .default_subdir = ".local/state", + }, + }; + return dir(alloc, opts, internal_opts); + } +}; /// Parses the xdg-terminal-exec specification. This expects argv[0] to /// be "xdg-terminal-exec". pub fn parseTerminalExec(argv: []const [*:0]const u8) ?[]const [*:0]const u8 { @@ -137,7 +147,7 @@ test { const alloc = testing.allocator; { - const value = try config(alloc, .{}); + const value = try UserDir.config.path(alloc, .{}); defer alloc.free(value); try testing.expect(value.len > 0); } @@ -152,14 +162,14 @@ test "cache directory paths" { { // Test base path { - const cache_path = try cache(alloc, .{ .home = mock_home }); + const cache_path = try UserDir.cache.path(alloc, .{ .home = mock_home }); defer alloc.free(cache_path); try testing.expectEqualStrings("/Users/test/.cache", cache_path); } // Test with subdir { - const cache_path = try cache(alloc, .{ + const cache_path = try UserDir.cache.path(alloc, .{ .home = mock_home, .subdir = "ghostty", }); @@ -193,3 +203,118 @@ test parseTerminalExec { try testing.expectEqualSlices([*:0]const u8, actual, &.{ "a", "-e", "b", "c" }); } } + +/// Iterator over XDG directories system directories and user directories +/// wraps SystemDirIterator using any values from that iterator and then +/// the path from UserDir.path() +const DirIterator = struct { + index: usize, + alloc: Allocator, + opts: Options, + user_dir: UserDir, + sys_dir_it: SystemDirIterator, + const Self = @This(); + pub fn next(self: *Self) !?[]const u8 { + // TODO ignore relative paths where + // path[0] != "/" and path not contains "/../?" + if (self.sys_dir_it.next()) |path| { + return try std.fs.path.join(self.alloc, &[_][]const u8{ + path, + self.opts.subdir orelse "", + }); + } + self.index += 1; + if (self.index == 1) return try self.user_dir.path(self.alloc, self.opts); + return null; + } +}; + +/// System and home directories for program configs and data, +/// these are the environment variables $XDG_CONFIG_DIRS:$XDG_CONFIG_HOME, and, +/// $XDG_DATA_DIRS:$XDG_DATA_HOME respectively +pub const Dir = enum { + config, + data, + + pub fn iter(self: Dir, alloc: Allocator, opts: Options) DirIterator { + const sys_dir: SystemDir = @enumFromInt(@intFromEnum(self)); + const user_dir: UserDir = @enumFromInt(@intFromEnum(self)); + return .{ .index = 0, .alloc = alloc, .opts = opts, .sys_dir_it = sys_dir.iter(), .user_dir = user_dir }; + } +}; + +/// Iterator over system directories in order from least importance to most +/// importance, reverse order to how they are defined in XDG_*_DIRS +const SystemDirIterator = struct { + data: []const u8, + iterator: std.mem.SplitBackwardsIterator(u8, .scalar), + + const Self = @This(); + + pub fn next(self: *Self) ?[]const u8 { + return self.iterator.next(); + } +}; + +/// XDG system directory for program configs or data +pub const SystemDir = enum { + config, + data, + + pub fn key(self: SystemDir) [:0]const u8 { + return switch (self) { + .config => "XDG_CONFIG_DIRS", + .data => "XDG_DATA_DIRS", + }; + } + + pub fn default(self: SystemDir) [:0]const u8 { + return switch (self) { + .config => "/etc/xdg", + .data => "/usr/local/share:/usr/share", + }; + } + + pub fn iter(self: SystemDir) SystemDirIterator { + const data = data: { + if (posix.getenv(self.key())) |data| { + if (std.mem.trim(u8, data, &std.ascii.whitespace).len > 0) + break :data data; + } + break :data self.default(); + }; + return .{ + .data = data, + .iterator = std.mem.splitBackwardsScalar(u8, data, ':'), + }; + } +}; + +test "xdg dirs" { + const c = @cImport({ + @cInclude("stdlib.h"); + }); + + const testing = std.testing; + { + _ = c.unsetenv(SystemDir.config.key()); + var it = SystemDir.config.iter(); + try testing.expectEqualStrings("/etc/xdg", it.next().?); + try testing.expect(it.next() == null); + } + { + _ = c.unsetenv(SystemDir.data.key()); + var it = SystemDir.data.iter(); + try testing.expectEqualStrings("/usr/share", it.next().?); + try testing.expectEqualStrings("/usr/local/share", it.next().?); + try testing.expect(it.next() == null); + } + { + _ = c.setenv(SystemDir.config.key(), "a:b:c", 1); + var it = SystemDir.config.iter(); + try testing.expectEqualStrings("c", it.next().?); + try testing.expectEqualStrings("b", it.next().?); + try testing.expectEqualStrings("a", it.next().?); + try testing.expect(it.next() == null); + } +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 4f63076de..3053a17b9 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -15,6 +15,7 @@ const configpkg = @import("../config.zig"); const crash = @import("../crash/main.zig"); const fastmem = @import("../fastmem.zig"); const internal_os = @import("../os/main.zig"); +const xdg = internal_os.xdg; const renderer = @import("../renderer.zig"); const shell_integration = @import("shell_integration.zig"); const terminal = @import("../terminal/main.zig"); @@ -802,18 +803,17 @@ const Subprocess = struct { var buf: [std.fs.max_path_bytes]u8 = undefined; - const xdg_data_dir_key = "XDG_DATA_DIRS"; if (std.fmt.bufPrint(&buf, "{s}/..", .{resources_dir})) |data_dir| { try env.put( - xdg_data_dir_key, + xdg.SystemDir.data.key(), try internal_os.appendEnv( alloc, - env.get(xdg_data_dir_key) orelse "/usr/local/share:/usr/share", + env.get(xdg.SystemDir.data.key()) orelse xdg.SystemDir.data.default(), data_dir, ), ); } else |err| { - log.warn("error building {s}; err={}", .{ xdg_data_dir_key, err }); + log.warn("error building {s}; err={}", .{ xdg.SystemDir.data.key(), err }); } const manpath_key = "MANPATH"; diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 423e2f518..1207a664e 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -6,6 +6,7 @@ const EnvMap = std.process.EnvMap; const config = @import("../config.zig"); const homedir = @import("../os/homedir.zig"); const internal_os = @import("../os/main.zig"); +const xdg = internal_os.xdg; const log = std.log.scoped(.shell_integration); @@ -492,12 +493,11 @@ fn setupXdgDataDirs( // This ensures that the default directories aren't lost by setting // our desired integration dir directly. See #2711. // - const xdg_data_dirs_key = "XDG_DATA_DIRS"; try env.put( - xdg_data_dirs_key, + xdg.SystemDir.data.key(), try internal_os.prependEnv( stack_alloc, - env.get(xdg_data_dirs_key) orelse "/usr/local/share:/usr/share", + env.get(xdg.SystemDir.data.key()) orelse xdg.SystemDir.data.default(), integ_dir, ), );