mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-19 05:50:27 +00:00
Make the "cache directory paths" test cross-platform by using std.fs.path.join for expected values and a platform-appropriate mock home path, since the function under test uses native path separators. Skip the two shell integration XDG_DATA_DIRS tests on Windows. These tests use hardcoded Unix path separators (:) and Unix default paths (/usr/local/share:/usr/share) which are not applicable on Windows where the path delimiter is ; and XDG_DATA_DIRS is not a standard concept. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
313 lines
9.8 KiB
Zig
313 lines
9.8 KiB
Zig
//! Implementation of the XDG Base Directory specification
|
|
//! (https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const Allocator = std.mem.Allocator;
|
|
const posix = std.posix;
|
|
const homedir = @import("homedir.zig");
|
|
const env_os = @import("env.zig");
|
|
|
|
pub const Options = struct {
|
|
/// Subdirectories to join to the base. This avoids extra allocations
|
|
/// when building up the directory. This is commonly the application.
|
|
subdir: ?[]const u8 = null,
|
|
|
|
/// The home directory for the user. If this is not set, we will attempt
|
|
/// to look it up which is an expensive process. By setting this, you can
|
|
/// avoid lookups.
|
|
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,
|
|
default_subdir: []const u8,
|
|
};
|
|
|
|
/// Unified helper to get XDG directories that follow a common pattern.
|
|
fn dir(
|
|
alloc: Allocator,
|
|
opts: Options,
|
|
internal_opts: InternalOptions,
|
|
) ![]u8 {
|
|
// If we have a cached home dir, use that.
|
|
if (opts.home) |home| {
|
|
return try std.fs.path.join(alloc, &[_][]const u8{
|
|
home,
|
|
internal_opts.default_subdir,
|
|
opts.subdir orelse "",
|
|
});
|
|
}
|
|
|
|
// First check the env var. On Windows we have to allocate so this tracks
|
|
// both whether we have the env var and whether we own it.
|
|
// on Windows we treat `LOCALAPPDATA` as a fallback for `XDG_CONFIG_HOME`
|
|
const env_ = try env_os.getenvNotEmpty(alloc, internal_opts.env) orelse switch (builtin.os.tag) {
|
|
else => null,
|
|
.windows => try env_os.getenvNotEmpty(alloc, internal_opts.windows_env),
|
|
};
|
|
defer if (env_) |env| env.deinit(alloc);
|
|
|
|
if (env_) |env| {
|
|
// If we have a subdir, then we use the env as-is to avoid a copy.
|
|
if (opts.subdir) |subdir| {
|
|
return try std.fs.path.join(alloc, &[_][]const u8{
|
|
env.value,
|
|
subdir,
|
|
});
|
|
}
|
|
|
|
return try alloc.dupe(u8, env.value);
|
|
}
|
|
|
|
// Get our home dir
|
|
var buf: [1024]u8 = undefined;
|
|
if (try homedir.home(&buf)) |home| {
|
|
return try std.fs.path.join(alloc, &[_][]const u8{
|
|
home,
|
|
internal_opts.default_subdir,
|
|
opts.subdir orelse "",
|
|
});
|
|
}
|
|
|
|
return error.NoHomeDir;
|
|
}
|
|
|
|
/// 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 {
|
|
if (!std.mem.eql(
|
|
u8,
|
|
std.fs.path.basename(std.mem.sliceTo(argv[0], 0)),
|
|
"xdg-terminal-exec",
|
|
)) return null;
|
|
|
|
// We expect at least one argument
|
|
if (argv.len < 2) return &.{};
|
|
|
|
// If the first argument is "-e" we skip it.
|
|
const start: usize = if (std.mem.eql(u8, std.mem.sliceTo(argv[1], 0), "-e")) 2 else 1;
|
|
return argv[start..];
|
|
}
|
|
|
|
test {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
{
|
|
const value = try config(alloc, .{});
|
|
defer alloc.free(value);
|
|
try testing.expect(value.len > 0);
|
|
}
|
|
}
|
|
|
|
test "cache directory paths" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
const mock_home = if (builtin.os.tag == .windows) "C:\\Users\\test" else "/Users/test";
|
|
|
|
// Test when XDG_CACHE_HOME is not set
|
|
{
|
|
// Test base path
|
|
{
|
|
const cache_path = try cache(alloc, .{ .home = mock_home });
|
|
defer alloc.free(cache_path);
|
|
const expected = try std.fs.path.join(alloc, &.{ mock_home, ".cache" });
|
|
defer alloc.free(expected);
|
|
try testing.expectEqualStrings(expected, cache_path);
|
|
}
|
|
|
|
// Test with subdir
|
|
{
|
|
const cache_path = try cache(alloc, .{
|
|
.home = mock_home,
|
|
.subdir = "ghostty",
|
|
});
|
|
defer alloc.free(cache_path);
|
|
const expected = try std.fs.path.join(alloc, &.{ mock_home, ".cache", "ghostty" });
|
|
defer alloc.free(expected);
|
|
try testing.expectEqualStrings(expected, cache_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
test "fallback when xdg env empty" {
|
|
if (builtin.os.tag == .windows) return error.SkipZigTest;
|
|
|
|
const alloc = std.testing.allocator;
|
|
|
|
const saved_home = home: {
|
|
const home = std.posix.getenv("HOME") orelse break :home null;
|
|
break :home try alloc.dupeZ(u8, home);
|
|
};
|
|
defer env: {
|
|
const home = saved_home orelse {
|
|
_ = env_os.unsetenv("HOME");
|
|
break :env;
|
|
};
|
|
_ = env_os.setenv("HOME", home);
|
|
std.testing.allocator.free(home);
|
|
}
|
|
const temp_home = "/tmp/ghostty-test-home";
|
|
_ = env_os.setenv("HOME", temp_home);
|
|
|
|
const DirCase = struct {
|
|
name: [:0]const u8,
|
|
func: fn (Allocator, Options) anyerror![]u8,
|
|
default_subdir: []const u8,
|
|
};
|
|
|
|
const cases = [_]DirCase{
|
|
.{ .name = "XDG_CONFIG_HOME", .func = config, .default_subdir = ".config" },
|
|
.{ .name = "XDG_CACHE_HOME", .func = cache, .default_subdir = ".cache" },
|
|
.{ .name = "XDG_STATE_HOME", .func = state, .default_subdir = ".local/state" },
|
|
};
|
|
|
|
inline for (cases) |case| {
|
|
// Save and restore each environment variable
|
|
const saved_env = blk: {
|
|
const value = std.posix.getenv(case.name) orelse break :blk null;
|
|
break :blk try alloc.dupeZ(u8, value);
|
|
};
|
|
defer env: {
|
|
const value = saved_env orelse {
|
|
_ = env_os.unsetenv(case.name);
|
|
break :env;
|
|
};
|
|
_ = env_os.setenv(case.name, value);
|
|
alloc.free(value);
|
|
}
|
|
|
|
const expected = try std.fs.path.join(alloc, &[_][]const u8{
|
|
temp_home,
|
|
case.default_subdir,
|
|
});
|
|
defer alloc.free(expected);
|
|
|
|
// Test with empty string - should fallback to home
|
|
_ = env_os.setenv(case.name, "");
|
|
const actual = try case.func(alloc, .{});
|
|
defer alloc.free(actual);
|
|
|
|
try std.testing.expectEqualStrings(expected, actual);
|
|
}
|
|
}
|
|
|
|
test "fallback when xdg env empty and subdir" {
|
|
if (builtin.os.tag == .windows) return error.SkipZigTest;
|
|
|
|
const env = @import("env.zig");
|
|
const alloc = std.testing.allocator;
|
|
|
|
const saved_home = home: {
|
|
const home = std.posix.getenv("HOME") orelse break :home null;
|
|
break :home try alloc.dupeZ(u8, home);
|
|
};
|
|
defer env: {
|
|
const home = saved_home orelse {
|
|
_ = env.unsetenv("HOME");
|
|
break :env;
|
|
};
|
|
_ = env.setenv("HOME", home);
|
|
std.testing.allocator.free(home);
|
|
}
|
|
|
|
const temp_home = "/tmp/ghostty-test-home";
|
|
_ = env.setenv("HOME", temp_home);
|
|
|
|
const DirCase = struct {
|
|
name: [:0]const u8,
|
|
func: fn (Allocator, Options) anyerror![]u8,
|
|
default_subdir: []const u8,
|
|
};
|
|
|
|
const cases = [_]DirCase{
|
|
.{ .name = "XDG_CONFIG_HOME", .func = config, .default_subdir = ".config" },
|
|
.{ .name = "XDG_CACHE_HOME", .func = cache, .default_subdir = ".cache" },
|
|
.{ .name = "XDG_STATE_HOME", .func = state, .default_subdir = ".local/state" },
|
|
};
|
|
|
|
inline for (cases) |case| {
|
|
// Save and restore each environment variable
|
|
const saved_env = blk: {
|
|
const value = std.posix.getenv(case.name) orelse break :blk null;
|
|
break :blk try alloc.dupeZ(u8, value);
|
|
};
|
|
defer env: {
|
|
const value = saved_env orelse {
|
|
_ = env.unsetenv(case.name);
|
|
break :env;
|
|
};
|
|
_ = env.setenv(case.name, value);
|
|
alloc.free(value);
|
|
}
|
|
|
|
const expected = try std.fs.path.join(alloc, &[_][]const u8{
|
|
temp_home,
|
|
case.default_subdir,
|
|
"ghostty",
|
|
});
|
|
defer alloc.free(expected);
|
|
|
|
// Test with empty string - should fallback to home
|
|
_ = env.setenv(case.name, "");
|
|
const actual = try case.func(alloc, .{ .subdir = "ghostty" });
|
|
defer alloc.free(actual);
|
|
|
|
try std.testing.expectEqualStrings(expected, actual);
|
|
}
|
|
}
|
|
|
|
test parseTerminalExec {
|
|
const testing = std.testing;
|
|
|
|
{
|
|
const actual = parseTerminalExec(&.{ "a", "b", "c" });
|
|
try testing.expect(actual == null);
|
|
}
|
|
{
|
|
const actual = parseTerminalExec(&.{"xdg-terminal-exec"}).?;
|
|
try testing.expectEqualSlices([*:0]const u8, actual, &.{});
|
|
}
|
|
{
|
|
const actual = parseTerminalExec(&.{ "xdg-terminal-exec", "a", "b", "c" }).?;
|
|
try testing.expectEqualSlices([*:0]const u8, actual, &.{ "a", "b", "c" });
|
|
}
|
|
{
|
|
const actual = parseTerminalExec(&.{ "xdg-terminal-exec", "-e", "a", "b", "c" }).?;
|
|
try testing.expectEqualSlices([*:0]const u8, actual, &.{ "a", "b", "c" });
|
|
}
|
|
{
|
|
const actual = parseTerminalExec(&.{ "xdg-terminal-exec", "a", "-e", "b", "c" }).?;
|
|
try testing.expectEqualSlices([*:0]const u8, actual, &.{ "a", "-e", "b", "c" });
|
|
}
|
|
}
|