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,