ghostty/src/os/xdg.zig
Mitchell Hashimoto e03c428728 os: directory functions should prefer cached home if available
This fixes tests as well if env vars are set.
2025-01-03 10:39:03 -08:00

196 lines
6.1 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 assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const posix = std.posix;
const homedir = @import("homedir.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_, const owned = switch (builtin.os.tag) {
else => .{ posix.getenv(internal_opts.env), false },
.windows => windows: {
if (std.process.getEnvVarOwned(alloc, internal_opts.env)) |env| {
break :windows .{ env, true };
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {
if (std.process.getEnvVarOwned(alloc, internal_opts.windows_env)) |env| {
break :windows .{ env, true };
} else |err2| switch (err2) {
error.EnvironmentVariableNotFound => break :windows .{ null, false },
else => return err,
}
},
else => return err,
}
},
};
defer if (owned) if (env_) |v| alloc.free(v);
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,
subdir,
});
}
return try alloc.dupe(u8, env);
}
// 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 = "/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);
try testing.expectEqualStrings("/Users/test/.cache", cache_path);
}
// Test with subdir
{
const cache_path = try cache(alloc, .{
.home = mock_home,
.subdir = "ghostty",
});
defer alloc.free(cache_path);
try testing.expectEqualStrings("/Users/test/.cache/ghostty", cache_path);
}
}
}
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" });
}
}