aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/os/xdg.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/os/xdg.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/os/xdg.zig')
-rw-r--r--vendor/libghostty-vt/src/os/xdg.zig312
1 files changed, 312 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/os/xdg.zig b/vendor/libghostty-vt/src/os/xdg.zig
new file mode 100644
index 00000000..f2a495af
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/xdg.zig
@@ -0,0 +1,312 @@
+//! 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" });
+ }
+}