xdg: Iterator for system dirs and user dirs user dirs are now enums

This commit is contained in:
Matt Rochford
2025-01-10 18:16:02 -08:00
parent 612cf8dd45
commit 65f3ab2c2f
7 changed files with 140 additions and 96 deletions

View File

@ -2930,33 +2930,21 @@ fn writeConfigTemplate(path: []const u8) !void {
/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` /// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config`
/// is also loaded. /// is also loaded.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
const config_subdir = "ghostty"; const config_dir = "ghostty";
const config_file = "config"; const config_file = "config";
// Load XDG system config first const config_subdir = try std.fs.path.join(alloc, &[_][]const u8{
var it = internal_os.xdg.DirIterator.init(.config); config_dir,
// 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);
}
// 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, config_file,
}); });
defer alloc.free(config_path); defer alloc.free(config_subdir);
_ = self.loadOptionalFile(alloc, config_path); 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);
} }
// 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 // On macOS load the app support directory as well
if (comptime builtin.os.tag == .macos) { if (comptime builtin.os.tag == .macos) {
@ -2973,7 +2961,8 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
} }
} else { } else {
if (xdg_action == .not_found) { if (xdg_action == .not_found) {
writeConfigTemplate(xdg_path) catch |err| { if (xdg_path) |p|
writeConfigTemplate(p) catch |err| {
log.warn("error creating template config file err={}", .{err}); log.warn("error creating template config file err={}", .{err});
}; };
} }

View File

@ -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, alloc_arena,
.{ .subdir = "ghostty/config" }, .{ .subdir = subdir },
)); ));
return paths.items; return paths.items;

View File

@ -31,7 +31,7 @@ pub const Location = enum {
"ghostty", "themes", "ghostty", "themes",
}) catch return error.OutOfMemory; }) catch return error.OutOfMemory;
break :user internal_os.xdg.config( break :user internal_os.xdg.UserDir.config.path(
arena_alloc, arena_alloc,
.{ .subdir = subdir }, .{ .subdir = subdir },
) catch |err| { ) catch |err| {

View File

@ -6,7 +6,9 @@ const internal_os = @import("../os/main.zig");
/// Returns a Dir for the default directory. The Dir.path field must be /// Returns a Dir for the default directory. The Dir.path field must be
/// freed with the given allocator. /// freed with the given allocator.
pub fn defaultDir(alloc: Allocator) !Dir { 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); errdefer alloc.free(crash_dir);
return .{ .path = crash_dir }; return .{ .path = crash_dir };
} }

View File

@ -19,33 +19,6 @@ pub const Options = struct {
home: ?[]const u8 = null, 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 { const InternalOptions = struct {
env: []const u8, env: []const u8,
windows_env: []const u8, windows_env: []const u8,
@ -115,6 +88,43 @@ fn dir(
return error.NoHomeDir; 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 /// Parses the xdg-terminal-exec specification. This expects argv[0] to
/// be "xdg-terminal-exec". /// be "xdg-terminal-exec".
pub fn parseTerminalExec(argv: []const [*:0]const u8) ?[]const [*:0]const u8 { pub fn parseTerminalExec(argv: []const [*:0]const u8) ?[]const [*:0]const u8 {
@ -137,7 +147,7 @@ test {
const alloc = testing.allocator; const alloc = testing.allocator;
{ {
const value = try config(alloc, .{}); const value = try UserDir.config.path(alloc, .{});
defer alloc.free(value); defer alloc.free(value);
try testing.expect(value.len > 0); try testing.expect(value.len > 0);
} }
@ -152,14 +162,14 @@ test "cache directory paths" {
{ {
// Test base path // 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); defer alloc.free(cache_path);
try testing.expectEqualStrings("/Users/test/.cache", cache_path); try testing.expectEqualStrings("/Users/test/.cache", cache_path);
} }
// Test with subdir // Test with subdir
{ {
const cache_path = try cache(alloc, .{ const cache_path = try UserDir.cache.path(alloc, .{
.home = mock_home, .home = mock_home,
.subdir = "ghostty", .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 { pub const Dir = enum {
config, config,
data, 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) { return switch (self) {
.config => "XDG_CONFIG_DIRS", .config => "XDG_CONFIG_DIRS",
.data => "XDG_DATA_DIRS", .data => "XDG_DATA_DIRS",
}; };
} }
pub fn default(self: Dir) [:0]const u8 { pub fn default(self: SystemDir) [:0]const u8 {
return switch (self) { return switch (self) {
.config => "/etc/xdg", .config => "/etc/xdg",
.data => "/usr/local/share:/usr/share", .data => "/usr/local/share:/usr/share",
}; };
} }
};
pub const DirIterator = struct { pub fn iter(self: SystemDir) SystemDirIterator {
data: []const u8,
iterator: std.mem.SplitIterator(u8, .scalar),
/// https://specifications.freedesktop.org/basedir-spec/latest/
pub fn init(key: Dir) DirIterator {
const data = data: { const data = data: {
if (posix.getenv(key.key())) |data| { if (posix.getenv(self.key())) |data| {
if (std.mem.trim(u8, data, &std.ascii.whitespace).len > 0) break :data data; if (std.mem.trim(u8, data, &std.ascii.whitespace).len > 0)
break :data data;
} }
break :data self.default();
break :data key.default();
}; };
return .{ return .{
.data = data, .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" { test "xdg dirs" {
@ -246,24 +297,24 @@ test "xdg dirs" {
const testing = std.testing; const testing = std.testing;
{ {
_ = c.unsetenv(Dir.config.key()); _ = c.unsetenv(SystemDir.config.key());
var it = DirIterator.init(.config); var it = SystemDir.config.iter();
try testing.expectEqualStrings("/etc/xdg", it.next().?); try testing.expectEqualStrings("/etc/xdg", it.next().?);
try testing.expect(it.next() == null); try testing.expect(it.next() == null);
} }
{ {
_ = c.unsetenv(Dir.data.key()); _ = c.unsetenv(SystemDir.data.key());
var it = DirIterator.init(.data); var it = SystemDir.data.iter();
try testing.expectEqualStrings("/usr/local/share", it.next().?);
try testing.expectEqualStrings("/usr/share", it.next().?); try testing.expectEqualStrings("/usr/share", it.next().?);
try testing.expectEqualStrings("/usr/local/share", it.next().?);
try testing.expect(it.next() == null); try testing.expect(it.next() == null);
} }
{ {
_ = c.setenv(Dir.config.key(), "a:b:c", 1); _ = c.setenv(SystemDir.config.key(), "a:b:c", 1);
var it = DirIterator.init(.config); var it = SystemDir.config.iter();
try testing.expectEqualStrings("a", it.next().?);
try testing.expectEqualStrings("b", it.next().?);
try testing.expectEqualStrings("c", it.next().?); try testing.expectEqualStrings("c", it.next().?);
try testing.expectEqualStrings("b", it.next().?);
try testing.expectEqualStrings("a", it.next().?);
try testing.expect(it.next() == null); try testing.expect(it.next() == null);
} }
} }

View File

@ -804,15 +804,15 @@ const Subprocess = struct {
if (std.fmt.bufPrint(&buf, "{s}/..", .{resources_dir})) |data_dir| { if (std.fmt.bufPrint(&buf, "{s}/..", .{resources_dir})) |data_dir| {
try env.put( try env.put(
xdg.Dir.data.key(), xdg.SystemDir.data.key(),
try internal_os.appendEnv( try internal_os.appendEnv(
alloc, 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, data_dir,
), ),
); );
} else |err| { } 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"; const manpath_key = "MANPATH";

View File

@ -412,10 +412,10 @@ fn setupXdgDataDirs(
// our desired integration dir directly. See #2711. // our desired integration dir directly. See #2711.
// <https://specifications.freedesktop.org/basedir-spec/0.6/#variables> // <https://specifications.freedesktop.org/basedir-spec/0.6/#variables>
try env.put( try env.put(
xdg.Dir.data.key(), xdg.SystemDir.data.key(),
try internal_os.prependEnv( try internal_os.prependEnv(
stack_alloc, 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, integ_dir,
), ),
); );