From 65f3ab2c2fde47fec5200734199e9d5b4ec5ca80 Mon Sep 17 00:00:00 2001 From: Matt Rochford Date: Fri, 10 Jan 2025 18:16:02 -0800 Subject: [PATCH] xdg: Iterator for system dirs and user dirs user dirs are now enums --- src/config/Config.zig | 45 ++++---- src/config/edit.zig | 6 +- src/config/theme.zig | 2 +- src/crash/dir.zig | 4 +- src/os/xdg.zig | 169 ++++++++++++++++++++----------- src/termio/Exec.zig | 6 +- src/termio/shell_integration.zig | 4 +- 7 files changed, 140 insertions(+), 96 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index aca757a1e..9f5cebab6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2930,33 +2930,21 @@ fn writeConfigTemplate(path: []const u8) !void { /// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` /// is also loaded. pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { - const config_subdir = "ghostty"; + const config_dir = "ghostty"; const config_file = "config"; - // Load XDG system config first - var it = internal_os.xdg.DirIterator.init(.config); - // We must reverse the order of the iterator because the first xdg config - // dir must takes importance per the spec https://specifications.freedesktop.org/basedir-spec/latest/#variables - var xdg_config_dirs = std.ArrayList([]const u8).init(alloc); - defer xdg_config_dirs.deinit(); - while (it.next()) |d| { - try xdg_config_dirs.insert(0, d); + 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); } - // Apply the system configs from last to first - for (xdg_config_dirs.items) |xdg_dir| { - const config_path = try std.fs.path.join(alloc, &[_][]const u8{ - xdg_dir, - config_subdir, - config_file, - }); - defer alloc.free(config_path); - _ = self.loadOptionalFile(alloc, config_path); - } - // Load XDG user config next - const xdg_config_dir = try internal_os.xdg.config(alloc, .{ .subdir = config_subdir }); - defer alloc.free(xdg_config_dir); - const xdg_path = try std.fs.path.join(alloc, &[_][]const u8{ xdg_config_dir, config_file }); - defer alloc.free(xdg_path); - const xdg_action = self.loadOptionalFile(alloc, xdg_path); // On macOS load the app support directory as well if (comptime builtin.os.tag == .macos) { @@ -2973,9 +2961,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 b851ec3d4..219449ce7 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 6f32df0bf..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", }); @@ -194,49 +204,90 @@ test parseTerminalExec { } } -/// https://specifications.freedesktop.org/basedir-spec/latest/ +/// 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 key(self: Dir) [:0]const u8 { + 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: Dir) [:0]const u8 { + pub fn default(self: SystemDir) [:0]const u8 { return switch (self) { .config => "/etc/xdg", .data => "/usr/local/share:/usr/share", }; } -}; -pub const DirIterator = struct { - data: []const u8, - iterator: std.mem.SplitIterator(u8, .scalar), - - /// https://specifications.freedesktop.org/basedir-spec/latest/ - pub fn init(key: Dir) DirIterator { + pub fn iter(self: SystemDir) SystemDirIterator { const data = data: { - if (posix.getenv(key.key())) |data| { - if (std.mem.trim(u8, data, &std.ascii.whitespace).len > 0) break :data data; + if (posix.getenv(self.key())) |data| { + if (std.mem.trim(u8, data, &std.ascii.whitespace).len > 0) + break :data data; } - - break :data key.default(); + break :data self.default(); }; - return .{ .data = data, - .iterator = std.mem.splitScalar(u8, data, ':'), + .iterator = std.mem.splitBackwardsScalar(u8, data, ':'), }; } - - pub fn next(self: *DirIterator) ?[]const u8 { - return self.iterator.next(); - } }; test "xdg dirs" { @@ -246,24 +297,24 @@ test "xdg dirs" { const testing = std.testing; { - _ = c.unsetenv(Dir.config.key()); - var it = DirIterator.init(.config); + _ = 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(Dir.data.key()); - var it = DirIterator.init(.data); - try testing.expectEqualStrings("/usr/local/share", it.next().?); + _ = 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(Dir.config.key(), "a:b:c", 1); - var it = DirIterator.init(.config); - try testing.expectEqualStrings("a", it.next().?); - try testing.expectEqualStrings("b", it.next().?); + _ = 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 3555845fb..cb99b02cd 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -804,15 +804,15 @@ const Subprocess = struct { if (std.fmt.bufPrint(&buf, "{s}/..", .{resources_dir})) |data_dir| { try env.put( - xdg.Dir.data.key(), + xdg.SystemDir.data.key(), try internal_os.appendEnv( alloc, - env.get(xdg.Dir.data.key()) orelse xdg.Dir.data.default(), + env.get(xdg.SystemDir.data.key()) orelse xdg.SystemDir.data.default(), data_dir, ), ); } else |err| { - log.warn("error building {s}; err={}", .{ xdg.Dir.data.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 db5a65d71..e32e9ae12 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -412,10 +412,10 @@ fn setupXdgDataDirs( // our desired integration dir directly. See #2711. // try env.put( - xdg.Dir.data.key(), + xdg.SystemDir.data.key(), try internal_os.prependEnv( stack_alloc, - env.get(xdg.Dir.data.key()) orelse xdg.Dir.data.default(), + env.get(xdg.SystemDir.data.key()) orelse xdg.SystemDir.data.default(), integ_dir, ), );