aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/libghostty-vt/src/os
diff options
context:
space:
mode:
authorsl0p <sl0p@sl0p.foo>2026-08-13 22:00:03 +0200
committersl0p <sl0p@sl0p.foo>2026-08-13 22:00:03 +0200
commit2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8 (patch)
tree7cd6274303ba1da1f8bd57371aec336dd687fb58 /vendor/libghostty-vt/src/os
downloadslosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.tar.gz
slosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.tar.xz
slosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.zip
sl0ptty: M0 — libghostty-vt vendored, one pane, compositor, diff renderer
Design contract in DESIGN.md. libghostty-vt vendored fresh from ghostty main@bedb6f2 (zig 0.16), built to a 14MB static lib. M0: pty spawn (no libutil), a lib-vt terminal per pane, a screen cell buffer, a diff emitter that repaints only what changed, raw mode, SIGWINCH, and a ~120Hz frame cap that coalesces pty reads. Also the seed of the headless harness: --headless dumps the composited screen without a tty, so composition is testable in milliseconds. tests/live_m0.py covers what headless cannot: raw mode, the diff emitter, resize propagation, and the quit path. 6/6 green.
Diffstat (limited to 'vendor/libghostty-vt/src/os')
-rw-r--r--vendor/libghostty-vt/src/os/TempDir.zig155
-rw-r--r--vendor/libghostty-vt/src/os/cf_release_thread.zig192
-rw-r--r--vendor/libghostty-vt/src/os/cgroup.zig27
-rw-r--r--vendor/libghostty-vt/src/os/dbus.zig21
-rw-r--r--vendor/libghostty-vt/src/os/desktop.zig169
-rw-r--r--vendor/libghostty-vt/src/os/edit.zig69
-rw-r--r--vendor/libghostty-vt/src/os/file.zig147
-rw-r--r--vendor/libghostty-vt/src/os/flatpak.zig521
-rw-r--r--vendor/libghostty-vt/src/os/homedir.zig185
-rw-r--r--vendor/libghostty-vt/src/os/hostname.zig63
-rw-r--r--vendor/libghostty-vt/src/os/i18n.zig210
-rw-r--r--vendor/libghostty-vt/src/os/i18n_locales.zig63
-rw-r--r--vendor/libghostty-vt/src/os/kernel_info.zig32
-rw-r--r--vendor/libghostty-vt/src/os/locale.c1
-rw-r--r--vendor/libghostty-vt/src/os/locale.zig232
-rw-r--r--vendor/libghostty-vt/src/os/mach.zig153
-rw-r--r--vendor/libghostty-vt/src/os/macos.zig177
-rw-r--r--vendor/libghostty-vt/src/os/main.zig76
-rw-r--r--vendor/libghostty-vt/src/os/mouse.zig25
-rw-r--r--vendor/libghostty-vt/src/os/open.zig101
-rw-r--r--vendor/libghostty-vt/src/os/passwd.zig158
-rw-r--r--vendor/libghostty-vt/src/os/path.zig100
-rw-r--r--vendor/libghostty-vt/src/os/pipe.zig22
-rw-r--r--vendor/libghostty-vt/src/os/resourcesdir.zig148
-rw-r--r--vendor/libghostty-vt/src/os/shell.zig196
-rw-r--r--vendor/libghostty-vt/src/os/stderr.zig60
-rw-r--r--vendor/libghostty-vt/src/os/string_encoding.zig307
-rw-r--r--vendor/libghostty-vt/src/os/systemd.zig200
-rw-r--r--vendor/libghostty-vt/src/os/uri.zig204
-rw-r--r--vendor/libghostty-vt/src/os/wasm.zig25
-rw-r--r--vendor/libghostty-vt/src/os/wasm/log.zig45
-rw-r--r--vendor/libghostty-vt/src/os/wasm/target.zig20
-rw-r--r--vendor/libghostty-vt/src/os/windows.zig273
-rw-r--r--vendor/libghostty-vt/src/os/xdg.zig269
34 files changed, 4646 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/os/TempDir.zig b/vendor/libghostty-vt/src/os/TempDir.zig
new file mode 100644
index 0000000..ca23426
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/TempDir.zig
@@ -0,0 +1,155 @@
+//! Creates a temporary directory at runtime that can be safely used to
+//! store temporary data and is destroyed on deinit.
+const TempDir = @This();
+
+const builtin = @import("builtin");
+const std = @import("std");
+const Dir = std.Io.Dir;
+const file = @import("file.zig");
+const global = @import("../global.zig");
+
+const log = std.log.scoped(.tempdir);
+
+/// Dir is the directory handle
+dir: Dir,
+
+/// Parent directory
+parent: Dir,
+
+/// Name buffer that name points into. Generally do not use. To get the
+/// name call the name() function.
+name_buf: [file.random_basename_len:0]u8,
+
+/// Create the temporary directory.
+pub fn init() !TempDir {
+ // Note: the tmp_path_buf sentinel is important because it ensures
+ // we actually always have random_basename_len+1 bytes of available
+ // space. We need that so we can set the sentinel in the case we use
+ // all the possible length.
+ var tmp_path_buf: [file.random_basename_len:0]u8 = undefined;
+
+ const dir = dir: {
+ 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(global.io(), tmp_dir, .{});
+ };
+
+ // We now loop forever until we can find a directory that we can create.
+ while (true) {
+ const tmp_path = try file.randomBasename(&tmp_path_buf);
+ tmp_path_buf[tmp_path.len] = 0;
+
+ 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(global.io(), tmp_path, .{}),
+ .parent = dir,
+ .name_buf = tmp_path_buf,
+ };
+ }
+}
+
+/// Name returns the name of the directory. This is just the basename
+/// and is not the full absolute path.
+pub fn name(self: *TempDir) []const u8 {
+ return std.mem.sliceTo(&self.name_buf, 0);
+}
+
+/// Finish with the temporary directory. This deletes all contents in the
+/// directory.
+pub fn deinit(self: *TempDir) void {
+ 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 path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ var path_len: usize = undefined;
+ var dir_handle: Dir.Handle = undefined;
+ var parent_handle: Dir.Handle = undefined;
+ {
+ var td = try init();
+ errdefer td.deinit();
+
+ 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);
+ dir_handle = td.dir.handle;
+ parent_handle = td.parent.handle;
+
+ // Should be deleted after we deinit.
+ td.deinit();
+ }
+
+ switch (builtin.os.tag) {
+ .freebsd, .ios, .linux, .macos => {
+ for ([_]Dir.Handle{ dir_handle, parent_handle }) |handle| {
+ const result = std.posix.system.fcntl(handle, std.posix.F.GETFD);
+ try testing.expectEqual(std.posix.E.BADF, std.posix.errno(result));
+ }
+ },
+
+ else => {},
+ }
+ 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 dir_handle: Dir.Handle = undefined;
+ var parent_handle: Dir.Handle = undefined;
+ {
+ var td = try init();
+ errdefer td.deinit();
+
+ path_len = try td.dir.realPath(testing.io, &path_buf);
+ dir_handle = td.dir.handle;
+ parent_handle = td.parent.handle;
+
+ td.close(.retain);
+ }
+ defer Dir.deleteDirAbsolute(testing.io, path_buf[0..path_len]) catch {};
+
+ switch (builtin.os.tag) {
+ .freebsd, .ios, .linux, .macos => {
+ for ([_]Dir.Handle{ dir_handle, parent_handle }) |handle| {
+ const result = std.posix.system.fcntl(handle, std.posix.F.GETFD);
+ try testing.expectEqual(std.posix.E.BADF, std.posix.errno(result));
+ }
+ },
+
+ else => {},
+ }
+ var dir = try Dir.openDirAbsolute(testing.io, path_buf[0..path_len], .{});
+ dir.close(testing.io);
+}
diff --git a/vendor/libghostty-vt/src/os/cf_release_thread.zig b/vendor/libghostty-vt/src/os/cf_release_thread.zig
new file mode 100644
index 0000000..e93120b
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/cf_release_thread.zig
@@ -0,0 +1,192 @@
+//! Represents the CFRelease thread. Pools of CFTypeRefs are sent to
+//! this thread to be released, so that their release callback logic
+//! doesn't block the execution of a high throughput thread like the
+//! renderer thread.
+pub const Thread = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const macos = @import("macos");
+
+const internal_os = @import("../os/main.zig");
+const global = @import("../global.zig");
+const xev = global.xev;
+const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
+
+const Allocator = std.mem.Allocator;
+const log = std.log.scoped(.cf_release_thread);
+
+pub const Message = union(enum) {
+ /// Release a slice of CFTypeRefs. Uses alloc to free the slice after
+ /// releasing all the refs.
+ release: struct {
+ refs: []*anyopaque,
+ alloc: Allocator,
+ },
+};
+
+/// The type used for sending messages to the thread. For now this is
+/// hardcoded with a capacity. We can make this a comptime parameter in
+/// the future if we want it configurable.
+pub const Mailbox = BlockingQueue(Message, 64);
+
+/// Allocator used for some state
+alloc: std.mem.Allocator,
+
+/// The main event loop for the thread. The user data of this loop
+/// is always the allocator used to create the loop. This is a convenience
+/// so that users of the loop always have an allocator.
+loop: xev.Loop,
+
+/// This can be used to wake up the thread.
+wakeup: xev.Async,
+wakeup_c: xev.Completion = .{},
+
+/// This can be used to stop the thread on the next loop iteration.
+stop: xev.Async,
+stop_c: xev.Completion = .{},
+
+/// The mailbox that can be used to send this thread messages. Note
+/// this is a blocking queue so if it is full you will get errors (or block).
+mailbox: *Mailbox,
+
+flags: packed struct {
+ /// This is set to true only when an abnormal exit is detected. It
+ /// tells our mailbox system to drain and ignore all messages.
+ drain: bool = false,
+} = .{},
+
+/// Initialize the thread. This does not START the thread. This only sets
+/// up all the internal state necessary prior to starting the thread. It
+/// is up to the caller to start the thread with the threadMain entrypoint.
+pub fn init(
+ alloc: Allocator,
+) !Thread {
+ // Create our event loop.
+ var loop = try xev.Loop.init(.{});
+ errdefer loop.deinit();
+
+ // This async handle is used to "wake up" the thread to collect objects.
+ var wakeup_h = try xev.Async.init();
+ errdefer wakeup_h.deinit();
+
+ // This async handle is used to stop the loop and force the thread to end.
+ var stop_h = try xev.Async.init();
+ errdefer stop_h.deinit();
+
+ // The mailbox for messaging this thread
+ var mailbox = try Mailbox.create(alloc);
+ errdefer mailbox.destroy(alloc);
+
+ return Thread{
+ .alloc = alloc,
+ .loop = loop,
+ .wakeup = wakeup_h,
+ .stop = stop_h,
+ .mailbox = mailbox,
+ };
+}
+
+/// Clean up the thread. This is only safe to call once the thread
+/// completes executing; the caller must join prior to this.
+pub fn deinit(self: *Thread) void {
+ self.stop.deinit();
+ self.wakeup.deinit();
+ self.loop.deinit();
+
+ // Nothing can possibly access the mailbox anymore, destroy it.
+ self.mailbox.destroy(self.alloc);
+}
+
+/// The main entrypoint for the thread.
+pub fn threadMain(self: *Thread) void {
+ // Call child function so we can use errors...
+ self.threadMain_() catch |err| {
+ log.warn("error in cf release thread err={}", .{err});
+ };
+
+ // If our loop is not stopped, then we need to keep running so that
+ // messages are drained and we can wait for the surface to send a stop
+ // message.
+ if (!self.loop.stopped()) {
+ log.warn("abrupt cf release thread exit detected, starting xev to drain mailbox", .{});
+ defer log.debug("cf release thread fully exiting after abnormal failure", .{});
+ self.flags.drain = true;
+ self.loop.run(.until_done) catch |err| {
+ log.err("failed to start xev loop for draining err={}", .{err});
+ };
+ }
+}
+
+fn threadMain_(self: *Thread) !void {
+ defer log.debug("cf release thread exited", .{});
+
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"cf_release".*);
+ }
+
+ // Start the async handlers. We start these first so that they're
+ // registered even if anything below fails so we can drain the mailbox.
+ self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
+ self.stop.wait(&self.loop, &self.stop_c, Thread, self, stopCallback);
+
+ // Run
+ log.debug("starting cf release thread", .{});
+ defer log.debug("starting cf release thread shutdown", .{});
+ try self.loop.run(.until_done);
+}
+
+/// Drain the mailbox, handling all the messages in our terminal implementation.
+fn drainMailbox(self: *Thread) !void {
+ // If we're draining, we just drain the mailbox and return.
+ if (self.flags.drain) {
+ while (self.mailbox.pop(global.io())) |_| {}
+ return;
+ }
+
+ while (self.mailbox.pop(global.io())) |message| {
+ // log.debug("mailbox message={}", .{message});
+ switch (message) {
+ .release => |msg| {
+ for (msg.refs) |ref| macos.foundation.CFRelease(ref);
+ // log.debug("Released {} CFTypeRefs.", .{ msg.refs.len });
+ msg.alloc.free(msg.refs);
+ },
+ }
+ }
+}
+
+fn wakeupCallback(
+ self_: ?*Thread,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Async.WaitError!void,
+) xev.CallbackAction {
+ _ = r catch |err| {
+ log.err("error in wakeup err={}", .{err});
+ return .rearm;
+ };
+
+ const t = self_.?;
+
+ // When we wake up, we check the mailbox. Mailbox producers should
+ // wake up our thread after publishing.
+ t.drainMailbox() catch |err|
+ log.err("error draining mailbox err={}", .{err});
+
+ return .rearm;
+}
+
+fn stopCallback(
+ self_: ?*Thread,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Async.WaitError!void,
+) xev.CallbackAction {
+ _ = r catch unreachable;
+ self_.?.loop.stop();
+ return .disarm;
+}
diff --git a/vendor/libghostty-vt/src/os/cgroup.zig b/vendor/libghostty-vt/src/os/cgroup.zig
new file mode 100644
index 0000000..f5eacc6
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/cgroup.zig
@@ -0,0 +1,27 @@
+const std = @import("std");
+const global = @import("../global.zig");
+
+const log = std.log.scoped(.@"linux-cgroup");
+
+/// Returns the path to the cgroup for the given pid.
+pub fn current(buf: []u8, pid: u32) ?[]const u8 {
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+
+ // Read our cgroup by opening /proc/<pid>/cgroup and reading the first
+ // line. The first line will look something like this:
+ // 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.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(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.trimEnd(u8, contents[idx + 1 ..], " \r\n");
+}
diff --git a/vendor/libghostty-vt/src/os/dbus.zig b/vendor/libghostty-vt/src/os/dbus.zig
new file mode 100644
index 0000000..99824db
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/dbus.zig
@@ -0,0 +1,21 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+/// Returns true if the program was launched by D-Bus activation.
+///
+/// On Linux GTK, this returns true if the program was launched using D-Bus
+/// activation. It will return false if Ghostty was launched any other way.
+///
+/// For other platforms and app runtimes, this returns false.
+pub fn launchedByDbusActivation() bool {
+ return switch (builtin.os.tag) {
+ // On Linux, D-Bus activation sets `DBUS_STARTER_ADDRESS` and
+ // `DBUS_STARTER_BUS_TYPE`. If these environment variables are present
+ // (no matter the value) we were launched by D-Bus activation.
+ .linux => std.posix.getenv("DBUS_STARTER_ADDRESS") != null and
+ std.posix.getenv("DBUS_STARTER_BUS_TYPE") != null,
+
+ // No other system supports D-Bus so always return false.
+ else => false,
+ };
+}
diff --git a/vendor/libghostty-vt/src/os/desktop.zig b/vendor/libghostty-vt/src/os/desktop.zig
new file mode 100644
index 0000000..60ec51a
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/desktop.zig
@@ -0,0 +1,169 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const build_config = @import("../build_config.zig");
+const global = @import("../global.zig");
+
+const c = @cImport({
+ @cInclude("unistd.h");
+});
+
+/// Returns true if the program was launched from a desktop environment.
+///
+/// On macOS, this returns true if the program was launched from Finder.
+///
+/// On Linux GTK, this returns true if the program was launched using the
+/// desktop file. This also includes when `gtk-launch` is used because I
+/// can't find a way to distinguish the two scenarios.
+///
+/// For other platforms and app runtimes, this returns false.
+pub fn launchedFromDesktop() bool {
+ return switch (builtin.os.tag) {
+ // macOS apps launched from finder or `open` always have the init
+ // process as their parent.
+ .macos => macos: {
+ // This special case is so that if we launch the app via the
+ // app bundle (i.e. via open) then we still treat it as if it
+ // was launched from the desktop.
+ if (build_config.artifact == .lib) lib: {
+ const env = "GHOSTTY_MAC_LAUNCH_SOURCE";
+ 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
+ // "zig_run" but at the moment there's no reason.
+ if (std.mem.eql(u8, source, "app")) break :macos true;
+ }
+
+ break :macos c.getppid() == 1;
+ },
+
+ // On Linux and BSD, GTK sets GIO_LAUNCHED_DESKTOP_FILE and
+ // GIO_LAUNCHED_DESKTOP_FILE_PID. We only check the latter to see if
+ // we match the PID and assume that if we do, we were launched from
+ // the desktop file. Pid comparing catches the scenario where
+ // another terminal was launched from a desktop file and then launches
+ // Ghostty and Ghostty inherits the env.
+ .linux, .freebsd => ul: {
+ const gio_pid_str = global.environ().getPosix("GIO_LAUNCHED_DESKTOP_FILE_PID") orelse
+ break :ul false;
+
+ const pid = c.getpid();
+ const gio_pid = std.fmt.parseInt(
+ @TypeOf(pid),
+ gio_pid_str,
+ 10,
+ ) catch break :ul false;
+
+ break :ul gio_pid == pid;
+ },
+
+ // TODO: This should have some logic to detect this. Perhaps std.builtin.subsystem
+ .windows => false,
+
+ // iPhone/iPad is always launched from the "desktop"
+ .ios => true,
+
+ else => @compileError("unsupported platform"),
+ };
+}
+
+/// The list of desktop environments that we detect. New Linux desktop
+/// environments should only be added to this list if there's a specific reason
+/// to differentiate between `gnome` and `other`.
+pub const DesktopEnvironment = enum {
+ gnome,
+ macos,
+ other,
+ windows,
+};
+
+/// 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(environ_map: *const std.process.Environ.Map) DesktopEnvironment {
+ return switch (comptime builtin.os.tag) {
+ .macos => .macos,
+ .windows => .windows,
+ .linux, .freebsd => de: {
+ if (@inComptime()) @compileError("Checking for the desktop environment on Linux/BSD must be done at runtime.");
+
+ // 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 (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;
+ }
+
+ // If $XDG_SESSION_DESKTOP is not set, or doesn't match any known
+ // DE, check $XDG_CURRENT_DESKTOP. $XDG_CURRENT_DESKTOP is a
+ // 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 (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;
+ }
+
+ break :de .other;
+ },
+ else => .other,
+ };
+}
+
+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) {
+ 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 => {
+ {
+ 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 0000000..3e14bca
--- /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/file.zig b/vendor/libghostty-vt/src/os/file.zig
new file mode 100644
index 0000000..3c170b4
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/file.zig
@@ -0,0 +1,147 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const global = @import("../global.zig");
+const posix = std.posix;
+const windows = @import("windows.zig");
+
+const log = std.log.scoped(.os);
+
+pub const rlimit = if (@hasDecl(posix.system, "rlimit")) posix.rlimit else struct {};
+
+/// This maximizes the number of file descriptors we can have open. We
+/// need to do this because each window consumes at least a handful of fds.
+/// This is extracted from the Zig compiler source code.
+pub fn fixMaxFiles() ?rlimit {
+ if (!@hasDecl(posix.system, "rlimit") or
+ posix.system.rlimit == void) return null;
+
+ const old = posix.getrlimit(.NOFILE) catch {
+ log.warn("failed to query file handle limit, may limit max windows", .{});
+ return null; // Oh well; we tried.
+ };
+
+ // If we're already at the max, we're done.
+ if (old.cur >= old.max) {
+ log.debug("file handle limit already maximized value={}", .{old.cur});
+ return old;
+ }
+
+ // Do a binary search for the limit.
+ var lim = old;
+ var min: posix.rlim_t = lim.cur;
+ var max: posix.rlim_t = 1 << 20;
+ // But if there's a defined upper bound, don't search, just set it.
+ if (lim.max != posix.RLIM.INFINITY) {
+ min = lim.max;
+ max = lim.max;
+ }
+
+ while (true) {
+ lim.cur = min + @divTrunc(max - min, 2); // on freebsd rlim_t is signed
+ if (posix.setrlimit(.NOFILE, lim)) |_| {
+ min = lim.cur;
+ } else |_| {
+ max = lim.cur;
+ }
+ if (min + 1 >= max) break;
+ }
+
+ log.debug("file handle limit raised value={}", .{lim.cur});
+ return old;
+}
+
+pub fn restoreMaxFiles(lim: rlimit) void {
+ if (!@hasDecl(posix.system, "rlimit")) return;
+ posix.setrlimit(.NOFILE, lim) catch {};
+}
+
+/// Return the recommended path for temporary files. Any trailing
+/// path separator is stripped so callers can safely join with their
+/// own separator (e.g. `"{tmp}/{name}"`).
+///
+/// On Windows this calls `GetTempPathW` and allocates a UTF-8 copy
+/// (or duplicates a hard-fallback string if the syscall fails). On
+/// 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, 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;
+ const len = windows.exp.kernel32.GetTempPathW(buf.len, &buf);
+ if (len > 0) {
+ // Trim the UTF-16 string before encoding as UT8-8 so that the
+ // returned slice's length matches its underlying allocation.
+ const trimmed = std.mem.trimEnd(u16, buf[0..len], &.{std.fs.path.sep});
+ if (std.unicode.utf16LeToUtf8Alloc(allocator, trimmed)) |utf8| {
+ return utf8;
+ } else |e| switch (e) {
+ error.OutOfMemory => return error.OutOfMemory,
+ else => log.warn("failed to convert temp dir path from windows string: {}", .{e}),
+ }
+ }
+ return allocator.dupe(u8, "C:\\Windows\\Temp");
+ }
+ const tmpdir = environ.getPosix("TMPDIR") orelse environ.getPosix("TMP") orelse return "/tmp";
+ return std.mem.trimEnd(u8, tmpdir, &.{std.fs.path.sep});
+}
+
+/// Free a path returned by `allocTmpDir` if it allocated memory.
+/// This is a no-op on POSIX.
+pub fn freeTmpDir(allocator: std.mem.Allocator, dir: []const u8) void {
+ if (builtin.os.tag != .windows) return;
+ allocator.free(dir);
+}
+
+const random_basename_bytes = 16;
+const b64_encoder = std.base64.url_safe_no_pad.Encoder;
+
+pub const RandomBasenameError = error{BufferTooSmall};
+
+/// Length of the basename produced by `randomBasename`.
+pub const random_basename_len = b64_encoder.calcSize(random_basename_bytes);
+
+/// Write a random filesystem-safe base64 basename of length
+/// `random_basename_len` into `buf` and return a slice over the
+/// written bytes. Returns `error.BufferTooSmall` if `buf` is too
+/// short.
+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;
+ global.io().random(&rand_buf);
+ return b64_encoder.encode(buf[0..random_basename_len], &rand_buf);
+}
+
+/// Return a freshly-allocated path of the form `{TMPDIR}/{prefix}{random}`.
+/// The caller owns the returned slice and must free it with `allocator`.
+///
+/// Nothing is created on disk; this only builds the path string. Useful
+/// for one-shot temporary file/socket paths where a full `TempDir` is
+/// overkill.
+pub fn randomTmpPath(
+ allocator: std.mem.Allocator,
+ prefix: []const u8,
+) std.mem.Allocator.Error![]u8 {
+ var name_buf: [random_basename_len]u8 = undefined;
+ const basename = randomBasename(&name_buf) catch unreachable;
+ return std.fmt.allocPrint(
+ allocator,
+ "{s}{c}{s}{s}",
+ .{ global.tmpDirPath(), std.fs.path.sep, prefix, basename },
+ );
+}
+
+test randomBasename {
+ const testing = std.testing;
+
+ var buf: [random_basename_len]u8 = undefined;
+ const name = try randomBasename(&buf);
+ try testing.expectEqual(random_basename_len, name.len);
+ for (name) |c| {
+ const ok = std.ascii.isAlphanumeric(c) or c == '-' or c == '_';
+ try testing.expect(ok);
+ }
+
+ var small: [random_basename_len - 1]u8 = undefined;
+ try testing.expectError(error.BufferTooSmall, randomBasename(&small));
+}
diff --git a/vendor/libghostty-vt/src/os/flatpak.zig b/vendor/libghostty-vt/src/os/flatpak.zig
new file mode 100644
index 0000000..15c619d
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/flatpak.zig
@@ -0,0 +1,521 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const builtin = @import("builtin");
+const posix = std.posix;
+const global = @import("../global.zig");
+const xev = global.xev;
+
+const log = std.log.scoped(.flatpak);
+
+/// Returns true if we're running in a Flatpak environment.
+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.Io.Dir.accessAbsolute(global.io(), "/.flatpak-info", .{}))
+ true
+ else |_|
+ false;
+}
+
+/// A struct to help execute commands on the host via the
+/// org.freedesktop.Flatpak.Development DBus module. This uses GIO/GLib
+/// under the hood.
+///
+/// This always spawns its own thread and maintains its own GLib event loop.
+/// This makes it easy for the command to behave synchronously similar to
+/// std.process.Child.
+///
+/// There are lots of chances for low-hanging improvements here (automatic
+/// pipes, /dev/null, etc.) but this was purpose built for my needs so
+/// it doesn't have all of those.
+///
+/// Requires GIO, GLib to be available and linked.
+pub const FlatpakHostCommand = struct {
+ const fd_t = posix.fd_t;
+ 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
+ const Flags = packed struct(c_uint) {
+ /// Clear the environment
+ clear_env: bool = false,
+ /// Kill the sandbox when the caller disappears from the session bus
+ watch_bus: bool = false,
+ _reserved: std.meta.Int(.unsigned, @bitSizeOf(c_uint) - 2) = 0,
+ };
+
+ /// Argv are the arguments to call on the host with argv[0] being
+ /// the command to execute.
+ argv: []const []const u8,
+
+ /// The cwd for the new process. If this is not set then it will use
+ /// the current cwd of the calling process.
+ cwd: ?[:0]const u8 = null,
+
+ /// Environment variables for the child process. If this is null, this
+ /// does not send any environment variables.
+ env: ?*const EnvMap = null,
+
+ /// File descriptors to send to the child process. It is up to the
+ /// caller to create the file descriptors and set them up.
+ stdin: fd_t,
+ stdout: fd_t,
+ stderr: fd_t,
+
+ /// 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.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.
+ const State = union(enum) {
+ /// Initial state
+ init: void,
+
+ /// Error starting. The error message is only available via logs.
+ /// (This isn't a fundamental limitation, just didn't need the
+ /// error message yet)
+ err: void,
+
+ /// Process started with the given pid on the host.
+ started: struct {
+ pid: u32,
+ loop_xev: ?*xev.Loop,
+ completion: ?*Completion,
+ subscription: gio_c.guint,
+ loop: *gio_c.GMainLoop,
+ },
+
+ /// Process exited
+ exited: struct {
+ pid: u32,
+ status: u8,
+ },
+ };
+
+ pub const Completion = struct {
+ callback: *const fn (ud: ?*anyopaque, l: *xev.Loop, c: *Completion, r: WaitError!u8) void = noopCallback,
+ c_xev: xev.Completion = .{},
+ userdata: ?*anyopaque = null,
+ timer: ?xev.Timer = null,
+ result: ?WaitError!u8 = null,
+ };
+
+ /// Errors that are possible from us.
+ pub const Error = error{
+ FlatpakMustBeStarted,
+ FlatpakSpawnFail,
+ FlatpakSetupFail,
+ FlatpakRPCFail,
+ };
+
+ pub const WaitError = xev.Timer.RunError || Error;
+
+ /// Spawn the command. This will start the host command. On return,
+ /// the pid will be available. This must only be called with the
+ /// state in "init".
+ ///
+ /// 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(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.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,
+ .err => Error.FlatpakSpawnFail,
+ .started => |v| v.pid,
+ .exited => |v| v.pid,
+ };
+ }
+
+ /// 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.lockUncancelable(global.io());
+ defer self.state_mutex.unlock(global.io());
+
+ while (true) {
+ switch (self.state) {
+ .init => return Error.FlatpakMustBeStarted,
+ .err => return Error.FlatpakSpawnFail,
+ .started => {},
+ .exited => |v| {
+ self.state = .{ .init = {} };
+ self.state_cv.broadcast(global.io());
+ return v.status;
+ },
+ }
+
+ self.state_cv.waitUncancelable(global.io(), &self.state_mutex);
+ }
+ }
+
+ /// Wait for the process to end asynchronously via libxev. This
+ /// can only be called ONCE.
+ pub fn waitXev(
+ self: *FlatpakHostCommand,
+ loop: *xev.Loop,
+ completion: *Completion,
+ comptime Userdata: type,
+ userdata: ?*Userdata,
+ comptime cb: *const fn (
+ ud: ?*Userdata,
+ l: *xev.Loop,
+ c: *Completion,
+ r: WaitError!u8,
+ ) void,
+ ) void {
+ self.state_mutex.lockUncancelable(global.io());
+ defer self.state_mutex.unlock(global.io());
+
+ completion.* = .{
+ .callback = (struct {
+ fn callback(
+ ud_: ?*anyopaque,
+ l_inner: *xev.Loop,
+ c_inner: *Completion,
+ r: WaitError!u8,
+ ) void {
+ const ud = @as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud_)));
+ @call(.always_inline, cb, .{ ud, l_inner, c_inner, r });
+ }
+ }).callback,
+ .userdata = userdata,
+ .timer = xev.Timer.init() catch unreachable, // not great, but xev timer can't fail atm
+ };
+
+ switch (self.state) {
+ .init => completion.result = Error.FlatpakMustBeStarted,
+ .err => completion.result = Error.FlatpakSpawnFail,
+ .started => |*v| {
+ v.loop_xev = loop;
+ v.completion = completion;
+ return;
+ },
+ .exited => |v| {
+ completion.result = v.status;
+ },
+ }
+
+ completion.timer.?.run(
+ loop,
+ &completion.c_xev,
+ 0,
+ anyopaque,
+ completion.userdata,
+ (struct {
+ fn callback(
+ ud: ?*anyopaque,
+ l_inner: *xev.Loop,
+ c_inner: *xev.Completion,
+ r: xev.Timer.RunError!void,
+ ) xev.CallbackAction {
+ const c_outer: *Completion = @fieldParentPtr("c_xev", c_inner);
+ defer if (c_outer.timer) |*t| t.deinit();
+
+ const result = if (r) |_| c_outer.result.? else |err| err;
+ c_outer.callback(ud, l_inner, c_outer, result);
+ return .disarm;
+ }
+ }).callback,
+ );
+ }
+
+ /// Send a signal to the started command. This does nothing if the
+ /// command is not in the started state.
+ pub fn signal(self: *FlatpakHostCommand, sig: u8, pg: bool) !void {
+ const pid = pid: {
+ self.state_mutex.lockUncancelable(global.io());
+ defer self.state_mutex.unlock(global.io());
+ switch (self.state) {
+ .started => |v| break :pid v.pid,
+ else => return,
+ }
+ };
+
+ // Get our bus connection.
+ 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 gio_c.g_object_unref(bus);
+
+ const reply = gio_c.g_dbus_connection_call_sync(
+ bus,
+ "org.freedesktop.Flatpak",
+ "/org/freedesktop/Flatpak/Development",
+ "org.freedesktop.Flatpak.Development",
+ "HostCommandSignal",
+ gio_c.g_variant_new(
+ "(uub)",
+ pid,
+ sig,
+ @as(c_int, @intCast(@intFromBool(pg))),
+ ),
+ gio_c.G_VARIANT_TYPE("()"),
+ gio_c.G_DBUS_CALL_FLAGS_NONE,
+ gio_c.G_MAXINT,
+ null,
+ &g_err,
+ );
+ if (g_err != null) {
+ log.warn("signal send error: {s}", .{g_err.?.*.message});
+ return;
+ }
+ 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 = 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 = 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: ?*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 gio_c.g_object_unref(bus);
+
+ // Spawn the command first. This will setup all our IO.
+ self.start(alloc, bus, loop) catch |err| {
+ log.warn("error starting host command: {}", .{err});
+ self.updateState(.{ .err = {} });
+ return;
+ };
+
+ // Run the event loop. It quits in the exit callback.
+ gio_c.g_main_loop_run(loop);
+ }
+
+ /// Start the command. This will start the host command and set the
+ /// pid field on success. This will not wait for completion.
+ ///
+ /// Once this is called, the self pointer MUST remain stable. This
+ /// requirement is due to using GLib under the covers with callbacks.
+ fn start(
+ self: *FlatpakHostCommand,
+ alloc: Allocator,
+ bus: *gio_c.GDBusConnection,
+ loop: *gio_c.GMainLoop,
+ ) !void {
+ 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 = 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 (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 (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 = 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 = 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.*);
+ gio_c.g_variant_builder_add(env_builder, "{ss}", key.ptr, value.ptr);
+ }
+ }
+
+ // Build our args
+ const args = try arena.alloc(?[*:0]u8, self.argv.len + 1);
+ for (0.., self.argv) |i, arg| {
+ const argZ = try arena.dupeZ(u8, arg);
+ args[i] = argZ.ptr;
+ }
+ args[args.len - 1] = null;
+
+ // 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 = 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 = 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,
+ gio_c.g_variant_builder_end(fd_builder),
+ gio_c.g_variant_builder_end(env_builder),
+ @as(c_uint, @bitCast(flags)),
+ );
+ _ = gio_c.g_variant_ref_sink(params); // take ownership
+ defer gio_c.g_variant_unref(params);
+
+ // Subscribe to exit notifications
+ const subscription_id = gio_c.g_dbus_connection_signal_subscribe(
+ bus,
+ "org.freedesktop.Flatpak",
+ "org.freedesktop.Flatpak.Development",
+ "HostCommandExited",
+ "/org/freedesktop/Flatpak/Development",
+ null,
+ 0,
+ onExit,
+ self,
+ null,
+ );
+ errdefer gio_c.g_dbus_connection_signal_unsubscribe(bus, subscription_id);
+
+ // Go!
+ 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,
+ gio_c.G_VARIANT_TYPE("(u)"),
+ gio_c.G_DBUS_CALL_FLAGS_NONE,
+ gio_c.G_MAXINT,
+ fd_list,
+ null,
+ null,
+ &err,
+ ) orelse {
+ log.warn("Flatpak.HostCommand failed: {s}", .{err.?.*.message});
+ return Error.FlatpakRPCFail;
+ };
+ defer gio_c.g_variant_unref(reply);
+
+ var pid: u32 = 0;
+ gio_c.g_variant_get(reply, "(u)", &pid);
+ log.debug("HostCommand started pid={} subscription={}", .{
+ pid,
+ subscription_id,
+ });
+
+ self.updateState(.{
+ .started = .{
+ .pid = pid,
+ .subscription = subscription_id,
+ .loop = loop,
+ .completion = null,
+ .loop_xev = null,
+ },
+ });
+ }
+
+ /// Helper to update the state and notify waiters via the cv.
+ fn updateState(self: *FlatpakHostCommand, state: State) void {
+ 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: ?*gio_c.GDBusConnection,
+ _: [*c]const u8,
+ _: [*c]const u8,
+ _: [*c]const u8,
+ _: [*c]const u8,
+ params: ?*gio_c.GVariant,
+ ud: ?*anyopaque,
+ ) callconv(.c) void {
+ const self = @as(*FlatpakHostCommand, @ptrCast(@alignCast(ud)));
+ const state = state: {
+ 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;
+ 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);
+ // Update our state
+ self.updateState(.{
+ .exited = .{
+ .pid = pid,
+ .status = exit_status,
+ },
+ });
+ if (state.completion) |completion| {
+ completion.result = exit_status;
+ completion.timer.?.run(
+ state.loop_xev.?,
+ &completion.c_xev,
+ 0,
+ anyopaque,
+ completion.userdata,
+ (struct {
+ fn callback(
+ ud_inner: ?*anyopaque,
+ l_inner: *xev.Loop,
+ c_inner: *xev.Completion,
+ r: xev.Timer.RunError!void,
+ ) xev.CallbackAction {
+ const c_outer: *Completion = @fieldParentPtr("c_xev", c_inner);
+ defer if (c_outer.timer) |*t| t.deinit();
+
+ const result = if (r) |_| c_outer.result.? else |err| err;
+ c_outer.callback(ud_inner, l_inner, c_outer, result);
+ return .disarm;
+ }
+ }).callback,
+ );
+ }
+ log.debug("HostCommand exited pid={} status={}", .{ pid, exit_status });
+
+ // We're done now, so we can unsubscribe
+ gio_c.g_dbus_connection_signal_unsubscribe(bus.?, state.subscription);
+
+ // We are also done with our loop so we can exit.
+ 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
new file mode 100644
index 0000000..8da9640
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/homedir.zig
@@ -0,0 +1,185 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const passwd = @import("passwd.zig");
+const objc = @import("objc");
+
+const Error = error{
+ /// The buffer used for output is not large enough to store the value.
+ BufferTooSmall,
+};
+
+/// 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(io: std.Io, environ_map: *const std.process.Environ.Map, buf: []u8) !?[]const u8 {
+ return switch (builtin.os.tag) {
+ .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,
+
+ else => @compileError("unimplemented"),
+ };
+}
+
+fn homeUnix(io: std.Io, environ_map: *const std.process.Environ.Map, buf: []u8) !?[]const u8 {
+ // First: if we have a HOME env var, then we use that.
+ if (environ_map.get("HOME")) |result| {
+ if (buf.len < result.len) return Error.BufferTooSmall;
+ @memcpy(buf[0..result.len], result);
+ return buf[0..result.len];
+ }
+
+ // On macOS: [NSFileManager defaultManager].homeDirectoryForCurrentUser.path
+ if (builtin.os.tag == .macos) {
+ const NSFileManager = objc.getClass("NSFileManager").?;
+ const manager = NSFileManager.msgSend(objc.Object, objc.sel("defaultManager"), .{});
+ const homeURL = manager.getProperty(objc.Object, "homeDirectoryForCurrentUser");
+ const homePath = homeURL.getProperty(objc.Object, "path");
+
+ const c_str = homePath.getProperty([*:0]const u8, "UTF8String");
+ const result = std.mem.sliceTo(c_str, 0);
+
+ if (buf.len < result.len) return Error.BufferTooSmall;
+ @memcpy(buf[0..result.len], result);
+ return buf[0..result.len];
+ }
+
+ // Everything below here will require some allocation
+ var tempBuf: [1024]u8 = undefined;
+ var fba = std.heap.FixedBufferAllocator.init(&tempBuf);
+
+ // We try passwd. This doesn't work on multi-user mac but we try it anyways.
+ const pw = try passwd.get(fba.allocator());
+ if (pw.home) |result| {
+ if (buf.len < result.len) return Error.BufferTooSmall;
+ @memcpy(buf[0..result.len], result);
+ return buf[0..result.len];
+ }
+
+ // If all else fails, have the shell tell us...
+ fba.reset();
+ const run = try std.process.run(fba.allocator(), io, .{
+ .argv = &[_][]const u8{ "/bin/sh", "-c", "cd && pwd" },
+ .environ_map = environ_map,
+ .stdout_limit = .limited(fba.buffer.len / 2),
+ .stderr_limit = .limited(fba.buffer.len / 2),
+ });
+
+ 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);
+ return buf[0..result.len];
+ }
+
+ return null;
+}
+
+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 {
+ return std.mem.trim(u8, input, " \n\t");
+}
+
+pub const ExpandError = error{
+ HomeDetectionFailed,
+ BufferTooSmall,
+};
+
+/// Expands a path that starts with a tilde (~) to the home directory of
+/// the current user.
+///
+/// Errors if `home` fails or if the size of the expanded path is larger
+/// than `buf.len`.
+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(io, environ_map, path, buf),
+
+ // `~/` is not an idiom generally used on Windows
+ .windows => return path,
+
+ // iOS doesn't have a user-writable home directory
+ .ios => return path,
+
+ else => @compileError("unimplemented"),
+ };
+}
+
+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(io, environ_map, buf)) |home_|
+ home_ orelse return error.HomeDetectionFailed
+ else |_|
+ return error.HomeDetectionFailed;
+ const rest = path[1..]; // Skip the ~
+ const expanded_len = home_dir.len + rest.len;
+
+ if (expanded_len > buf.len) return Error.BufferTooSmall;
+ @memcpy(buf[home_dir.len..expanded_len], rest);
+
+ return buf[0..expanded_len];
+}
+
+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(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(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(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..],
+ ));
+}
+
+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(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
new file mode 100644
index 0000000..969b4ba
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/hostname.zig
@@ -0,0 +1,63 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const posix = std.posix;
+
+pub const LocalHostnameValidationError = error{
+ PermissionDenied,
+ Unexpected,
+};
+
+/// 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`).
+pub fn isLocal(hostname: []const u8) LocalHostnameValidationError!bool {
+ // A 'localhost' hostname is always considered local.
+ if (std.mem.eql(u8, "localhost", hostname)) return true;
+
+ // If hostname is not "localhost" it must match our hostname.
+ switch (builtin.os.tag) {
+ .windows => {
+ const windows = @import("windows.zig");
+ var buf: [256:0]u8 = undefined;
+ var nSize: windows.DWORD = buf.len;
+ if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == windows.FALSE) return false;
+ const ourHostname = buf[0..nSize];
+ return std.mem.eql(u8, hostname, ourHostname);
+ },
+ else => {
+ var buf: [posix.HOST_NAME_MAX]u8 = undefined;
+ const ourHostname = try posix.gethostname(&buf);
+ return std.mem.eql(u8, hostname, ourHostname);
+ },
+ }
+}
+
+test "isLocal returns true when provided hostname is localhost" {
+ try std.testing.expect(try isLocal("localhost"));
+}
+
+test "isLocal returns true when hostname is local" {
+ switch (builtin.os.tag) {
+ .windows => {
+ const windows = @import("windows.zig");
+ var buf: [256:0]u8 = undefined;
+ var nSize: windows.DWORD = buf.len;
+ if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == windows.FALSE)
+ return error.GetComputerNameFailed;
+ const localHostname = buf[0..nSize];
+ try std.testing.expect(try isLocal(localHostname));
+ },
+ else => {
+ var buf: [posix.HOST_NAME_MAX]u8 = undefined;
+ const localHostname = try posix.gethostname(&buf);
+ try std.testing.expect(try isLocal(localHostname));
+ },
+ }
+}
+
+test "isLocal returns false when hostname is not local" {
+ try std.testing.expectEqual(
+ false,
+ try isLocal("not-the-local-hostname"),
+ );
+}
diff --git a/vendor/libghostty-vt/src/os/i18n.zig b/vendor/libghostty-vt/src/os/i18n.zig
new file mode 100644
index 0000000..20d3c1b
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/i18n.zig
@@ -0,0 +1,210 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const build_config = @import("../build_config.zig");
+const locales = @import("i18n_locales.zig");
+
+const log = std.log.scoped(.i18n);
+
+/// Set for faster membership lookup of locales.
+pub const locales_map = map: {
+ var kvs: [locales.len]struct { []const u8 } = undefined;
+ for (locales, 0..) |locale, i| kvs[i] = .{locale};
+ break :map std.StaticStringMap(void).initComptime(kvs);
+};
+
+pub const InitError = error{
+ InvalidResourcesDir,
+ OutOfMemory,
+};
+
+/// Initialize i18n support for the application. This should be
+/// called automatically by the global state initialization
+/// in global.zig.
+///
+/// This calls `bindtextdomain` for gettext with the proper directory
+/// of translations. This does NOT call `textdomain` as we don't
+/// want to set the domain for the entire application since this is also
+/// used by libghostty.
+pub fn init(resources_dir: []const u8) InitError!void {
+ if (comptime !build_config.i18n) return;
+
+ switch (builtin.os.tag) {
+ // i18n is unsupported on Windows
+ .windows => return,
+
+ else => {
+ // Our resources dir is always nested below the share dir that
+ // is standard for translations.
+ const share_dir = std.fs.path.dirname(resources_dir) orelse
+ return error.InvalidResourcesDir;
+
+ // Build our locale path
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = std.fmt.bufPrintZ(&buf, "{s}/locale", .{share_dir}) catch
+ return error.OutOfMemory;
+
+ // Bind our bundle ID to the given locale path
+ log.debug("binding domain={s} path={s}", .{ build_config.bundle_id, path });
+ _ = bindtextdomain(build_config.bundle_id, path.ptr) orelse
+ return error.OutOfMemory;
+ },
+ }
+}
+
+/// Set the global gettext domain to our bundle ID, allowing unqualified
+/// `gettext` (`_`) calls to look up translations for our application.
+///
+/// This should only be called for apprts that are fully owning the
+/// Ghostty application. This should not be called for libghostty users.
+pub fn initGlobalDomain() error{OutOfMemory}!void {
+ if (comptime !build_config.i18n) return;
+ _ = textdomain(build_config.bundle_id) orelse return error.OutOfMemory;
+}
+
+/// 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.
+///
+/// The gnulib-lib function modifies the buffer in place but has
+/// zero bounds checking, so we do a bit extra to ensure we don't
+/// overflow the buffer. This is likely slightly more expensive but
+/// this isn't a hot path so it should be fine.
+///
+/// The buffer must be at least 16 bytes long. This ensures we can
+/// fit the longest possible hardcoded locale name. Additionally,
+/// it should be at least as long as locale in case the locale
+/// is unchanged.
+///
+/// Here is the logic for macOS, but other platforms also have
+/// their own canonicalization logic:
+///
+/// https://github.com/coreutils/gnulib/blob/5b92dd0a45c8d27f13a21076b57095ea5e220870/lib/localename.c#L1171
+pub fn canonicalizeLocale(
+ buf: []u8,
+ locale: []const u8,
+) error{NoSpaceLeft}![:0]const u8 {
+ if (comptime !build_config.i18n) {
+ if (buf.len < locale.len + 1) return error.NoSpaceLeft;
+ @memcpy(buf[0..locale.len], locale);
+ buf[locale.len] = 0;
+ return buf[0..locale.len :0];
+ }
+
+ // Fix zh locales for macOS
+ if (fixZhLocale(locale)) |fixed| {
+ if (buf.len < fixed.len + 1) return error.NoSpaceLeft;
+ @memcpy(buf[0..fixed.len], fixed);
+ buf[fixed.len] = 0;
+ return buf[0..fixed.len :0];
+ }
+
+ // Buffer must be 16 or at least as long as the locale and null term
+ if (buf.len < @max(16, locale.len + 1)) return error.NoSpaceLeft;
+
+ // Copy our locale into the buffer since it modifies in place.
+ // This must be null-terminated.
+ @memcpy(buf[0..locale.len], locale);
+ buf[locale.len] = 0;
+
+ _libintl_locale_name_canonicalize(buf[0..locale.len :0]);
+
+ // Convert the null-terminated result buffer into a slice. We
+ // need to search for the null terminator and slice it back.
+ // We have to use `buf` since `slice` len will exclude the
+ // null.
+ const slice = std.mem.sliceTo(buf, 0);
+ return buf[0..slice.len :0];
+}
+
+/// Handles some zh locales canonicalization because internal libintl
+/// canonicalization function doesn't handle correctly in these cases.
+fn fixZhLocale(locale: []const u8) ?[:0]const u8 {
+ var it = std.mem.splitScalar(u8, locale, '-');
+ const name = it.next() orelse return null;
+ if (!std.mem.eql(u8, name, "zh")) return null;
+
+ const script = it.next() orelse return null;
+ const region = it.next() orelse return null;
+
+ if (std.mem.eql(u8, script, "Hans")) {
+ if (std.mem.eql(u8, region, "SG")) return "zh_SG";
+ return "zh_CN";
+ }
+
+ if (std.mem.eql(u8, script, "Hant")) {
+ if (std.mem.eql(u8, region, "MO")) return "zh_MO";
+ if (std.mem.eql(u8, region, "HK")) return "zh_HK";
+ return "zh_TW";
+ }
+
+ return null;
+}
+
+/// This can be called at any point a compile-time-known locale is
+/// available. This will use comptime to verify the locale is supported.
+pub fn staticLocale(comptime v: [*:0]const u8) [*:0]const u8 {
+ comptime {
+ for (locales) |locale| {
+ if (std.mem.eql(u8, locale, v)) {
+ return locale;
+ }
+ }
+
+ @compileError("unsupported locale");
+ }
+}
+
+// Manually include function definitions for the gettext functions
+// as libintl.h isn't always easily available (e.g. in musl)
+extern fn bindtextdomain(domainname: [*:0]const u8, dirname: [*:0]const u8) ?[*:0]const u8;
+extern fn textdomain(domainname: [*:0]const u8) ?[*:0]const u8;
+extern fn dgettext(domainname: [*:0]const u8, msgid: [*:0]const u8) [*:0]const u8;
+
+// This is only available if we're building libintl from source
+// since its otherwise not exported. We only need it on macOS
+// currently but probably will on Windows as well.
+extern fn _libintl_locale_name_canonicalize(name: [*:0]u8) void;
+
+test "canonicalizeLocale darwin" {
+ if (!builtin.target.os.tag.isDarwin()) return error.SkipZigTest;
+
+ const testing = std.testing;
+ var buf: [256]u8 = undefined;
+ try testing.expectEqualStrings("en_US", try canonicalizeLocale(&buf, "en_US"));
+ try testing.expectEqualStrings("zh_CN", try canonicalizeLocale(&buf, "zh-Hans"));
+ try testing.expectEqualStrings("zh_TW", try canonicalizeLocale(&buf, "zh-Hant"));
+
+ try testing.expectEqualStrings("zh_CN", try canonicalizeLocale(&buf, "zh-Hans-CN"));
+ try testing.expectEqualStrings("zh_SG", try canonicalizeLocale(&buf, "zh-Hans-SG"));
+ try testing.expectEqualStrings("zh_TW", try canonicalizeLocale(&buf, "zh-Hant-TW"));
+ try testing.expectEqualStrings("zh_HK", try canonicalizeLocale(&buf, "zh-Hant-HK"));
+ try testing.expectEqualStrings("zh_MO", try canonicalizeLocale(&buf, "zh-Hant-MO"));
+
+ // This is just an edge case I want to make sure we're aware of:
+ // canonicalizeLocale does not handle encodings and will turn them into
+ // 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
new file mode 100644
index 0000000..54fa2c9
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/i18n_locales.zig
@@ -0,0 +1,63 @@
+// NOTE: This is in a separate file because our build depends on it and
+// we want to minimize the transitive dependencies of the build binary
+// itself.
+
+/// Supported locales for the application. This must be kept up to date
+/// with the translations available in the `po/` directory; this is used
+/// by our build process as well as runtime libghostty APIs.
+///
+/// The order also matters. For incomplete locale information (i.e. only
+/// a language code available), the first match is used. For example, if
+/// we know the user requested `zh` but has no script code, then we'd pick
+/// the first locale that matches `zh`.
+///
+/// For ordering, we prefer:
+///
+/// 1. The most common locales first, since there are places in the code
+/// where we do linear searches for a locale and we want to minimize
+/// the number of iterations for the common case.
+///
+/// 2. Alphabetical for otherwise equally common locales.
+///
+/// 3. Most preferred locale for a language without a country code.
+///
+/// Note for "most common" locales, this is subjective and based on
+/// the perceived userbase of Ghostty, which may not be representative
+/// of general populations or global language distribution. Also note
+/// that ordering may be weird when we first merge a new locale since
+/// we don't have a good way to determine this. We can always reorder
+/// with some data.
+pub const locales = [_][:0]const u8{
+ "zh_CN",
+ "de",
+ "fr",
+ "ja",
+ "nl",
+ "nb",
+ "ru",
+ "uk",
+ "pl",
+ "ko_KR",
+ "mk",
+ "tr",
+ "id",
+ "es_BO",
+ "es_AR",
+ "es_ES",
+ "pt_BR",
+ "ca",
+ "it",
+ "bg",
+ "ga",
+ "hu",
+ "he",
+ "zh_TW",
+ "hr",
+ "lt",
+ "lv",
+ "vi",
+ "kk",
+ "be",
+ "eu",
+ "da",
+};
diff --git a/vendor/libghostty-vt/src/os/kernel_info.zig b/vendor/libghostty-vt/src/os/kernel_info.zig
new file mode 100644
index 0000000..60e6b49
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/kernel_info.zig
@@ -0,0 +1,32 @@
+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.Io.Dir.openFileAbsolute(global.io(), path, .{}) catch return null;
+ defer file.close(global.io());
+
+ // 128 bytes should be enough to hold the kernel information
+ 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;
+}
+
+test "read /proc/sys/kernel/osrelease" {
+ if (comptime builtin.os.tag != .linux) return null;
+ const allocator = std.testing.allocator;
+
+ const kernel_info = getKernelInfo(allocator).?;
+ defer allocator.free(kernel_info);
+
+ // Since we can't hardcode the info in tests, just check
+ // if something was read from the file
+ try std.testing.expect(kernel_info.len > 0);
+ try std.testing.expect(!std.mem.eql(u8, kernel_info, ""));
+}
diff --git a/vendor/libghostty-vt/src/os/locale.c b/vendor/libghostty-vt/src/os/locale.c
new file mode 100644
index 0000000..a889a57
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/locale.c
@@ -0,0 +1 @@
+#include <locale.h>
diff --git a/vendor/libghostty-vt/src/os/locale.zig b/vendor/libghostty-vt/src/os/locale.zig
new file mode 100644
index 0000000..ceb3c88
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/locale.zig
@@ -0,0 +1,232 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = @import("../quirks.zig").inlineAssert;
+const macos = @import("macos");
+const objc = @import("objc");
+const internal_os = @import("main.zig");
+const i18n = internal_os.i18n;
+
+const log = std.log.scoped(.os_locale);
+
+/// Ensure that the locale is set.
+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
+ // OS. When launching from the CLI, LANG is usually set by the parent
+ // process.
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ // Set the lang if it is not set or if its empty.
+ const lang = std.posix.system.getenv("LANG");
+ if (lang == null or lang.?[0] == 0) {
+ setLangFromCocoa();
+ }
+ }
+
+ // Set the locale to whatever is set in env vars.
+ if (setlocale(LC_ALL, "")) |v| {
+ log.info("setlocale from env result={s}", .{v});
+ 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 (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.
+ _ = setenv("LANG", "", 1);
+ _ = unsetenv("LANG");
+
+ if (setlocale(LC_ALL, "")) |v| {
+ log.info("setlocale after unset lang result={s}", .{v});
+
+ // If we try to setlocale to an unsupported locale it'll return "C"
+ // as the POSIX/C fallback, if that's the case we want to not use
+ // it and move to our fallback of en_US.UTF-8
+ if (!std.mem.eql(u8, std.mem.sliceTo(v, 0), "C")) return;
+ }
+ }
+ }
+
+ // 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| {
+ _ = 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", .{});
+}
+
+/// This sets the LANG environment variable based on the macOS system
+/// preferences selected locale settings.
+fn setLangFromCocoa() void {
+ const pool = objc.AutoreleasePool.init();
+ defer pool.deinit();
+
+ // The classes we're going to need.
+ const NSLocale = objc.getClass("NSLocale") orelse {
+ log.warn("NSLocale class not found. Locale may be incorrect.", .{});
+ return;
+ };
+
+ // Get our current locale and extract the language code ("en") and
+ // country code ("US")
+ const locale = NSLocale.msgSend(objc.Object, objc.sel("currentLocale"), .{});
+ const lang = locale.getProperty(objc.Object, "languageCode");
+ const country = locale.getProperty(objc.Object, "countryCode");
+
+ if (lang.value == null or country.value == null) {
+ log.warn("languageCode or countryCode not found. Locale may be incorrect.", .{});
+ return;
+ }
+
+ // Get our UTF8 string values
+ const c_lang = lang.getProperty([*:0]const u8, "UTF8String");
+ const c_country = country.getProperty([*:0]const u8, "UTF8String");
+
+ // Convert them to Zig slices
+ const z_lang = std.mem.sliceTo(c_lang, 0);
+ const z_country = std.mem.sliceTo(c_country, 0);
+
+ // Format our locale as "<lang>_<country>.UTF-8" and set it as LANG.
+ {
+ var buf: [128]u8 = undefined;
+ const env_value = std.fmt.bufPrintZ(&buf, "{s}_{s}.UTF-8", .{ z_lang, z_country }) catch |err| {
+ log.warn("error setting locale from system. err={}", .{err});
+ return;
+ };
+ log.info("detected system locale={s}", .{env_value});
+
+ // Set it onto our environment
+ if (setenv("LANG", @ptrCast(env_value), 1) < 0) {
+ log.warn("error setting locale env var", .{});
+ return;
+ }
+ }
+
+ // Get our preferred languages and set that to the LANGUAGE
+ // env var in case our language differs from our locale.
+ language: {
+ var buf: [1024]u8 = undefined;
+ const pref_ = preferredLanguageFromCocoa(
+ &buf,
+ NSLocale,
+ ) catch |err| {
+ log.warn("error getting preferred languages. err={}", .{err});
+ break :language;
+ };
+
+ const pref = pref_ orelse break :language;
+ log.debug(
+ "setting LANGUAGE from preferred languages value={s}",
+ .{pref},
+ );
+ _ = setenv("LANGUAGE", @ptrCast(pref), 1);
+ }
+}
+
+/// Sets the LANGUAGE environment variable based on the preferred languages
+/// as reported by NSLocale.
+///
+/// macOS has a concept of preferred languages separate from the system
+/// locale. The set of preferred languages is a list in priority order
+/// of what translations the user prefers. A user can have, for example,
+/// "fr_FR" as their locale but "en" as their preferred language. This would
+/// mean that they want to use French units, date formats, etc. but they
+/// prefer English translations.
+///
+/// gettext uses the LANGUAGE environment variable to override only
+/// translations and a priority order can be specified by separating
+/// the languages with colons. For example, "en:fr" would mean that
+/// English translations are preferred but if they are not available
+/// then French translations should be used.
+///
+/// To further complicate things, Apple reports the languages in BCP-47
+/// format which is not compatible with gettext's POSIX locale format so
+/// we have to canonicalize them.
+fn preferredLanguageFromCocoa(
+ buf: []u8,
+ NSLocale: objc.Class,
+) error{NoSpaceLeft}!?[:0]const u8 {
+ 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).
+ const preferred: *macos.foundation.Array = array: {
+ const ns = NSLocale.msgSend(
+ objc.Object,
+ objc.sel("preferredLanguages"),
+ .{},
+ );
+ break :array @ptrCast(ns.value);
+ };
+ for (0..preferred.getCount()) |i| {
+ var str_buf: [255:0]u8 = undefined;
+ const str = preferred.getValueAtIndex(macos.foundation.String, i);
+ const c_str = str.cstring(&str_buf, .utf8) orelse {
+ // I don't think this can happen but if it does then I want
+ // to know about it if a user has translation issues.
+ log.warn("failed to convert a preferred language to UTF-8", .{});
+ continue;
+ };
+
+ // Append our separator if we have any previous languages
+ if (writer.end > 0) {
+ _ = writer.writeByte(':') catch
+ return error.NoSpaceLeft;
+ }
+
+ // Apple languages are in BCP-47 format, and we need to
+ // canonicalize them to the POSIX format.
+ const canon = try i18n.canonicalizeLocale(
+ writer.buffer[writer.end..],
+ c_str,
+ );
+ writer.end += canon.len;
+
+ // The canonicalized locale never contains the encoding and
+ // all of our translations require UTF-8 so we add that.
+ _ = writer.writeAll(".UTF-8") catch return error.NoSpaceLeft;
+ }
+
+ // If we had no preferred languages then we return nothing.
+ 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 = writer.buffered();
+ return slice[0 .. slice.len - 1 :0];
+}
+
+const c = @import("locale-c");
+const LC_ALL: c_int = c.LC_ALL;
+const LC_ALL_MASK: c_int = c.LC_ALL_MASK;
+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
new file mode 100644
index 0000000..5f5a3ed
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/mach.zig
@@ -0,0 +1,153 @@
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const mem = std.mem;
+const Allocator = std.mem.Allocator;
+
+/// macOS virtual memory tags for use with mach_vm_map/mach_vm_allocate.
+/// These identify memory regions in tools like vmmap and Instruments.
+pub const VMTag = enum(u8) {
+ application_specific_1 = 240,
+ application_specific_2 = 241,
+ application_specific_3 = 242,
+ application_specific_4 = 243,
+ application_specific_5 = 244,
+ application_specific_6 = 245,
+ application_specific_7 = 246,
+ application_specific_8 = 247,
+ application_specific_9 = 248,
+ application_specific_10 = 249,
+ application_specific_11 = 250,
+ application_specific_12 = 251,
+ application_specific_13 = 252,
+ application_specific_14 = 253,
+ application_specific_15 = 254,
+ application_specific_16 = 255,
+
+ // We ignore the rest because we never realistic set them.
+ _,
+
+ /// Converts the tag to the format expected by mach_vm_map/mach_vm_allocate.
+ /// Equivalent to C macro: VM_MAKE_TAG(tag)
+ pub fn make(self: VMTag) i32 {
+ return @bitCast(@as(u32, @intFromEnum(self)) << 24);
+ }
+};
+
+/// Creates a page allocator that tags all allocated memory with the given
+/// VMTag.
+pub fn taggedPageAllocator(tag: VMTag) Allocator {
+ return .{
+ // We smuggle the tag in as the context pointer.
+ .ptr = @ptrFromInt(@as(usize, @intFromEnum(tag))),
+ .vtable = &TaggedPageAllocator.vtable,
+ };
+}
+
+/// 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,
+ .remap = remap,
+ .free = free,
+ };
+
+ fn alloc(context: *anyopaque, n: usize, alignment: mem.Alignment, ra: usize) ?[*]u8 {
+ _ = ra;
+ assert(n > 0);
+ const tag: VMTag = @enumFromInt(@as(u8, @truncate(@intFromPtr(context))));
+ return map(n, alignment, tag);
+ }
+
+ fn resize(context: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, return_address: usize) bool {
+ _ = context;
+ _ = alignment;
+ _ = return_address;
+ return realloc(memory, new_len, false) != null;
+ }
+
+ fn remap(context: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, return_address: usize) ?[*]u8 {
+ _ = context;
+ _ = alignment;
+ _ = return_address;
+ return realloc(memory, new_len, true);
+ }
+
+ fn free(context: *anyopaque, memory: []u8, alignment: mem.Alignment, return_address: usize) void {
+ _ = context;
+ _ = alignment;
+ _ = return_address;
+ return unmap(@alignCast(memory));
+ }
+
+ pub fn map(n: usize, alignment: mem.Alignment, tag: VMTag) ?[*]u8 {
+ const page_size = std.heap.pageSize();
+ if (n >= std.math.maxInt(usize) - page_size) return null;
+ const alignment_bytes = alignment.toByteUnits();
+
+ const aligned_len = mem.alignForward(usize, n, page_size);
+ const max_drop_len = alignment_bytes - @min(alignment_bytes, page_size);
+ const overalloc_len = if (max_drop_len <= aligned_len - n)
+ aligned_len
+ else
+ mem.alignForward(usize, aligned_len + max_drop_len, page_size);
+ const hint = @atomicLoad(@TypeOf(next_mmap_addr_hint), &next_mmap_addr_hint, .unordered);
+ const slice = std.posix.mmap(
+ hint,
+ overalloc_len,
+ .{ .READ = true, .WRITE = true },
+ .{ .TYPE = .PRIVATE, .ANONYMOUS = true },
+ tag.make(),
+ 0,
+ ) catch return null;
+ const result_ptr = mem.alignPointer(slice.ptr, alignment_bytes) orelse return null;
+ // Unmap the extra bytes that were only requested in order to guarantee
+ // that the range of memory we were provided had a proper alignment in it
+ // somewhere. The extra bytes could be at the beginning, or end, or both.
+ const drop_len = result_ptr - slice.ptr;
+ if (drop_len != 0) std.posix.munmap(slice[0..drop_len]);
+ 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(next_mmap_addr_hint), &next_mmap_addr_hint, hint, new_hint, .monotonic, .monotonic);
+ return result_ptr;
+ }
+
+ pub fn unmap(memory: []align(std.heap.page_size_min) u8) void {
+ const page_aligned_len = mem.alignForward(usize, memory.len, std.heap.pageSize());
+ std.posix.munmap(memory.ptr[0..page_aligned_len]);
+ }
+
+ pub fn realloc(uncasted_memory: []u8, new_len: usize, may_move: bool) ?[*]u8 {
+ const memory: []align(std.heap.page_size_min) u8 = @alignCast(uncasted_memory);
+ const page_size = std.heap.pageSize();
+ const new_size_aligned = mem.alignForward(usize, new_len, page_size);
+
+ const page_aligned_len = mem.alignForward(usize, memory.len, page_size);
+ if (new_size_aligned == page_aligned_len)
+ return memory.ptr;
+
+ if (std.posix.MREMAP != void) {
+ // TODO: if the next_mmap_addr_hint is within the remapped range, update it
+ const new_memory = std.posix.mremap(memory.ptr, memory.len, new_len, .{ .MAYMOVE = may_move }, null) catch return null;
+ return new_memory.ptr;
+ }
+
+ if (new_size_aligned < page_aligned_len) {
+ const ptr = memory.ptr + new_size_aligned;
+ // TODO: if the next_mmap_addr_hint is within the unmapped range, update it
+ std.posix.munmap(@alignCast(ptr[0 .. page_aligned_len - new_size_aligned]));
+ return memory.ptr;
+ }
+
+ return null;
+ }
+};
+
+test "VMTag.make" {
+ try std.testing.expectEqual(@as(i32, @bitCast(@as(u32, 240) << 24)), VMTag.application_specific_1.make());
+}
diff --git a/vendor/libghostty-vt/src/os/macos.zig b/vendor/libghostty-vt/src/os/macos.zig
new file mode 100644
index 0000000..fcd1c3e
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/macos.zig
@@ -0,0 +1,177 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const build_config = @import("../build_config.zig");
+const assert = @import("../quirks.zig").inlineAssert;
+const objc = @import("objc");
+const Allocator = std.mem.Allocator;
+
+/// Verifies that the running macOS system version is at least the given version.
+pub fn isAtLeastVersion(major: i64, minor: i64, patch: i64) bool {
+ comptime assert(builtin.target.os.tag.isDarwin());
+
+ const NSProcessInfo = objc.getClass("NSProcessInfo").?;
+ const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{});
+ return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{
+ NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch },
+ });
+}
+
+pub const AppSupportDirError = Allocator.Error || error{AppleAPIFailed};
+
+/// Return the path to the application support directory for Ghostty
+/// with the given sub path joined. This allocates the result using the
+/// given allocator.
+pub fn appSupportDir(
+ alloc: Allocator,
+ sub_path: []const u8,
+) AppSupportDirError![]const u8 {
+ return try commonDir(
+ alloc,
+ .NSApplicationSupportDirectory,
+ &.{ build_config.bundle_id, sub_path },
+ );
+}
+
+pub const CacheDirError = Allocator.Error || error{AppleAPIFailed};
+
+/// Return the path to the system cache directory with the given sub path joined.
+/// This allocates the result using the given allocator.
+pub fn cacheDir(
+ alloc: Allocator,
+ sub_path: []const u8,
+) CacheDirError![]const u8 {
+ return try commonDir(
+ alloc,
+ .NSCachesDirectory,
+ &.{ build_config.bundle_id, sub_path },
+ );
+}
+
+pub const SetQosClassError = error{
+ // The thread can't have its QoS class changed usually because
+ // a different pthread API was called that makes it an invalid
+ // target.
+ ThreadIncompatible,
+};
+
+/// Set the QoS class of the running thread.
+///
+/// https://developer.apple.com/documentation/apple-silicon/tuning-your-code-s-performance-for-apple-silicon?preferredLanguage=occ
+pub fn setQosClass(class: QosClass) !void {
+ return switch (std.posix.errno(pthread_set_qos_class_self_np(
+ class,
+ 0,
+ ))) {
+ .SUCCESS => {},
+ .PERM => error.ThreadIncompatible,
+
+ // EPERM is the only known error that can happen based on
+ // the man pages for pthread_set_qos_class_self_np. I haven't
+ // checked the XNU source code to see if there are other
+ // possible errors.
+ else => @panic("unexpected pthread_set_qos_class_self_np error"),
+ };
+}
+
+/// https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW1
+pub const QosClass = enum(c_uint) {
+ user_interactive = 0x21,
+ user_initiated = 0x19,
+ default = 0x15,
+ utility = 0x11,
+ background = 0x09,
+ unspecified = 0x00,
+};
+
+extern "c" fn pthread_set_qos_class_self_np(
+ qos_class: QosClass,
+ relative_priority: c_int,
+) c_int;
+
+pub extern "c" fn pthread_setname_np(
+ name: [*:0]const u8,
+) void;
+
+pub const NSOperatingSystemVersion = extern struct {
+ major: i64,
+ minor: i64,
+ patch: i64,
+};
+
+pub const NSSearchPathDirectory = enum(c_ulong) {
+ NSCachesDirectory = 13,
+ NSApplicationSupportDirectory = 14,
+};
+
+pub const NSSearchPathDomainMask = enum(c_ulong) {
+ NSUserDomainMask = 1,
+};
+
+fn commonDir(
+ alloc: Allocator,
+ directory: NSSearchPathDirectory,
+ sub_paths: []const []const u8,
+) (error{AppleAPIFailed} || Allocator.Error)![]const u8 {
+ comptime assert(builtin.target.os.tag.isDarwin());
+
+ const NSFileManager = objc.getClass("NSFileManager").?;
+ const manager = NSFileManager.msgSend(
+ objc.Object,
+ objc.sel("defaultManager"),
+ .{},
+ );
+
+ const url = manager.msgSend(
+ objc.Object,
+ objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"),
+ .{
+ directory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ @as(?*anyopaque, null),
+ true,
+ @as(?*anyopaque, null),
+ },
+ );
+
+ if (url.value == null) return error.AppleAPIFailed;
+
+ const path = url.getProperty(objc.Object, "path");
+ const c_str = path.getProperty(?[*:0]const u8, "UTF8String") orelse
+ 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);
+}
+
+test "cacheDir paths" {
+ if (!builtin.target.os.tag.isDarwin()) return;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Test base path
+ {
+ const cache_path = try cacheDir(alloc, "");
+ defer alloc.free(cache_path);
+ try testing.expect(std.mem.indexOf(u8, cache_path, "Caches") != null);
+ try testing.expect(std.mem.indexOf(u8, cache_path, build_config.bundle_id) != null);
+ }
+
+ // Test with subdir
+ {
+ const cache_path = try cacheDir(alloc, "test");
+ defer alloc.free(cache_path);
+ try testing.expect(std.mem.indexOf(u8, cache_path, "Caches") != null);
+ try testing.expect(std.mem.indexOf(u8, cache_path, build_config.bundle_id) != null);
+
+ const bundle_path = try std.fmt.allocPrint(alloc, "{s}/test", .{build_config.bundle_id});
+ defer alloc.free(bundle_path);
+ try testing.expect(std.mem.indexOf(u8, cache_path, bundle_path) != null);
+ }
+}
diff --git a/vendor/libghostty-vt/src/os/main.zig b/vendor/libghostty-vt/src/os/main.zig
new file mode 100644
index 0000000..05f2a45
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/main.zig
@@ -0,0 +1,76 @@
+//! The "os" package contains utilities for interfacing with the operating
+//! 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 file = @import("file.zig");
+const flatpak = @import("flatpak.zig");
+const homedir = @import("homedir.zig");
+const locale = @import("locale.zig");
+const mouse = @import("mouse.zig");
+const openpkg = @import("open.zig");
+const pipepkg = @import("pipe.zig");
+const resourcesdir = @import("resourcesdir.zig");
+const systemd = @import("systemd.zig");
+const kernel_info = @import("kernel_info.zig");
+
+// Namespaces
+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");
+pub const path = @import("path.zig");
+pub const passwd = @import("passwd.zig");
+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 launchedFromDesktop = desktop.launchedFromDesktop;
+pub const launchedByDbusActivation = dbus.launchedByDbusActivation;
+pub const launchedBySystemd = systemd.launchedBySystemd;
+pub const desktopEnvironment = desktop.desktopEnvironment;
+pub const rlimit = file.rlimit;
+pub const fixMaxFiles = file.fixMaxFiles;
+pub const restoreMaxFiles = file.restoreMaxFiles;
+pub const randomTmpPath = file.randomTmpPath;
+pub const isFlatpak = flatpak.isFlatpak;
+pub const FlatpakHostCommand = flatpak.FlatpakHostCommand;
+pub const home = homedir.home;
+pub const expandHome = homedir.expandHome;
+pub const ensureLocale = locale.ensureLocale;
+pub const clickInterval = mouse.clickInterval;
+pub const open = openpkg.open;
+pub const OpenType = openpkg.Type;
+pub const pipe = pipepkg.pipe;
+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;
+ _ = shell;
+
+ if (comptime builtin.os.tag == .linux) {
+ _ = kernel_info;
+ } else if (comptime builtin.os.tag.isDarwin()) {
+ _ = mach;
+ _ = macos;
+ }
+}
diff --git a/vendor/libghostty-vt/src/os/mouse.zig b/vendor/libghostty-vt/src/os/mouse.zig
new file mode 100644
index 0000000..d68bb22
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/mouse.zig
@@ -0,0 +1,25 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const objc = @import("objc");
+
+const log = std.log.scoped(.os);
+
+/// The system-configured double-click interval if its available.
+pub fn clickInterval() ?u32 {
+ return switch (builtin.os.tag) {
+ // On macOS, we can ask the system.
+ .macos => macos: {
+ const NSEvent = objc.getClass("NSEvent") orelse {
+ log.err("NSEvent class not found. Can't get click interval.", .{});
+ return null;
+ };
+
+ // Get the interval and convert to ms
+ const interval = NSEvent.msgSend(f64, objc.sel("doubleClickInterval"), .{});
+ const ms = @as(u32, @intFromFloat(@ceil(interval * 1000)));
+ break :macos ms;
+ },
+
+ else => null,
+ };
+}
diff --git a/vendor/libghostty-vt/src/os/open.zig b/vendor/libghostty-vt/src/os/open.zig
new file mode 100644
index 0000000..bc09518
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/open.zig
@@ -0,0 +1,101 @@
+const std = @import("std");
+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");
+
+/// Open a URL in the default handling application.
+///
+/// Any output on stderr is logged as a warning in the application logs.
+/// Output on stdout is ignored.
+///
+/// This function is purposely simple for the sake of providing some portable
+/// 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(
+ kind: apprt.action.OpenUrl.Kind,
+ url: []const u8,
+) !void {
+ // 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.
+ spawn_opts.stdout = .ignore;
+ // Pipe stderr so we can log the stderr from the command. This must be set
+ // before spawning the process.
+ spawn_opts.stderr = .pipe;
+
+ 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);
+
+ const thread = try std.Thread.spawn(.{}, openThread, .{ global.io(), exe });
+ thread.detach();
+}
+
+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(io, &buffer);
+ const reader = &stream.interface;
+ while (true) {
+ const line = reader.takeDelimiterExclusive('\n') catch |outer| switch (outer) {
+ error.EndOfStream => break,
+ error.ReadFailed => break,
+ error.StreamTooLong => reader.take(buffer.len) catch |inner| switch (inner) {
+ error.ReadFailed => break,
+ error.EndOfStream => break,
+ },
+ };
+ log.warn("open stderr={s}", .{line});
+ }
+ }
+ _ = exe.wait(io) catch {};
+}
diff --git a/vendor/libghostty-vt/src/os/passwd.zig b/vendor/libghostty-vt/src/os/passwd.zig
new file mode 100644
index 0000000..7c16e54
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/passwd.zig
@@ -0,0 +1,158 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const internal_os = @import("main.zig");
+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);
+
+// We want to be extra sure since this will force bad symbols into our import table
+comptime {
+ if (builtin.target.cpu.arch.isWasm()) {
+ @compileError("passwd is not available for wasm");
+ }
+}
+
+/// Used to determine the default shell and directory on Unixes.
+const c = if (builtin.os.tag != .windows) @cImport({
+ @cInclude("sys/types.h");
+ @cInclude("unistd.h");
+ @cInclude("pwd.h");
+}) else {};
+
+// Entry that is retrieved from the passwd API. This only contains the fields
+// we care about.
+pub const Entry = struct {
+ shell: ?[:0]const u8 = null,
+ home: ?[:0]const u8 = null,
+ name: ?[:0]const u8 = null,
+};
+
+/// Get the passwd entry for the currently executing user.
+pub fn get(alloc: Allocator) !Entry {
+ if (builtin.os.tag == .windows) @compileError("passwd is not available on windows");
+
+ var buf: [1024]u8 = undefined;
+ var pw: c.struct_passwd = undefined;
+ var pw_ptr: ?*c.struct_passwd = null;
+ const res = c.getpwuid_r(c.getuid(), &pw, &buf, buf.len, &pw_ptr);
+ if (res != 0) {
+ log.warn("error retrieving pw entry code={d}", .{res});
+ return Entry{};
+ }
+
+ if (pw_ptr == null) {
+ // Future: let's check if a better shell is available like zsh
+ log.warn("no pw entry to detect default shell, will default to 'sh'", .{});
+ return Entry{};
+ }
+
+ var result: Entry = .{};
+
+ // If we're in flatpak then our entry is always empty so we grab it
+ // by shelling out to the host. note that we do HAVE an entry in the
+ // sandbox but only the username is correct.
+ if (internal_os.isFlatpak()) flatpak: {
+ if (comptime !build_config.flatpak) {
+ log.warn("flatpak detected, but this build doesn't contain flatpak support", .{});
+ break :flatpak;
+ }
+
+ log.info("flatpak detected, will use host command to get our entry", .{});
+
+ // Note: we wrap our getent call in a /bin/sh login shell because
+ // some operating systems (NixOS tested) don't set the PATH for various
+ // utilities properly until we get a login shell.
+ const Pty = @import("../pty.zig").Pty;
+ var pty = try Pty.open(.{});
+ defer pty.deinit();
+ var cmd: internal_os.FlatpakHostCommand = .{
+ .argv = &[_][]const u8{
+ "/bin/sh",
+ "-l",
+ "-c",
+ try std.fmt.allocPrint(
+ alloc,
+ "getent passwd {s}",
+ .{std.mem.sliceTo(pw.pw_name, 0)},
+ ),
+ },
+ .stdin = pty.slave,
+ .stdout = pty.slave,
+ .stderr = pty.slave,
+ };
+ _ = try cmd.spawn(alloc);
+ _ = try cmd.wait();
+
+ // 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.
+ _ = compat_fd.close(pty.slave);
+
+ // Read all of our output
+ const output = output: {
+ var output: std.ArrayListUnmanaged(u8) = .empty;
+ while (true) {
+ const n = posix.read(pty.master, &buf) catch |err| {
+ switch (err) {
+ // EIO is triggered at the end since we closed our
+ // child side. This is just EOF for this. I'm not sure
+ // if I'm doing this wrong.
+ error.InputOutput => break,
+ else => return err,
+ }
+ };
+
+ try output.appendSlice(alloc, buf[0..n]);
+
+ // Max total size is buf.len. We can do better here by trimming
+ // the front and continuing reading but we choose to just exit.
+ if (output.items.len > buf.len) break;
+ }
+
+ break :output try output.toOwnedSlice(alloc);
+ };
+
+ // Shell and home are the last two entries
+ 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;
+ }
+
+ if (pw.pw_shell) |ptr| {
+ const source = std.mem.sliceTo(ptr, 0);
+ const value = try alloc.dupeZ(u8, source);
+ result.shell = value;
+ }
+
+ if (pw.pw_dir) |ptr| {
+ const source = std.mem.sliceTo(ptr, 0);
+ const value = try alloc.dupeZ(u8, source);
+ result.home = value;
+ }
+
+ if (pw.pw_name) |ptr| {
+ const source = std.mem.sliceTo(ptr, 0);
+ const value = try alloc.dupeZ(u8, source);
+ result.name = value;
+ }
+
+ return result;
+}
+
+test {
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ // We should be able to get an entry
+ const entry = try get(alloc);
+ try testing.expect(entry.shell != null);
+ try testing.expect(entry.home != null);
+}
diff --git a/vendor/libghostty-vt/src/os/path.zig b/vendor/libghostty-vt/src/os/path.zig
new file mode 100644
index 0000000..ba1a63f
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/path.zig
@@ -0,0 +1,100 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+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(
+ 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 = 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);
+ var seen_eacces = false;
+ while (it.next()) |search_path| {
+ // We need enough space in our path buffer to store this
+ const path_len = search_path.len + cmd.len + 1;
+ if (path_buf.len < path_len) return error.PathTooLong;
+
+ // Copy in the full path
+ @memcpy(path_buf[0..search_path.len], search_path);
+ path_buf[search_path.len] = std.fs.path.sep;
+ @memcpy(path_buf[search_path.len + 1 ..][0..cmd.len], cmd);
+ path_buf[path_len] = 0;
+ const full_path = path_buf[0..path_len :0];
+
+ // Stat it
+ const f = std.Io.Dir.cwd().openFile(
+ io,
+ full_path,
+ .{},
+ ) catch |err| switch (err) {
+ error.FileNotFound => continue,
+ error.AccessDenied => {
+ // Accumulate this and return it later so we can try other
+ // paths that we have access to.
+ seen_eacces = true;
+ continue;
+ },
+ else => return err,
+ };
+ 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);
+ }
+ }
+
+ if (seen_eacces) return error.AccessDenied;
+
+ return null;
+}
+
+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.io, testing.allocator, &environ_map, executable)).?;
+ defer testing.allocator.free(path);
+ try testing.expect(path.len > executable.len);
+}
+
+test "expand: does not exist" {
+ 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" {
+ 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
new file mode 100644
index 0000000..0310fb3
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/pipe.zig
@@ -0,0 +1,22 @@
+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 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) == 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
new file mode 100644
index 0000000..ef8b5a2
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/resourcesdir.zig
@@ -0,0 +1,148 @@
+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.
+ app_path: ?[]const u8 = null,
+ host_path: ?[]const u8 = null,
+
+ /// Free resources held. Requires the same allocator as when resourcesDir()
+ /// is called.
+ pub fn deinit(self: *ResourcesDir, alloc: Allocator) void {
+ if (self.app_path) |p| alloc.free(p);
+ if (self.host_path) |p| alloc.free(p);
+ }
+
+ /// Get the directory to the bundled resources directory accessible
+ /// by the application.
+ pub fn app(self: *const ResourcesDir) ?[]const u8 {
+ return self.app_path;
+ }
+
+ /// Get the directory to the bundled resources directory accessible
+ /// by the host environment (i.e. for sandboxed applications). The
+ /// returned directory might not be accessible from the application
+ /// itself.
+ ///
+ /// In non-sandboxed environment, this should be the same as app().
+ pub fn host(self: *const ResourcesDir) ?[]const u8 {
+ return self.host_path orelse self.app_path;
+ }
+};
+
+/// Gets the directory to the bundled resources directory, if it
+/// exists (not all platforms or packages have it). The output is
+/// owned by the caller.
+///
+/// This is highly Ghostty-specific and can likely be generalized at
+/// some point but we can cross that bridge if we ever need to.
+pub fn resourcesDir(alloc: Allocator) !ResourcesDir {
+ // Use the GHOSTTY_RESOURCES_DIR environment variable in release builds.
+ //
+ // In debug builds we try using terminfo detection first instead, since
+ // if debug Ghostty is launched by an older version of Ghostty, it
+ // would inherit the old, stale resources of older Ghostty instead of the
+ // freshly built ones under zig-out/share/ghostty.
+ //
+ // 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) 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
+ // we've found the resources directory.
+ const sentinels = switch (comptime builtin.target.os.tag) {
+ .windows => .{"terminfo/ghostty.terminfo"},
+ .macos => .{"terminfo/78/xterm-ghostty"},
+ .freebsd => .{ "site-terminfo/g/ghostty", "site-terminfo/x/xterm-ghostty" },
+ else => .{ "terminfo/g/ghostty", "terminfo/x/xterm-ghostty" },
+ };
+
+ // Get the path to our running binary
+ var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
+ 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.
+ var dir_buf: [std.fs.max_path_bytes]u8 = undefined;
+ while (std.fs.path.dirname(exe)) |dir| {
+ exe = dir;
+
+ // 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| {
+ return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
+ }
+ }
+ }
+
+ // On all platforms (except BSD), we look for a /usr/share style path. This
+ // is valid even on Mac since there is nothing that requires
+ // Ghostty to be in an app bundle.
+ inline for (sentinels) |sentinel| {
+ if (try maybeDir(
+ &dir_buf,
+ dir,
+ if (builtin.target.os.tag == .freebsd) "local/share" else "share",
+ sentinel,
+ )) |v| {
+ return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
+ }
+ }
+ }
+
+ // If terminfo detection failed in debug builds (somehow),
+ // fallback and use the provided resources dir.
+ if (comptime builtin.mode == .Debug) {
+ if (global.environ().getAlloc(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
+ if (dir.len > 0) return .{ .app_path = dir };
+ } else |err| switch (err) {
+ error.InvalidWtf8, error.EnvironmentVariableMissing => {},
+ else => return err,
+ }
+ }
+
+ return .{};
+}
+
+/// Little helper to check if the "base/sub/suffix" directory exists and
+/// if so return true. The "suffix" is just used as a way to verify a directory
+/// seems roughly right.
+///
+/// "buf" must be large enough to fit base + sub + suffix. This is generally
+/// max_path_bytes so its not a big deal.
+pub fn maybeDir(
+ buf: []u8,
+ base: []const u8,
+ sub: []const u8,
+ suffix: []const u8,
+) !?[]const u8 {
+ const path = try std.fmt.bufPrint(buf, "{s}/{s}/{s}", .{ base, sub, suffix });
+
+ if (std.Io.Dir.accessAbsolute(global.io(), path, .{})) {
+ const len = path.len - suffix.len - 1;
+ return buf[0..len];
+ } else |_| {
+ // Folder doesn't exist. If a different error happens its okay
+ // we just ignore it and move on.
+ }
+
+ return null;
+}
diff --git a/vendor/libghostty-vt/src/os/shell.zig b/vendor/libghostty-vt/src/os/shell.zig
new file mode 100644
index 0000000..fe8f1b2
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/shell.zig
@@ -0,0 +1,196 @@
+const std = @import("std");
+const testing = std.testing;
+const Allocator = std.mem.Allocator;
+const Writer = std.Io.Writer;
+
+/// Builder for constructing space-separated shell command strings.
+/// Uses a caller-provided allocator (typically with stackFallback).
+pub const ShellCommandBuilder = struct {
+ buffer: std.Io.Writer.Allocating,
+
+ pub fn init(allocator: Allocator) ShellCommandBuilder {
+ return .{ .buffer = .init(allocator) };
+ }
+
+ pub fn deinit(self: *ShellCommandBuilder) void {
+ self.buffer.deinit();
+ }
+
+ /// Append an argument to the command with automatic space separation.
+ pub fn appendArg(self: *ShellCommandBuilder, arg: []const u8) (Allocator.Error || Writer.Error)!void {
+ if (arg.len == 0) return;
+ if (self.buffer.written().len > 0) {
+ try self.buffer.writer.writeByte(' ');
+ }
+ try self.buffer.writer.writeAll(arg);
+ }
+
+ /// Get the final null-terminated command string, transferring ownership to caller.
+ /// Calling deinit() after this is safe but unnecessary.
+ pub fn toOwnedSlice(self: *ShellCommandBuilder) Allocator.Error![:0]const u8 {
+ return try self.buffer.toOwnedSliceSentinel(0);
+ }
+};
+
+test ShellCommandBuilder {
+ // Empty command
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ defer cmd.deinit();
+ try testing.expectEqualStrings("", cmd.buffer.written());
+ }
+
+ // Single arg
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ defer cmd.deinit();
+ try cmd.appendArg("bash");
+ try testing.expectEqualStrings("bash", cmd.buffer.written());
+ }
+
+ // Multiple args
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ defer cmd.deinit();
+ try cmd.appendArg("bash");
+ try cmd.appendArg("--posix");
+ try cmd.appendArg("-l");
+ try testing.expectEqualStrings("bash --posix -l", cmd.buffer.written());
+ }
+
+ // Empty arg
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ defer cmd.deinit();
+ try cmd.appendArg("bash");
+ try cmd.appendArg("");
+ try testing.expectEqualStrings("bash", cmd.buffer.written());
+ }
+
+ // toOwnedSlice
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ try cmd.appendArg("bash");
+ try cmd.appendArg("--posix");
+ const result = try cmd.toOwnedSlice();
+ defer testing.allocator.free(result);
+ try testing.expectEqualStrings("bash --posix", result);
+ try testing.expectEqual(@as(u8, 0), result[result.len]);
+ }
+}
+
+/// Writer that escapes characters that shells treat specially to reduce the
+/// risk of injection attacks or other such weirdness. Specifically excludes
+/// linefeeds so that they can be used to delineate lists of file paths.
+pub const ShellEscapeWriter = struct {
+ writer: Writer,
+ child: *Writer,
+
+ pub fn init(child: *Writer) ShellEscapeWriter {
+ return .{
+ .writer = .{
+ // TODO: Actually use a buffer here
+ .buffer = &.{},
+ .vtable = &.{ .drain = ShellEscapeWriter.drain },
+ },
+ .child = child,
+ };
+ }
+
+ fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
+ const self: *ShellEscapeWriter = @fieldParentPtr("writer", w);
+
+ // TODO: This is a very naive implementation and does not really make
+ // full use of the post-Writergate API. However, since we know that
+ // this is going into an Allocating writer anyways, we can be a bit
+ // less strict here.
+
+ var count: usize = 0;
+ for (data[0 .. data.len - 1]) |chunk| try self.writeEscaped(chunk, &count);
+
+ for (0..splat) |_| try self.writeEscaped(data[data.len - 1], &count);
+ return count;
+ }
+
+ fn writeEscaped(
+ self: *ShellEscapeWriter,
+ s: []const u8,
+ count: *usize,
+ ) Writer.Error!void {
+ for (s) |byte| {
+ const buf = switch (byte) {
+ '\\',
+ '"',
+ '\'',
+ '$',
+ '`',
+ '*',
+ '?',
+ ' ',
+ '|',
+ '(',
+ ')',
+ => &[_]u8{ '\\', byte },
+ else => &[_]u8{byte},
+ };
+ try self.child.writeAll(buf);
+ count.* += 1;
+ }
+ }
+};
+
+test "shell escape 1" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("abc");
+ try testing.expectEqualStrings("abc", writer.buffered());
+}
+
+test "shell escape 2" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a c");
+ try testing.expectEqualStrings("a\\ c", writer.buffered());
+}
+
+test "shell escape 3" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a?c");
+ try testing.expectEqualStrings("a\\?c", writer.buffered());
+}
+
+test "shell escape 4" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a\\c");
+ try testing.expectEqualStrings("a\\\\c", writer.buffered());
+}
+
+test "shell escape 5" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a|c");
+ try testing.expectEqualStrings("a\\|c", writer.buffered());
+}
+
+test "shell escape 6" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a\"c");
+ try testing.expectEqualStrings("a\\\"c", writer.buffered());
+}
+
+test "shell escape 7" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a(1)");
+ try testing.expectEqualStrings("a\\(1\\)", writer.buffered());
+}
diff --git a/vendor/libghostty-vt/src/os/stderr.zig b/vendor/libghostty-vt/src/os/stderr.zig
new file mode 100644
index 0000000..201bc43
--- /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/string_encoding.zig b/vendor/libghostty-vt/src/os/string_encoding.zig
new file mode 100644
index 0000000..875a5bc
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/string_encoding.zig
@@ -0,0 +1,307 @@
+const std = @import("std");
+
+/// Decode date from the buffer that has been encoded in the same way that
+/// `bash`'s `printf %q` encodes a string and write it to the writer. If an
+/// error is returned garbage may have been written to the buffer.
+pub fn printfQDecode(writer: *std.Io.Writer, buf: []const u8) (std.Io.Writer.Error || error{DecodeError})!void {
+ const data: []const u8 = data: {
+ // Strip off `$''` quoting.
+ if (std.mem.startsWith(u8, buf, "$'")) {
+ if (buf.len < 3 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError;
+ break :data buf[2 .. buf.len - 1];
+ }
+ // Strip off `''` quoting.
+ if (std.mem.startsWith(u8, buf, "'")) {
+ if (buf.len < 2 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError;
+ break :data buf[1 .. buf.len - 1];
+ }
+ break :data buf;
+ };
+
+ var src: usize = 0;
+
+ while (src < data.len) {
+ switch (data[src]) {
+ else => {
+ try writer.writeByte(data[src]);
+ src += 1;
+ },
+ '\\' => {
+ if (src + 1 >= data.len) return error.DecodeError;
+ switch (data[src + 1]) {
+ ' ',
+ '\\',
+ '"',
+ '\'',
+ '$',
+ => |c| {
+ try writer.writeByte(c);
+ src += 2;
+ },
+ 'e' => {
+ try writer.writeByte(std.ascii.control_code.esc);
+ src += 2;
+ },
+ 'n' => {
+ try writer.writeByte(std.ascii.control_code.lf);
+ src += 2;
+ },
+ 'r' => {
+ try writer.writeByte(std.ascii.control_code.cr);
+ src += 2;
+ },
+ 't' => {
+ try writer.writeByte(std.ascii.control_code.ht);
+ src += 2;
+ },
+ 'v' => {
+ try writer.writeByte(std.ascii.control_code.vt);
+ src += 2;
+ },
+ else => return error.DecodeError,
+ }
+ },
+ }
+ }
+}
+
+test "printf_q 1" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: []const u8 = "bobr\\ kurwa";
+
+ try printfQDecode(&w.writer, s);
+ try std.testing.expectEqualStrings("bobr kurwa", w.written());
+}
+
+test "printf_q 2" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: [:0]const u8 = "bobr\\nkurwa";
+
+ try printfQDecode(&w.writer, s);
+ try std.testing.expectEqualStrings("bobr\nkurwa", w.written());
+}
+
+test "printf_q 3" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: [:0]const u8 = "bobr\\dkurwa";
+
+ try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s));
+}
+
+test "printf_q 4" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: [:0]const u8 = "bobr kurwa\\";
+
+ try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s));
+}
+
+test "printf_q 5" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: [:0]const u8 = "$'bobr kurwa'";
+
+ try printfQDecode(&w.writer, s);
+ try std.testing.expectEqualStrings("bobr kurwa", w.written());
+}
+
+test "printf_q 6" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: [:0]const u8 = "'bobr kurwa'";
+
+ try printfQDecode(&w.writer, s);
+ try std.testing.expectEqualStrings("bobr kurwa", w.written());
+}
+
+test "printf_q 7" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: [:0]const u8 = "$'bobr kurwa";
+
+ try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s));
+}
+
+test "printf_q 8" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+ const s: [:0]const u8 = "$'";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s));
+}
+
+test "printf_q 9" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+ const s: [:0]const u8 = "'bobr kurwa";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s));
+}
+
+test "printf_q 10" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: [:0]const u8 = "'";
+ var src: [s.len:0]u8 = undefined;
+ @memcpy(&src, s);
+ try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s));
+}
+
+/// Decode data from the buffer that has been URL percent encoded and write
+/// it to the given buffer. If an error is returned the garbage may have been
+/// written to the writer.
+pub fn urlPercentDecode(writer: *std.Io.Writer, buf: []const u8) (std.Io.Writer.Error || error{DecodeError})!void {
+ var src: usize = 0;
+ while (src < buf.len) {
+ switch (buf[src]) {
+ else => {
+ try writer.writeByte(buf[src]);
+ src += 1;
+ },
+ '%' => {
+ if (src + 2 >= buf.len) return error.DecodeError;
+ switch (buf[src + 1]) {
+ '0'...'9', 'a'...'f', 'A'...'F' => {
+ switch (buf[src + 2]) {
+ '0'...'9', 'a'...'f', 'A'...'F' => {
+ try writer.writeByte(std.math.shl(u8, hex(buf[src + 1]), 4) | hex(buf[src + 2]));
+ src += 3;
+ },
+ else => return error.DecodeError,
+ }
+ },
+ else => return error.DecodeError,
+ }
+ },
+ }
+ }
+}
+
+inline fn hex(c: u8) u4 {
+ switch (c) {
+ '0'...'9' => return @truncate(c - '0'),
+ 'a'...'f' => return @truncate(c - 'a' + 10),
+ 'A'...'F' => return @truncate(c - 'A' + 10),
+ else => unreachable,
+ }
+}
+
+test "singles percent" {
+ for (0..255) |c| {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ var buf_: [4]u8 = undefined;
+ const buf = try std.fmt.bufPrintZ(&buf_, "%{x:0>2}", .{c});
+
+ try urlPercentDecode(&w.writer, buf);
+ const decoded = w.written();
+
+ try std.testing.expectEqual(1, decoded.len);
+ try std.testing.expectEqual(c, decoded[0]);
+ }
+ for (0..255) |c| {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ var buf_: [4]u8 = undefined;
+ const buf = try std.fmt.bufPrintZ(&buf_, "%{X:0>2}", .{c});
+
+ try urlPercentDecode(&w.writer, buf);
+ const decoded = w.written();
+
+ try std.testing.expectEqual(1, decoded.len);
+ try std.testing.expectEqual(c, decoded[0]);
+ }
+}
+
+test "percent 1" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: []const u8 = "bobr%20kurwa";
+
+ try urlPercentDecode(&w.writer, s);
+ try std.testing.expectEqualStrings("bobr kurwa", w.written());
+}
+
+test "percent 2" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: []const u8 = "bobr%2kurwa";
+
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s));
+}
+
+test "percent 3" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: []const u8 = "bobr%kurwa";
+
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s));
+}
+
+test "percent 4" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: []const u8 = "bobr%%kurwa";
+
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s));
+}
+
+test "percent 5" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: []const u8 = "bobr%20kurwa%20";
+
+ try urlPercentDecode(&w.writer, s);
+ try std.testing.expectEqualStrings("bobr kurwa ", w.written());
+}
+
+test "percent 6" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: []const u8 = "bobr%20kurwa%2";
+
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s));
+}
+
+test "percent 7" {
+ var w: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer w.deinit();
+
+ const s: []const u8 = "bobr%20kurwa%";
+
+ try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s));
+}
+
+/// Is the given character valid in URI percent encoding?
+fn isValidChar(c: u8) bool {
+ return switch (c) {
+ ' ', ';', '=' => false,
+ else => return std.ascii.isPrint(c),
+ };
+}
+
+/// Write data to the writer after URI percent encoding.
+pub fn urlPercentEncode(writer: *std.Io.Writer, data: []const u8) std.Io.Writer.Error!void {
+ try std.Uri.Component.percentEncode(writer, data, isValidChar);
+}
diff --git a/vendor/libghostty-vt/src/os/systemd.zig b/vendor/libghostty-vt/src/os/systemd.zig
new file mode 100644
index 0000000..5815648
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/systemd.zig
@@ -0,0 +1,200 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const global = @import("../global.zig");
+
+const log = std.log.scoped(.systemd);
+
+/// Returns true if the program was launched as a systemd service.
+///
+/// On Linux, this returns true if the program was launched as a systemd
+/// service. It will return false if Ghostty was launched any other way.
+///
+/// For other platforms and app runtimes, this returns false.
+pub fn launchedBySystemd() bool {
+ return switch (builtin.os.tag) {
+ .linux => linux: {
+ // On Linux, systemd sets the `INVOCATION_ID` (v232+) and the
+ // `JOURNAL_STREAM` (v231+) environment variables. If these
+ // environment variables are not present we were not launched by
+ // systemd.
+ 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
+ // emulator that doesn't clean up those environment variables.
+ const ppid = std.os.linux.getppid();
+ if (ppid == 1) break :linux true;
+
+ // If the parent PID is not 1 we need to check to see if we were launched by
+ // a user systemd daemon. Do that by checking the `/proc/<ppid>/comm`
+ // to see if it ends with `systemd`.
+ var comm_path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const comm_path = std.fmt.bufPrint(&comm_path_buf, "/proc/{d}/comm", .{ppid}) catch {
+ log.err("unable to format comm path for pid {d}", .{ppid});
+ break :linux false;
+ };
+ 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;
+ };
+ defer comm_file.close();
+
+ // The maximum length of the command name is defined by
+ // `TASK_COMM_LEN` in the Linux kernel. This is usually 16
+ // bytes at the time of writing (Jun 2025) so its set to that.
+ // Also, since we only care to compare to "systemd", anything
+ // longer can be assumed to not be systemd.
+ const TASK_COMM_LEN = 16;
+ var comm_data_buf: [TASK_COMM_LEN]u8 = undefined;
+ const comm_size = comm_file.readAll(&comm_data_buf) catch {
+ log.err("problems reading from '{s}'", .{comm_path});
+ break :linux false;
+ };
+ const comm_data = comm_data_buf[0..comm_size];
+
+ break :linux std.mem.eql(
+ u8,
+ std.mem.trimRight(u8, comm_data, "\n"),
+ "systemd",
+ );
+ },
+
+ // No other system supports systemd so always return false.
+ else => false,
+ };
+}
+
+/// systemd notifications. Used by Ghostty to inform systemd of the state of the
+/// process. Currently only used to notify systemd that we are ready and that
+/// configuration reloading has started.
+///
+/// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html
+///
+/// These functions were re-implemented in Zig instead of using the `libsystemd`
+/// library to avoid the complexity of another external dependency, as well as
+/// to take advantage of Zig features like `comptime` to ensure minimal impact
+/// on non-Linux systems (like FreeBSD) that will never support `systemd`.
+///
+/// Linux systems that do not use `systemd` should not be impacted as they
+/// should never start Ghostty with the `NOTIFY_SOCKET` environment variable set
+/// and these functions essentially become a no-op.
+///
+/// See `systemd`'s [Interface Portability and Stability Promise](https://systemd.io/PORTABILITY_AND_STABILITY/)
+/// for assurances that the interfaces used here will be supported and stable for
+/// the long term.
+pub const notify = struct {
+ /// Send the given message to the UNIX socket specified in the NOTIFY_SOCKET
+ /// environment variable. If there NOTIFY_SOCKET environment variable does
+ /// not exist then no message is sent.
+ fn send(message: []const u8) void {
+ // systemd is Linux-only so this is a no-op anywhere else
+ if (comptime builtin.os.tag != .linux) return;
+
+ // Get the socket address that should receive notifications.
+ 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;
+
+ // The socket address must be a path or an abstract socket.
+ if (socket_path[0] != '/' and socket_path[0] != '@') {
+ log.warn("only AF_UNIX sockets with path or abstract namespace addresses are supported!", .{});
+ return;
+ }
+
+ var socket_address: std.os.linux.sockaddr.un = undefined;
+
+ // Error out if the supplied socket path is too long.
+ if (socket_address.path.len < socket_path.len) {
+ log.warn("NOTIFY_SOCKET path is too long!", .{});
+ return;
+ }
+
+ socket_address.family = std.os.linux.AF.UNIX;
+
+ @memcpy(socket_address.path[0..socket_path.len], socket_path);
+ socket_address.path[socket_path.len] = 0;
+
+ const socket: std.os.linux.socket_t = socket: {
+ const rc = std.os.linux.socket(
+ std.os.linux.AF.UNIX,
+ std.os.linux.SOCK.DGRAM | std.os.linux.SOCK.CLOEXEC,
+ 0,
+ );
+ switch (linuxErrnoFromSyscall(rc)) {
+ .SUCCESS => break :socket @intCast(rc),
+ else => |e| {
+ log.warn("creating socket failed: {s}", .{@tagName(e)});
+ return;
+ },
+ }
+ };
+
+ defer _ = std.os.linux.close(socket);
+
+ connect: {
+ const rc = std.os.linux.connect(
+ socket,
+ &socket_address,
+ @offsetOf(std.os.linux.sockaddr.un, "path") + socket_address.path.len,
+ );
+ switch (linuxErrnoFromSyscall(rc)) {
+ .SUCCESS => break :connect,
+ else => |e| {
+ log.warn("unable to connect to notify socket: {s}", .{@tagName(e)});
+ return;
+ },
+ }
+ }
+
+ write: {
+ const rc = std.os.linux.write(socket, message.ptr, message.len);
+ switch (linuxErrnoFromSyscall(rc)) {
+ .SUCCESS => {
+ const written = rc;
+ if (written < message.len) {
+ log.warn("short write to notify socket: {d} < {d}", .{ rc, message.len });
+ return;
+ }
+ break :write;
+ },
+ else => |e| {
+ log.warn("unable to write to notify socket: {s}", .{@tagName(e)});
+ return;
+ },
+ }
+ }
+ }
+
+ /// Tell systemd that we are ready or that we are finished reloading.
+ /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#READY=1
+ pub fn ready() void {
+ if (comptime builtin.os.tag != .linux) return;
+
+ send("READY=1");
+ }
+
+ /// Tell systemd that we have started reloading our configuration.
+ /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#RELOADING=1
+ /// and: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#MONOTONIC_USEC=%E2%80%A6
+ pub fn reloading() void {
+ if (comptime builtin.os.tag != .linux) return;
+
+ 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| {
+ log.err("unable to format reloading message: {}", .{err});
+ return;
+ };
+
+ 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/uri.zig b/vendor/libghostty-vt/src/os/uri.zig
new file mode 100644
index 0000000..3d67487
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/uri.zig
@@ -0,0 +1,204 @@
+const std = @import("std");
+
+pub const ParseOptions = struct {
+ /// Parse MAC addresses in the host component.
+ ///
+ /// This is useful when the "Private Wi-Fi address" is enabled on macOS,
+ /// which sets the hostname to a rotating MAC address (12:34:56:ab:cd:ef).
+ mac_address: bool = false,
+
+ /// Return the full, raw, unencoded path string. Any query and fragment
+ /// values will be return as part of the path instead of as distinct
+ /// fields.
+ raw_path: bool = false,
+};
+
+pub const ParseError = std.Uri.ParseError || error{InvalidMacAddress};
+
+/// Parses a URI from the given string.
+///
+/// This extends std.Uri.parse with some additional ParseOptions.
+pub fn parse(text: []const u8, options: ParseOptions) ParseError!std.Uri {
+ var uri = std.Uri.parse(text) catch |err| uri: {
+ // We can attempt to re-parse the text as a URI that has a MAC address
+ // in its host field (which tripped up std.Uri.parse's port parsing):
+ //
+ // file://12:34:56:78:90:aa/path/to/file
+ // ^^ InvalidPort
+ //
+ if (err != error.InvalidPort or !options.mac_address) return err;
+
+ // We can assume that the initial Uri.parse already validated the
+ // scheme, so we only need to find its bounds within the string.
+ const scheme_end = std.mem.indexOf(u8, text, "://") orelse {
+ return error.InvalidFormat;
+ };
+ const scheme = text[0..scheme_end];
+
+ // We similarly find the bounds of the host component by looking
+ // for the first slash (/) after the scheme. This is all we need
+ // for this case because the resulting slice can be unambiguously
+ // determined to be a MAC address (or not).
+ const host_start = scheme_end + "://".len;
+ const host_end = std.mem.indexOfScalarPos(u8, text, host_start, '/') orelse text.len;
+ const mac_address = text[host_start..host_end];
+ if (!isValidMacAddress(mac_address)) return error.InvalidMacAddress;
+
+ // Parse the rest of the text (starting with the path component) as a
+ // partial URI and then add our MAC address as its host component.
+ var uri = try std.Uri.parseAfterScheme(scheme, text[host_end..]);
+ uri.host = .{ .percent_encoded = mac_address };
+ break :uri uri;
+ };
+
+ // When MAC address parsing is enabled, we need to handle the case where
+ // std.Uri.parse parsed the address's last octet as a numeric port number.
+ // We use a few heuristics to identify this case (14 characters, 4 colons)
+ // and then "repair" the result by reassign the .host component to the full
+ // MAC address and clearing the .port component.
+ //
+ // 12:34:56:78:90:99 -> [12:34:56:78:90, 99] -> 12:34:56:78:90:99
+ // (original host) (parsed host + port) (restored host)
+ //
+ if (options.mac_address and uri.host != null) mac: {
+ const host = uri.host.?.percent_encoded;
+ if (host.len != 14 or std.mem.count(u8, host, ":") != 4) break :mac;
+
+ const port = uri.port orelse break :mac;
+ if (port > 99) break :mac;
+
+ // std.Uri.parse returns slices pointing into the original text string.
+ const host_start = @intFromPtr(host.ptr) - @intFromPtr(text.ptr);
+ const path_start = @intFromPtr(uri.path.percent_encoded.ptr) - @intFromPtr(text.ptr);
+ const mac_address = text[host_start..path_start];
+ if (!isValidMacAddress(mac_address)) return error.InvalidMacAddress;
+
+ uri.host = .{ .percent_encoded = mac_address };
+ uri.port = null;
+ }
+
+ // When the raw_path option is active, return everything after the authority
+ // (host) in the .path component, including any query and fragment values.
+ if (options.raw_path) {
+ // std.Uri.parse returns slices pointing into the original text string.
+ const path_start = @intFromPtr(uri.path.percent_encoded.ptr) - @intFromPtr(text.ptr);
+ uri.path = .{ .raw = text[path_start..] };
+ uri.query = null;
+ uri.fragment = null;
+ }
+
+ return uri;
+}
+
+test "parse: mac_address" {
+ const testing = @import("std").testing;
+
+ // Numeric MAC address without a port
+ const uri1 = try parse("file://00:12:34:56:78:90/path", .{ .mac_address = true });
+ try testing.expectEqualStrings("file", uri1.scheme);
+ try testing.expectEqualStrings("00:12:34:56:78:90", uri1.host.?.percent_encoded);
+ try testing.expectEqualStrings("/path", uri1.path.percent_encoded);
+ try testing.expectEqual(null, uri1.port);
+
+ // Numeric MAC address with a port
+ const uri2 = try parse("file://00:12:34:56:78:90:999/path", .{ .mac_address = true });
+ try testing.expectEqualStrings("file", uri2.scheme);
+ try testing.expectEqualStrings("00:12:34:56:78:90", uri2.host.?.percent_encoded);
+ try testing.expectEqualStrings("/path", uri2.path.percent_encoded);
+ try testing.expectEqual(999, uri2.port);
+
+ // Alphabetic MAC address without a port
+ const uri3 = try parse("file://ab:cd:ef:ab:cd:ef/path", .{ .mac_address = true });
+ try testing.expectEqualStrings("file", uri3.scheme);
+ try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri3.host.?.percent_encoded);
+ try testing.expectEqualStrings("/path", uri3.path.percent_encoded);
+ try testing.expectEqual(null, uri3.port);
+
+ // Alphabetic MAC address with a port
+ const uri4 = try parse("file://ab:cd:ef:ab:cd:ef:999/path", .{ .mac_address = true });
+ try testing.expectEqualStrings("file", uri4.scheme);
+ try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri4.host.?.percent_encoded);
+ try testing.expectEqualStrings("/path", uri4.path.percent_encoded);
+ try testing.expectEqual(999, uri4.port);
+
+ // Numeric MAC address without a path component
+ const uri5 = try parse("file://00:12:34:56:78:90", .{ .mac_address = true });
+ try testing.expectEqualStrings("file", uri5.scheme);
+ try testing.expectEqualStrings("00:12:34:56:78:90", uri5.host.?.percent_encoded);
+ try testing.expect(uri5.path.isEmpty());
+
+ // Alphabetic MAC address without a path component
+ const uri6 = try parse("file://ab:cd:ef:ab:cd:ef", .{ .mac_address = true });
+ try testing.expectEqualStrings("file", uri6.scheme);
+ try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri6.host.?.percent_encoded);
+ try testing.expect(uri6.path.isEmpty());
+
+ // Invalid MAC addresses
+ try testing.expectError(error.InvalidMacAddress, parse(
+ "file://zz:zz:zz:zz:zz:00/path",
+ .{ .mac_address = true },
+ ));
+ try testing.expectError(error.InvalidMacAddress, parse(
+ "file://zz:zz:zz:zz:zz:zz/path",
+ .{ .mac_address = true },
+ ));
+}
+
+test "parse: raw_path" {
+ const testing = @import("std").testing;
+
+ const text = "file://localhost/path??#fragment";
+ var buf: [256]u8 = undefined;
+
+ const uri1 = try parse(text, .{ .raw_path = false });
+ try testing.expectEqualStrings("file", uri1.scheme);
+ try testing.expectEqualStrings("localhost", uri1.host.?.percent_encoded);
+ try testing.expectEqualStrings("/path", try uri1.path.toRaw(&buf));
+ try testing.expectEqualStrings("?", uri1.query.?.percent_encoded);
+ try testing.expectEqualStrings("fragment", uri1.fragment.?.percent_encoded);
+
+ const uri2 = try parse(text, .{ .raw_path = true });
+ try testing.expectEqualStrings("file", uri2.scheme);
+ try testing.expectEqualStrings("localhost", uri2.host.?.percent_encoded);
+ try testing.expectEqualStrings("/path??#fragment", try uri2.path.toRaw(&buf));
+ try testing.expectEqual(null, uri2.query);
+ try testing.expectEqual(null, uri2.fragment);
+
+ const uri3 = try parse("file://localhost", .{ .raw_path = true });
+ try testing.expectEqualStrings("file", uri3.scheme);
+ try testing.expectEqualStrings("localhost", uri3.host.?.percent_encoded);
+ try testing.expect(uri3.path.isEmpty());
+ try testing.expectEqual(null, uri3.query);
+ try testing.expectEqual(null, uri3.fragment);
+}
+
+/// Checks if a string represents a valid MAC address, e.g. 12:34:56:ab:cd:ef.
+fn isValidMacAddress(s: []const u8) bool {
+ if (s.len != 17) return false;
+
+ for (s, 0..) |c, i| {
+ if (i % 3 == 2) {
+ if (c != ':') return false;
+ } else {
+ switch (c) {
+ '0'...'9', 'A'...'F', 'a'...'f' => {},
+ else => return false,
+ }
+ }
+ }
+
+ return true;
+}
+
+test isValidMacAddress {
+ const testing = @import("std").testing;
+
+ try testing.expect(isValidMacAddress("01:23:45:67:89:Aa"));
+ try testing.expect(isValidMacAddress("Aa:Bb:Cc:Dd:Ee:Ff"));
+
+ try testing.expect(!isValidMacAddress(""));
+ try testing.expect(!isValidMacAddress("00:23:45"));
+ try testing.expect(!isValidMacAddress("00:23:45:Xx:Yy:Zz"));
+ try testing.expect(!isValidMacAddress("01-23-45-67-89-Aa"));
+ try testing.expect(!isValidMacAddress("01:23:45:67:89:Aa:Bb"));
+}
diff --git a/vendor/libghostty-vt/src/os/wasm.zig b/vendor/libghostty-vt/src/os/wasm.zig
new file mode 100644
index 0000000..3d0b90e
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/wasm.zig
@@ -0,0 +1,25 @@
+//! This file contains helpers for wasm compilation.
+const std = @import("std");
+const builtin = @import("builtin");
+const options = @import("build_options");
+
+comptime {
+ if (!builtin.target.cpu.arch.isWasm()) {
+ @compileError("wasm.zig should only be analyzed for wasm32 builds");
+ }
+}
+
+/// True if we're in shared memory mode. If true, then the memory buffer
+/// in JS will be backed by a SharedArrayBuffer and some behaviors change.
+pub const shared_mem = options.wasm_shared;
+
+/// The allocator to use in wasm environments.
+///
+/// The return values of this should NOT be sent to the host environment
+/// unless toHostOwned is called on them. In this case, the caller is expected
+/// to call free. If a pointer is NOT host-owned, then the wasm module is
+/// expected to call the normal alloc.free/destroy functions.
+pub const alloc = if (builtin.is_test)
+ std.testing.allocator
+else
+ std.heap.wasm_allocator;
diff --git a/vendor/libghostty-vt/src/os/wasm/log.zig b/vendor/libghostty-vt/src/os/wasm/log.zig
new file mode 100644
index 0000000..faa885c
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/wasm/log.zig
@@ -0,0 +1,45 @@
+const std = @import("std");
+const wasm = @import("../wasm.zig");
+
+// Use the correct implementation
+pub const log = Freestanding.log;
+
+/// Freestanding implementation calls an extern "log" function.
+pub const Freestanding = struct {
+ // The function std.log will call.
+ pub fn log(
+ comptime level: std.log.Level,
+ comptime scope: @TypeOf(.EnumLiteral),
+ comptime format: []const u8,
+ args: anytype,
+ ) void {
+ // The buffer for putting our log message. We try to use a stack-allocated
+ // buffer first because we want to avoid allocation. If we are logging
+ // an error DUE to an OOM, allocating will of course fail and we'd like
+ // to see the error message so we prefer to use this.
+ var buf: [2048]u8 = undefined;
+
+ // Build the string
+ const level_txt = comptime level.asText();
+ const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
+ const txt = level_txt ++ prefix ++ format;
+
+ // Format. We attempt to use a stack-allocated string first and if that
+ // fails we'll try to allocate.
+ var allocated: bool = false;
+ const str = nosuspend std.fmt.bufPrint(&buf, txt, args) catch str: {
+ allocated = true;
+ break :str std.fmt.allocPrint(wasm.alloc, txt, args) catch return;
+ };
+ defer if (allocated) wasm.alloc.free(str);
+
+ // Send it over to the JS side
+ JS.log(str.ptr, str.len);
+ }
+
+ // We wrap our externs in this namespace so we can reuse symbols, otherwise
+ // "log" would collide.
+ const JS = struct {
+ extern "env" fn log(ptr: [*]const u8, len: usize) void;
+ };
+};
diff --git a/vendor/libghostty-vt/src/os/wasm/target.zig b/vendor/libghostty-vt/src/os/wasm/target.zig
new file mode 100644
index 0000000..a6a29e2
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/wasm/target.zig
@@ -0,0 +1,20 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const options = @import("build_options");
+
+/// The wasm target platform. This is used to toggle certain features
+/// on and off since the standard triple target is often not specific
+/// enough (i.e. we can't tell wasm32-freestanding is for browser or not).
+pub const Target = enum {
+ browser,
+};
+
+/// Our specific target platform.
+pub const target: ?Target = if (!builtin.target.cpu.arch.isWasm()) null else target: {
+ const result = @as(Target, @enumFromInt(@intFromEnum(options.wasm_target)));
+ // This maybe isn't necessary but I don't know if enums without a specific
+ // tag type and value are guaranteed to be the same between build.zig
+ // compilation and our own source compilation so I have this just in case.
+ std.debug.assert(std.mem.eql(u8, @tagName(result), @tagName(options.wasm_target)));
+ break :target result;
+};
diff --git a/vendor/libghostty-vt/src/os/windows.zig b/vendor/libghostty-vt/src/os/windows.zig
new file mode 100644
index 0000000..feef8ce
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/windows.zig
@@ -0,0 +1,273 @@
+const std = @import("std");
+const windows = std.os.windows;
+
+// 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 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 DWORD_PTR = windows.DWORD_PTR;
+pub const HANDLE = windows.HANDLE;
+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 STARTF_USESTDHANDLES = windows.STARTF_USESTDHANDLES;
+pub const STARTUPINFOW = windows.STARTUPINFOW;
+
+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);
+
+// Bit-field and enum constant values
+pub const CREATE_UNICODE_ENVIRONMENT = 0x00000400;
+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;
+pub const FILE_NON_DIRECTORY_FILE = 0x00000040;
+pub const FILE_SHARE_READ = 0x00000001;
+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 WAIT_FAILED = 0xFFFFFFFF;
+
+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 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: *HANDLE,
+ hWritePipe: *HANDLE,
+ lpPipeAttributes: ?*const SECURITY_ATTRIBUTES,
+ nSize: DWORD,
+ ) callconv(.winapi) BOOL;
+ pub extern "kernel32" fn CreatePseudoConsole(
+ size: COORD,
+ hInput: HANDLE,
+ hOutput: HANDLE,
+ dwFlags: DWORD,
+ phPC: *HPCON,
+ ) 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: DWORD,
+ dwFlags: DWORD,
+ lpSize: *SIZE_T,
+ ) callconv(.winapi) BOOL;
+ pub extern "kernel32" fn UpdateProcThreadAttribute(
+ lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST,
+ dwFlags: DWORD,
+ Attribute: DWORD_PTR,
+ lpValue: PVOID,
+ cbSize: SIZE_T,
+ lpPreviousValue: ?PVOID,
+ lpReturnSize: ?*SIZE_T,
+ ) callconv(.winapi) BOOL;
+ pub extern "kernel32" fn PeekNamedPipe(
+ 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: ?LPWSTR,
+ lpCommandLine: ?LPWSTR,
+ lpProcessAttributes: ?*SECURITY_ATTRIBUTES,
+ lpThreadAttributes: ?*SECURITY_ATTRIBUTES,
+ bInheritHandles: BOOL,
+ dwCreationFlags: DWORD,
+ lpEnvironment: ?*anyopaque,
+ 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: 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: 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;
+ 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;
+ };
+ 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 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
new file mode 100644
index 0000000..9202958
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/xdg.zig
@@ -0,0 +1,269 @@
+//! Implementation of the XDG Base Directory specification
+//! (https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
+
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+const posix = std.posix;
+const homedir = @import("homedir.zig");
+
+pub const Options = struct {
+ /// Subdirectories to join to the base. This avoids extra allocations
+ /// when building up the directory. This is commonly the application.
+ subdir: ?[]const u8 = null,
+
+ /// The home directory for the user. If this is not set, we will attempt
+ /// to look it up which is an expensive process. By setting this, you can
+ /// avoid lookups.
+ home: ?[]const u8 = null,
+};
+
+/// Get the XDG user config directory. The returned value is allocated.
+pub fn config(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",
+ });
+}
+
+/// Get the XDG cache directory. The returned value is allocated.
+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",
+ });
+}
+
+/// Get the XDG state directory. The returned value is allocated.
+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",
+ });
+}
+
+const InternalOptions = struct {
+ env: []const u8,
+ windows_env: []const u8,
+ default_subdir: []const u8,
+};
+
+/// Unified helper to get XDG directories that follow a common pattern.
+fn dir(
+ io: std.Io,
+ alloc: Allocator,
+ environ_map: *const std.process.Environ.Map,
+ opts: Options,
+ internal_opts: InternalOptions,
+) ![]u8 {
+ // If we have a cached home dir, use that.
+ if (opts.home) |home| {
+ return try std.fs.path.join(alloc, &[_][]const u8{
+ home,
+ internal_opts.default_subdir,
+ opts.subdir orelse "",
+ });
+ }
+
+ // First check the env var. On Windows we 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 "",
+ };
+
+ 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,
+ subdir,
+ });
+ }
+
+ return try alloc.dupe(u8, env);
+ }
+
+ // Get our home dir
+ var buf: [1024]u8 = undefined;
+ if (try homedir.home(io, environ_map, &buf)) |home| {
+ return try std.fs.path.join(alloc, &[_][]const u8{
+ home,
+ internal_opts.default_subdir,
+ opts.subdir orelse "",
+ });
+ }
+
+ return error.NoHomeDir;
+}
+
+/// Parses the xdg-terminal-exec specification. This expects argv[0] to
+/// be "xdg-terminal-exec".
+pub fn parseTerminalExec(argv: []const [*:0]const u8) ?[]const [*:0]const u8 {
+ if (!std.mem.eql(
+ u8,
+ std.fs.path.basename(std.mem.sliceTo(argv[0], 0)),
+ "xdg-terminal-exec",
+ )) return null;
+
+ // We expect at least one argument
+ if (argv.len < 2) return &.{};
+
+ // If the first argument is "-e" we skip it.
+ const start: usize = if (std.mem.eql(u8, std.mem.sliceTo(argv[1], 0), "-e")) 2 else 1;
+ return argv[start..];
+}
+
+test {
+ const testing = std.testing;
+ const io = testing.io;
+ const alloc = testing.allocator;
+ var environ_map = try testing.environ.createMap(alloc);
+ defer environ_map.deinit();
+
+ {
+ const value = try config(io, alloc, &environ_map, .{});
+ defer alloc.free(value);
+ try testing.expect(value.len > 0);
+ }
+}
+
+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(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);
+ try testing.expectEqualStrings(expected, cache_path);
+ }
+
+ // Test with subdir
+ {
+ const cache_path = try cache(io, alloc, &environ_map, .{
+ .home = mock_home,
+ .subdir = "ghostty",
+ });
+ defer alloc.free(cache_path);
+ const expected = try std.fs.path.join(alloc, &.{ mock_home, ".cache", "ghostty" });
+ defer alloc.free(expected);
+ try testing.expectEqualStrings(expected, cache_path);
+ }
+ }
+}
+
+test "fallback when xdg env empty" {
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
+
+ const io = std.testing.io;
+ const alloc = std.testing.allocator;
+
+ const DirCase = struct {
+ name: [:0]const u8,
+ func: fn (std.Io, Allocator, *std.process.Environ.Map, Options) anyerror![]u8,
+ default_subdir: []const u8,
+ };
+
+ const cases = [_]DirCase{
+ .{ .name = "XDG_CONFIG_HOME", .func = config, .default_subdir = ".config" },
+ .{ .name = "XDG_CACHE_HOME", .func = cache, .default_subdir = ".cache" },
+ .{ .name = "XDG_STATE_HOME", .func = state, .default_subdir = ".local/state" },
+ };
+
+ inline for (cases) |case| {
+ 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,
+ case.default_subdir,
+ });
+ defer alloc.free(expected);
+
+ // Test with empty string - should fallback to home
+ 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);
+ }
+}
+
+test "fallback when xdg env empty and subdir" {
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
+
+ const io = std.testing.io;
+ const alloc = std.testing.allocator;
+
+ const DirCase = struct {
+ name: [:0]const u8,
+ func: fn (std.Io, Allocator, *const std.process.Environ.Map, Options) anyerror![]u8,
+ default_subdir: []const u8,
+ };
+
+ const cases = [_]DirCase{
+ .{ .name = "XDG_CONFIG_HOME", .func = config, .default_subdir = ".config" },
+ .{ .name = "XDG_CACHE_HOME", .func = cache, .default_subdir = ".cache" },
+ .{ .name = "XDG_STATE_HOME", .func = state, .default_subdir = ".local/state" },
+ };
+
+ inline for (cases) |case| {
+ 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,
+ case.default_subdir,
+ "ghostty",
+ });
+ defer alloc.free(expected);
+
+ // Test with empty string - should fallback to home
+ 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);
+ }
+}
+
+test parseTerminalExec {
+ const testing = std.testing;
+
+ {
+ const actual = parseTerminalExec(&.{ "a", "b", "c" });
+ try testing.expect(actual == null);
+ }
+ {
+ const actual = parseTerminalExec(&.{"xdg-terminal-exec"}).?;
+ try testing.expectEqualSlices([*:0]const u8, actual, &.{});
+ }
+ {
+ const actual = parseTerminalExec(&.{ "xdg-terminal-exec", "a", "b", "c" }).?;
+ try testing.expectEqualSlices([*:0]const u8, actual, &.{ "a", "b", "c" });
+ }
+ {
+ const actual = parseTerminalExec(&.{ "xdg-terminal-exec", "-e", "a", "b", "c" }).?;
+ try testing.expectEqualSlices([*:0]const u8, actual, &.{ "a", "b", "c" });
+ }
+ {
+ const actual = parseTerminalExec(&.{ "xdg-terminal-exec", "a", "-e", "b", "c" }).?;
+ try testing.expectEqualSlices([*:0]const u8, actual, &.{ "a", "-e", "b", "c" });
+ }
+}