aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/libghostty-vt/src/pty.zig
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/pty.zig
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/pty.zig')
-rw-r--r--vendor/libghostty-vt/src/pty.zig519
1 files changed, 519 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/pty.zig b/vendor/libghostty-vt/src/pty.zig
new file mode 100644
index 0000000..de42ef5
--- /dev/null
+++ b/vendor/libghostty-vt/src/pty.zig
@@ -0,0 +1,519 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const windows = @import("os/main.zig").windows;
+const posix = std.posix;
+const assert = @import("quirks.zig").inlineAssert;
+
+const log = std.log.scoped(.pty);
+
+/// Redeclare this winsize struct so we can just use a Zig struct. This
+/// layout should be correct on all tested platforms. The defaults on this
+/// are some reasonable screen size but you should probably not use them.
+pub const winsize = extern struct {
+ ws_row: u16 = 100,
+ ws_col: u16 = 80,
+ ws_xpixel: u16 = 800,
+ ws_ypixel: u16 = 600,
+};
+
+pub const Pty = switch (builtin.os.tag) {
+ .windows => WindowsPty,
+ .ios => NullPty,
+ else => PosixPty,
+};
+
+/// The modes of a pty. Not all of these modes are supported on
+/// all platforms but all platforms share the same mode struct.
+///
+/// The default values of fields in this struct are set to the
+/// most typical values for a pty. This makes it easier for cross-platform
+/// code which doesn't support all of the modes to work correctly.
+pub const Mode = packed struct {
+ /// ICANON on POSIX
+ canonical: bool = true,
+
+ /// ECHO on POSIX
+ echo: bool = true,
+};
+
+pub const ProcessInfo = enum {
+ /// The PID of the process that controls the PTY.
+ foreground_pid,
+ /// Gets the name of the slave PTY. Returned name points to an internal buffer
+ /// so it should not be modified or freed.
+ tty_name,
+
+ pub fn Type(comptime info: ProcessInfo) type {
+ return switch (info) {
+ .foreground_pid => u64,
+ .tty_name => [:0]const u8,
+ };
+ }
+};
+
+// A pty implementation that does nothing.
+//
+// TODO: This should be removed. This is only temporary until we have
+// a termio that doesn't use a pty. This isn't used in any user-facing
+// artifacts, this is just a stopgap to get compilation to work on iOS.
+const NullPty = struct {
+ pub const Error = OpenError || GetModeError || SetSizeError || ChildPreExecError;
+
+ pub const Fd = posix.fd_t;
+
+ master: Fd,
+ slave: Fd,
+
+ pub const OpenError = error{};
+
+ pub fn open(size: winsize) OpenError!Pty {
+ _ = size;
+ return .{ .master = 0, .slave = 0 };
+ }
+
+ pub fn deinit(self: *Pty) void {
+ _ = self;
+ }
+
+ pub const GetModeError = error{GetModeFailed};
+
+ pub fn getMode(self: Pty) GetModeError!Mode {
+ _ = self;
+ return .{};
+ }
+
+ pub const SetSizeError = error{};
+
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
+ _ = self;
+ _ = size;
+ }
+
+ pub const ChildPreExecError = error{};
+
+ pub fn childPreExec(self: Pty) ChildPreExecError!void {
+ _ = self;
+ }
+
+ /// Get information about the process(es) attached to the PTY. Returns
+ /// `null` if there was an error getting the information or the information
+ /// is not available on a particular platform.
+ pub fn getProcessInfo(_: *Pty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
+ return null;
+ }
+};
+
+/// Posix PTY creation and management. This is just a thin layer on top
+/// of Posix syscalls. The caller is responsible for detail-oriented handling
+/// of the returned file handles.
+const PosixPty = struct {
+ pub const Error = OpenError || GetModeError || GetSizeError || SetSizeError || ChildPreExecError;
+
+ pub const Fd = posix.fd_t;
+
+ const c = @import("pty-c");
+
+ /// The file descriptors for the master and slave side of the pty.
+ /// The slave side is never closed automatically by this struct
+ /// so the caller is responsible for closing it if things
+ /// go wrong.
+ master: Fd,
+ slave: Fd,
+
+ /// Buffer for storage of slave tty name so that we don't have to recompute
+ /// it every time we need it.
+ tty_name_buf: [std.fs.max_path_bytes:0]u8 = undefined,
+ /// The name of slave tty. If `null` it has not yet been computed or
+ /// may not be available. Should not be accessed directly, but through
+ /// `self.getProcessInfo(.tty_name)`
+ tty_name: ?[:0]const u8 = null,
+
+ pub const OpenError = error{OpenptyFailed};
+
+ /// Open a new PTY with the given initial size.
+ pub fn open(size: winsize) OpenError!Pty {
+ // Need to copy so that it becomes non-const.
+ var sizeCopy = size;
+
+ var master_fd: Fd = undefined;
+ var slave_fd: Fd = undefined;
+ if (c.openpty(
+ &master_fd,
+ &slave_fd,
+ null,
+ null,
+ @ptrCast(&sizeCopy),
+ ) < 0)
+ return error.OpenptyFailed;
+ errdefer {
+ _ = posix.system.close(master_fd);
+ _ = posix.system.close(slave_fd);
+ }
+
+ // Set CLOEXEC on the master fd, only the slave fd should be inherited
+ // by the child process (shell/command).
+ cloexec: {
+ const flags = posix.system.fcntl(master_fd, posix.F.GETFD);
+ if (flags == -1) {
+ log.warn("error getting flags for master fd err=E{s}", .{@tagName(posix.errno(-1))});
+ break :cloexec;
+ }
+
+ switch (posix.errno(posix.system.fcntl(
+ master_fd,
+ posix.F.SETFD,
+ flags | posix.FD_CLOEXEC,
+ ))) {
+ .SUCCESS => {},
+ else => |e| log.warn("error setting CLOEXEC on master fd err=E{}", .{e}),
+ }
+ }
+
+ // Enable UTF-8 mode. I think this is on by default on Linux but it
+ // is NOT on by default on macOS so we ensure that it is always set.
+ var attrs: c.termios = undefined;
+ if (c.tcgetattr(master_fd, &attrs) != 0)
+ return error.OpenptyFailed;
+ attrs.c_iflag |= c.IUTF8;
+ if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0)
+ return error.OpenptyFailed;
+
+ return .{
+ .master = master_fd,
+ .slave = slave_fd,
+ .tty_name_buf = undefined,
+ .tty_name = null,
+ };
+ }
+
+ pub fn deinit(self: *Pty) void {
+ _ = posix.system.close(self.master);
+ self.* = undefined;
+ }
+
+ pub const GetModeError = error{GetModeFailed};
+
+ pub fn getMode(self: Pty) GetModeError!Mode {
+ var attrs: c.termios = undefined;
+ if (c.tcgetattr(self.master, &attrs) != 0)
+ return error.GetModeFailed;
+
+ return .{
+ .canonical = (attrs.c_lflag & c.ICANON) != 0,
+ .echo = (attrs.c_lflag & c.ECHO) != 0,
+ };
+ }
+
+ pub const GetSizeError = error{IoctlFailed};
+
+ /// Return the size of the pty.
+ pub fn getSize(self: Pty) GetSizeError!winsize {
+ var ws: winsize = undefined;
+ if (c.ioctl(self.master, c.TIOCGWINSZ, @intFromPtr(&ws)) < 0)
+ return error.IoctlFailed;
+
+ return ws;
+ }
+
+ pub const SetSizeError = error{IoctlFailed};
+
+ /// Set the size of the pty.
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
+ if (c.ioctl(self.master, c.TIOCSWINSZ, @intFromPtr(&size)) < 0)
+ return error.IoctlFailed;
+ }
+
+ pub const ChildPreExecError = error{ OperationNotSupported, ProcessGroupFailed, SetControllingTerminalFailed };
+
+ /// This should be called prior to exec in the forked child process
+ /// in order to setup the tty properly.
+ pub fn childPreExec(self: Pty) ChildPreExecError!void {
+ // Reset our signals
+ var sa: posix.Sigaction = .{
+ .handler = .{ .handler = posix.SIG.DFL },
+ .mask = posix.sigemptyset(),
+ .flags = 0,
+ };
+ posix.sigaction(posix.SIG.ABRT, &sa, null);
+ posix.sigaction(posix.SIG.ALRM, &sa, null);
+ posix.sigaction(posix.SIG.BUS, &sa, null);
+ posix.sigaction(posix.SIG.CHLD, &sa, null);
+ posix.sigaction(posix.SIG.FPE, &sa, null);
+ posix.sigaction(posix.SIG.HUP, &sa, null);
+ posix.sigaction(posix.SIG.ILL, &sa, null);
+ posix.sigaction(posix.SIG.INT, &sa, null);
+ posix.sigaction(posix.SIG.PIPE, &sa, null);
+ posix.sigaction(posix.SIG.SEGV, &sa, null);
+ posix.sigaction(posix.SIG.TRAP, &sa, null);
+ posix.sigaction(posix.SIG.TERM, &sa, null);
+ posix.sigaction(posix.SIG.QUIT, &sa, null);
+
+ // Create a new process group
+ if (c.setsid() < 0) return error.ProcessGroupFailed;
+
+ // Set controlling terminal
+ switch (posix.errno(c.ioctl(self.slave, c.TIOCSCTTY, @as(c_ulong, 0)))) {
+ .SUCCESS => {},
+ else => |err| {
+ log.err("error setting controlling terminal errno={}", .{err});
+ return error.SetControllingTerminalFailed;
+ },
+ }
+
+ // Can close master/slave pair now
+ _ = posix.system.close(self.slave);
+ _ = posix.system.close(self.master);
+ }
+
+ /// Get information about the process(es) attached to the PTY. Returns
+ /// `null` if there was an error getting the information or the information
+ /// is not available on a particular platform.
+ pub fn getProcessInfo(self: *PosixPty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
+ return switch (info) {
+ .foreground_pid => {
+ switch (builtin.os.tag) {
+ .linux => {
+ const linux = std.os.linux;
+ var pgrp: i32 = undefined;
+ const rc = linux.tcgetpgrp(self.master, &pgrp);
+ switch (rc) {
+ 0 => return @intCast(pgrp), // SUCCESS
+ else => return null, // Anything else
+ }
+ },
+ else => {
+ const rc = c.tcgetpgrp(self.master);
+ if (rc < 0) return null;
+ return @intCast(rc);
+ },
+ }
+ },
+ .tty_name => {
+ if (self.tty_name) |tty_name| return tty_name;
+
+ switch (builtin.os.tag) {
+ .macos => {
+ // The macOS TIOCPTYGNAME ioctl does not allow us to
+ // specify the length of the buffer passed to it, but
+ // expects it to be at least 128 bytes long.
+ assert(self.tty_name_buf.len >= 128);
+ switch (posix.errno(c.ioctl(self.master, c.TIOCPTYGNAME, @intFromPtr(&self.tty_name_buf)))) {
+ .SUCCESS => {
+ const tty_name: [:0]const u8 = std.mem.sliceTo(&self.tty_name_buf, 0);
+ self.tty_name = tty_name;
+ return tty_name;
+ },
+ else => |err| {
+ log.err("error getting name of slave PTY errno={t}", .{err});
+ return null;
+ },
+ }
+ },
+ .linux => {
+ if (c.ptsname_r(self.master, &self.tty_name_buf, self.tty_name_buf.len) != 0) return null;
+ const tty_name: [:0]const u8 = std.mem.sliceTo(&self.tty_name_buf, 0);
+ self.tty_name = tty_name;
+ return tty_name;
+ },
+ else => return null,
+ }
+ },
+ };
+ }
+};
+
+/// Windows PTY creation and management.
+const WindowsPty = struct {
+ pub const Error = OpenError || GetSizeError || SetSizeError;
+
+ pub const Fd = windows.HANDLE;
+
+ // Process-wide counter for pipe names
+ var pipe_name_counter = std.atomic.Value(u32).init(1);
+
+ out_pipe: windows.HANDLE,
+ in_pipe: windows.HANDLE,
+ out_pipe_pty: windows.HANDLE,
+ in_pipe_pty: windows.HANDLE,
+ pseudo_console: windows.HPCON,
+ size: winsize,
+
+ pub const OpenError = error{Unexpected};
+
+ /// Open a new PTY with the given initial size.
+ pub fn open(size: winsize) OpenError!Pty {
+ var pty: Pty = undefined;
+
+ var pipe_path_buf: [128]u8 = undefined;
+ var pipe_path_buf_w: [128]u16 = undefined;
+ const pipe_path = std.fmt.bufPrintZ(
+ &pipe_path_buf,
+ "\\\\.\\pipe\\LOCAL\\ghostty-pty-{d}-{d}",
+ .{
+ windows.GetCurrentProcessId(),
+ pipe_name_counter.fetchAdd(1, .monotonic),
+ },
+ ) catch unreachable;
+
+ const pipe_path_w_len = std.unicode.utf8ToUtf16Le(
+ &pipe_path_buf_w,
+ pipe_path,
+ ) catch unreachable;
+ pipe_path_buf_w[pipe_path_w_len] = 0;
+ const pipe_path_w = pipe_path_buf_w[0..pipe_path_w_len :0];
+
+ const security_attributes = windows.SECURITY_ATTRIBUTES{
+ .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
+ .bInheritHandle = windows.FALSE,
+ .lpSecurityDescriptor = null,
+ };
+
+ pty.in_pipe = windows.exp.kernel32.CreateNamedPipeW(
+ pipe_path_w.ptr,
+ windows.PIPE_ACCESS_OUTBOUND |
+ windows.FILE_FLAG_FIRST_PIPE_INSTANCE |
+ windows.FILE_FLAG_OVERLAPPED,
+ windows.PIPE_TYPE_BYTE,
+ 1,
+ 4096,
+ 4096,
+ 0,
+ &security_attributes,
+ );
+ if (pty.in_pipe == windows.INVALID_HANDLE_VALUE) {
+ return windows.unexpectedError(windows.GetLastError());
+ }
+ errdefer _ = windows.exp.kernel32.CloseHandle(pty.in_pipe);
+
+ var security_attributes_read = security_attributes;
+ pty.in_pipe_pty = windows.exp.kernel32.CreateFileW(
+ pipe_path_w.ptr,
+ windows.GENERIC_READ,
+ 0,
+ &security_attributes_read,
+ windows.OPEN_EXISTING,
+ windows.FILE_ATTRIBUTE_NORMAL,
+ null,
+ );
+ if (pty.in_pipe_pty == windows.INVALID_HANDLE_VALUE) {
+ return windows.unexpectedError(windows.GetLastError());
+ }
+ errdefer _ = windows.exp.kernel32.CloseHandle(pty.in_pipe_pty);
+
+ // The in_pipe needs to be created as a named pipe, since anonymous
+ // pipes created with CreatePipe do not support overlapped operations,
+ // and the IOCP backend of libxev only uses overlapped operations on files.
+ //
+ // It would be ideal to use CreatePipe here, so that our pipe isn't
+ // visible to any other processes.
+
+ // if (windows.exp.kernel32.CreatePipe(&pty.in_pipe_pty, &pty.in_pipe, null, 0) == 0) {
+ // return windows.unexpectedError(windows.kernel32.GetLastError());
+ // }
+ // errdefer {
+ // _ = windows.CloseHandle(pty.in_pipe_pty);
+ // _ = windows.CloseHandle(pty.in_pipe);
+ // }
+
+ if (windows.exp.kernel32.CreatePipe(&pty.out_pipe, &pty.out_pipe_pty, null, 0) == windows.FALSE) {
+ return windows.unexpectedError(windows.GetLastError());
+ }
+ errdefer {
+ _ = windows.exp.kernel32.CloseHandle(pty.out_pipe);
+ _ = windows.exp.kernel32.CloseHandle(pty.out_pipe_pty);
+ }
+
+ const SetHandleInformation = struct {
+ fn f(hObject: windows.HANDLE) !void {
+ if (windows.exp.kernel32.SetHandleInformation(
+ hObject,
+ windows.HANDLE_FLAG_INHERIT,
+ 0,
+ ) == windows.FALSE) {
+ return windows.unexpectedError(windows.GetLastError());
+ }
+ }
+ };
+
+ try SetHandleInformation.f(pty.in_pipe);
+ try SetHandleInformation.f(pty.in_pipe_pty);
+ try SetHandleInformation.f(pty.out_pipe);
+ try SetHandleInformation.f(pty.out_pipe_pty);
+
+ const result = windows.exp.kernel32.CreatePseudoConsole(
+ .{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) },
+ pty.in_pipe_pty,
+ pty.out_pipe_pty,
+ 0,
+ &pty.pseudo_console,
+ );
+ if (result != windows.S_OK) return error.Unexpected;
+
+ pty.size = size;
+ return pty;
+ }
+
+ pub fn deinit(self: *Pty) void {
+ _ = windows.exp.kernel32.CloseHandle(self.in_pipe_pty);
+ _ = windows.exp.kernel32.CloseHandle(self.in_pipe);
+ _ = windows.exp.kernel32.CloseHandle(self.out_pipe_pty);
+ _ = windows.exp.kernel32.CloseHandle(self.out_pipe);
+ _ = windows.exp.kernel32.ClosePseudoConsole(self.pseudo_console);
+ self.* = undefined;
+ }
+
+ pub const GetSizeError = error{};
+
+ /// Return the size of the pty.
+ pub fn getSize(self: Pty) GetSizeError!winsize {
+ return self.size;
+ }
+
+ pub const SetSizeError = error{ResizeFailed};
+
+ /// Set the size of the pty.
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
+ const result = windows.exp.kernel32.ResizePseudoConsole(
+ self.pseudo_console,
+ .{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) },
+ );
+
+ if (result != windows.S_OK) return error.ResizeFailed;
+ self.size = size;
+ }
+
+ /// Get information about the process(es) attached to the PTY. Returns
+ /// `null` if there was an error getting the information or the information
+ /// is not available on a particular platform.
+ pub fn getProcessInfo(_: *WindowsPty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
+ return null;
+ }
+};
+
+test {
+ const testing = std.testing;
+ var ws: winsize = .{
+ .ws_row = 50,
+ .ws_col = 80,
+ .ws_xpixel = 1,
+ .ws_ypixel = 1,
+ };
+
+ var pty = try Pty.open(ws);
+ defer pty.deinit();
+
+ // Initialize size should match what we gave it
+ try testing.expectEqual(ws, try pty.getSize());
+
+ // Can set and read new sizes
+ ws.ws_row *= 2;
+ try pty.setSize(ws);
+ try testing.expectEqual(ws, try pty.getSize());
+
+ switch (builtin.os.tag) {
+ .freebsd => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/")),
+ .linux => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/pts/")),
+ .macos => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/")),
+ else => try testing.expect(pty.getProcessInfo(.tty_name) == null),
+ }
+}