From 15c50c1db1983961c9aa37a2fc0f51327ad26608 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Sun, 2 Aug 2026 09:51:23 -0700 Subject: [PATCH] crash: do not use global state This removes use of global state from the crash reporting functionality (everything in src/crash). This particularly ensures that there are no races on the system environment during the execution of the initialization thread that would possibly cause crashes, particularly in any (albeit unsupported) 3rd party integrations of libghostty-internal. Ultimately, this pushes any coupling of I/O and environment to places that would more correctly interface with global state, such as the same-thread global.init, and the crash report CLI. Note that similar de-coupling actions have been taken on XDG and home directory functionality, pushing their coupling points up the stack in a similar way. --- src/cli/crash_report.zig | 12 ++-- src/cli/new_window.zig | 2 +- src/cli/ssh-cache/DiskCache.zig | 1 + src/config/Config.zig | 7 +- src/config/file_load.zig | 2 + src/config/path.zig | 1 + src/config/theme.zig | 1 + src/crash/dir.zig | 23 +++---- src/crash/sentry.zig | 110 ++++++++++++++++++++----------- src/global.zig | 19 ++++-- src/os/homedir.zig | 45 ++++++++----- src/os/xdg.zig | 34 +++++----- src/termio/shell_integration.zig | 2 +- 13 files changed, 160 insertions(+), 99 deletions(-) diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig index 2c0484e96..e189ddae9 100644 --- a/src/cli/crash_report.zig +++ b/src/cli/crash_report.zig @@ -44,22 +44,26 @@ pub fn run(alloc_gpa: Allocator) !u8 { var stdout_writer = stdout_file.writer(global.io(), &buffer); const stdout = &stdout_writer.interface; - const result = runInner(alloc, &stdout_file, stdout); + var environ_map = try global.environMap(); + defer environ_map.deinit(); + const result = runInner(global.io(), alloc, &environ_map, &stdout_file, stdout); stdout.flush() catch {}; return result; } fn runInner( + io: std.Io, alloc: Allocator, + environ_map: *const std.process.Environ.Map, stdout_file: *std.Io.File, stdout: *std.Io.Writer, ) !u8 { - const crash_dir = try crash.defaultDir(alloc); + const crash_dir = try crash.defaultDir(io, alloc, environ_map); var reports: std.ArrayList(crash.Report) = .empty; errdefer reports.deinit(alloc); - var it = try crash_dir.iterator(); - while (try it.next()) |report| try reports.append(alloc, .{ + var it = try crash_dir.iterator(io); + while (try it.next(io)) |report| try reports.append(alloc, .{ .name = try alloc.dupe(u8, report.name), .mtime = report.mtime, }); diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index 68a1c14e0..333cb06c0 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -84,7 +84,7 @@ pub const Options = struct { const expanded = expanded: { var environ_map = try global.environMap(); defer environ_map.deinit(); - break :expanded try homedir.expandHome(&environ_map, stripped, &expandhome_buf); + break :expanded try homedir.expandHome(global.io(), &environ_map, stripped, &expandhome_buf); }; var realpath_buf: [std.fs.max_path_bytes]u8 = undefined; const realpath = realpath_buf[0..try cwd.realPathFile(self._io, expanded, &realpath_buf)]; diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig index 29f97efb1..bc5619d71 100644 --- a/src/cli/ssh-cache/DiskCache.zig +++ b/src/cli/ssh-cache/DiskCache.zig @@ -30,6 +30,7 @@ pub fn defaultPath( var environ_map = try global.environMap(); defer environ_map.deinit(); const state_dir: []const u8 = xdg.state( + global.io(), alloc, &environ_map, .{ .subdir = program }, diff --git a/src/config/Config.zig b/src/config/Config.zig index 136dd574c..5db6c9fb4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4684,7 +4684,7 @@ pub fn finalize(self: *Config) !void { var environ_map = try global.environMap(); defer environ_map.deinit(); var buf: [std.fs.max_path_bytes]u8 = undefined; - if (try internal_os.home(&environ_map, &buf)) |home| { + if (try internal_os.home(global.io(), &environ_map, &buf)) |home| { wd = .{ .path = try alloc.dupe(u8, home) }; } else { wd = .inherit; @@ -5414,7 +5414,7 @@ pub const WorkingDirectory = union(enum) { const expanded = expanded: { var environ_map = global.environMap() catch |err| break :expanded err; defer environ_map.deinit(); - break :expanded internal_os.expandHome(&environ_map, path, &buf); + break :expanded internal_os.expandHome(global.io(), &environ_map, path, &buf); } catch |err| { log.warn( "error expanding home directory for working-directory path={s}: {}", @@ -5483,6 +5483,7 @@ pub const WorkingDirectory = union(enum) { var buf: [std.fs.max_path_bytes]u8 = undefined; const expected = internal_os.expandHome( + testing.io, &environ_map, "~/projects/ghostty", &buf, @@ -10549,6 +10550,7 @@ test "clone preserves conditional set" { test "working-directory expands tilde" { const testing = std.testing; + const io = testing.io; const alloc = testing.allocator; var environ_map = try testing.environ.createMap(testing.allocator); defer environ_map.deinit(); @@ -10563,6 +10565,7 @@ test "working-directory expands tilde" { var buf: [std.fs.max_path_bytes]u8 = undefined; const expected = internal_os.expandHome( + io, &environ_map, "~/projects/ghostty", &buf, diff --git a/src/config/file_load.zig b/src/config/file_load.zig index ac5ddff6a..7495e899f 100644 --- a/src/config/file_load.zig +++ b/src/config/file_load.zig @@ -13,6 +13,7 @@ pub fn defaultXdgPath(alloc: Allocator) ![]const u8 { var environ_map = try global.environMap(); defer environ_map.deinit(); return try internal_os.xdg.config( + global.io(), alloc, &environ_map, .{ .subdir = "ghostty/config.ghostty" }, @@ -25,6 +26,7 @@ pub fn legacyDefaultXdgPath(alloc: Allocator) ![]const u8 { var environ_map = try global.environMap(); defer environ_map.deinit(); return try internal_os.xdg.config( + global.io(), alloc, &environ_map, .{ .subdir = "ghostty/config" }, diff --git a/src/config/path.zig b/src/config/path.zig index 4d9c8586a..5fd88f332 100644 --- a/src/config/path.zig +++ b/src/config/path.zig @@ -168,6 +168,7 @@ pub const Path = union(enum) { defer environ_map.deinit(); const expanded: []const u8 = internal_os.expandHome( + global.io(), &environ_map, path, &buf, diff --git a/src/config/theme.zig b/src/config/theme.zig index e7609a165..a8c39cad9 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -33,6 +33,7 @@ pub const Location = enum { }) catch return error.OutOfMemory; break :user internal_os.xdg.config( + global.io(), arena_alloc, &environ_map, .{ .subdir = subdir }, diff --git a/src/crash/dir.zig b/src/crash/dir.zig index 46e3ba2ac..0f877ee32 100644 --- a/src/crash/dir.zig +++ b/src/crash/dir.zig @@ -1,14 +1,11 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const internal_os = @import("../os/main.zig"); -const global = @import("../global.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 { - var environ_map = try global.environMap(); - defer environ_map.deinit(); - const crash_dir = try internal_os.xdg.state(alloc, &environ_map, .{ .subdir = "ghostty/crash" }); +pub fn defaultDir(io: std.Io, alloc: Allocator, environ_map: *const std.process.Environ.Map) !Dir { + const crash_dir = try internal_os.xdg.state(io, alloc, environ_map, .{ .subdir = "ghostty/crash" }); errdefer alloc.free(crash_dir); return .{ .path = crash_dir }; } @@ -21,13 +18,13 @@ pub const Dir = struct { /// Returns an iterator over the crash reports in this directory. This /// iterator must be freed with `ReportIterator.deinit`. The iterator /// may have no reports. - pub fn iterator(self: *const Dir) !ReportIterator { + pub fn iterator(self: *const Dir, io: std.Io) !ReportIterator { var dir = std.Io.Dir.openDirAbsolute( - global.io(), + io, self.path, .{ .iterate = true }, ) catch return .{}; - errdefer dir.close(global.io()); + errdefer dir.close(io); return .{ .dir = dir, @@ -40,22 +37,22 @@ pub const ReportIterator = struct { dir: ?std.Io.Dir = null, it: std.Io.Dir.Iterator = undefined, - pub fn deinit(self: *ReportIterator) void { - if (self.dir) |dir| dir.close(global.io()); + pub fn deinit(self: *ReportIterator, io: std.Io) void { + if (self.dir) |dir| dir.close(io); } - pub fn next(self: *ReportIterator) !?Report { + pub fn next(self: *ReportIterator, io: std.Io) !?Report { // If we have no dir then we failed to open the directory. const dir = self.dir orelse return null; // Get the next file entry, if any. const entry = entry: while (true) { - const entry = try self.it.next(global.io()) orelse return null; + const entry = try self.it.next(io) orelse return null; if (entry.kind != .file) continue; break :entry entry; }; - const stat = try dir.statFile(global.io(), entry.name, .{}); + const stat = try dir.statFile(io, entry.name, .{}); return .{ .name = entry.name, .mtime = stat.mtime.toNanoseconds(), diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index b0ad68937..8b6471f16 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -7,7 +7,6 @@ const build_options = @import("build_options"); const sentry = if (build_options.sentry) @import("sentry"); const internal_os = @import("../os/main.zig"); const crash = @import("main.zig"); -const global = @import("../global.zig"); const Surface = @import("../Surface.zig"); const log = std.log.scoped(.sentry); @@ -16,6 +15,16 @@ const log = std.log.scoped(.sentry); /// handling is a global process-wide thing. var init_thread: ?std.Thread = null; +/// Directory memory, holds the cache and state dirs persistently. This +/// prevents any sort of crashes due to initialization races. +var dir_mem: [std.fs.max_path_bytes * 2]u8 = undefined; + +/// Holds the XDG cache dir. +var cache_dir_: ?[]const u8 = null; + +/// Holds the XDG state dir. +var state_dir_: ?[]const u8 = null; + /// Thread-local state that can be set by thread main functions so that /// crashes have more context. /// @@ -46,8 +55,8 @@ pub threadlocal var thread_state: ?ThreadState = null; /// NOT send any data over the network. We use the Sentry native SDK to collect /// crash reports and logs, but we only store them locally (see Transport). /// It is up to the user to grab the logs and manually send them to us -/// (or they own Sentry instance) if they want to. -pub fn init(gpa: Allocator) !void { +/// (or to their own Sentry instance) if they want to. +pub fn init(gpa: Allocator, environ_map: *const std.process.Environ.Map) !void { if (comptime !build_options.sentry) return; // Not supported on Windows currently, doesn't build. @@ -56,6 +65,26 @@ pub fn init(gpa: Allocator) !void { // Must only start once assert(init_thread == null); + // Get our directories. + var single_threaded: std.Io.Threaded = .init_single_threaded; + defer single_threaded.deinit(); + var fba: std.heap.FixedBufferAllocator = .init(&dir_mem); + + state_dir_ = state_dir: { + const dir = try crash.defaultDir(single_threaded.io(), gpa, environ_map); + defer gpa.free(dir.path); + break :state_dir try fba.allocator().dupe(u8, dir.path); + }; + errdefer state_dir_ = null; + + const cache_dir = cache_dir: { + const dir = try cacheDir(single_threaded.io(), gpa, environ_map); + defer gpa.free(dir); + break :cache_dir try fba.allocator().dupe(u8, dir); + }; + cache_dir_ = cache_dir; + errdefer cache_dir_ = null; + // We use a thread for initializing Sentry because initialization takes // ~2k ns on my M3 Max. That's not a LOT of time but it's enough to be // 90% of our pre-App startup time. Everything Sentry is doing initially @@ -64,13 +93,14 @@ pub fn init(gpa: Allocator) !void { const thr = try std.Thread.spawn( .{}, initThread, - .{gpa}, + .{cache_dir}, ); - thr.setName(global.io(), "sentry-init") catch {}; + + thr.setName(single_threaded.io(), "sentry-init") catch {}; init_thread = thr; } -fn initThread(gpa: Allocator) !void { +fn initThread(cache_dir: []const u8) !void { if (comptime !build_options.sentry) return; // Right now, on Darwin, `std.Thread.setName` can only name the current @@ -80,10 +110,6 @@ fn initThread(gpa: Allocator) !void { internal_os.macos.pthread_setname_np(&"sentry-init".*); } - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const alloc = arena.allocator(); - const transport = sentry.Transport.init(&Transport.send); // This will crash if the transport was never used so we avoid // that for now. This probably leaks some memory but it'd be very @@ -104,27 +130,6 @@ fn initThread(gpa: Allocator) !void { // do here and why we use this. sentry.c.sentry_options_set_before_send(opts, beforeSend, null); - // Determine the Sentry cache directory. - const cache_dir = cache_dir: { - // On macOS, we prefer to use the NSCachesDirectory value to be - // a more idiomatic macOS application. But if XDG env vars are set - // we will respect them. - if (comptime builtin.os.tag == .macos) macos: { - if (global.environ().containsUnemptyConstant("XDG_CACHE_HOME")) break :macos; - break :cache_dir try internal_os.macos.cacheDir( - alloc, - "sentry", - ); - } - - var environ_map = try global.environMap(); - defer environ_map.deinit(); - break :cache_dir try internal_os.xdg.cache( - alloc, - &environ_map, - .{ .subdir = "ghostty/sentry" }, - ); - }; sentry.c.sentry_options_set_database_path_n( opts, cache_dir.ptr, @@ -149,6 +154,28 @@ fn initThread(gpa: Allocator) !void { log.debug("sentry initialized database={s}", .{cache_dir}); } +fn cacheDir(io: std.Io, alloc: Allocator, environ_map: *const std.process.Environ.Map) ![]const u8 { + // On macOS, we prefer to use the NSCachesDirectory value to be + // a more idiomatic macOS application. But if XDG env vars are set + // we will respect them. + if (comptime builtin.os.tag == .macos) macos: { + const xdg_cache_home = environ_map.get("XDG_CACHE_HOME") orelse break :macos; + if (xdg_cache_home.len > 0) { + return try internal_os.macos.cacheDir( + alloc, + "sentry", + ); + } + } + + return try internal_os.xdg.cache( + io, + alloc, + environ_map, + .{ .subdir = "ghostty/sentry" }, + ); +} + /// Process-wide deinitialization of our Sentry client. This ensures all /// our data is flushed. pub fn deinit() void { @@ -252,7 +279,13 @@ pub const Transport = struct { /// Implementation of send but we can use Zig errors. fn sendInternal(envelope: *sentry.Envelope) !void { - var arena = std.heap.ArenaAllocator.init(global.alloc()); + const state_dir = state_dir_ orelse return error.StateDirNotInitialized; + + // The I/O and allocator we use here are just meant to get the job + // done for saving the crash report. + var single_threaded: std.Io.Threaded = .init_single_threaded; + defer single_threaded.deinit(); + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); @@ -281,20 +314,17 @@ pub const Transport = struct { // conflict. const uuid = sentry.UUID.init(); - // Get our XDG state directory where we'll store the crash reports. - // This directory must exist for writing to work. - const dir = try crash.defaultDir(alloc); - try std.Io.Dir.cwd().createDirPath(global.io(), dir.path); + try std.Io.Dir.cwd().createDirPath(single_threaded.io(), state_dir); // Build our final path and write to it. const path = try std.fs.path.join(alloc, &.{ - dir.path, + state_dir, try std.fmt.allocPrint(alloc, "{s}.ghosttycrash", .{uuid.string()}), }); - const file = try std.Io.Dir.cwd().createFile(global.io(), path, .{}); - defer file.close(global.io()); + const file = try std.Io.Dir.cwd().createFile(single_threaded.io(), path, .{}); + defer file.close(single_threaded.io()); var buf: [4096]u8 = undefined; - var file_writer = file.writer(global.io(), &buf); + var file_writer = file.writer(single_threaded.io(), &buf); try file_writer.interface.writeAll(json); try file_writer.end(); diff --git a/src/global.zig b/src/global.zig index 3c2ddbb72..11aa17b3a 100644 --- a/src/global.zig +++ b/src/global.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const build_config = @import("build_config.zig"); +const build_options = @import("build_options"); const cli = @import("cli.zig"); const internal_os = @import("os/main.zig"); const fontconfig = @import("fontconfig"); @@ -179,13 +180,17 @@ pub fn init(opts: InitOpts) !void { // As early as possible, initialize our resource limits. self.rlimits = .init(); - // Initialize our crash reporting. - crash.init(self.alloc) catch |err| { - std.log.warn( - "sentry init failed, no crash capture available err={}", - .{err}, - ); - }; + if (build_options.sentry) { + // Initialize our crash reporting. + var environ_map = try self.environ.createMap(self.alloc); + defer environ_map.deinit(); + crash.init(self.alloc, &environ_map) catch |err| { + std.log.warn( + "sentry init failed, no crash capture available err={}", + .{err}, + ); + }; + } // const sentrylib = @import("sentry"); // if (sentrylib.captureEvent(sentrylib.Value.initMessageEvent( diff --git a/src/os/homedir.zig b/src/os/homedir.zig index 4972621eb..8da9640a2 100644 --- a/src/os/homedir.zig +++ b/src/os/homedir.zig @@ -1,7 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); const passwd = @import("passwd.zig"); -const global = @import("../global.zig"); const objc = @import("objc"); const Error = error{ @@ -11,9 +10,9 @@ const Error = error{ /// Determine the home directory for the currently executing user. This /// is generally an expensive process so the value should be cached. -pub inline fn home(environ_map: *const std.process.Environ.Map, buf: []u8) !?[]const u8 { +pub inline fn home(io: std.Io, environ_map: *const std.process.Environ.Map, buf: []u8) !?[]const u8 { return switch (builtin.os.tag) { - .linux, .freebsd, .macos => try homeUnix(environ_map, buf), + .linux, .freebsd, .macos => try homeUnix(io, environ_map, buf), .windows => homeWindows(environ_map, buf) catch return error.BufferTooSmall, // iOS doesn't have a user-writable home directory @@ -23,7 +22,7 @@ pub inline fn home(environ_map: *const std.process.Environ.Map, buf: []u8) !?[]c }; } -fn homeUnix(environ_map: *const std.process.Environ.Map, buf: []u8) !?[]const u8 { +fn homeUnix(io: std.Io, environ_map: *const std.process.Environ.Map, buf: []u8) !?[]const u8 { // First: if we have a HOME env var, then we use that. if (environ_map.get("HOME")) |result| { if (buf.len < result.len) return Error.BufferTooSmall; @@ -60,8 +59,9 @@ fn homeUnix(environ_map: *const std.process.Environ.Map, buf: []u8) !?[]const u8 // If all else fails, have the shell tell us... fba.reset(); - const run = try std.process.run(fba.allocator(), global.io(), .{ + const run = try std.process.run(fba.allocator(), io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", "cd && pwd" }, + .environ_map = environ_map, .stdout_limit = .limited(fba.buffer.len / 2), .stderr_limit = .limited(fba.buffer.len / 2), }); @@ -97,9 +97,14 @@ pub const ExpandError = error{ /// /// Errors if `home` fails or if the size of the expanded path is larger /// than `buf.len`. -pub fn expandHome(environ_map: *const std.process.Environ.Map, path: []const u8, buf: []u8) ExpandError![]const u8 { +pub fn expandHome( + io: std.Io, + environ_map: *const std.process.Environ.Map, + path: []const u8, + buf: []u8, +) ExpandError![]const u8 { return switch (builtin.os.tag) { - .linux, .freebsd, .macos => try expandHomeUnix(environ_map, path, buf), + .linux, .freebsd, .macos => try expandHomeUnix(io, environ_map, path, buf), // `~/` is not an idiom generally used on Windows .windows => return path, @@ -111,9 +116,14 @@ pub fn expandHome(environ_map: *const std.process.Environ.Map, path: []const u8, }; } -fn expandHomeUnix(environ_map: *const std.process.Environ.Map, path: []const u8, buf: []u8) ExpandError![]const u8 { +fn expandHomeUnix( + io: std.Io, + environ_map: *const std.process.Environ.Map, + path: []const u8, + buf: []u8, +) ExpandError![]const u8 { if (!std.mem.startsWith(u8, path, "~/")) return path; - const home_dir: []const u8 = if (home(environ_map, buf)) |home_| + const home_dir: []const u8 = if (home(io, environ_map, buf)) |home_| home_ orelse return error.HomeDetectionFailed else |_| return error.HomeDetectionFailed; @@ -130,30 +140,32 @@ test "expandHomeUnix" { if (builtin.os.tag == .windows) return error.SkipZigTest; const testing = std.testing; + const io = testing.io; const allocator = testing.allocator; var environ_map = try testing.environ.createMap(testing.allocator); defer environ_map.deinit(); var buf: [std.fs.max_path_bytes]u8 = undefined; - const home_dir = try expandHomeUnix(&environ_map, "~/", &buf); + const home_dir = try expandHomeUnix(io, &environ_map, "~/", &buf); // Joining the home directory `~` with the path `/` // the result should end with a separator here. (e.g. `/home/user/`) try testing.expect(home_dir[home_dir.len - 1] == std.fs.path.sep); - const downloads = try expandHomeUnix(&environ_map, "~/Downloads/shader.glsl", &buf); + const downloads = try expandHomeUnix(io, &environ_map, "~/Downloads/shader.glsl", &buf); const expected_downloads = try std.mem.concat(allocator, u8, &[_][]const u8{ home_dir, "Downloads/shader.glsl" }); defer allocator.free(expected_downloads); try testing.expectEqualStrings(expected_downloads, downloads); - try testing.expectEqualStrings("~", try expandHomeUnix(&environ_map, "~", &buf)); - try testing.expectEqualStrings("~abc/", try expandHomeUnix(&environ_map, "~abc/", &buf)); - try testing.expectEqualStrings("/home/user", try expandHomeUnix(&environ_map, "/home/user", &buf)); - try testing.expectEqualStrings("", try expandHomeUnix(&environ_map, "", &buf)); + try testing.expectEqualStrings("~", try expandHomeUnix(io, &environ_map, "~", &buf)); + try testing.expectEqualStrings("~abc/", try expandHomeUnix(io, &environ_map, "~abc/", &buf)); + try testing.expectEqualStrings("/home/user", try expandHomeUnix(io, &environ_map, "/home/user", &buf)); + try testing.expectEqualStrings("", try expandHomeUnix(io, &environ_map, "", &buf)); // Expect an error if the buffer is large enough to hold the home directory, // but not the expanded path var small_buf = try allocator.alloc(u8, home_dir.len); defer allocator.free(small_buf); try testing.expectError(error.BufferTooSmall, expandHomeUnix( + io, &environ_map, "~/Downloads", small_buf[0..], @@ -162,11 +174,12 @@ test "expandHomeUnix" { test { const testing = std.testing; + const io = testing.io; var environ_map = try testing.environ.createMap(testing.allocator); defer environ_map.deinit(); var buf: [1024]u8 = undefined; - const result = try home(&environ_map, &buf); + const result = try home(io, &environ_map, &buf); try testing.expect(result != null); try testing.expect(result.?.len > 0); } diff --git a/src/os/xdg.zig b/src/os/xdg.zig index 7659bbd14..92029586d 100644 --- a/src/os/xdg.zig +++ b/src/os/xdg.zig @@ -6,7 +6,6 @@ const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const posix = std.posix; const homedir = @import("homedir.zig"); -const global = @import("../global.zig"); pub const Options = struct { /// Subdirectories to join to the base. This avoids extra allocations @@ -20,8 +19,8 @@ pub const Options = struct { }; /// Get the XDG user config directory. The returned value is allocated. -pub fn config(alloc: Allocator, environ_map: *const std.process.Environ.Map, opts: Options) ![]u8 { - return try dir(alloc, environ_map, opts, .{ +pub fn config(io: std.Io, alloc: Allocator, environ_map: *const std.process.Environ.Map, opts: Options) ![]u8 { + return try dir(io, alloc, environ_map, opts, .{ .env = "XDG_CONFIG_HOME", .windows_env = "LOCALAPPDATA", .default_subdir = ".config", @@ -29,8 +28,8 @@ pub fn config(alloc: Allocator, environ_map: *const std.process.Environ.Map, opt } /// Get the XDG cache directory. The returned value is allocated. -pub fn cache(alloc: Allocator, environ_map: *const std.process.Environ.Map, opts: Options) ![]u8 { - return try dir(alloc, environ_map, opts, .{ +pub fn cache(io: std.Io, alloc: Allocator, environ_map: *const std.process.Environ.Map, opts: Options) ![]u8 { + return try dir(io, alloc, environ_map, opts, .{ .env = "XDG_CACHE_HOME", .windows_env = "LOCALAPPDATA", .default_subdir = ".cache", @@ -38,8 +37,8 @@ pub fn cache(alloc: Allocator, environ_map: *const std.process.Environ.Map, opts } /// Get the XDG state directory. The returned value is allocated. -pub fn state(alloc: Allocator, environ_map: *const std.process.Environ.Map, opts: Options) ![]u8 { - return try dir(alloc, environ_map, opts, .{ +pub fn state(io: std.Io, alloc: Allocator, environ_map: *const std.process.Environ.Map, opts: Options) ![]u8 { + return try dir(io, alloc, environ_map, opts, .{ .env = "XDG_STATE_HOME", .windows_env = "LOCALAPPDATA", .default_subdir = ".local/state", @@ -54,6 +53,7 @@ const InternalOptions = struct { /// Unified helper to get XDG directories that follow a common pattern. fn dir( + io: std.Io, alloc: Allocator, environ_map: *const std.process.Environ.Map, opts: Options, @@ -89,7 +89,7 @@ fn dir( // Get our home dir var buf: [1024]u8 = undefined; - if (try homedir.home(environ_map, &buf)) |home| { + if (try homedir.home(io, environ_map, &buf)) |home| { return try std.fs.path.join(alloc, &[_][]const u8{ home, internal_opts.default_subdir, @@ -119,12 +119,13 @@ pub fn parseTerminalExec(argv: []const [*:0]const u8) ?[]const [*:0]const u8 { test { const testing = std.testing; + const io = testing.io; const alloc = testing.allocator; var environ_map = try testing.environ.createMap(alloc); defer environ_map.deinit(); { - const value = try config(alloc, &environ_map, .{}); + const value = try config(io, alloc, &environ_map, .{}); defer alloc.free(value); try testing.expect(value.len > 0); } @@ -132,6 +133,7 @@ test { test "cache directory paths" { const testing = std.testing; + const io = testing.io; const alloc = testing.allocator; const mock_home = if (builtin.os.tag == .windows) "C:\\Users\\test" else "/Users/test"; var environ_map = try testing.environ.createMap(alloc); @@ -141,7 +143,7 @@ test "cache directory paths" { { // Test base path { - const cache_path = try cache(alloc, &environ_map, .{ .home = mock_home }); + const cache_path = try cache(io, alloc, &environ_map, .{ .home = mock_home }); defer alloc.free(cache_path); const expected = try std.fs.path.join(alloc, &.{ mock_home, ".cache" }); defer alloc.free(expected); @@ -150,7 +152,7 @@ test "cache directory paths" { // Test with subdir { - const cache_path = try cache(alloc, &environ_map, .{ + const cache_path = try cache(io, alloc, &environ_map, .{ .home = mock_home, .subdir = "ghostty", }); @@ -165,11 +167,12 @@ test "cache directory paths" { test "fallback when xdg env empty" { if (builtin.os.tag == .windows) return error.SkipZigTest; + const io = std.testing.io; const alloc = std.testing.allocator; const DirCase = struct { name: [:0]const u8, - func: fn (Allocator, *std.process.Environ.Map, Options) anyerror![]u8, + func: fn (std.Io, Allocator, *std.process.Environ.Map, Options) anyerror![]u8, default_subdir: []const u8, }; @@ -193,7 +196,7 @@ test "fallback when xdg env empty" { // Test with empty string - should fallback to home try environ_map.put(case.name, ""); - const actual = try case.func(alloc, &environ_map, .{}); + const actual = try case.func(io, alloc, &environ_map, .{}); defer alloc.free(actual); try std.testing.expectEqualStrings(expected, actual); @@ -203,11 +206,12 @@ test "fallback when xdg env empty" { test "fallback when xdg env empty and subdir" { if (builtin.os.tag == .windows) return error.SkipZigTest; + const io = std.testing.io; const alloc = std.testing.allocator; const DirCase = struct { name: [:0]const u8, - func: fn (Allocator, *const std.process.Environ.Map, Options) anyerror![]u8, + func: fn (std.Io, Allocator, *const std.process.Environ.Map, Options) anyerror![]u8, default_subdir: []const u8, }; @@ -232,7 +236,7 @@ test "fallback when xdg env empty and subdir" { // Test with empty string - should fallback to home try environ_map.put(case.name, ""); - const actual = try case.func(alloc, &environ_map, .{ .subdir = "ghostty" }); + const actual = try case.func(io, alloc, &environ_map, .{ .subdir = "ghostty" }); defer alloc.free(actual); try std.testing.expectEqualStrings(expected, actual); diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 372ccb41c..c23ff9ad9 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -394,7 +394,7 @@ fn setupBash( var environ_map = try global.environMap(); defer environ_map.deinit(); var home_buf: [1024]u8 = undefined; - if (try homedir.home(&environ_map, &home_buf)) |home| { + if (try homedir.home(global.io(), &environ_map, &home_buf)) |home| { var histfile_buf: [std.fs.max_path_bytes]u8 = undefined; const histfile = try std.fmt.bufPrint( &histfile_buf,