diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/os | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/os')
28 files changed, 1010 insertions, 946 deletions
diff --git a/vendor/libghostty-vt/src/os/TempDir.zig b/vendor/libghostty-vt/src/os/TempDir.zig index c3d344d9..c25589a2 100644 --- a/vendor/libghostty-vt/src/os/TempDir.zig +++ b/vendor/libghostty-vt/src/os/TempDir.zig @@ -3,8 +3,9 @@ const TempDir = @This(); const std = @import("std"); -const Dir = std.fs.Dir; +const Dir = std.Io.Dir; const file = @import("file.zig"); +const global = @import("../global.zig"); const log = std.log.scoped(.tempdir); @@ -27,10 +28,10 @@ pub fn init() !TempDir { var tmp_path_buf: [file.random_basename_len:0]u8 = undefined; const dir = dir: { - const cwd = std.fs.cwd(); - const tmp_dir = try file.allocTmpDir(std.heap.page_allocator); + const cwd = std.Io.Dir.cwd(); + const tmp_dir = try file.allocTmpDir(std.heap.page_allocator, global.environ()); defer file.freeTmpDir(std.heap.page_allocator, tmp_dir); - break :dir try cwd.openDir(tmp_dir, .{}); + break :dir try cwd.openDir(global.io(), tmp_dir, .{}); }; // We now loop forever until we can find a directory that we can create. @@ -38,13 +39,13 @@ pub fn init() !TempDir { const tmp_path = try file.randomBasename(&tmp_path_buf); tmp_path_buf[tmp_path.len] = 0; - dir.makeDir(tmp_path) catch |err| switch (err) { + dir.createDir(global.io(), tmp_path, .default_dir) catch |err| switch (err) { error.PathAlreadyExists => continue, else => |e| return e, }; return TempDir{ - .dir = try dir.openDir(tmp_path, .{}), + .dir = try dir.openDir(global.io(), tmp_path, .{}), .parent = dir, .name_buf = tmp_path_buf, }; @@ -60,25 +61,69 @@ pub fn name(self: *TempDir) []const u8 { /// Finish with the temporary directory. This deletes all contents in the /// directory. pub fn deinit(self: *TempDir) void { - self.dir.close(); - self.parent.deleteTree(self.name()) catch |err| - log.err("error deleting temp dir err={}", .{err}); + self.close(.delete); +} + +pub const CloseMode = enum { delete, retain }; + +/// Close the directory handles, optionally retaining the temporary directory +/// and its contents on disk. +pub fn close(self: *TempDir, mode: CloseMode) void { + self.dir.close(global.io()); + switch (mode) { + .delete => self.parent.deleteTree(global.io(), self.name()) catch |err| + log.err("error deleting temp dir err={}", .{err}), + .retain => {}, + } + self.parent.close(global.io()); } test { const testing = std.testing; - var td = try init(); - errdefer td.deinit(); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + var path_len: usize = undefined; + { + var td = try init(); + errdefer td.deinit(); - const nameval = td.name(); - try testing.expect(nameval.len > 0); + const nameval = td.name(); + try testing.expect(nameval.len > 0); + + // Can open a new handle to it proves it exists. + var dir = try td.parent.openDir(testing.io, nameval, .{}); + dir.close(testing.io); + + path_len = try td.dir.realPath(testing.io, &path_buf); + + // Don't check the raw handles after close: descriptor numbers can be + // reused immediately, so a valid number does not imply that we leaked it. + td.deinit(); + } - // Can open a new handle to it proves it exists. - var dir = try td.parent.openDir(nameval, .{}); - dir.close(); + try testing.expectError( + error.FileNotFound, + Dir.openDirAbsolute(testing.io, path_buf[0..path_len], .{}), + ); +} + +test "close retains temporary directory" { + const testing = std.testing; + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + var path_len: usize = undefined; + { + var td = try init(); + errdefer td.deinit(); + + path_len = try td.dir.realPath(testing.io, &path_buf); + + // Don't check the raw handles after close: descriptor numbers can be + // reused immediately, so a valid number does not imply that we leaked it. + td.close(.retain); + } + defer Dir.deleteDirAbsolute(testing.io, path_buf[0..path_len]) catch {}; - // Should be deleted after we deinit - td.deinit(); - try testing.expectError(error.FileNotFound, td.parent.openDir(nameval, .{})); + var dir = try Dir.openDirAbsolute(testing.io, path_buf[0..path_len], .{}); + dir.close(testing.io); } diff --git a/vendor/libghostty-vt/src/os/args.zig b/vendor/libghostty-vt/src/os/args.zig deleted file mode 100644 index 9ef5bba4..00000000 --- a/vendor/libghostty-vt/src/os/args.zig +++ /dev/null @@ -1,130 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const Allocator = std.mem.Allocator; -const objc = @import("objc"); -const macos = @import("macos"); - -/// Returns an iterator over the command line arguments. This may or may -/// not allocate depending on the platform. -/// -/// For Zig-aware readers: this is the same as std.process.argsWithAllocator -/// but handles macOS using NSProcessInfo instead of libc argc/argv. -pub fn iterator(allocator: Allocator) ArgIterator.InitError!ArgIterator { - //if (true) return try std.process.argsWithAllocator(allocator); - return .initWithAllocator(allocator); -} - -/// Duck-typed to std.process.ArgIterator -pub const ArgIterator = switch (builtin.os.tag) { - .macos => IteratorMacOS, - else => std.process.ArgIterator, -}; - -/// This is an ArgIterator (duck-typed for std.process.ArgIterator) for -/// NSApplicationMain-based applications on macOS. It uses NSProcessInfo to -/// get the command line arguments since libc argc/argv pointers are not -/// valid. -/// -/// I believe this should work for all macOS applications even if -/// NSApplicationMain is not used, but I haven't tested that so I'm not -/// sure. If/when libghostty is ever used outside of NSApplicationMain -/// then we can revisit this. -const IteratorMacOS = struct { - alloc: Allocator, - index: usize, - count: usize, - buf: [:0]u8, - args: objc.Object, - - pub const InitError = Allocator.Error; - - pub fn initWithAllocator(alloc: Allocator) InitError!IteratorMacOS { - const NSProcessInfo = objc.getClass("NSProcessInfo").?; - const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); - const args = info.getProperty(objc.Object, "arguments"); - errdefer args.release(); - - // Determine our maximum length so we can allocate the buffer to - // fit all values. - var max: usize = 0; - const count: usize = @intCast(args.getProperty(c_ulong, "count")); - for (0..count) |i| { - const nsstr = args.msgSend( - objc.Object, - objc.sel("objectAtIndex:"), - .{@as(c_ulong, @intCast(i))}, - ); - - const maxlen: usize = @intCast(nsstr.msgSend( - c_ulong, - objc.sel("maximumLengthOfBytesUsingEncoding:"), - .{@as(c_ulong, 4)}, - )); - - max = @max(max, maxlen); - } - - // Allocate our buffer. We add 1 for the null terminator. - const buf = try alloc.allocSentinel(u8, max, 0); - errdefer alloc.free(buf); - - return .{ - .alloc = alloc, - .index = 0, - .count = count, - .buf = buf, - .args = args, - }; - } - - pub fn deinit(self: *IteratorMacOS) void { - self.alloc.free(self.buf); - - // Note: we don't release self.args because it is a pointer copy - // not a retained object. - } - - pub fn next(self: *IteratorMacOS) ?[:0]const u8 { - if (self.index == self.count) return null; - - // NSString. No release because not a copy. - const nsstr = self.args.msgSend( - objc.Object, - objc.sel("objectAtIndex:"), - .{@as(c_ulong, @intCast(self.index))}, - ); - self.index += 1; - - // Convert to string using getCString. Our buffer should always - // be big enough because we precomputed the maximum length. - if (!nsstr.msgSend( - bool, - objc.sel("getCString:maxLength:encoding:"), - .{ - @as([*]u8, @ptrCast(self.buf.ptr)), - @as(c_ulong, @intCast(self.buf.len)), - @as(c_ulong, 4), // NSUTF8StringEncoding - }, - )) { - // This should never happen... if it does, we just return empty. - return ""; - } - - return std.mem.sliceTo(self.buf, 0); - } - - pub fn skip(self: *IteratorMacOS) bool { - if (self.index == self.count) return false; - self.index += 1; - return true; - } -}; - -test "args" { - const testing = std.testing; - const alloc = testing.allocator; - - var iter = try iterator(alloc); - defer iter.deinit(); - try testing.expect(iter.next().?.len > 0); -} diff --git a/vendor/libghostty-vt/src/os/cf_release_thread.zig b/vendor/libghostty-vt/src/os/cf_release_thread.zig index 445dc486..e93120b3 100644 --- a/vendor/libghostty-vt/src/os/cf_release_thread.zig +++ b/vendor/libghostty-vt/src/os/cf_release_thread.zig @@ -9,7 +9,8 @@ const builtin = @import("builtin"); const macos = @import("macos"); const internal_os = @import("../os/main.zig"); -const xev = @import("../global.zig").xev; +const global = @import("../global.zig"); +const xev = global.xev; const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; const Allocator = std.mem.Allocator; @@ -142,11 +143,11 @@ fn threadMain_(self: *Thread) !void { fn drainMailbox(self: *Thread) !void { // If we're draining, we just drain the mailbox and return. if (self.flags.drain) { - while (self.mailbox.pop()) |_| {} + while (self.mailbox.pop(global.io())) |_| {} return; } - while (self.mailbox.pop()) |message| { + while (self.mailbox.pop(global.io())) |message| { // log.debug("mailbox message={}", .{message}); switch (message) { .release => |msg| { diff --git a/vendor/libghostty-vt/src/os/cgroup.zig b/vendor/libghostty-vt/src/os/cgroup.zig index 9e68a50f..f5eacc6c 100644 --- a/vendor/libghostty-vt/src/os/cgroup.zig +++ b/vendor/libghostty-vt/src/os/cgroup.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const global = @import("../global.zig"); const log = std.log.scoped(.@"linux-cgroup"); @@ -11,16 +12,16 @@ pub fn current(buf: []u8, pid: u32) ?[]const u8 { // 0::/user.slice/user-1000.slice/session-1.scope // The cgroup path is the third field. const path = std.fmt.bufPrint(&path_buf, "/proc/{}/cgroup", .{pid}) catch return null; - const file = std.fs.openFileAbsolute(path, .{}) catch return null; - defer file.close(); + const file = std.Io.Dir.openFileAbsolute(global.io(), path, .{}) catch return null; + defer file.close(global.io()); var read_buf: [64]u8 = undefined; - var file_reader = file.reader(&read_buf); + var file_reader = file.reader(global.io(), &read_buf); const reader = &file_reader.interface; const len = reader.readSliceShort(buf) catch return null; const contents = buf[0..len]; // Find the last ':' const idx = std.mem.lastIndexOfScalar(u8, contents, ':') orelse return null; - return std.mem.trimRight(u8, contents[idx + 1 ..], " \r\n"); + return std.mem.trimEnd(u8, contents[idx + 1 ..], " \r\n"); } diff --git a/vendor/libghostty-vt/src/os/desktop.zig b/vendor/libghostty-vt/src/os/desktop.zig index 93bfb74b..60ec51a5 100644 --- a/vendor/libghostty-vt/src/os/desktop.zig +++ b/vendor/libghostty-vt/src/os/desktop.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const build_config = @import("../build_config.zig"); -const posix = std.posix; +const global = @import("../global.zig"); const c = @cImport({ @cInclude("unistd.h"); @@ -26,7 +26,7 @@ pub fn launchedFromDesktop() bool { // was launched from the desktop. if (build_config.artifact == .lib) lib: { const env = "GHOSTTY_MAC_LAUNCH_SOURCE"; - const source = posix.getenv(env) orelse break :lib; + const source = global.environ().getPosix(env) orelse break :lib; // Source can be "app", "cli", or "zig_run". We assume // its the desktop only if its "app". We may want to do @@ -44,7 +44,7 @@ pub fn launchedFromDesktop() bool { // another terminal was launched from a desktop file and then launches // Ghostty and Ghostty inherits the env. .linux, .freebsd => ul: { - const gio_pid_str = posix.getenv("GIO_LAUNCHED_DESKTOP_FILE_PID") orelse + const gio_pid_str = global.environ().getPosix("GIO_LAUNCHED_DESKTOP_FILE_PID") orelse break :ul false; const pid = c.getpid(); @@ -80,7 +80,7 @@ pub const DesktopEnvironment = enum { /// Detect what desktop environment we are running under. This is mainly used /// on Linux and BSD to enable or disable certain features but there may be more uses in /// the future. -pub fn desktopEnvironment() DesktopEnvironment { +pub fn desktopEnvironment(environ_map: *const std.process.Environ.Map) DesktopEnvironment { return switch (comptime builtin.os.tag) { .macos => .macos, .windows => .windows, @@ -89,7 +89,7 @@ pub fn desktopEnvironment() DesktopEnvironment { // Use $XDG_SESSION_DESKTOP to determine what DE we are using on Linux // https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html#desktop= - if (posix.getenv("XDG_SESSION_DESKTOP")) |sd| { + if (environ_map.get("XDG_SESSION_DESKTOP")) |sd| { if (std.ascii.eqlIgnoreCase("gnome", sd)) break :de .gnome; if (std.ascii.eqlIgnoreCase("gnome-xorg", sd)) break :de .gnome; } @@ -99,7 +99,7 @@ pub fn desktopEnvironment() DesktopEnvironment { // colon-separated list of up to three desktop names, although we // only look at the first. // https://specifications.freedesktop.org/desktop-entry-spec/latest/recognized-keys.html - if (posix.getenv("XDG_CURRENT_DESKTOP")) |cd| { + if (environ_map.get("XDG_CURRENT_DESKTOP")) |cd| { var cd_it = std.mem.splitScalar(u8, cd, ':'); const cd_first = cd_it.first(); if (std.ascii.eqlIgnoreCase(cd_first, "gnome")) break :de .gnome; @@ -114,46 +114,55 @@ pub fn desktopEnvironment() DesktopEnvironment { test "desktop environment" { const testing = std.testing; + // Always run these tests with a blank evironment map so that we don't get any + // failures from values in the "real" evironment leaking in. switch (builtin.os.tag) { - .macos => try testing.expectEqual(.macos, desktopEnvironment()), - .windows => try testing.expectEqual(.windows, desktopEnvironment()), + inline .macos, .windows => |tag| { + var environ_map: std.process.Environ.Map = .init(testing.allocator); + defer environ_map.deinit(); + try testing.expectEqual(@tagName(tag), @tagName(desktopEnvironment(&environ_map))); + }, .linux, .freebsd => { - const getenv = std.posix.getenv; - const setenv = @import("env.zig").setenv; - const unsetenv = @import("env.zig").unsetenv; - - const xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); - defer if (xdg_current_desktop) |v| { - _ = setenv("XDG_CURRENT_DESKTOP", v); - } else { - _ = unsetenv("XDG_CURRENT_DESKTOP"); - }; - _ = unsetenv("XDG_CURRENT_DESKTOP"); - - const xdg_session_desktop = getenv("XDG_SESSION_DESKTOP"); - defer if (xdg_session_desktop) |v| { - _ = setenv("XDG_SESSION_DESKTOP", v); - } else { - _ = unsetenv("XDG_SESSION_DESKTOP"); - }; - _ = unsetenv("XDG_SESSION_DESKTOP"); - - _ = setenv("XDG_SESSION_DESKTOP", "gnome"); - try testing.expectEqual(.gnome, desktopEnvironment()); - _ = setenv("XDG_SESSION_DESKTOP", "gnome-xorg"); - try testing.expectEqual(.gnome, desktopEnvironment()); - _ = setenv("XDG_SESSION_DESKTOP", "foobar"); - try testing.expectEqual(.other, desktopEnvironment()); - - _ = unsetenv("XDG_SESSION_DESKTOP"); - try testing.expectEqual(.other, desktopEnvironment()); - - _ = setenv("XDG_CURRENT_DESKTOP", "GNOME"); - try testing.expectEqual(.gnome, desktopEnvironment()); - _ = setenv("XDG_CURRENT_DESKTOP", "FOOBAR"); - try testing.expectEqual(.other, desktopEnvironment()); - _ = unsetenv("XDG_CURRENT_DESKTOP"); - try testing.expectEqual(.other, desktopEnvironment()); + { + var environ_map: std.process.Environ.Map = .init(testing.allocator); + defer environ_map.deinit(); + try environ_map.put("XDG_SESSION_DESKTOP", "gnome"); + try testing.expectEqual(.gnome, desktopEnvironment(&environ_map)); + } + + { + var environ_map: std.process.Environ.Map = .init(testing.allocator); + defer environ_map.deinit(); + try environ_map.put("XDG_SESSION_DESKTOP", "gnome-xorg"); + try testing.expectEqual(.gnome, desktopEnvironment(&environ_map)); + } + + { + var environ_map: std.process.Environ.Map = .init(testing.allocator); + defer environ_map.deinit(); + try environ_map.put("XDG_SESSION_DESKTOP", "foobar"); + try testing.expectEqual(.other, desktopEnvironment(&environ_map)); + } + + { + var environ_map: std.process.Environ.Map = .init(testing.allocator); + defer environ_map.deinit(); + try environ_map.put("XDG_CURRENT_DESKTOP", "GNOME"); + try testing.expectEqual(.gnome, desktopEnvironment(&environ_map)); + } + + { + var environ_map: std.process.Environ.Map = .init(testing.allocator); + defer environ_map.deinit(); + try environ_map.put("XDG_CURRENT_DESKTOP", "FOOBAR"); + try testing.expectEqual(.other, desktopEnvironment(&environ_map)); + } + + { + var environ_map: std.process.Environ.Map = .init(testing.allocator); + defer environ_map.deinit(); + try testing.expectEqual(.other, desktopEnvironment(&environ_map)); + } }, else => try testing.expectEqual(.other, DesktopEnvironment()), } diff --git a/vendor/libghostty-vt/src/os/edit.zig b/vendor/libghostty-vt/src/os/edit.zig new file mode 100644 index 00000000..3e14bcaa --- /dev/null +++ b/vendor/libghostty-vt/src/os/edit.zig @@ -0,0 +1,69 @@ +const std = @import("std"); + +const global = @import("../global.zig"); +const ShellEscapeWriter = @import("shell.zig").ShellEscapeWriter; + +const log = std.log.scoped(.os_edit); + +const Options = struct { + default_editor: enum { + /// Return an error if $EDITOR or $VISUAL is not defined. + failure, + /// Default to "vi" if $EDITOR or $VISUAL is not defined. + vi, + /// Default to "nano" if $EDITOR or $VISUAL is not defined. + nano, + } = .vi, + + pub const default: Options = .{}; +}; + +pub const Error = error{NoEditorConfigured}; + +pub fn getConfigEditCommand(alloc: std.mem.Allocator, path: []const u8, options: Options) (Error || std.Io.Writer.Error || std.mem.Allocator.Error)![:0]const u8 { + const editor: []const u8 = editor: { + // VISUAL vs. EDITOR: https://unix.stackexchange.com/questions/4859/visual-vs-editor-what-s-the-difference + v: { + if (global.environ().containsUnempty(alloc, "VISUAL") catch break :v) { + break :editor global.environ().getAlloc(alloc, "VISUAL") catch break :v; + } + } + + e: { + if (global.environ().containsUnempty(alloc, "EDITOR") catch break :e) { + break :editor global.environ().getAlloc(alloc, "EDITOR") catch break :e; + } + } + + switch (options.default_editor) { + .failure => { + log.warn("$EDITOR or $VISUAL must be set to open config in a new window", .{}); + return error.NoEditorConfigured; + }, + .vi => { + log.warn("$EDITOR or $VISUAL not set, falling back to vi", .{}); + break :editor try alloc.dupe(u8, "vi"); + }, + .nano => { + log.warn("$EDITOR or $VISUAL not set, falling back to nano", .{}); + break :editor try alloc.dupe(u8, "nano"); + }, + } + }; + defer alloc.free(editor); + + var buffer: std.Io.Writer.Allocating = .init(alloc); + defer buffer.deinit(); + const writer = &buffer.writer; + + try writer.writeAll(editor); + try writer.writeByte(' '); + { + var sh: ShellEscapeWriter = .init(writer); + try sh.writer.writeAll(path); + try sh.writer.flush(); + } + try writer.flush(); + + return try buffer.toOwnedSliceSentinel(0); +} diff --git a/vendor/libghostty-vt/src/os/env.zig b/vendor/libghostty-vt/src/os/env.zig deleted file mode 100644 index 8f01a14d..00000000 --- a/vendor/libghostty-vt/src/os/env.zig +++ /dev/null @@ -1,178 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const Allocator = std.mem.Allocator; -const posix = std.posix; -const isFlatpak = @import("flatpak.zig").isFlatpak; - -pub const Error = Allocator.Error; - -/// Get the environment map. -pub fn getEnvMap(alloc: Allocator) !std.process.EnvMap { - return if (isFlatpak()) - std.process.EnvMap.init(alloc) - else - try std.process.getEnvMap(alloc); -} - -/// Append a value to an environment variable such as PATH. -/// The returned value is always allocated so it must be freed. -pub fn appendEnv( - alloc: Allocator, - current: []const u8, - value: []const u8, -) Error![]u8 { - // If there is no prior value, we return it as-is - if (current.len == 0) return try alloc.dupe(u8, value); - - // Otherwise we must prefix. - return try appendEnvAlways(alloc, current, value); -} - -/// Always append value to environment, even when it is empty. -/// This is useful because some env vars (like MANPATH) want there -/// to be an empty prefix to preserve existing values. -/// -/// The returned value is always allocated so it must be freed. -pub fn appendEnvAlways( - alloc: Allocator, - current: []const u8, - value: []const u8, -) Error![]u8 { - return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{ - current, - std.fs.path.delimiter, - value, - }); -} - -/// Prepend a value to an environment variable such as PATH. -/// The returned value is always allocated so it must be freed. -pub fn prependEnv( - alloc: Allocator, - current: []const u8, - value: []const u8, -) Error![]u8 { - // If there is no prior value, we return it as-is - if (current.len == 0) return try alloc.dupe(u8, value); - - return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{ - value, - std.fs.path.delimiter, - current, - }); -} - -/// The result of getenv, with a shared deinit to properly handle allocation -/// on Windows. -pub const GetEnvResult = struct { - value: []const u8, - - pub fn deinit(self: GetEnvResult, alloc: Allocator) void { - switch (builtin.os.tag) { - .windows => alloc.free(self.value), - else => {}, - } - } -}; - -/// Gets the value of an environment variable, or null if not found. -/// This will allocate on Windows but not on other platforms. The returned -/// value should have deinit called to do the proper cleanup no matter what -/// platform you are on. -pub fn getenv(alloc: Allocator, key: []const u8) Error!?GetEnvResult { - return switch (builtin.os.tag) { - // Non-Windows doesn't need to allocate - else => if (posix.getenv(key)) |v| .{ .value = v } else null, - - // Windows needs to allocate - .windows => if (std.process.getEnvVarOwned(alloc, key)) |v| .{ - .value = v, - } else |err| switch (err) { - error.EnvironmentVariableNotFound => null, - error.InvalidWtf8 => null, - else => |e| e, - }, - }; -} - -/// Gets the value of an environment variable. Returns null if not found or the -/// value is empty. This will allocate on Windows but not on other platforms. -/// The returned value should have deinit called to do the proper cleanup no -/// matter what platform you are on. -pub fn getenvNotEmpty(alloc: Allocator, key: []const u8) !?GetEnvResult { - const result_ = try getenv(alloc, key); - if (result_) |result| { - if (result.value.len == 0) { - result.deinit(alloc); - return null; - } - } - return result_; -} - -pub fn setenv(key: [:0]const u8, value: [:0]const u8) c_int { - return switch (builtin.os.tag) { - .windows => c._putenv_s(key.ptr, value.ptr), - else => c.setenv(key.ptr, value.ptr, 1), - }; -} - -pub fn unsetenv(key: [:0]const u8) c_int { - return switch (builtin.os.tag) { - .windows => c._putenv_s(key.ptr, ""), - else => c.unsetenv(key.ptr), - }; -} - -const c = struct { - // POSIX - extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int; - extern "c" fn unsetenv(name: ?[*]const u8) c_int; - - // Windows - extern "c" fn _putenv_s(varname: ?[*]const u8, value_string: ?[*]const u8) c_int; -}; - -test "appendEnv empty" { - const testing = std.testing; - const alloc = testing.allocator; - - const result = try appendEnv(alloc, "", "foo"); - defer alloc.free(result); - try testing.expectEqualStrings(result, "foo"); -} - -test "appendEnv existing" { - const testing = std.testing; - const alloc = testing.allocator; - - const result = try appendEnv(alloc, "a:b", "foo"); - defer alloc.free(result); - if (builtin.os.tag == .windows) { - try testing.expectEqualStrings(result, "a:b;foo"); - } else { - try testing.expectEqualStrings(result, "a:b:foo"); - } -} - -test "prependEnv empty" { - const testing = std.testing; - const alloc = testing.allocator; - - const result = try prependEnv(alloc, "", "foo"); - defer alloc.free(result); - try testing.expectEqualStrings(result, "foo"); -} - -test "prependEnv existing" { - const testing = std.testing; - const alloc = testing.allocator; - - const result = try prependEnv(alloc, "a:b", "foo"); - defer alloc.free(result); - if (builtin.os.tag == .windows) { - try testing.expectEqualStrings(result, "foo;a:b"); - } else { - try testing.expectEqualStrings(result, "foo:a:b"); - } -} diff --git a/vendor/libghostty-vt/src/os/file.zig b/vendor/libghostty-vt/src/os/file.zig index 6007ae49..3c170b46 100644 --- a/vendor/libghostty-vt/src/os/file.zig +++ b/vendor/libghostty-vt/src/os/file.zig @@ -1,5 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); +const global = @import("../global.zig"); const posix = std.posix; const windows = @import("windows.zig"); @@ -63,7 +64,7 @@ pub fn restoreMaxFiles(lim: rlimit) void { /// POSIX this returns `$TMPDIR`/`$TMP` (or `"/tmp"` as a fallback) /// without allocating. Always pair with `freeTmpDir` to release any /// allocation. -pub fn allocTmpDir(allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 { +pub fn allocTmpDir(allocator: std.mem.Allocator, environ: std.process.Environ) std.mem.Allocator.Error![]const u8 { if (builtin.os.tag == .windows) { // GetTempPathW guarantees the result fits in MAX_PATH+1. var buf: [windows.MAX_PATH + 1:0]u16 = undefined; @@ -81,7 +82,7 @@ pub fn allocTmpDir(allocator: std.mem.Allocator) std.mem.Allocator.Error![]const } return allocator.dupe(u8, "C:\\Windows\\Temp"); } - const tmpdir = posix.getenv("TMPDIR") orelse posix.getenv("TMP") orelse return "/tmp"; + const tmpdir = environ.getPosix("TMPDIR") orelse environ.getPosix("TMP") orelse return "/tmp"; return std.mem.trimEnd(u8, tmpdir, &.{std.fs.path.sep}); } @@ -107,7 +108,7 @@ pub const random_basename_len = b64_encoder.calcSize(random_basename_bytes); pub fn randomBasename(buf: []u8) RandomBasenameError![]const u8 { if (buf.len < random_basename_len) return error.BufferTooSmall; var rand_buf: [random_basename_bytes]u8 = undefined; - std.crypto.random.bytes(&rand_buf); + global.io().random(&rand_buf); return b64_encoder.encode(buf[0..random_basename_len], &rand_buf); } @@ -121,14 +122,12 @@ pub fn randomTmpPath( allocator: std.mem.Allocator, prefix: []const u8, ) std.mem.Allocator.Error![]u8 { - const tmp_dir = try allocTmpDir(allocator); - defer freeTmpDir(allocator, tmp_dir); var name_buf: [random_basename_len]u8 = undefined; const basename = randomBasename(&name_buf) catch unreachable; return std.fmt.allocPrint( allocator, "{s}{c}{s}{s}", - .{ tmp_dir, std.fs.path.sep, prefix, basename }, + .{ global.tmpDirPath(), std.fs.path.sep, prefix, basename }, ); } diff --git a/vendor/libghostty-vt/src/os/flatpak.zig b/vendor/libghostty-vt/src/os/flatpak.zig index 44f820ad..15c619de 100644 --- a/vendor/libghostty-vt/src/os/flatpak.zig +++ b/vendor/libghostty-vt/src/os/flatpak.zig @@ -2,7 +2,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const builtin = @import("builtin"); const posix = std.posix; -const xev = @import("../global.zig").xev; +const global = @import("../global.zig"); +const xev = global.xev; const log = std.log.scoped(.flatpak); @@ -10,7 +11,10 @@ const log = std.log.scoped(.flatpak); pub fn isFlatpak() bool { // If we're not on Linux then we'll make this comptime false. if (comptime builtin.os.tag != .linux) return false; - return if (std.fs.accessAbsolute("/.flatpak-info", .{})) true else |_| false; + return if (std.Io.Dir.accessAbsolute(global.io(), "/.flatpak-info", .{})) + true + else |_| + false; } /// A struct to help execute commands on the host via the @@ -28,11 +32,8 @@ pub fn isFlatpak() bool { /// Requires GIO, GLib to be available and linked. pub const FlatpakHostCommand = struct { const fd_t = posix.fd_t; - const EnvMap = std.process.EnvMap; - const c = @cImport({ - @cInclude("gio/gio.h"); - @cInclude("gio/gunixfdlist.h"); - }); + const EnvMap = std.process.Environ.Map; + const gio_c = @import("gio_c"); /// Flags for HostCommand method /// /// Ref: https://docs.flatpak.org/en/latest/libflatpak-api-reference.html#gdbus-method-org-freedesktop-Flatpak-Development.HostCommand @@ -65,8 +66,8 @@ pub const FlatpakHostCommand = struct { /// State of the process. This is updated by the dedicated thread it /// runs in and is protected by the given lock and condition variable. state: State = .{ .init = {} }, - state_mutex: std.Thread.Mutex = .{}, - state_cv: std.Thread.Condition = .{}, + state_mutex: std.Io.Mutex = .init, + state_cv: std.Io.Condition = .init, /// State the process is in. This can't be inspected directly, you /// must use getters on the struct to get access. @@ -84,8 +85,8 @@ pub const FlatpakHostCommand = struct { pid: u32, loop_xev: ?*xev.Loop, completion: ?*Completion, - subscription: c.guint, - loop: *c.GMainLoop, + subscription: gio_c.guint, + loop: *gio_c.GMainLoop, }, /// Process exited @@ -120,14 +121,14 @@ pub const FlatpakHostCommand = struct { /// Precondition: The self pointer MUST be stable. pub fn spawn(self: *FlatpakHostCommand, alloc: Allocator) !u32 { const thread = try std.Thread.spawn(.{}, threadMain, .{ self, alloc }); - thread.setName("flatpak-host-command") catch {}; + thread.setName(global.io(), "flatpak-host-command") catch {}; // We don't track this thread, it will terminate on its own on command exit thread.detach(); // Wait for the process to start or error. - self.state_mutex.lock(); - defer self.state_mutex.unlock(); - while (self.state == .init) self.state_cv.wait(&self.state_mutex); + self.state_mutex.lockUncancelable(global.io()); + defer self.state_mutex.unlock(global.io()); + while (self.state == .init) self.state_cv.waitUncancelable(global.io(), &self.state_mutex); return switch (self.state) { .init => unreachable, @@ -140,8 +141,8 @@ pub const FlatpakHostCommand = struct { /// Wait for the process to end and return the exit status. This /// can only be called ONCE. Once this returns, the state is reset. pub fn wait(self: *FlatpakHostCommand) !u8 { - self.state_mutex.lock(); - defer self.state_mutex.unlock(); + self.state_mutex.lockUncancelable(global.io()); + defer self.state_mutex.unlock(global.io()); while (true) { switch (self.state) { @@ -150,12 +151,12 @@ pub const FlatpakHostCommand = struct { .started => {}, .exited => |v| { self.state = .{ .init = {} }; - self.state_cv.broadcast(); + self.state_cv.broadcast(global.io()); return v.status; }, } - self.state_cv.wait(&self.state_mutex); + self.state_cv.waitUncancelable(global.io(), &self.state_mutex); } } @@ -174,8 +175,8 @@ pub const FlatpakHostCommand = struct { r: WaitError!u8, ) void, ) void { - self.state_mutex.lock(); - defer self.state_mutex.unlock(); + self.state_mutex.lockUncancelable(global.io()); + defer self.state_mutex.unlock(global.io()); completion.* = .{ .callback = (struct { @@ -234,8 +235,8 @@ pub const FlatpakHostCommand = struct { /// command is not in the started state. pub fn signal(self: *FlatpakHostCommand, sig: u8, pg: bool) !void { const pid = pid: { - self.state_mutex.lock(); - defer self.state_mutex.unlock(); + self.state_mutex.lockUncancelable(global.io()); + defer self.state_mutex.unlock(global.io()); switch (self.state) { .started => |v| break :pid v.pid, else => return, @@ -243,29 +244,29 @@ pub const FlatpakHostCommand = struct { }; // Get our bus connection. - var g_err: ?*c.GError = null; - defer if (g_err) |ptr| c.g_error_free(ptr); - const bus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_err) orelse { + var g_err: ?*gio_c.GError = null; + defer if (g_err) |ptr| gio_c.g_error_free(ptr); + const bus = gio_c.g_bus_get_sync(gio_c.G_BUS_TYPE_SESSION, null, &g_err) orelse { log.warn("signal error getting bus: {s}", .{g_err.?.*.message}); return Error.FlatpakSetupFail; }; - defer c.g_object_unref(bus); + defer gio_c.g_object_unref(bus); - const reply = c.g_dbus_connection_call_sync( + const reply = gio_c.g_dbus_connection_call_sync( bus, "org.freedesktop.Flatpak", "/org/freedesktop/Flatpak/Development", "org.freedesktop.Flatpak.Development", "HostCommandSignal", - c.g_variant_new( + gio_c.g_variant_new( "(uub)", pid, sig, @as(c_int, @intCast(@intFromBool(pg))), ), - c.G_VARIANT_TYPE("()"), - c.G_DBUS_CALL_FLAGS_NONE, - c.G_MAXINT, + gio_c.G_VARIANT_TYPE("()"), + gio_c.G_DBUS_CALL_FLAGS_NONE, + gio_c.G_MAXINT, null, &g_err, ); @@ -273,31 +274,31 @@ pub const FlatpakHostCommand = struct { log.warn("signal send error: {s}", .{g_err.?.*.message}); return; } - defer c.g_variant_unref(reply); + defer gio_c.g_variant_unref(reply); } fn threadMain(self: *FlatpakHostCommand, alloc: Allocator) void { // Create a new thread-local context so that all our sources go // to this context and we can run our loop correctly. - const ctx = c.g_main_context_new(); - defer c.g_main_context_unref(ctx); - c.g_main_context_push_thread_default(ctx); - defer c.g_main_context_pop_thread_default(ctx); + const ctx = gio_c.g_main_context_new(); + defer gio_c.g_main_context_unref(ctx); + gio_c.g_main_context_push_thread_default(ctx); + defer gio_c.g_main_context_pop_thread_default(ctx); // Get our loop for the current thread - const loop = c.g_main_loop_new(ctx, 1).?; - defer c.g_main_loop_unref(loop); + const loop = gio_c.g_main_loop_new(ctx, 1).?; + defer gio_c.g_main_loop_unref(loop); // Get our bus connection. This has to remain active until we exit // the thread otherwise our signals won't be called. - var g_err: ?*c.GError = null; - defer if (g_err) |ptr| c.g_error_free(ptr); - const bus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_err) orelse { + var g_err: ?*gio_c.GError = null; + defer if (g_err) |ptr| gio_c.g_error_free(ptr); + const bus = gio_c.g_bus_get_sync(gio_c.G_BUS_TYPE_SESSION, null, &g_err) orelse { log.warn("spawn error getting bus: {s}", .{g_err.?.*.message}); self.updateState(.{ .err = {} }); return; }; - defer c.g_object_unref(bus); + defer gio_c.g_object_unref(bus); // Spawn the command first. This will setup all our IO. self.start(alloc, bus, loop) catch |err| { @@ -307,7 +308,7 @@ pub const FlatpakHostCommand = struct { }; // Run the event loop. It quits in the exit callback. - c.g_main_loop_run(loop); + gio_c.g_main_loop_run(loop); } /// Start the command. This will start the host command and set the @@ -318,47 +319,47 @@ pub const FlatpakHostCommand = struct { fn start( self: *FlatpakHostCommand, alloc: Allocator, - bus: *c.GDBusConnection, - loop: *c.GMainLoop, + bus: *gio_c.GDBusConnection, + loop: *gio_c.GMainLoop, ) !void { - var err: ?*c.GError = null; - defer if (err) |ptr| c.g_error_free(ptr); + var err: ?*gio_c.GError = null; + defer if (err) |ptr| gio_c.g_error_free(ptr); var arena_allocator = std.heap.ArenaAllocator.init(alloc); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); // Our list of file descriptors that we need to send to the process. - const fd_list = c.g_unix_fd_list_new(); - defer c.g_object_unref(fd_list); - if (c.g_unix_fd_list_append(fd_list, self.stdin, &err) < 0) { + const fd_list = gio_c.g_unix_fd_list_new(); + defer gio_c.g_object_unref(fd_list); + if (gio_c.g_unix_fd_list_append(fd_list, self.stdin, &err) < 0) { log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } - if (c.g_unix_fd_list_append(fd_list, self.stdout, &err) < 0) { + if (gio_c.g_unix_fd_list_append(fd_list, self.stdout, &err) < 0) { log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } - if (c.g_unix_fd_list_append(fd_list, self.stderr, &err) < 0) { + if (gio_c.g_unix_fd_list_append(fd_list, self.stderr, &err) < 0) { log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } // Build our arguments for the file descriptors. - const fd_builder = c.g_variant_builder_new(c.G_VARIANT_TYPE("a{uh}")); - defer c.g_variant_builder_unref(fd_builder); - c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 0), self.stdin); - c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 1), self.stdout); - c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 2), self.stderr); + const fd_builder = gio_c.g_variant_builder_new(gio_c.G_VARIANT_TYPE("a{uh}")); + defer gio_c.g_variant_builder_unref(fd_builder); + gio_c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 0), self.stdin); + gio_c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 1), self.stdout); + gio_c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 2), self.stderr); // Build our env vars - const env_builder = c.g_variant_builder_new(c.G_VARIANT_TYPE("a{ss}")); - defer c.g_variant_builder_unref(env_builder); + const env_builder = gio_c.g_variant_builder_new(gio_c.G_VARIANT_TYPE("a{ss}")); + defer gio_c.g_variant_builder_unref(env_builder); if (self.env) |env| { var it = env.iterator(); while (it.next()) |pair| { const key = try arena.dupeZ(u8, pair.key_ptr.*); const value = try arena.dupeZ(u8, pair.value_ptr.*); - c.g_variant_builder_add(env_builder, "{ss}", key.ptr, value.ptr); + gio_c.g_variant_builder_add(env_builder, "{ss}", key.ptr, value.ptr); } } @@ -373,26 +374,26 @@ pub const FlatpakHostCommand = struct { // Get the cwd in case we don't have ours set. A small optimization // would be to do this only if we need it but this isn't a // common code path. - const g_cwd = c.g_get_current_dir(); - defer c.g_free(g_cwd); + const g_cwd = gio_c.g_get_current_dir(); + defer gio_c.g_free(g_cwd); // Terminate session if Ghostty drops off the bus (e.g. due to crashes) const flags: Flags = .{ .watch_bus = true }; // The params for our RPC call - const params = c.g_variant_new( + const params = gio_c.g_variant_new( "(^ay^aay@a{uh}@a{ss}u)", @as(*const anyopaque, if (self.cwd) |*cwd| cwd.ptr else g_cwd), args.ptr, - c.g_variant_builder_end(fd_builder), - c.g_variant_builder_end(env_builder), + gio_c.g_variant_builder_end(fd_builder), + gio_c.g_variant_builder_end(env_builder), @as(c_uint, @bitCast(flags)), ); - _ = c.g_variant_ref_sink(params); // take ownership - defer c.g_variant_unref(params); + _ = gio_c.g_variant_ref_sink(params); // take ownership + defer gio_c.g_variant_unref(params); // Subscribe to exit notifications - const subscription_id = c.g_dbus_connection_signal_subscribe( + const subscription_id = gio_c.g_dbus_connection_signal_subscribe( bus, "org.freedesktop.Flatpak", "org.freedesktop.Flatpak.Development", @@ -404,19 +405,19 @@ pub const FlatpakHostCommand = struct { self, null, ); - errdefer c.g_dbus_connection_signal_unsubscribe(bus, subscription_id); + errdefer gio_c.g_dbus_connection_signal_unsubscribe(bus, subscription_id); // Go! - const reply = c.g_dbus_connection_call_with_unix_fd_list_sync( + const reply = gio_c.g_dbus_connection_call_with_unix_fd_list_sync( bus, "org.freedesktop.Flatpak", "/org/freedesktop/Flatpak/Development", "org.freedesktop.Flatpak.Development", "HostCommand", params, - c.G_VARIANT_TYPE("(u)"), - c.G_DBUS_CALL_FLAGS_NONE, - c.G_MAXINT, + gio_c.G_VARIANT_TYPE("(u)"), + gio_c.G_DBUS_CALL_FLAGS_NONE, + gio_c.G_MAXINT, fd_list, null, null, @@ -425,10 +426,10 @@ pub const FlatpakHostCommand = struct { log.warn("Flatpak.HostCommand failed: {s}", .{err.?.*.message}); return Error.FlatpakRPCFail; }; - defer c.g_variant_unref(reply); + defer gio_c.g_variant_unref(reply); var pid: u32 = 0; - c.g_variant_get(reply, "(u)", &pid); + gio_c.g_variant_get(reply, "(u)", &pid); log.debug("HostCommand started pid={} subscription={}", .{ pid, subscription_id, @@ -447,31 +448,31 @@ pub const FlatpakHostCommand = struct { /// Helper to update the state and notify waiters via the cv. fn updateState(self: *FlatpakHostCommand, state: State) void { - self.state_mutex.lock(); - defer self.state_mutex.unlock(); - defer self.state_cv.broadcast(); + self.state_mutex.lockUncancelable(global.io()); + defer self.state_mutex.unlock(global.io()); + defer self.state_cv.broadcast(global.io()); self.state = state; } fn onExit( - bus: ?*c.GDBusConnection, + bus: ?*gio_c.GDBusConnection, _: [*c]const u8, _: [*c]const u8, _: [*c]const u8, _: [*c]const u8, - params: ?*c.GVariant, + params: ?*gio_c.GVariant, ud: ?*anyopaque, ) callconv(.c) void { const self = @as(*FlatpakHostCommand, @ptrCast(@alignCast(ud))); const state = state: { - self.state_mutex.lock(); - defer self.state_mutex.unlock(); + self.state_mutex.lockUncancelable(global.io()); + defer self.state_mutex.unlock(global.io()); break :state self.state.started; }; var pid: u32 = 0; var exit_status_raw: u32 = 0; - c.g_variant_get(params.?, "(uu)", &pid, &exit_status_raw); + gio_c.g_variant_get(params.?, "(uu)", &pid, &exit_status_raw); if (state.pid != pid) return; const exit_status = posix.W.EXITSTATUS(exit_status_raw); @@ -510,10 +511,10 @@ pub const FlatpakHostCommand = struct { log.debug("HostCommand exited pid={} status={}", .{ pid, exit_status }); // We're done now, so we can unsubscribe - c.g_dbus_connection_signal_unsubscribe(bus.?, state.subscription); + gio_c.g_dbus_connection_signal_unsubscribe(bus.?, state.subscription); // We are also done with our loop so we can exit. - c.g_main_loop_quit(state.loop); + gio_c.g_main_loop_quit(state.loop); } fn noopCallback(_: ?*anyopaque, _: *xev.Loop, _: *Completion, _: WaitError!u8) void {} diff --git a/vendor/libghostty-vt/src/os/homedir.zig b/vendor/libghostty-vt/src/os/homedir.zig index 9e0420f4..8da9640a 100644 --- a/vendor/libghostty-vt/src/os/homedir.zig +++ b/vendor/libghostty-vt/src/os/homedir.zig @@ -1,7 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); const passwd = @import("passwd.zig"); -const posix = std.posix; const objc = @import("objc"); const Error = error{ @@ -11,10 +10,10 @@ 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(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(buf), - .windows => try homeWindows(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 .ios => null, @@ -23,9 +22,9 @@ pub inline fn home(buf: []u8) !?[]const u8 { }; } -fn homeUnix(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 (posix.getenv("HOME")) |result| { + if (environ_map.get("HOME")) |result| { if (buf.len < result.len) return Error.BufferTooSmall; @memcpy(buf[0..result.len], result); return buf[0..result.len]; @@ -60,13 +59,14 @@ fn homeUnix(buf: []u8) !?[]const u8 { // If all else fails, have the shell tell us... fba.reset(); - const run = try std.process.Child.run(.{ - .allocator = fba.allocator(), + const run = try std.process.run(fba.allocator(), io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", "cd && pwd" }, - .max_output_bytes = fba.buffer.len / 2, + .environ_map = environ_map, + .stdout_limit = .limited(fba.buffer.len / 2), + .stderr_limit = .limited(fba.buffer.len / 2), }); - if (run.term == .Exited and run.term.Exited == 0) { + if (run.term == .exited and run.term.exited == 0) { const result = trimSpace(run.stdout); if (buf.len < result.len) return Error.BufferTooSmall; @memcpy(buf[0..result.len], result); @@ -76,33 +76,11 @@ fn homeUnix(buf: []u8) !?[]const u8 { return null; } -fn homeWindows(buf: []u8) !?[]const u8 { - const drive_len = blk: { - var fba_instance = std.heap.FixedBufferAllocator.init(buf); - const fba = fba_instance.allocator(); - const drive = std.process.getEnvVarOwned(fba, "HOMEDRIVE") catch |err| switch (err) { - error.OutOfMemory => return Error.BufferTooSmall, - error.InvalidWtf8, error.EnvironmentVariableNotFound => return null, - }; - // could shift the contents if this ever happens - if (drive.ptr != buf.ptr) @panic("codebug"); - break :blk drive.len; - }; - - const path_len = blk: { - const path_buf = buf[drive_len..]; - var fba_instance = std.heap.FixedBufferAllocator.init(buf[drive_len..]); - const fba = fba_instance.allocator(); - const homepath = std.process.getEnvVarOwned(fba, "HOMEPATH") catch |err| switch (err) { - error.OutOfMemory => return Error.BufferTooSmall, - error.InvalidWtf8, error.EnvironmentVariableNotFound => return null, - }; - // could shift the contents if this ever happens - if (homepath.ptr != path_buf.ptr) @panic("codebug"); - break :blk homepath.len; - }; - - return buf[0 .. drive_len + path_len]; +fn homeWindows(environ_map: *const std.process.Environ.Map, buf: []u8) !?[]const u8 { + var writer: std.Io.Writer = .fixed(buf); + _ = try writer.write(environ_map.get("HOMEDRIVE") orelse return null); + _ = try writer.write(environ_map.get("HOMEPATH") orelse return null); + return writer.buffered(); } fn trimSpace(input: []const u8) []const u8 { @@ -119,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(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(path, buf), + .linux, .freebsd, .macos => try expandHomeUnix(io, environ_map, path, buf), // `~/` is not an idiom generally used on Windows .windows => return path, @@ -133,9 +116,14 @@ pub fn expandHome(path: []const u8, buf: []u8) ExpandError![]const u8 { }; } -fn expandHomeUnix(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(buf)) |home_| + const home_dir: []const u8 = if (home(io, environ_map, buf)) |home_| home_ orelse return error.HomeDetectionFailed else |_| return error.HomeDetectionFailed; @@ -152,28 +140,33 @@ 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("~/", &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("~/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("~", &buf)); - try testing.expectEqualStrings("~abc/", try expandHomeUnix("~abc/", &buf)); - try testing.expectEqualStrings("/home/user", try expandHomeUnix("/home/user", &buf)); - try testing.expectEqualStrings("", try expandHomeUnix("", &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..], )); @@ -181,9 +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(&buf); + const result = try home(io, &environ_map, &buf); try testing.expect(result != null); try testing.expect(result.?.len > 0); } diff --git a/vendor/libghostty-vt/src/os/hostname.zig b/vendor/libghostty-vt/src/os/hostname.zig index af9148fb..969b4baa 100644 --- a/vendor/libghostty-vt/src/os/hostname.zig +++ b/vendor/libghostty-vt/src/os/hostname.zig @@ -7,91 +7,6 @@ pub const LocalHostnameValidationError = error{ Unexpected, }; -/// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) -/// -/// std.net.isValidHostname is (currently) too generous. It considers strings like -/// ".example.com", "exa..mple.com", and "-example.com" to be valid hostnames, which -/// is incorrect. -pub fn isValid(hostname: []const u8) bool { - if (hostname.len == 0) return false; - if (hostname[0] == '.') return false; - - // Ignore trailing dot (FQDN). It doesn't count toward our length. - const end = if (hostname[hostname.len - 1] == '.') end: { - if (hostname.len == 1) return false; - break :end hostname.len - 1; - } else hostname.len; - - if (end > 253) return false; - - // Hostnames are divided into dot-separated "labels", which: - // - // - Start with a letter or digit - // - Can contain letters, digits, or hyphens - // - Must end with a letter or digit - // - Have a minimum of 1 character and a maximum of 63 - var label_start: usize = 0; - var label_len: usize = 0; - for (hostname[0..end], 0..) |c, i| { - switch (c) { - '.' => { - if (label_len == 0 or label_len > 63) return false; - if (!std.ascii.isAlphanumeric(hostname[label_start])) return false; - if (!std.ascii.isAlphanumeric(hostname[i - 1])) return false; - - label_start = i + 1; - label_len = 0; - }, - '-' => { - label_len += 1; - }, - else => { - if (!std.ascii.isAlphanumeric(c)) return false; - label_len += 1; - }, - } - } - - // Validate the final label - if (label_len == 0 or label_len > 63) return false; - if (!std.ascii.isAlphanumeric(hostname[label_start])) return false; - if (!std.ascii.isAlphanumeric(hostname[end - 1])) return false; - - return true; -} - -test isValid { - const testing = std.testing; - - // Valid hostnames - try testing.expect(isValid("example")); - try testing.expect(isValid("example.com")); - try testing.expect(isValid("www.example.com")); - try testing.expect(isValid("sub.domain.example.com")); - try testing.expect(isValid("example.com.")); - try testing.expect(isValid("host-name.example.com.")); - try testing.expect(isValid("123.example.com.")); - try testing.expect(isValid("a-b.com")); - try testing.expect(isValid("a.b.c.d.e.f.g")); - try testing.expect(isValid("127.0.0.1")); // Also a valid hostname - try testing.expect(isValid("a" ** 63 ++ ".com")); // Label exactly 63 chars (valid) - try testing.expect(isValid("a." ** 126 ++ "a")); // Total length 253 (valid) - - // Invalid hostnames - try testing.expect(!isValid("")); - try testing.expect(!isValid(".example.com")); - try testing.expect(!isValid("example.com..")); - try testing.expect(!isValid("host..domain")); - try testing.expect(!isValid("-hostname")); - try testing.expect(!isValid("hostname-")); - try testing.expect(!isValid("a.-.b")); - try testing.expect(!isValid("host_name.com")); - try testing.expect(!isValid(".")); - try testing.expect(!isValid("..")); - try testing.expect(!isValid("a" ** 64 ++ ".com")); // Label length 64 (too long) - try testing.expect(!isValid("a." ** 126 ++ "ab")); // Total length 254 (too long) -} - /// Checks if a hostname is local to the current machine. This matches /// both "localhost" and the current hostname of the machine (as returned /// by `gethostname`). @@ -105,7 +20,7 @@ pub fn isLocal(hostname: []const u8) LocalHostnameValidationError!bool { const windows = @import("windows.zig"); var buf: [256:0]u8 = undefined; var nSize: windows.DWORD = buf.len; - if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == 0) return false; + if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == windows.FALSE) return false; const ourHostname = buf[0..nSize]; return std.mem.eql(u8, hostname, ourHostname); }, @@ -127,7 +42,8 @@ test "isLocal returns true when hostname is local" { const windows = @import("windows.zig"); var buf: [256:0]u8 = undefined; var nSize: windows.DWORD = buf.len; - if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == 0) return error.GetComputerNameFailed; + if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == windows.FALSE) + return error.GetComputerNameFailed; const localHostname = buf[0..nSize]; try std.testing.expect(try isLocal(localHostname)); }, diff --git a/vendor/libghostty-vt/src/os/i18n.zig b/vendor/libghostty-vt/src/os/i18n.zig index 0f7790fa..20d3c1b9 100644 --- a/vendor/libghostty-vt/src/os/i18n.zig +++ b/vendor/libghostty-vt/src/os/i18n.zig @@ -62,11 +62,21 @@ pub fn initGlobalDomain() error{OutOfMemory}!void { } /// Translate a message for the Ghostty domain. +/// +/// If this is called at comptime, the direct msgid is returned without +/// translation. This still allows the string to be marked for translation +/// while remaining usable in comptime contexts. pub fn _(msgid: [*:0]const u8) [*:0]const u8 { if (comptime !build_config.i18n) return msgid; + if (@inComptime()) return msgid; return dgettext(build_config.bundle_id, msgid); } +/// Mark a string for translation without translating it immediately. +pub fn N_(msgid: [:0]const u8) [:0]const u8 { + return msgid; +} + /// Canonicalize a locale name from a platform-specific value to /// a POSIX-compliant value. This is a thin layer over the unexported /// gnulib-lib function in gettext that does this already. @@ -191,3 +201,10 @@ test "canonicalizeLocale darwin" { // underscores. We should parse them out before calling this function. try testing.expectEqualStrings("en_US.UTF_8", try canonicalizeLocale(&buf, "en_US.UTF-8")); } + +test "_ returns msgid at comptime" { + const testing = std.testing; + + const msgid = comptime @"_"("Ghostty"); + try testing.expectEqualStrings("Ghostty", std.mem.span(msgid)); +} diff --git a/vendor/libghostty-vt/src/os/i18n_locales.zig b/vendor/libghostty-vt/src/os/i18n_locales.zig index 5c1ee063..9df96ad8 100644 --- a/vendor/libghostty-vt/src/os/i18n_locales.zig +++ b/vendor/libghostty-vt/src/os/i18n_locales.zig @@ -59,4 +59,7 @@ pub const locales = [_][:0]const u8{ "kk", "be", "eu", + "da", + "sr", + "sr@latin", }; diff --git a/vendor/libghostty-vt/src/os/kernel_info.zig b/vendor/libghostty-vt/src/os/kernel_info.zig index e57cc304..60e6b49f 100644 --- a/vendor/libghostty-vt/src/os/kernel_info.zig +++ b/vendor/libghostty-vt/src/os/kernel_info.zig @@ -1,15 +1,20 @@ const std = @import("std"); const builtin = @import("builtin"); +const global = @import("../global.zig"); pub fn getKernelInfo(alloc: std.mem.Allocator) ?[]const u8 { if (comptime builtin.os.tag != .linux) return null; const path = "/proc/sys/kernel/osrelease"; - var file = std.fs.openFileAbsolute(path, .{}) catch return null; - defer file.close(); + var file = std.Io.Dir.openFileAbsolute(global.io(), path, .{}) catch return null; + defer file.close(global.io()); // 128 bytes should be enough to hold the kernel information - const kernel_info = file.readToEndAlloc(alloc, 128) catch return null; - defer alloc.free(kernel_info); + var kernel_info_buf: [128]u8 = undefined; + const kernel_info = kernel_info_buf[0 .. file.readPositionalAll( + global.io(), + &kernel_info_buf, + 0, + ) catch return null]; return alloc.dupe(u8, std.mem.trim(u8, kernel_info, &std.ascii.whitespace)) catch return null; } diff --git a/vendor/libghostty-vt/src/os/locale.zig b/vendor/libghostty-vt/src/os/locale.zig index faba3bd2..ceb3c883 100644 --- a/vendor/libghostty-vt/src/os/locale.zig +++ b/vendor/libghostty-vt/src/os/locale.zig @@ -9,13 +9,10 @@ const i18n = internal_os.i18n; const log = std.log.scoped(.os_locale); /// Ensure that the locale is set. -pub fn ensureLocale(alloc: std.mem.Allocator) !void { - assert(builtin.link_libc); - - // Get our LANG env var. We use this many times but we also need - // the original value later. - const lang = try internal_os.getenv(alloc, "LANG"); - defer if (lang) |v| v.deinit(alloc); +pub fn ensureLocale() !void { + // This does a lot of in-process mutation of the environment and can't be + // run in a test as a result. + assert(!builtin.is_test); // On macOS, pre-populate the LANG env var with system preferences. // When launching the .app, LANG is not set so we must query it from the @@ -23,7 +20,8 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void { // process. if (comptime builtin.target.os.tag.isDarwin()) { // Set the lang if it is not set or if its empty. - if (lang == null or lang.?.value.len == 0) { + const lang = std.posix.system.getenv("LANG"); + if (lang == null or lang.?[0] == 0) { setLangFromCocoa(); } } @@ -34,16 +32,29 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void { return; } + if (builtin.os.tag == .windows) { + // Exit early for Windows. + // + // NOTE: There currently is no official Windows version of Ghostty, + // either by way of an official Windows port or through use of + // libghostty-internal. As such this function is likely unused on those + // platforms, so this serves as a TODO stub to support more robust + // locale support on Windows. setlocale on Windows sets an + // "implementation-defined native environment" with an empty string, so + // we are currently just doing best-effort for now. + log.info("TODO: setlocale failed on Windows, implement better fallbacks", .{}); + return; + } + // setlocale failed. This is probably because the LANG env var is // invalid. Try to set it without the LANG var set to use the system // default. - if ((try internal_os.getenv(alloc, "LANG"))) |old_lang| { - defer old_lang.deinit(alloc); - if (old_lang.value.len > 0) { + if (std.posix.system.getenv("LANG")) |lang| { + if (lang[0] != 0) { // We don't need to do both of these things but we do them // both to be sure that lang is either empty or unset completely. - _ = internal_os.setenv("LANG", ""); - _ = internal_os.unsetenv("LANG"); + _ = setenv("LANG", "", 1); + _ = unsetenv("LANG"); if (setlocale(LC_ALL, "")) |v| { log.info("setlocale after unset lang result={s}", .{v}); @@ -59,7 +70,7 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void { // Failure again... fallback to en_US.UTF-8 log.warn("setlocale failed with LANG and system default. Falling back to en_US.UTF-8", .{}); if (setlocale(LC_ALL, "en_US.UTF-8")) |v| { - _ = internal_os.setenv("LANG", "en_US.UTF-8"); + _ = setenv("LANG", "en_US.UTF-8", 1); log.info("setlocale default result={s}", .{v}); return; } else log.warn("setlocale failed even with the fallback, uncertain results", .{}); @@ -106,7 +117,7 @@ fn setLangFromCocoa() void { log.info("detected system locale={s}", .{env_value}); // Set it onto our environment - if (internal_os.setenv("LANG", env_value) < 0) { + if (setenv("LANG", @ptrCast(env_value), 1) < 0) { log.warn("error setting locale env var", .{}); return; } @@ -129,7 +140,7 @@ fn setLangFromCocoa() void { "setting LANGUAGE from preferred languages value={s}", .{pref}, ); - _ = internal_os.setenv("LANGUAGE", pref); + _ = setenv("LANGUAGE", @ptrCast(pref), 1); } } @@ -156,8 +167,7 @@ fn preferredLanguageFromCocoa( buf: []u8, NSLocale: objc.Class, ) error{NoSpaceLeft}!?[:0]const u8 { - var fbs = std.io.fixedBufferStream(buf); - const writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(buf); // We need to get our app's preferred languages. These may not // match the system locale (NSLocale.currentLocale). @@ -180,7 +190,7 @@ fn preferredLanguageFromCocoa( }; // Append our separator if we have any previous languages - if (fbs.pos > 0) { + if (writer.end > 0) { _ = writer.writeByte(':') catch return error.NoSpaceLeft; } @@ -188,10 +198,10 @@ fn preferredLanguageFromCocoa( // Apple languages are in BCP-47 format, and we need to // canonicalize them to the POSIX format. const canon = try i18n.canonicalizeLocale( - fbs.buffer[fbs.pos..], + writer.buffer[writer.end..], c_str, ); - fbs.seekBy(@intCast(canon.len)) catch unreachable; + writer.end += canon.len; // The canonicalized locale never contains the encoding and // all of our translations require UTF-8 so we add that. @@ -199,14 +209,14 @@ fn preferredLanguageFromCocoa( } // If we had no preferred languages then we return nothing. - if (fbs.pos == 0) return null; + if (writer.end == 0) return null; // Null terminate it _ = writer.writeByte(0) catch return error.NoSpaceLeft; // Get our slice, this won't be null terminated so we have to // reslice it with the null terminator. - const slice = fbs.getWritten(); + const slice = writer.buffered(); return slice[0 .. slice.len - 1 :0]; } @@ -217,3 +227,6 @@ const locale_t = c.locale_t; const setlocale = c.setlocale; const newlocale = c.newlocale; const freelocale = c.freelocale; + +extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int; +extern "c" fn unsetenv(name: ?[*]const u8) c_int; diff --git a/vendor/libghostty-vt/src/os/mach.zig b/vendor/libghostty-vt/src/os/mach.zig index 4477bd12..5f5a3ed8 100644 --- a/vendor/libghostty-vt/src/os/mach.zig +++ b/vendor/libghostty-vt/src/os/mach.zig @@ -46,6 +46,9 @@ pub fn taggedPageAllocator(tag: VMTag) Allocator { /// This is based heavily on the Zig 0.15.2 PageAllocator implementation, /// with only the posix implementation. Zig 0.15.2 is MIT licensed. const TaggedPageAllocator = struct { + // Static local next_mmap_addr_hint since this has been removed from stdlib now + var next_mmap_addr_hint: ?[*]align(std.heap.page_size_min) u8 = null; + pub const vtable: Allocator.VTable = .{ .alloc = alloc, .resize = resize, @@ -92,11 +95,11 @@ const TaggedPageAllocator = struct { aligned_len else mem.alignForward(usize, aligned_len + max_drop_len, page_size); - const hint = @atomicLoad(@TypeOf(std.heap.next_mmap_addr_hint), &std.heap.next_mmap_addr_hint, .unordered); + const hint = @atomicLoad(@TypeOf(next_mmap_addr_hint), &next_mmap_addr_hint, .unordered); const slice = std.posix.mmap( hint, overalloc_len, - std.posix.PROT.READ | std.posix.PROT.WRITE, + .{ .READ = true, .WRITE = true }, .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, tag.make(), 0, @@ -110,7 +113,7 @@ const TaggedPageAllocator = struct { const remaining_len = overalloc_len - drop_len; if (remaining_len > aligned_len) std.posix.munmap(@alignCast(result_ptr[aligned_len..remaining_len])); const new_hint: [*]align(std.heap.page_size_min) u8 = @alignCast(result_ptr + aligned_len); - _ = @cmpxchgStrong(@TypeOf(std.heap.next_mmap_addr_hint), &std.heap.next_mmap_addr_hint, hint, new_hint, .monotonic, .monotonic); + _ = @cmpxchgStrong(@TypeOf(next_mmap_addr_hint), &next_mmap_addr_hint, hint, new_hint, .monotonic, .monotonic); return result_ptr; } diff --git a/vendor/libghostty-vt/src/os/macos.zig b/vendor/libghostty-vt/src/os/macos.zig index fcd1c3e5..b59882d7 100644 --- a/vendor/libghostty-vt/src/os/macos.zig +++ b/vendor/libghostty-vt/src/os/macos.zig @@ -28,7 +28,7 @@ pub fn appSupportDir( return try commonDir( alloc, .NSApplicationSupportDirectory, - &.{ build_config.bundle_id, sub_path }, + sub_path, ); } @@ -43,7 +43,7 @@ pub fn cacheDir( return try commonDir( alloc, .NSCachesDirectory, - &.{ build_config.bundle_id, sub_path }, + sub_path, ); } @@ -110,7 +110,7 @@ pub const NSSearchPathDomainMask = enum(c_ulong) { fn commonDir( alloc: Allocator, directory: NSSearchPathDirectory, - sub_paths: []const []const u8, + sub_path: []const u8, ) (error{AppleAPIFailed} || Allocator.Error)![]const u8 { comptime assert(builtin.target.os.tag.isDarwin()); @@ -140,13 +140,10 @@ fn commonDir( return error.AppleAPIFailed; const base_dir = std.mem.sliceTo(c_str, 0); - // Create a new array with base_dir as the first element - var paths = try alloc.alloc([]const u8, sub_paths.len + 1); - paths[0] = base_dir; - @memcpy(paths[1..], sub_paths); - defer alloc.free(paths); - - return try std.fs.path.join(alloc, paths); + return try std.fs.path.join( + alloc, + &.{ base_dir, build_config.bundle_id, sub_path }, + ); } test "cacheDir paths" { diff --git a/vendor/libghostty-vt/src/os/main.zig b/vendor/libghostty-vt/src/os/main.zig index b3018274..05f2a45a 100644 --- a/vendor/libghostty-vt/src/os/main.zig +++ b/vendor/libghostty-vt/src/os/main.zig @@ -1,12 +1,11 @@ //! The "os" package contains utilities for interfacing with the operating -//! system. These aren't restricted to syscalls or low-level operations, but +//! system. These aren't restricted to syscalls or low-level operations, butos/main.zig //! also OS-specific features and conventions. const builtin = @import("builtin"); const dbus = @import("dbus.zig"); const desktop = @import("desktop.zig"); -const env = @import("env.zig"); const file = @import("file.zig"); const flatpak = @import("flatpak.zig"); const homedir = @import("homedir.zig"); @@ -19,8 +18,8 @@ const systemd = @import("systemd.zig"); const kernel_info = @import("kernel_info.zig"); // Namespaces -pub const args = @import("args.zig"); pub const cgroup = @import("cgroup.zig"); +pub const edit = @import("edit.zig"); pub const hostname = @import("hostname.zig"); pub const i18n = @import("i18n.zig"); pub const mach = @import("mach.zig"); @@ -30,19 +29,12 @@ pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); pub const macos = @import("macos.zig"); pub const shell = @import("shell.zig"); +pub const stderr = @import("stderr.zig"); pub const uri = @import("uri.zig"); // Functions and types pub const CFReleaseThread = @import("cf_release_thread.zig"); pub const TempDir = @import("TempDir.zig"); -pub const GetEnvResult = env.GetEnvResult; -pub const getEnvMap = env.getEnvMap; -pub const appendEnv = env.appendEnv; -pub const appendEnvAlways = env.appendEnvAlways; -pub const prependEnv = env.prependEnv; -pub const getenv = env.getenv; -pub const setenv = env.setenv; -pub const unsetenv = env.unsetenv; pub const launchedFromDesktop = desktop.launchedFromDesktop; pub const launchedByDbusActivation = dbus.launchedByDbusActivation; pub const launchedBySystemd = systemd.launchedBySystemd; @@ -50,8 +42,6 @@ pub const desktopEnvironment = desktop.desktopEnvironment; pub const rlimit = file.rlimit; pub const fixMaxFiles = file.fixMaxFiles; pub const restoreMaxFiles = file.restoreMaxFiles; -pub const allocTmpDir = file.allocTmpDir; -pub const freeTmpDir = file.freeTmpDir; pub const randomTmpPath = file.randomTmpPath; pub const isFlatpak = flatpak.isFlatpak; pub const FlatpakHostCommand = flatpak.FlatpakHostCommand; @@ -66,9 +56,12 @@ pub const resourcesDir = resourcesdir.resourcesDir; pub const ResourcesDir = resourcesdir.ResourcesDir; pub const ShellEscapeWriter = shell.ShellEscapeWriter; pub const getKernelInfo = kernel_info.getKernelInfo; +pub const getConfigEditCommand = edit.getConfigEditCommand; test { _ = file; + _ = stderr; + _ = edit; _ = i18n; _ = path; _ = uri; diff --git a/vendor/libghostty-vt/src/os/open.zig b/vendor/libghostty-vt/src/os/open.zig index 86c3d76f..e7574618 100644 --- a/vendor/libghostty-vt/src/os/open.zig +++ b/vendor/libghostty-vt/src/os/open.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const build_config = @import("../build_config.zig"); const apprt = @import("../apprt.zig"); +const global = @import("../global.zig"); const log = std.log.scoped(.@"os-open"); @@ -15,74 +16,81 @@ const log = std.log.scoped(.@"os-open"); /// way to open URLs. If you are implementing an apprt for Ghostty, you should /// consider doing something special-cased for your platform. pub fn open( - alloc: Allocator, kind: apprt.action.OpenUrl.Kind, url: []const u8, ) !void { - var exe: std.process.Child = switch (builtin.os.tag) { - .linux, .freebsd => .init( - &.{ "xdg-open", url }, - alloc, - ), - - .windows => .init( - &.{ "rundll32", "url.dll,FileProtocolHandler", url }, - alloc, - ), - - .macos => .init( - switch (kind) { - .text => &.{ "open", "-t", url }, - .html, .unknown => &.{ "open", url }, - }, - alloc, - ), + // On macOS, the apprt handles OSC 8 targets before this fallback. Ghostty's + // native apprt applies its allowlist, confirmation, and file safety policy. + // If a macOS embedder declines the action, fail closed rather than bypassing + // that policy by handing producer-controlled terminal output to `open`. + if (comptime builtin.os.tag == .macos) { + if (kind == .osc8) return error.UnsafeOSC8Link; + } + var spawn_opts: std.process.SpawnOptions = switch (builtin.os.tag) { + .linux, .freebsd => .{ .argv = &.{ "xdg-open", url } }, + .windows => .{ .argv = &.{ "rundll32", "url.dll,FileProtocolHandler", url } }, + .macos => switch (kind) { + .text => .{ .argv = &.{ "open", "-t", url } }, + .html, .unknown => .{ .argv = &.{ "open", url } }, + .osc8 => unreachable, + }, .ios => return error.Unimplemented, else => @compileError("unsupported OS"), }; - // Ignore anything from stdout. This must be set before spawning the // process. - exe.stdout_behavior = .Ignore; + spawn_opts.stdout = .ignore; // Pipe stderr so we can log the stderr from the command. This must be set // before spawning the process. - exe.stderr_behavior = .Pipe; + spawn_opts.stderr = .pipe; - // In the snap on Linux the launcher exports LD_LIBRARY_PATH pointing at - // the snap's bundled libraries. Leaking this into child process can can be - // problematic, so let's drop it from the env - var snap_env: std.process.EnvMap = if (comptime build_config.snap) blk: { - var env = try std.process.getEnvMap(alloc); - env.remove("LD_LIBRARY_PATH"); - break :blk env; - } else undefined; - defer if (comptime build_config.snap) snap_env.deinit(); - if (comptime build_config.snap) exe.env_map = &snap_env; + const exe = if (comptime build_config.snap) local_env: { + // In the snap on Linux the launcher exports LD_LIBRARY_PATH + // pointing at the snap's bundled libraries. Leaking this into + // child process can can be problematic, so let's drop it from the + // env. + // + // Note that `spawn` copies the passed in `Environ.Map` into a + // fresh `Environ` block, so this is safe to release immediately + // after spawn. + var environ_map = try global.environMap(); + defer environ_map.deinit(); + _ = environ_map.orderedRemove("LD_LIBRARY_PATH"); + spawn_opts.environ_map = &environ_map; + break :local_env try std.process.spawn(global.io(), spawn_opts); + } else + // Non-snap releases don't need to alter the env. + try std.process.spawn(global.io(), spawn_opts); - // Spawn the process on our same thread so we can detect failure - // quickly. - try exe.spawn(); - - // Create a thread that handles collecting output and reaping the process. - // This is done in a separate thread because SOME open implementations block - // and some do not. It's easier to just spawn a thread to handle this so - // that we never block. - const thread = try std.Thread.spawn(.{}, openThread, .{exe}); + const thread = try std.Thread.spawn(.{}, openThread, .{ global.io(), exe }); thread.detach(); } -fn openThread(exe_: std.process.Child) void { +test "macOS OSC 8 links have no generic opener fallback" { + if (builtin.os.tag != .macos) return error.SkipZigTest; + + try std.testing.expectError( + error.UnsafeOSC8Link, + open(.osc8, "file:///tmp/payload.command"), + ); +} + +fn openThread(io: std.Io, exe_: std.process.Child) void { // Copy the exe so it is non-const. This is necessary because wait() // requires a mutable reference and we can't have one as a thread // param. var exe = exe_; if (exe.stderr) |stderr| { var buffer: [256]u8 = undefined; - var stream = stderr.readerStreaming(&buffer); + var stream = stderr.readerStreaming(io, &buffer); const reader = &stream.interface; while (true) { - const line = reader.takeDelimiterExclusive('\n') catch |outer| switch (outer) { + // Read inclusively so the delimiter is consumed: + // takeDelimiterExclusive leaves the '\n' buffered, so once the + // child writes a line this loop would receive an empty slice + // forever, pinning a core and spamming empty warnings. + const line = reader.takeDelimiterInclusive('\n') catch |outer| switch (outer) { error.EndOfStream => break, error.ReadFailed => break, error.StreamTooLong => reader.take(buffer.len) catch |inner| switch (inner) { @@ -90,8 +98,8 @@ fn openThread(exe_: std.process.Child) void { error.EndOfStream => break, }, }; - log.warn("open stderr={s}", .{line}); + log.warn("open stderr={s}", .{std.mem.trimEnd(u8, line, "\n")}); } } - _ = exe.wait() catch {}; + _ = exe.wait(io) catch {}; } diff --git a/vendor/libghostty-vt/src/os/passwd.zig b/vendor/libghostty-vt/src/os/passwd.zig index e9bbff06..7c16e54e 100644 --- a/vendor/libghostty-vt/src/os/passwd.zig +++ b/vendor/libghostty-vt/src/os/passwd.zig @@ -5,6 +5,7 @@ const build_config = @import("../build_config.zig"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const posix = std.posix; +const compat_fd = @import("../lib/compat/fd.zig"); const log = std.log.scoped(.passwd); @@ -89,11 +90,11 @@ pub fn get(alloc: Allocator) !Entry { // Once started, we can close the child side. We do this after // wait right now but that is fine too. This lets us read the // parent and detect EOF. - _ = posix.close(pty.slave); + _ = compat_fd.close(pty.slave); // Read all of our output const output = output: { - var output: std.ArrayListUnmanaged(u8) = .{}; + var output: std.ArrayListUnmanaged(u8) = .empty; while (true) { const n = posix.read(pty.master, &buf) catch |err| { switch (err) { @@ -116,7 +117,7 @@ pub fn get(alloc: Allocator) !Entry { }; // Shell and home are the last two entries - var it = std.mem.splitBackwardsScalar(u8, std.mem.trimRight(u8, output, " \r\n"), ':'); + var it = std.mem.splitBackwardsScalar(u8, std.mem.trimEnd(u8, output, " \r\n"), ':'); result.shell = if (it.next()) |v| try alloc.dupeZ(u8, v) else null; result.home = if (it.next()) |v| try alloc.dupeZ(u8, v) else null; return result; diff --git a/vendor/libghostty-vt/src/os/path.zig b/vendor/libghostty-vt/src/os/path.zig index 730ae692..ba1a63f7 100644 --- a/vendor/libghostty-vt/src/os/path.zig +++ b/vendor/libghostty-vt/src/os/path.zig @@ -6,22 +6,19 @@ const testing = std.testing; /// Search for "cmd" in the PATH and return the absolute path. This will /// always allocate if there is a non-null result. The caller must free the /// resulting value. -pub fn expand(alloc: Allocator, cmd: []const u8) !?[]u8 { +pub fn expand( + io: std.Io, + alloc: Allocator, + environ_map: *const std.process.Environ.Map, + cmd: []const u8, +) !?[]u8 { // If the command already contains a slash, then we return it as-is // because it is assumed to be absolute or relative. if (std.mem.indexOfScalar(u8, cmd, '/') != null) { return try alloc.dupe(u8, cmd); } - const PATH = switch (builtin.os.tag) { - .windows => blk: { - const win_path = std.process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse return null; - const path = try std.unicode.utf16LeToUtf8Alloc(alloc, win_path); - break :blk path; - }, - else => std.posix.getenvZ("PATH") orelse return null, - }; - defer if (builtin.os.tag == .windows) alloc.free(PATH); + const PATH = environ_map.get("PATH") orelse return null; var path_buf: [std.fs.max_path_bytes]u8 = undefined; var it = std.mem.tokenizeScalar(u8, PATH, std.fs.path.delimiter); @@ -39,7 +36,8 @@ pub fn expand(alloc: Allocator, cmd: []const u8) !?[]u8 { const full_path = path_buf[0..path_len :0]; // Stat it - const f = std.fs.cwd().openFile( + const f = std.Io.Dir.cwd().openFile( + io, full_path, .{}, ) catch |err| switch (err) { @@ -52,9 +50,9 @@ pub fn expand(alloc: Allocator, cmd: []const u8) !?[]u8 { }, else => return err, }; - defer f.close(); - const stat = try f.stat(); - if (stat.kind != .directory and isExecutable(stat.mode)) { + defer f.close(io); + const stat = try f.stat(io); + if (stat.kind != .directory and isExecutable(stat.permissions)) { return try alloc.dupe(u8, full_path); } } @@ -64,26 +62,39 @@ pub fn expand(alloc: Allocator, cmd: []const u8) !?[]u8 { return null; } -fn isExecutable(mode: std.fs.File.Mode) bool { - if (builtin.os.tag == .windows) return true; - return mode & 0o0111 != 0; +fn isExecutable(perms: std.Io.File.Permissions) bool { + return switch (builtin.os.tag) { + .windows => true, + else => posix: { + break :posix switch (std.posix.mode_t) { + u0 => true, + else => perms.toMode() & 0o0111 != 0, + }; + }, + }; } // `uname -n` is the *nix equivalent of `hostname.exe` on Windows test "expand: hostname" { + var environ_map = try testing.environ.createMap(testing.allocator); + defer environ_map.deinit(); const executable = if (builtin.os.tag == .windows) "hostname.exe" else "uname"; - const path = (try expand(testing.allocator, executable)).?; + const path = (try expand(testing.io, testing.allocator, &environ_map, executable)).?; defer testing.allocator.free(path); try testing.expect(path.len > executable.len); } test "expand: does not exist" { - const path = try expand(testing.allocator, "thisreallyprobablydoesntexist123"); + var environ_map = try testing.environ.createMap(testing.allocator); + defer environ_map.deinit(); + const path = try expand(testing.io, testing.allocator, &environ_map, "thisreallyprobablydoesntexist123"); try testing.expect(path == null); } test "expand: slash" { - const path = (try expand(testing.allocator, "foo/env")).?; + var environ_map = try testing.environ.createMap(testing.allocator); + defer environ_map.deinit(); + const path = (try expand(testing.io, testing.allocator, &environ_map, "foo/env")).?; defer testing.allocator.free(path); try testing.expect(path.len == 7); } diff --git a/vendor/libghostty-vt/src/os/pipe.zig b/vendor/libghostty-vt/src/os/pipe.zig index 2cb7bd4a..0310fb3e 100644 --- a/vendor/libghostty-vt/src/os/pipe.zig +++ b/vendor/libghostty-vt/src/os/pipe.zig @@ -2,17 +2,18 @@ const std = @import("std"); const builtin = @import("builtin"); const windows = @import("windows.zig"); const posix = std.posix; +const compat_fd = @import("../lib/compat/fd.zig"); /// pipe() that works on Windows and POSIX. For POSIX systems, this sets /// CLOEXEC on the file descriptors. pub fn pipe() ![2]posix.fd_t { switch (builtin.os.tag) { - else => return try posix.pipe2(.{ .CLOEXEC = true }), + else => return compat_fd.pipe2(.{ .CLOEXEC = true }), .windows => { var read: windows.HANDLE = undefined; var write: windows.HANDLE = undefined; - if (windows.exp.kernel32.CreatePipe(&read, &write, null, 0) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (windows.exp.kernel32.CreatePipe(&read, &write, null, 0) == windows.FALSE) { + return windows.unexpectedError(windows.GetLastError()); } return .{ read, write }; diff --git a/vendor/libghostty-vt/src/os/resourcesdir.zig b/vendor/libghostty-vt/src/os/resourcesdir.zig index 278de44f..ef8b5a28 100644 --- a/vendor/libghostty-vt/src/os/resourcesdir.zig +++ b/vendor/libghostty-vt/src/os/resourcesdir.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; +const global = @import("../global.zig"); pub const ResourcesDir = struct { /// Avoid accessing these directly, use the app() and host() methods instead. @@ -16,7 +17,7 @@ pub const ResourcesDir = struct { /// Get the directory to the bundled resources directory accessible /// by the application. - pub fn app(self: *ResourcesDir) ?[]const u8 { + pub fn app(self: *const ResourcesDir) ?[]const u8 { return self.app_path; } @@ -26,7 +27,7 @@ pub const ResourcesDir = struct { /// itself. /// /// In non-sandboxed environment, this should be the same as app(). - pub fn host(self: *ResourcesDir) ?[]const u8 { + pub fn host(self: *const ResourcesDir) ?[]const u8 { return self.host_path orelse self.app_path; } }; @@ -47,13 +48,13 @@ pub fn resourcesDir(alloc: Allocator) !ResourcesDir { // // Note: we ALWAYS want to allocate here because the result is always // freed, do not try to use internal_os.getenv or posix getenv. - if (comptime builtin.mode != .Debug) { - if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { - if (dir.len > 0) return .{ .app_path = dir }; - } else |err| switch (err) { - error.EnvironmentVariableNotFound => {}, + if (comptime builtin.mode != .Debug) env: { + const dir = global.environ().getAlloc(alloc, "GHOSTTY_RESOURCES_DIR") catch |err| switch (err) { + error.EnvironmentVariableMissing => break :env, else => return err, - } + }; + + if (dir.len > 0) return .{ .app_path = dir }; } // This is the sentinel value we look for in the path to know @@ -67,7 +68,10 @@ pub fn resourcesDir(alloc: Allocator) !ResourcesDir { // Get the path to our running binary var exe_buf: [std.fs.max_path_bytes]u8 = undefined; - var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return .{}; + var exe: []const u8 = exe_buf[0 .. std.process.executablePath( + global.io(), + &exe_buf, + ) catch return .{}]; // We have an exe path! Climb the tree looking for the terminfo // bundle as we expect it. @@ -78,7 +82,12 @@ pub fn resourcesDir(alloc: Allocator) !ResourcesDir { // On MacOS, we look for the app bundle path. if (comptime builtin.target.os.tag.isDarwin()) { inline for (sentinels) |sentinel| { - if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| { + if (try maybeDir( + &dir_buf, + dir, + "Contents/Resources", + sentinel, + )) |v| { return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) }; } } @@ -102,10 +111,10 @@ pub fn resourcesDir(alloc: Allocator) !ResourcesDir { // If terminfo detection failed in debug builds (somehow), // fallback and use the provided resources dir. if (comptime builtin.mode == .Debug) { - if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { + if (global.environ().getAlloc(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { if (dir.len > 0) return .{ .app_path = dir }; } else |err| switch (err) { - error.EnvironmentVariableNotFound => {}, + error.InvalidWtf8, error.EnvironmentVariableMissing => {}, else => return err, } } @@ -127,7 +136,7 @@ pub fn maybeDir( ) !?[]const u8 { const path = try std.fmt.bufPrint(buf, "{s}/{s}/{s}", .{ base, sub, suffix }); - if (std.fs.accessAbsolute(path, .{})) { + if (std.Io.Dir.accessAbsolute(global.io(), path, .{})) { const len = path.len - suffix.len - 1; return buf[0..len]; } else |_| { diff --git a/vendor/libghostty-vt/src/os/stderr.zig b/vendor/libghostty-vt/src/os/stderr.zig new file mode 100644 index 00000000..201bc438 --- /dev/null +++ b/vendor/libghostty-vt/src/os/stderr.zig @@ -0,0 +1,60 @@ +//! Raw, best-effort stderr writing that bypasses `std.Io` entirely. +//! +//! `std.debug.lockStderr` (and everything layered on top of it) routes +//! through `std.Options.debug_io`, whose default implementation is +//! `std.Io.Threaded`. This keeps ~70KB of unreachable code in binaries. + +const std = @import("std"); +const builtin = @import("builtin"); + +/// Write bytes to stderr using the most primitive mechanism available +/// for the target. This is best-effort: errors are ignored, since this +/// is only used for diagnostics (logging and panic messages). +/// +/// Freestanding targets (e.g. wasm) have no stderr, so this is a no-op +/// there. +pub fn write(bytes: []const u8) void { + switch (builtin.os.tag) { + .freestanding, .other => {}, + + .windows => { + const windows = std.os.windows; + const handle = windows.peb().ProcessParameters.hStdError; + var iosb: windows.IO_STATUS_BLOCK = undefined; + _ = windows.ntdll.NtWriteFile( + handle, + null, // event + null, // APC routine + null, // APC context + &iosb, + bytes.ptr, + @intCast(bytes.len), + null, // byte offset + null, // key + ); + }, + + else => { + const posix = std.posix; + var i: usize = 0; + while (i < bytes.len) { + const rc = posix.system.write( + posix.STDERR_FILENO, + bytes[i..].ptr, + bytes.len - i, + ); + switch (posix.errno(rc)) { + .SUCCESS => i += @as(usize, @intCast(rc)), + .INTR => continue, + else => return, + } + } + }, + } +} + +test write { + // Smoke test: must not crash. We can't assert on the output without + // capturing stderr, which isn't worth the complexity here. + write(""); +} diff --git a/vendor/libghostty-vt/src/os/systemd.zig b/vendor/libghostty-vt/src/os/systemd.zig index 1a9a10ee..5815648a 100644 --- a/vendor/libghostty-vt/src/os/systemd.zig +++ b/vendor/libghostty-vt/src/os/systemd.zig @@ -1,5 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); +const global = @import("../global.zig"); const log = std.log.scoped(.systemd); @@ -16,8 +17,8 @@ pub fn launchedBySystemd() bool { // `JOURNAL_STREAM` (v231+) environment variables. If these // environment variables are not present we were not launched by // systemd. - if (std.posix.getenv("INVOCATION_ID") == null) break :linux false; - if (std.posix.getenv("JOURNAL_STREAM") == null) break :linux false; + if (global.environ().getPosix("INVOCATION_ID") == null) break :linux false; + if (global.environ().getPosix("JOURNAL_STREAM") == null) break :linux false; // If `INVOCATION_ID` and `JOURNAL_STREAM` are present, check to make sure // that our parent process is actually `systemd`, not some other terminal @@ -33,7 +34,7 @@ pub fn launchedBySystemd() bool { log.err("unable to format comm path for pid {d}", .{ppid}); break :linux false; }; - const comm_file = std.fs.openFileAbsolute(comm_path, .{ .mode = .read_only }) catch { + const comm_file = std.Io.Dir.openFileAbsolute(global.io(), comm_path, .{ .mode = .read_only }) catch { log.err("unable to open '{s}' for reading", .{comm_path}); break :linux false; }; @@ -91,7 +92,7 @@ pub const notify = struct { if (comptime builtin.os.tag != .linux) return; // Get the socket address that should receive notifications. - const socket_path = std.posix.getenv("NOTIFY_SOCKET") orelse return; + const socket_path = global.environ().getPosix("NOTIFY_SOCKET") orelse return; // If the socket address is an empty string return. if (socket_path.len == 0) return; @@ -121,7 +122,7 @@ pub const notify = struct { std.os.linux.SOCK.DGRAM | std.os.linux.SOCK.CLOEXEC, 0, ); - switch (std.os.linux.E.init(rc)) { + switch (linuxErrnoFromSyscall(rc)) { .SUCCESS => break :socket @intCast(rc), else => |e| { log.warn("creating socket failed: {s}", .{@tagName(e)}); @@ -138,7 +139,7 @@ pub const notify = struct { &socket_address, @offsetOf(std.os.linux.sockaddr.un, "path") + socket_address.path.len, ); - switch (std.os.linux.E.init(rc)) { + switch (linuxErrnoFromSyscall(rc)) { .SUCCESS => break :connect, else => |e| { log.warn("unable to connect to notify socket: {s}", .{@tagName(e)}); @@ -149,7 +150,7 @@ pub const notify = struct { write: { const rc = std.os.linux.write(socket, message.ptr, message.len); - switch (std.os.linux.E.init(rc)) { + switch (linuxErrnoFromSyscall(rc)) { .SUCCESS => { const written = rc; if (written < message.len) { @@ -180,12 +181,7 @@ pub const notify = struct { pub fn reloading() void { if (comptime builtin.os.tag != .linux) return; - const ts = std.posix.clock_gettime(.MONOTONIC) catch |err| { - log.err("unable to get MONOTONIC clock: {}", .{err}); - return; - }; - - const now = ts.sec * std.time.us_per_s + @divFloor(ts.nsec, std.time.ns_per_us); + const now = std.Io.Timestamp.now(global.io(), .awake).toMicroseconds(); var buffer: [64]u8 = undefined; const message = std.fmt.bufPrint(&buffer, "RELOADING=1\nMONOTONIC_USEC={d}", .{now}) catch |err| { @@ -196,3 +192,9 @@ pub const notify = struct { send(message); } }; + +fn linuxErrnoFromSyscall(r: usize) std.os.linux.E { + const signed_r: isize = @bitCast(r); + const int = if (signed_r > -4096 and signed_r < 0) -signed_r else 0; + return @enumFromInt(int); +} diff --git a/vendor/libghostty-vt/src/os/wasm/log.zig b/vendor/libghostty-vt/src/os/wasm/log.zig index faa885c6..5c5a8f66 100644 --- a/vendor/libghostty-vt/src/os/wasm/log.zig +++ b/vendor/libghostty-vt/src/os/wasm/log.zig @@ -4,6 +4,22 @@ const wasm = @import("../wasm.zig"); // Use the correct implementation pub const log = Freestanding.log; +/// A log function that discards everything. Since the format string and +/// scope are comptime parameters, using this compiles out all logging +/// machinery: call sites, format strings, and the std.fmt code they +/// reference (~220KB in a ReleaseFast wasm build). +pub fn noop( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + _ = level; + _ = scope; + _ = format; + _ = args; +} + /// Freestanding implementation calls an extern "log" function. pub const Freestanding = struct { // The function std.log will call. diff --git a/vendor/libghostty-vt/src/os/windows.zig b/vendor/libghostty-vt/src/os/windows.zig index 6e452fb7..30b011a2 100644 --- a/vendor/libghostty-vt/src/os/windows.zig +++ b/vendor/libghostty-vt/src/os/windows.zig @@ -1,139 +1,378 @@ const std = @import("std"); const windows = std.os.windows; -// Export any constants or functions we need from the Windows API so -// we can just import one file. -pub const kernel32 = windows.kernel32; -pub const unexpectedError = windows.unexpectedError; -pub const OpenFile = windows.OpenFile; -pub const CloseHandle = windows.CloseHandle; +// NOTE: The Windows part of the Zig stdlib is currently in the process of +// having most of its features removed, with the ultimate goal of switching to +// serve as a support for higher-level functionality offered in places like +// `std.Io` only. As such this file serves as a "bridge" between type +// information (mostly coming from stdlib) and manually-defined constants and +// external functions. + +// Utility functions pub const GetCurrentProcessId = windows.GetCurrentProcessId; -pub const SetHandleInformation = windows.SetHandleInformation; +pub const GetLastError = windows.GetLastError; +pub const unexpectedError = windows.unexpectedError; +pub const unexpectedStatus = windows.unexpectedStatus; + +// Primitive types +pub const BOOL = windows.BOOL; +pub const COORD = windows.COORD; pub const DWORD = windows.DWORD; -pub const FILE_ATTRIBUTE_NORMAL = windows.FILE_ATTRIBUTE_NORMAL; -pub const FILE_FLAG_OVERLAPPED = windows.FILE_FLAG_OVERLAPPED; -pub const FILE_SHARE_READ = windows.FILE_SHARE_READ; -pub const GENERIC_READ = windows.GENERIC_READ; +pub const DWORD_PTR = windows.DWORD_PTR; pub const HANDLE = windows.HANDLE; -pub const HANDLE_FLAG_INHERIT = windows.HANDLE_FLAG_INHERIT; -pub const INFINITE = windows.INFINITE; -pub const INVALID_HANDLE_VALUE = windows.INVALID_HANDLE_VALUE; -pub const MAX_PATH = windows.MAX_PATH; -pub const OPEN_EXISTING = windows.OPEN_EXISTING; -pub const PIPE_ACCESS_OUTBOUND = windows.PIPE_ACCESS_OUTBOUND; -pub const PIPE_TYPE_BYTE = windows.PIPE_TYPE_BYTE; -pub const PROCESS_INFORMATION = windows.PROCESS_INFORMATION; -pub const S_OK = windows.S_OK; +pub const HINSTANCE = windows.HINSTANCE; +pub const HPCON = windows.LPVOID; +pub const HRESULT = c_long; +pub const LARGE_INTEGER = windows.LARGE_INTEGER; +pub const LPCWSTR = windows.LPCWSTR; +pub const LPSTR = windows.LPSTR; +pub const LPVOID = windows.LPVOID; +pub const LPWSTR = windows.LPWSTR; +pub const PVOID = windows.PVOID; +pub const SIZE_T = windows.SIZE_T; +pub const UINT = windows.UINT; +pub const ULONG = windows.ULONG; +pub const ULONG_PTR = windows.ULONG_PTR; +pub const UNICODE_STRING = windows.UNICODE_STRING; + +// Structs and opaque types +pub const LPPROC_THREAD_ATTRIBUTE_LIST = ?*anyopaque; pub const SECURITY_ATTRIBUTES = windows.SECURITY_ATTRIBUTES; -pub const STARTUPINFOW = windows.STARTUPINFOW; pub const STARTF_USESTDHANDLES = windows.STARTF_USESTDHANDLES; -pub const SYNCHRONIZE = windows.SYNCHRONIZE; -pub const WAIT_FAILED = windows.WAIT_FAILED; -pub const FALSE = windows.FALSE; -pub const TRUE = windows.TRUE; +pub const STARTUPINFOW = windows.STARTUPINFOW; -pub const exp = struct { - pub const HPCON = windows.LPVOID; +pub const OVERLAPPED = extern struct { + Internal: ULONG_PTR, + InternalHigh: ULONG_PTR, + DUMMYUNIONNAME: extern union { + DUMMYSTRUCTNAME: extern struct { + Offset: DWORD, + OffsetHigh: DWORD, + }, + Pointer: ?PVOID, + }, + hEvent: ?HANDLE, +}; +pub const PROCESS_INFORMATION = extern struct { + hProcess: HANDLE, + hThread: HANDLE, + dwProcessId: DWORD, + dwThreadId: DWORD, +}; +pub const STARTUPINFOEX = extern struct { + StartupInfo: windows.STARTUPINFOW, + lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, +}; + +// Well-known constant values +pub const INFINITE = 4294967295; +pub const INVALID_HANDLE_VALUE = windows.INVALID_HANDLE_VALUE; +pub const MAX_PATH = windows.MAX_PATH; +pub const FALSE: windows.BOOL = .fromBool(false); +pub const TRUE: windows.BOOL = .fromBool(true); - pub const CREATE_UNICODE_ENVIRONMENT = 0x00000400; - pub const EXTENDED_STARTUPINFO_PRESENT = 0x00080000; - pub const LPPROC_THREAD_ATTRIBUTE_LIST = ?*anyopaque; - pub const FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000; +// Bit-field and enum constant values +pub const CREATE_UNICODE_ENVIRONMENT = 0x00000400; +pub const DELETE = 0x00010000; +pub const ERROR_SUCCESS = 0; +pub const EXTENDED_STARTUPINFO_PRESENT = 0x00080000; +pub const FILE_ATTRIBUTE_NORMAL = 0x80; +pub const FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000; +pub const FILE_FLAG_OVERLAPPED = 0x40000000; +/// GetFinalPathNameByHandleW dwFlags: normalized name with a DOS drive +/// letter (both are the zero flag values). +pub const FILE_NAME_NORMALIZED = 0x0; +pub const FILE_NON_DIRECTORY_FILE = 0x00000040; +/// NtCreateFile CreateDisposition: open an existing file, never create. +/// Distinct from the Win32 CreateFileW value OPEN_EXISTING. +pub const FILE_OPEN = 0x00000001; +pub const FILE_OPEN_REPARSE_POINT = 0x00200000; +pub const FILE_SHARE_DELETE = 0x00000004; +pub const FILE_SHARE_READ = 0x00000001; +pub const FILE_SHARE_WRITE = 0x00000002; +pub const FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020; +pub const GENERIC_READ = 0x80000000; +pub const HANDLE_FLAG_INHERIT = 0x00000001; +pub const MEM_COMMIT = 0x1000; +pub const MEM_RELEASE = 0x8000; +pub const MEM_RESERVE = 0x2000; +pub const OPEN_EXISTING = 3; // Known as FILE_OPEN in Windows docs +pub const PAGE_READWRITE = 0x04; +pub const PIPE_ACCESS_OUTBOUND = 0x00000002; +pub const PIPE_TYPE_BYTE = 0x00000000; +pub const PROC_THREAD_ATTRIBUTE_ADDITIVE = 0x00040000; +pub const PROC_THREAD_ATTRIBUTE_INPUT = 0x00020000; +pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF; +pub const PROC_THREAD_ATTRIBUTE_THREAD = 0x00010000; +pub const S_OK = 0; +pub const SYNCHRONIZE = 0x00100000; +pub const VOLUME_NAME_DOS = 0x0; +pub const WAIT_FAILED = 0xFFFFFFFF; - pub const STATUS_PENDING = 0x00000103; - pub const STILL_ACTIVE = STATUS_PENDING; +/// IOCTL that fills the output buffer with bytes from the kernel CSPRNG +/// behind `\Device\CNG` (what ProcessPrng and BCryptGenRandom draw from). +pub const IOCTL_KSEC_GEN_RANDOM: CTL_CODE = windows.IOCTL.KSEC.GEN_RANDOM; - pub const STARTUPINFOEX = extern struct { - StartupInfo: windows.STARTUPINFOW, - lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, - }; +pub const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = ProcThreadAttributeValue( + .ProcThreadAttributePseudoConsole, + false, + true, + false, +); + +// Types needed for ntdll calls +pub const ACCESS_MASK = windows.ACCESS_MASK; +pub const CTL_CODE = windows.CTL_CODE; +pub const FILE_ALL_INFORMATION = windows.FILE.ALL_INFORMATION; +pub const FILE_ATTRIBUTE_TAG_INFORMATION = windows.FILE.ATTRIBUTE_TAG_INFO; +pub const FILE_DISPOSITION_INFORMATION = windows.FILE.DISPOSITION.INFORMATION; +pub const FILE_DISPOSITION_INFORMATION_EX = windows.FILE.DISPOSITION.INFORMATION.EX; +pub const FILE_INFORMATION_CLASS = windows.FILE.INFORMATION_CLASS; +pub const FILE_POSITION_INFORMATION = windows.FILE.POSITION_INFORMATION; +pub const FILE_STANDARD_INFORMATION = windows.FILE.STANDARD_INFORMATION; +pub const IO_STATUS_BLOCK = windows.IO_STATUS_BLOCK; +pub const NTSTATUS = windows.NTSTATUS; +pub const OBJECT_ATTRIBUTES = windows.OBJECT.ATTRIBUTES; +// Exported functions by library +pub const exp = struct { pub const kernel32 = struct { pub extern "kernel32" fn CreatePipe( - hReadPipe: *windows.HANDLE, - hWritePipe: *windows.HANDLE, - lpPipeAttributes: ?*const windows.SECURITY_ATTRIBUTES, - nSize: windows.DWORD, - ) callconv(.winapi) windows.BOOL; + hReadPipe: *HANDLE, + hWritePipe: *HANDLE, + lpPipeAttributes: ?*const SECURITY_ATTRIBUTES, + nSize: DWORD, + ) callconv(.winapi) BOOL; pub extern "kernel32" fn CreatePseudoConsole( - size: windows.COORD, - hInput: windows.HANDLE, - hOutput: windows.HANDLE, - dwFlags: windows.DWORD, + size: COORD, + hInput: HANDLE, + hOutput: HANDLE, + dwFlags: DWORD, phPC: *HPCON, - ) callconv(.winapi) windows.HRESULT; - pub extern "kernel32" fn ResizePseudoConsole(hPC: HPCON, size: windows.COORD) callconv(.winapi) windows.HRESULT; + ) callconv(.winapi) HRESULT; + pub extern "kernel32" fn ResizePseudoConsole( + hPC: HPCON, + size: COORD, + ) callconv(.winapi) HRESULT; pub extern "kernel32" fn ClosePseudoConsole(hPC: HPCON) callconv(.winapi) void; pub extern "kernel32" fn InitializeProcThreadAttributeList( lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, - dwAttributeCount: windows.DWORD, - dwFlags: windows.DWORD, - lpSize: *windows.SIZE_T, - ) callconv(.winapi) windows.BOOL; + dwAttributeCount: DWORD, + dwFlags: DWORD, + lpSize: *SIZE_T, + ) callconv(.winapi) BOOL; pub extern "kernel32" fn UpdateProcThreadAttribute( lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, - dwFlags: windows.DWORD, - Attribute: windows.DWORD_PTR, - lpValue: windows.PVOID, - cbSize: windows.SIZE_T, - lpPreviousValue: ?windows.PVOID, - lpReturnSize: ?*windows.SIZE_T, - ) callconv(.winapi) windows.BOOL; + dwFlags: DWORD, + Attribute: DWORD_PTR, + lpValue: PVOID, + cbSize: SIZE_T, + lpPreviousValue: ?PVOID, + lpReturnSize: ?*SIZE_T, + ) callconv(.winapi) BOOL; pub extern "kernel32" fn PeekNamedPipe( - hNamedPipe: windows.HANDLE, - lpBuffer: ?windows.LPVOID, - nBufferSize: windows.DWORD, - lpBytesRead: ?*windows.DWORD, - lpTotalBytesAvail: ?*windows.DWORD, - lpBytesLeftThisMessage: ?*windows.DWORD, - ) callconv(.winapi) windows.BOOL; + hNamedPipe: HANDLE, + lpBuffer: ?LPVOID, + nBufferSize: DWORD, + lpBytesRead: ?*DWORD, + lpTotalBytesAvail: ?*DWORD, + lpBytesLeftThisMessage: ?*DWORD, + ) callconv(.winapi) BOOL; // Duplicated here because lpCommandLine is not marked optional in zig std pub extern "kernel32" fn CreateProcessW( - lpApplicationName: ?windows.LPWSTR, - lpCommandLine: ?windows.LPWSTR, - lpProcessAttributes: ?*windows.SECURITY_ATTRIBUTES, - lpThreadAttributes: ?*windows.SECURITY_ATTRIBUTES, - bInheritHandles: windows.BOOL, - dwCreationFlags: windows.DWORD, + lpApplicationName: ?LPWSTR, + lpCommandLine: ?LPWSTR, + lpProcessAttributes: ?*SECURITY_ATTRIBUTES, + lpThreadAttributes: ?*SECURITY_ATTRIBUTES, + bInheritHandles: BOOL, + dwCreationFlags: DWORD, lpEnvironment: ?*anyopaque, - lpCurrentDirectory: ?windows.LPWSTR, - lpStartupInfo: *windows.STARTUPINFOW, - lpProcessInformation: *windows.PROCESS_INFORMATION, - ) callconv(.winapi) windows.BOOL; + lpCurrentDirectory: ?LPWSTR, + lpStartupInfo: *STARTUPINFOW, + lpProcessInformation: *PROCESS_INFORMATION, + ) callconv(.winapi) BOOL; /// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcomputernamea pub extern "kernel32" fn GetComputerNameA( - lpBuffer: windows.LPSTR, - nSize: *windows.DWORD, - ) callconv(.winapi) windows.BOOL; + lpBuffer: LPSTR, + nSize: *DWORD, + ) callconv(.winapi) BOOL; /// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw pub extern "kernel32" fn GetTempPathW( - nBufferLength: windows.DWORD, - lpBuffer: windows.LPWSTR, - ) callconv(.winapi) windows.DWORD; + nBufferLength: DWORD, + lpBuffer: LPWSTR, + ) callconv(.winapi) DWORD; + pub extern "kernel32" fn SetHandleInformation( + hObject: HANDLE, + dwMask: DWORD, + dwFlags: DWORD, + ) callconv(.winapi) BOOL; + pub extern "kernel32" fn CreateFileW( + lpFileName: LPCWSTR, + dwDesiredAccess: DWORD, + dwShareMode: DWORD, + lpSecurityAttributes: ?*SECURITY_ATTRIBUTES, + dwCreationDisposition: DWORD, + dwFlagsAndAttributes: DWORD, + hTemplateFile: ?HANDLE, + ) callconv(.winapi) HANDLE; + pub extern "kernel32" fn CreateNamedPipeW( + lpName: LPCWSTR, + dwOpenMode: DWORD, + dwPipeMode: DWORD, + nMaxInstances: DWORD, + nOutBufferSize: DWORD, + nInBufferSize: DWORD, + nDefaultTimeOut: DWORD, + lpSecurityAttributes: ?*const SECURITY_ATTRIBUTES, + ) callconv(.winapi) HANDLE; + pub extern "kernel32" fn CloseHandle( + hObject: HANDLE, + ) callconv(.winapi) BOOL; + pub extern "kernel32" fn VirtualAlloc( + lpAddress: ?LPVOID, + dwSize: SIZE_T, + flAllocationType: DWORD, + flProtect: DWORD, + ) callconv(.winapi) ?LPVOID; + pub extern "kernel32" fn VirtualFree( + lpAddress: ?LPVOID, + dwSize: SIZE_T, + dwFreeType: DWORD, + ) callconv(.winapi) BOOL; + /// https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-discardvirtualmemory + pub extern "kernel32" fn DiscardVirtualMemory( + VirtualAddress: PVOID, + Size: SIZE_T, + ) callconv(.winapi) DWORD; + pub extern "kernel32" fn WaitForSingleObject( + hHandle: HANDLE, + dwMilliseconds: DWORD, + ) callconv(.winapi) DWORD; + pub extern "kernel32" fn GetExitCodeProcess( + hProcess: HANDLE, + lpExitCode: *DWORD, + ) callconv(.winapi) BOOL; + pub extern "kernel32" fn TerminateProcess( + hProcess: HANDLE, + uExitCode: UINT, + ) callconv(.winapi) BOOL; + pub extern "kernel32" fn CancelIoEx( + hFile: HANDLE, + lpOverlapped: ?*OVERLAPPED, + ) callconv(.winapi) BOOL; + pub extern "kernel32" fn ReadFile( + hFile: HANDLE, + lpBuffer: LPVOID, + nNumberOfBytesToRead: DWORD, + lpNumberOfBytesRead: ?*DWORD, + lpOverlapped: ?*OVERLAPPED, + ) callconv(.winapi) BOOL; + /// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew + pub extern "kernel32" fn GetFinalPathNameByHandleW( + hFile: HANDLE, + lpszFilePath: [*]u16, + cchFilePath: DWORD, + dwFlags: DWORD, + ) callconv(.winapi) DWORD; }; - - pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF; - pub const PROC_THREAD_ATTRIBUTE_THREAD = 0x00010000; - pub const PROC_THREAD_ATTRIBUTE_INPUT = 0x00020000; - pub const PROC_THREAD_ATTRIBUTE_ADDITIVE = 0x00040000; - - pub const ProcThreadAttributeNumber = enum(windows.DWORD) { - ProcThreadAttributePseudoConsole = 22, - _, + pub const ntdll = struct { + pub extern "ntdll" fn NtCreateFile( + FileHandle: *HANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: *OBJECT_ATTRIBUTES, + IoStatusBlock: *IO_STATUS_BLOCK, + AllocationSize: ?*LARGE_INTEGER, + FileAttributes: ULONG, + ShareAccess: ULONG, + CreateDisposition: ULONG, + CreateOptions: ULONG, + EaBuffer: ?*anyopaque, + EaLength: ULONG, + ) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtOpenFile( + FileHandle: *HANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: *OBJECT_ATTRIBUTES, + IoStatusBlock: *IO_STATUS_BLOCK, + ShareAccess: ULONG, + OpenOptions: ULONG, + ) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtClose(Handle: HANDLE) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtReadFile( + FileHandle: HANDLE, + Event: ?HANDLE, + ApcRoutine: ?*const anyopaque, + ApcContext: ?*anyopaque, + IoStatusBlock: *IO_STATUS_BLOCK, + Buffer: *anyopaque, + Length: ULONG, + ByteOffset: ?*const LARGE_INTEGER, + Key: ?*const ULONG, + ) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtQueryInformationFile( + FileHandle: HANDLE, + IoStatusBlock: *IO_STATUS_BLOCK, + FileInformation: *anyopaque, + Length: ULONG, + FileInformationClass: FILE_INFORMATION_CLASS, + ) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtSetInformationFile( + FileHandle: HANDLE, + IoStatusBlock: *IO_STATUS_BLOCK, + FileInformation: *anyopaque, + Length: ULONG, + FileInformationClass: FILE_INFORMATION_CLASS, + ) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtDeviceIoControlFile( + FileHandle: HANDLE, + Event: ?HANDLE, + ApcRoutine: ?*const anyopaque, + ApcContext: ?*anyopaque, + IoStatusBlock: *IO_STATUS_BLOCK, + IoControlCode: CTL_CODE, + InputBuffer: ?*const anyopaque, + InputBufferLength: ULONG, + OutputBuffer: ?*anyopaque, + OutputBufferLength: ULONG, + ) callconv(.winapi) NTSTATUS; + /// Resolves a Win32 path against the process working directory and + /// canonicalizes it, keeping any `\\?\` or `\\.\` prefix. Returns + /// the byte length written excluding the terminator, the required + /// byte length if the buffer is too small, or 0 on failure. + pub extern "ntdll" fn RtlGetFullPathName_U( + FileName: [*:0]const u16, + BufferByteLength: ULONG, + Buffer: [*]u16, + ShortName: ?*[*:0]const u16, + ) callconv(.winapi) ULONG; + /// The futex primitives that kernel32's WaitOnAddress and + /// WakeByAddress* forward to. Timeout is a relative (negative) + /// interval in 100ns units, null to wait forever. + pub extern "ntdll" fn RtlWaitOnAddress( + Address: *const anyopaque, + CompareAddress: *const anyopaque, + AddressSize: SIZE_T, + Timeout: ?*const LARGE_INTEGER, + ) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn RtlWakeAddressSingle(Address: *const anyopaque) callconv(.winapi) void; + pub extern "ntdll" fn RtlWakeAddressAll(Address: *const anyopaque) callconv(.winapi) void; }; +}; - /// Corresponds to the ProcThreadAttributeValue define in WinBase.h - pub fn ProcThreadAttributeValue( - comptime attribute: ProcThreadAttributeNumber, - comptime thread: bool, - comptime input: bool, - comptime additive: bool, - ) windows.DWORD { - return (@intFromEnum(attribute) & PROC_THREAD_ATTRIBUTE_NUMBER) | - (if (thread) PROC_THREAD_ATTRIBUTE_THREAD else 0) | - (if (input) PROC_THREAD_ATTRIBUTE_INPUT else 0) | - (if (additive) PROC_THREAD_ATTRIBUTE_ADDITIVE else 0); - } - - pub const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = ProcThreadAttributeValue(.ProcThreadAttributePseudoConsole, false, true, false); +pub const ProcThreadAttributeNumber = enum(DWORD) { + ProcThreadAttributePseudoConsole = 22, + _, }; + +/// Corresponds to the ProcThreadAttributeValue define in WinBase.h +pub fn ProcThreadAttributeValue( + comptime attribute: ProcThreadAttributeNumber, + comptime thread: bool, + comptime input: bool, + comptime additive: bool, +) DWORD { + return (@intFromEnum(attribute) & PROC_THREAD_ATTRIBUTE_NUMBER) | + (if (thread) PROC_THREAD_ATTRIBUTE_THREAD else 0) | + (if (input) PROC_THREAD_ATTRIBUTE_INPUT else 0) | + (if (additive) PROC_THREAD_ATTRIBUTE_ADDITIVE else 0); +} diff --git a/vendor/libghostty-vt/src/os/xdg.zig b/vendor/libghostty-vt/src/os/xdg.zig index f2a495af..92029586 100644 --- a/vendor/libghostty-vt/src/os/xdg.zig +++ b/vendor/libghostty-vt/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 env_os = @import("env.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, opts: Options) ![]u8 { - return try dir(alloc, 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, opts: Options) ![]u8 { } /// Get the XDG cache directory. The returned value is allocated. -pub fn cache(alloc: Allocator, opts: Options) ![]u8 { - return try dir(alloc, 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, opts: Options) ![]u8 { } /// Get the XDG state directory. The returned value is allocated. -pub fn state(alloc: Allocator, opts: Options) ![]u8 { - return try dir(alloc, 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,7 +53,9 @@ 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, internal_opts: InternalOptions, ) ![]u8 { @@ -67,30 +68,28 @@ fn dir( }); } - // 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), + // First check the env var. On Windows we treat `LOCALAPPDATA` as a + // fallback for `XDG_CONFIG_HOME` + const env = switch (builtin.os.tag) { + .windows => environ_map.get(internal_opts.env) orelse environ_map.get(internal_opts.windows_env) orelse "", + else => environ_map.get(internal_opts.env) orelse "", }; - defer if (env_) |env| env.deinit(alloc); - if (env_) |env| { + if (env.len > 0) { // 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, + env, subdir, }); } - return try alloc.dupe(u8, env.value); + return try alloc.dupe(u8, env); } // Get our home dir var buf: [1024]u8 = undefined; - if (try homedir.home(&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, @@ -120,10 +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, .{}); + const value = try config(io, alloc, &environ_map, .{}); defer alloc.free(value); try testing.expect(value.len > 0); } @@ -131,14 +133,17 @@ 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); + defer environ_map.deinit(); // Test when XDG_CACHE_HOME is not set { // Test base path { - const cache_path = try cache(alloc, .{ .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); @@ -147,7 +152,7 @@ test "cache directory paths" { // Test with subdir { - const cache_path = try cache(alloc, .{ + const cache_path = try cache(io, alloc, &environ_map, .{ .home = mock_home, .subdir = "ghostty", }); @@ -162,26 +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 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, + func: fn (std.Io, Allocator, *std.process.Environ.Map, Options) anyerror![]u8, default_subdir: []const u8, }; @@ -192,19 +183,10 @@ test "fallback when xdg env empty" { }; 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); - } + var environ_map = try std.testing.environ.createMap(alloc); + defer environ_map.deinit(); + const temp_home = "/tmp/ghostty-test-home"; + try environ_map.put("HOME", temp_home); const expected = try std.fs.path.join(alloc, &[_][]const u8{ temp_home, @@ -213,8 +195,8 @@ test "fallback when xdg env empty" { defer alloc.free(expected); // Test with empty string - should fallback to home - _ = env_os.setenv(case.name, ""); - const actual = try case.func(alloc, .{}); + try environ_map.put(case.name, ""); + const actual = try case.func(io, alloc, &environ_map, .{}); defer alloc.free(actual); try std.testing.expectEqualStrings(expected, actual); @@ -224,28 +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 env = @import("env.zig"); + const io = std.testing.io; 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, + func: fn (std.Io, Allocator, *const std.process.Environ.Map, Options) anyerror![]u8, default_subdir: []const u8, }; @@ -256,19 +222,10 @@ test "fallback when xdg env empty and subdir" { }; 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); - } + var environ_map = try std.testing.environ.createMap(alloc); + defer environ_map.deinit(); + const temp_home = "/tmp/ghostty-test-home"; + try environ_map.put("HOME", temp_home); const expected = try std.fs.path.join(alloc, &[_][]const u8{ temp_home, @@ -278,8 +235,8 @@ test "fallback when xdg env empty and subdir" { defer alloc.free(expected); // Test with empty string - should fallback to home - _ = env.setenv(case.name, ""); - const actual = try case.func(alloc, .{ .subdir = "ghostty" }); + try environ_map.put(case.name, ""); + const actual = try case.func(io, alloc, &environ_map, .{ .subdir = "ghostty" }); defer alloc.free(actual); try std.testing.expectEqualStrings(expected, actual); |
