From 89ae0ea6ef089389aec0011e53c9b00e380c4e65 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 18 Mar 2026 16:36:18 -0500 Subject: [PATCH 1/7] core: add function to get process info from the surface This adds a function to the core surface to get process information about the process(es) running in the terminal. Currently supported is the PID of the foreground process and the name of the slave PTY. If there is an error retrieving the information, or the platform does not support retieving that information `null` is returned. This will be useful in exposing the foreground PID and slave PTY name to AppleScript or other APIs. --- src/Surface.zig | 25 ++++++ src/build/SharedDeps.zig | 29 +++++++ src/pty.zig | 174 ++++++++++++++++++++++++++++++++------- src/pty/freebsd.c | 4 + src/pty/linux.c | 5 ++ src/pty/macos.c | 17 ++++ src/termio/Exec.zig | 51 ++++++++++++ src/termio/Termio.zig | 25 ++++++ src/termio/backend.zig | 27 ++++++ 9 files changed, 327 insertions(+), 30 deletions(-) create mode 100644 src/pty/freebsd.c create mode 100644 src/pty/linux.c create mode 100644 src/pty/macos.c diff --git a/src/Surface.zig b/src/Surface.zig index ebc2a2f43..8c6064d8c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -6342,6 +6342,31 @@ fn testMouseSelectionIsNull( ); } +pub const ProcessInfo = enum { + /// The PID of the process that currently 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, + }; + } +}; + +/// Get information about the process(es) running within the surface. Returns +/// `null` if there was an error getting the information or the information is +/// not available on a particular platform. +pub fn getProcessInfo(self: *Surface, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { + return switch (info) { + .foreground_pid => self.io.getProcessInfo(.foreground_pid), + .tty_name => self.io.getProcessInfo(.tty_name), + }; +} + test "Surface: selection logic" { // We disable format to make these easier to // read by pairing sets of coordinates per line. diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 4b1ea936d..f14006951 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -135,6 +135,35 @@ pub fn add( // Every exe needs the terminal options self.config.terminalOptions().add(b, step.root_module); + // C imports needed to manage/create PTYs + switch (target.result.os.tag) { + .freebsd => { + const c = b.addTranslateC(.{ + .root_source_file = b.path("src/pty/freebsd.c"), + .target = target, + .optimize = optimize, + }); + step.root_module.addImport("pty-c", c.createModule()); + }, + .linux => { + const c = b.addTranslateC(.{ + .root_source_file = b.path("src/pty/linux.c"), + .target = target, + .optimize = optimize, + }); + step.root_module.addImport("pty-c", c.createModule()); + }, + .macos => { + const c = b.addTranslateC(.{ + .root_source_file = b.path("src/pty/macos.c"), + .target = target, + .optimize = optimize, + }); + step.root_module.addImport("pty-c", c.createModule()); + }, + else => {}, + } + // Freetype. We always include this even if our font backend doesn't // use it because Dear Imgui uses Freetype. _ = b.systemIntegrationOption("freetype", .{}); // Shows it in help diff --git a/src/pty.zig b/src/pty.zig index 1ab88d40f..21f2ba3cc 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -2,6 +2,7 @@ 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); @@ -78,36 +79,39 @@ const NullPty = struct { pub fn childPreExec(self: Pty) ChildPreExecError!void { _ = self; } + + 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, + }; + } + }; + + /// 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; + } }; -/// Linux PTY creation and management. This is just a thin layer on top -/// of Linux syscalls. The caller is responsible for detail-oriented handling +/// 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; - // https://github.com/ziglang/zig/issues/13277 - // Once above is fixed, use `c.TIOCSCTTY` - const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY; - const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ; - const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ; - extern "c" fn setsid() std.c.pid_t; - const c = switch (builtin.os.tag) { - .macos => @cImport({ - @cInclude("sys/ioctl.h"); // ioctl and constants - @cInclude("util.h"); // openpty() - }), - .freebsd => @cImport({ - @cInclude("termios.h"); // ioctl and constants - @cInclude("libutil.h"); // openpty() - }), - else => @cImport({ - @cInclude("sys/ioctl.h"); // ioctl and constants - @cInclude("pty.h"); - }), - }; + 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 @@ -116,6 +120,14 @@ const PosixPty = struct { 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. @@ -141,15 +153,15 @@ const PosixPty = struct { // Set CLOEXEC on the master fd, only the slave fd should be inherited // by the child process (shell/command). cloexec: { - const flags = std.posix.fcntl(master_fd, std.posix.F.GETFD, 0) catch |err| { + const flags = posix.fcntl(master_fd, posix.F.GETFD, 0) catch |err| { log.warn("error getting flags for master fd err={}", .{err}); break :cloexec; }; - _ = std.posix.fcntl( + _ = posix.fcntl( master_fd, - std.posix.F.SETFD, - flags | std.posix.FD_CLOEXEC, + posix.F.SETFD, + flags | posix.FD_CLOEXEC, ) catch |err| { log.warn("error setting CLOEXEC on master fd err={}", .{err}); break :cloexec; @@ -168,6 +180,8 @@ const PosixPty = struct { return .{ .master = master_fd, .slave = slave_fd, + .tty_name_buf = undefined, + .tty_name = null, }; } @@ -194,7 +208,7 @@ const PosixPty = struct { /// Return the size of the pty. pub fn getSize(self: Pty) GetSizeError!winsize { var ws: winsize = undefined; - if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0) + if (c.ioctl(self.master, c.TIOCGWINSZ, @intFromPtr(&ws)) < 0) return error.IoctlFailed; return ws; @@ -204,7 +218,7 @@ const PosixPty = struct { /// Set the size of the pty. pub fn setSize(self: *Pty, size: winsize) SetSizeError!void { - if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0) + if (c.ioctl(self.master, c.TIOCSWINSZ, @intFromPtr(&size)) < 0) return error.IoctlFailed; } @@ -234,10 +248,10 @@ const PosixPty = struct { posix.sigaction(posix.SIG.QUIT, &sa, null); // Create a new process group - if (setsid() < 0) return error.ProcessGroupFailed; + if (c.setsid() < 0) return error.ProcessGroupFailed; // Set controlling terminal - switch (posix.errno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) { + switch (posix.errno(c.ioctl(self.slave, c.TIOCSCTTY, @as(c_ulong, 0)))) { .SUCCESS => {}, else => |err| { log.err("error setting controlling terminal errno={}", .{err}); @@ -249,6 +263,77 @@ const PosixPty = struct { posix.close(self.slave); posix.close(self.master); } + + pub const ProcessInfo = enum { + /// The PID of the process that currently 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, + }; + } + }; + + /// 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 (linux.E.init(rc)) { + .SUCCESS => return @intCast(pgrp), + else => return null, + } + }, + 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. @@ -398,6 +483,28 @@ const WindowsPty = struct { if (result != windows.S_OK) return error.ResizeFailed; self.size = size; } + + pub const ProcessInfo = enum { + /// The PID of the process that currently 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 => []const u8, + }; + } + }; + + /// 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 { @@ -419,4 +526,11 @@ test { 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), + } } diff --git a/src/pty/freebsd.c b/src/pty/freebsd.c new file mode 100644 index 000000000..fbca41852 --- /dev/null +++ b/src/pty/freebsd.c @@ -0,0 +1,4 @@ +#include // ioctl and constants +#include // openpty +#include // ptsname_r +#include // tcgetpgrp diff --git a/src/pty/linux.c b/src/pty/linux.c new file mode 100644 index 000000000..2625c62dc --- /dev/null +++ b/src/pty/linux.c @@ -0,0 +1,5 @@ +#define _GNU_SOURCE // ptsname_r +#include // openpty +#include // ptsname_r +#include // ioctl and constants +#include // tcgetpgrp, setsid diff --git a/src/pty/macos.c b/src/pty/macos.c new file mode 100644 index 000000000..b0125df69 --- /dev/null +++ b/src/pty/macos.c @@ -0,0 +1,17 @@ +#include // ioctl and constants +#include // ioctl and constants for TIOCPTYGNAME +#include +#include // tcgetpgrp +#include // openpty + +#ifndef TIOCSCTTY +#define TIOCSCTTY 536900705 +#endif + +#ifndef TIOCSWINSZ +#define TIOCSWINSZ 2148037735 +#endif + +#ifndef TIOCGWINSZ +#define TIOCGWINSZ 1074295912 +#endif diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index af4df3fef..15a892620 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1226,6 +1226,32 @@ const Subprocess = struct { fn killCommandFlatpak(command: *FlatpakHostCommand) !void { try command.signal(c.SIGHUP, true); } + + pub const ProcessInfo = enum { + /// The PID of the process that currently 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: Subprocess.ProcessInfo) type { + return switch (info) { + .foreground_pid => u64, + .tty_name => [:0]const u8, + }; + } + }; + + /// Get information about the process(es) running within the subprocess. + /// Returns `null` if there was an error getting the information or the + /// information is not available on a particular platform. + pub fn getProcessInfo(self: *Subprocess, comptime info: Subprocess.ProcessInfo) ?Subprocess.ProcessInfo.Type(info) { + const pty = &(self.pty orelse return null); + return switch (info) { + .foreground_pid => pty.getProcessInfo(.foreground_pid), + .tty_name => pty.getProcessInfo(.tty_name), + }; + } }; /// The read thread sits in a loop doing the following pseudo code: @@ -1580,6 +1606,31 @@ fn execCommand( }; } +pub const ProcessInfo = enum { + /// The PID of the process that currently 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, + }; + } +}; + +/// Get information about the process(es) running within the backend. Returns +/// `null` if there was an error getting the information or the information is +/// not available on a particular platform. +pub fn getProcessInfo(self: *Exec, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { + return switch (info) { + .foreground_pid => self.subprocess.getProcessInfo(.foreground_pid), + .tty_name => self.subprocess.getProcessInfo(.tty_name), + }; +} + test "execCommand darwin: shell command" { if (comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest; diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 4a99e8221..aab275605 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -764,3 +764,28 @@ pub const ThreadData = struct { self.* = undefined; } }; + +pub const ProcessInfo = enum { + /// The PID of the process that currently 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, + }; + } +}; + +/// Get information about the process(es) attached to the backend. Returns +/// `null` if there was an error getting the information or the information is +/// not available on a particular platform. +pub fn getProcessInfo(self: *Termio, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { + return switch (info) { + .foreground_pid => self.backend.getProcessInfo(.foreground_pid), + .tty_name => self.backend.getProcessInfo(.tty_name), + }; +} diff --git a/src/termio/backend.zig b/src/termio/backend.zig index ae0e2004f..ef8ac22e6 100644 --- a/src/termio/backend.zig +++ b/src/termio/backend.zig @@ -100,6 +100,33 @@ pub const Backend = union(Kind) { ), } } + + pub const ProcessInfo = enum { + /// The PID of the process that currently 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, + }; + } + }; + + /// Get information about the process(es) attached to the backend. Returns + /// `null` if there was an error getting the information or the information + /// is not available on a particular platform. + pub fn getProcessInfo(self: *Backend, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { + return switch (self.*) { + .exec => |*exec| switch (info) { + .foreground_pid => exec.getProcessInfo(.foreground_pid), + .tty_name => exec.getProcessInfo(.tty_name), + }, + }; + } }; /// Termio thread data. See termio.ThreadData for docs. From 64de418f38f8739ba2259b5e8cc6ba51a74cdc30 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 19 Mar 2026 00:06:59 -0500 Subject: [PATCH 2/7] core: add macos system include path --- src/build/SharedDeps.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index f14006951..f1c874240 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -159,6 +159,12 @@ pub fn add( .target = target, .optimize = optimize, }); + const libc = try std.zig.LibCInstallation.findNative(.{ + .allocator = b.allocator, + .target = target, + .verbose = false, + }); + c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); step.root_module.addImport("pty-c", c.createModule()); }, else => {}, From 264a1a7cddee96b978c40ba538af3444d7a6c550 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 19 Mar 2026 00:10:12 -0500 Subject: [PATCH 3/7] core: fix target for macos libc search --- src/build/SharedDeps.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index f1c874240..463308ffe 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -161,7 +161,7 @@ pub fn add( }); const libc = try std.zig.LibCInstallation.findNative(.{ .allocator = b.allocator, - .target = target, + .target = &target.result, .verbose = false, }); c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); From 2ea6029c7adc08f245ef243a77915ec40c7566b4 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 19 Mar 2026 11:41:14 -0500 Subject: [PATCH 4/7] core: address getProcessInfo feedback * consolidate *.c files into a single file * consolidate ProcessInfo enums into a single enum --- src/Surface.zig | 21 ++------------ src/build/SharedDeps.zig | 41 ++++++++++++--------------- src/pty.c | 34 +++++++++++++++++++++++ src/pty.zig | 60 ++++++++++------------------------------ src/pty/freebsd.c | 4 --- src/pty/linux.c | 5 ---- src/pty/macos.c | 17 ------------ src/termio/Exec.zig | 43 +++------------------------- src/termio/Termio.zig | 21 ++------------ src/termio/backend.zig | 21 ++------------ 10 files changed, 76 insertions(+), 191 deletions(-) create mode 100644 src/pty.c delete mode 100644 src/pty/freebsd.c delete mode 100644 src/pty/linux.c delete mode 100644 src/pty/macos.c diff --git a/src/Surface.zig b/src/Surface.zig index 8c6064d8c..c8055cfee 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -36,6 +36,7 @@ const App = @import("App.zig"); const internal_os = @import("os/main.zig"); const inspectorpkg = @import("inspector/main.zig"); const SurfaceMouse = @import("surface_mouse.zig"); +const ProcessInfo = @import("pty.zig").ProcessInfo; const log = std.log.scoped(.surface); @@ -6342,29 +6343,11 @@ fn testMouseSelectionIsNull( ); } -pub const ProcessInfo = enum { - /// The PID of the process that currently 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, - }; - } -}; - /// Get information about the process(es) running within the surface. Returns /// `null` if there was an error getting the information or the information is /// not available on a particular platform. pub fn getProcessInfo(self: *Surface, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { - return switch (info) { - .foreground_pid => self.io.getProcessInfo(.foreground_pid), - .tty_name => self.io.getProcessInfo(.tty_name), - }; + return self.io.getProcessInfo(info); } test "Surface: selection logic" { diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 463308ffe..1b24c7429 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -137,34 +137,27 @@ pub fn add( // C imports needed to manage/create PTYs switch (target.result.os.tag) { - .freebsd => { + .freebsd, + .linux, + .macos, + => { const c = b.addTranslateC(.{ - .root_source_file = b.path("src/pty/freebsd.c"), + .root_source_file = b.path("src/pty.c"), .target = target, .optimize = optimize, }); - step.root_module.addImport("pty-c", c.createModule()); - }, - .linux => { - const c = b.addTranslateC(.{ - .root_source_file = b.path("src/pty/linux.c"), - .target = target, - .optimize = optimize, - }); - step.root_module.addImport("pty-c", c.createModule()); - }, - .macos => { - const c = b.addTranslateC(.{ - .root_source_file = b.path("src/pty/macos.c"), - .target = target, - .optimize = optimize, - }); - const libc = try std.zig.LibCInstallation.findNative(.{ - .allocator = b.allocator, - .target = &target.result, - .verbose = false, - }); - c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); + c.defineCMacro("ZIG_TARGET", @tagName(target.result.os.tag)); + switch (target.result.os.tag) { + .macos => { + const libc = try std.zig.LibCInstallation.findNative(.{ + .allocator = b.allocator, + .target = &target.result, + .verbose = false, + }); + c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); + }, + else => {}, + } step.root_module.addImport("pty-c", c.createModule()); }, else => {}, diff --git a/src/pty.c b/src/pty.c new file mode 100644 index 000000000..f4f25ad5e --- /dev/null +++ b/src/pty.c @@ -0,0 +1,34 @@ +#if ZIG_TARGET == freebsd + #include // ioctl and constants + #include // openpty + #include // ptsname_r + #include // tcgetpgrp +#endif + +#if ZIG_TARGET == linux + #define _GNU_SOURCE // ptsname_r + #include // openpty + #include // ptsname_r + #include // ioctl and constants + #include // tcgetpgrp, setsid +#endif + +#if ZIG_TARGET == macos + #include // ioctl and constants + #include // ioctl and constants for TIOCPTYGNAME + #include + #include // tcgetpgrp + #include // openpty + + #ifndef tiocsctty + #define tiocsctty 536900705 + #endif + + #ifndef tiocswinsz + #define tiocswinsz 2148037735 + #endif + + #ifndef tiocgwinsz + #define tiocgwinsz 1074295912 + #endif +#endif diff --git a/src/pty.zig b/src/pty.zig index 21f2ba3cc..40277a8ba 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -36,6 +36,21 @@ pub const Mode = packed struct { 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 @@ -80,21 +95,6 @@ const NullPty = struct { _ = self; } - 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, - }; - } - }; - /// 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. @@ -264,21 +264,6 @@ const PosixPty = struct { posix.close(self.master); } - pub const ProcessInfo = enum { - /// The PID of the process that currently 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, - }; - } - }; - /// 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. @@ -484,21 +469,6 @@ const WindowsPty = struct { self.size = size; } - pub const ProcessInfo = enum { - /// The PID of the process that currently 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 => []const u8, - }; - } - }; - /// 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. diff --git a/src/pty/freebsd.c b/src/pty/freebsd.c deleted file mode 100644 index fbca41852..000000000 --- a/src/pty/freebsd.c +++ /dev/null @@ -1,4 +0,0 @@ -#include // ioctl and constants -#include // openpty -#include // ptsname_r -#include // tcgetpgrp diff --git a/src/pty/linux.c b/src/pty/linux.c deleted file mode 100644 index 2625c62dc..000000000 --- a/src/pty/linux.c +++ /dev/null @@ -1,5 +0,0 @@ -#define _GNU_SOURCE // ptsname_r -#include // openpty -#include // ptsname_r -#include // ioctl and constants -#include // tcgetpgrp, setsid diff --git a/src/pty/macos.c b/src/pty/macos.c deleted file mode 100644 index b0125df69..000000000 --- a/src/pty/macos.c +++ /dev/null @@ -1,17 +0,0 @@ -#include // ioctl and constants -#include // ioctl and constants for TIOCPTYGNAME -#include -#include // tcgetpgrp -#include // openpty - -#ifndef TIOCSCTTY -#define TIOCSCTTY 536900705 -#endif - -#ifndef TIOCSWINSZ -#define TIOCSWINSZ 2148037735 -#endif - -#ifndef TIOCGWINSZ -#define TIOCGWINSZ 1074295912 -#endif diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 15a892620..0f35b5787 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -27,6 +27,7 @@ const Pty = ptypkg.Pty; const EnvMap = std.process.EnvMap; const PasswdEntry = internal_os.passwd.Entry; const windows = internal_os.windows; +const ProcessInfo = @import("../pty.zig").ProcessInfo; const log = std.log.scoped(.io_exec); @@ -1227,30 +1228,12 @@ const Subprocess = struct { try command.signal(c.SIGHUP, true); } - pub const ProcessInfo = enum { - /// The PID of the process that currently 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: Subprocess.ProcessInfo) type { - return switch (info) { - .foreground_pid => u64, - .tty_name => [:0]const u8, - }; - } - }; - /// Get information about the process(es) running within the subprocess. /// Returns `null` if there was an error getting the information or the /// information is not available on a particular platform. - pub fn getProcessInfo(self: *Subprocess, comptime info: Subprocess.ProcessInfo) ?Subprocess.ProcessInfo.Type(info) { + pub fn getProcessInfo(self: *Subprocess, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { const pty = &(self.pty orelse return null); - return switch (info) { - .foreground_pid => pty.getProcessInfo(.foreground_pid), - .tty_name => pty.getProcessInfo(.tty_name), - }; + return pty.getProcessInfo(info); } }; @@ -1606,29 +1589,11 @@ fn execCommand( }; } -pub const ProcessInfo = enum { - /// The PID of the process that currently 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, - }; - } -}; - /// Get information about the process(es) running within the backend. Returns /// `null` if there was an error getting the information or the information is /// not available on a particular platform. pub fn getProcessInfo(self: *Exec, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { - return switch (info) { - .foreground_pid => self.subprocess.getProcessInfo(.foreground_pid), - .tty_name => self.subprocess.getProcessInfo(.tty_name), - }; + return self.subprocess.getProcessInfo(info); } test "execCommand darwin: shell command" { diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index aab275605..f05379077 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -19,6 +19,7 @@ const apprt = @import("../apprt.zig"); const internal_os = @import("../os/main.zig"); const windows = internal_os.windows; const configpkg = @import("../config.zig"); +const ProcessInfo = @import("../pty.zig").ProcessInfo; const log = std.log.scoped(.io_exec); @@ -765,27 +766,9 @@ pub const ThreadData = struct { } }; -pub const ProcessInfo = enum { - /// The PID of the process that currently 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, - }; - } -}; - /// Get information about the process(es) attached to the backend. Returns /// `null` if there was an error getting the information or the information is /// not available on a particular platform. pub fn getProcessInfo(self: *Termio, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { - return switch (info) { - .foreground_pid => self.backend.getProcessInfo(.foreground_pid), - .tty_name => self.backend.getProcessInfo(.tty_name), - }; + return self.backend.getProcessInfo(info); } diff --git a/src/termio/backend.zig b/src/termio/backend.zig index ef8ac22e6..c29009acb 100644 --- a/src/termio/backend.zig +++ b/src/termio/backend.zig @@ -4,6 +4,7 @@ const posix = std.posix; const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); const termio = @import("../termio.zig"); +const ProcessInfo = @import("../pty.zig").ProcessInfo; // The preallocation size for the write request pool. This should be big // enough to satisfy most write requests. It must be a power of 2. @@ -101,30 +102,12 @@ pub const Backend = union(Kind) { } } - pub const ProcessInfo = enum { - /// The PID of the process that currently 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, - }; - } - }; - /// Get information about the process(es) attached to the backend. Returns /// `null` if there was an error getting the information or the information /// is not available on a particular platform. pub fn getProcessInfo(self: *Backend, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { return switch (self.*) { - .exec => |*exec| switch (info) { - .foreground_pid => exec.getProcessInfo(.foreground_pid), - .tty_name => exec.getProcessInfo(.tty_name), - }, + .exec => |*exec| exec.getProcessInfo(info), }; } }; From b0789af583c9aac555bdc384041d02123dbbd2e7 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 19 Mar 2026 22:00:24 -0500 Subject: [PATCH 5/7] core: fix c macro comparisons --- src/build/SharedDeps.zig | 22 +++++++++++++++++++++- src/pty.c | 6 +++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 1b24c7429..581b0b64e 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -146,7 +146,27 @@ pub fn add( .target = target, .optimize = optimize, }); - c.defineCMacro("ZIG_TARGET", @tagName(target.result.os.tag)); + inline for (@typeInfo(std.Target.Os.Tag).@"enum".fields) |field| { + c.defineCMacro( + b.fmt( + "ZIG_TARGET_{s}", + .{try std.ascii.allocUpperString(b.allocator, field.name)}, + ), + b.fmt("{d}", .{field.value}), + ); + } + c.defineCMacro( + "ZIG_TARGET", + b.fmt( + "ZIG_TARGET_{s}", + .{ + try std.ascii.allocUpperString( + b.allocator, + @tagName(target.result.os.tag), + ), + }, + ), + ); switch (target.result.os.tag) { .macos => { const libc = try std.zig.LibCInstallation.findNative(.{ diff --git a/src/pty.c b/src/pty.c index f4f25ad5e..4708d0b4e 100644 --- a/src/pty.c +++ b/src/pty.c @@ -1,11 +1,11 @@ -#if ZIG_TARGET == freebsd +#if ZIG_TARGET == ZIG_TARGET_FREEBSD #include // ioctl and constants #include // openpty #include // ptsname_r #include // tcgetpgrp #endif -#if ZIG_TARGET == linux +#if ZIG_TARGET == ZIG_TARGET_LINUX #define _GNU_SOURCE // ptsname_r #include // openpty #include // ptsname_r @@ -13,7 +13,7 @@ #include // tcgetpgrp, setsid #endif -#if ZIG_TARGET == macos +#if ZIG_TARGET == ZIG_TARGET_MACOS #include // ioctl and constants #include // ioctl and constants for TIOCPTYGNAME #include From d5ce05fd37d8f9c94025e71cfd0b7c9ec290a5ce Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 20 Mar 2026 10:37:42 -0500 Subject: [PATCH 6/7] core: simplify pty.c macro usage --- src/build/SharedDeps.zig | 63 ++++++++++++---------------------------- src/pty.c | 16 ++++++---- 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 581b0b64e..cb0b609d5 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -136,51 +136,24 @@ pub fn add( self.config.terminalOptions().add(b, step.root_module); // C imports needed to manage/create PTYs - switch (target.result.os.tag) { - .freebsd, - .linux, - .macos, - => { - const c = b.addTranslateC(.{ - .root_source_file = b.path("src/pty.c"), - .target = target, - .optimize = optimize, - }); - inline for (@typeInfo(std.Target.Os.Tag).@"enum".fields) |field| { - c.defineCMacro( - b.fmt( - "ZIG_TARGET_{s}", - .{try std.ascii.allocUpperString(b.allocator, field.name)}, - ), - b.fmt("{d}", .{field.value}), - ); - } - c.defineCMacro( - "ZIG_TARGET", - b.fmt( - "ZIG_TARGET_{s}", - .{ - try std.ascii.allocUpperString( - b.allocator, - @tagName(target.result.os.tag), - ), - }, - ), - ); - switch (target.result.os.tag) { - .macos => { - const libc = try std.zig.LibCInstallation.findNative(.{ - .allocator = b.allocator, - .target = &target.result, - .verbose = false, - }); - c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); - }, - else => {}, - } - step.root_module.addImport("pty-c", c.createModule()); - }, - else => {}, + { + const c = b.addTranslateC(.{ + .root_source_file = b.path("src/pty.c"), + .target = target, + .optimize = optimize, + }); + switch (target.result.os.tag) { + .macos => { + const libc = try std.zig.LibCInstallation.findNative(.{ + .allocator = b.allocator, + .target = &target.result, + .verbose = false, + }); + c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); + }, + else => {}, + } + step.root_module.addImport("pty-c", c.createModule()); } // Freetype. We always include this even if our font backend doesn't diff --git a/src/pty.c b/src/pty.c index 4708d0b4e..fe50d2b4b 100644 --- a/src/pty.c +++ b/src/pty.c @@ -1,19 +1,20 @@ -#if ZIG_TARGET == ZIG_TARGET_FREEBSD +#if defined(__FreeBSD__) + #include // ioctl and constants #include // openpty #include // ptsname_r #include // tcgetpgrp -#endif -#if ZIG_TARGET == ZIG_TARGET_LINUX +#elif defined(__linux__) + #define _GNU_SOURCE // ptsname_r #include // openpty #include // ptsname_r #include // ioctl and constants #include // tcgetpgrp, setsid -#endif -#if ZIG_TARGET == ZIG_TARGET_MACOS +#elif defined(__APPLE__) + #include // ioctl and constants #include // ioctl and constants for TIOCPTYGNAME #include @@ -31,4 +32,9 @@ #ifndef tiocgwinsz #define tiocgwinsz 1074295912 #endif + +#else + + #error "unsupported platform" + #endif From 7b9e49a47fbacd2c776aab47470f1b769a6de58f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 20 Mar 2026 12:10:47 -0500 Subject: [PATCH 7/7] core: build pty.c only on certain platforms (avoids building os iOS) --- src/build/SharedDeps.zig | 42 +++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index cb0b609d5..fe3fb1b2f 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -136,24 +136,30 @@ pub fn add( self.config.terminalOptions().add(b, step.root_module); // C imports needed to manage/create PTYs - { - const c = b.addTranslateC(.{ - .root_source_file = b.path("src/pty.c"), - .target = target, - .optimize = optimize, - }); - switch (target.result.os.tag) { - .macos => { - const libc = try std.zig.LibCInstallation.findNative(.{ - .allocator = b.allocator, - .target = &target.result, - .verbose = false, - }); - c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); - }, - else => {}, - } - step.root_module.addImport("pty-c", c.createModule()); + switch (target.result.os.tag) { + .freebsd, + .linux, + .macos, + => { + const c = b.addTranslateC(.{ + .root_source_file = b.path("src/pty.c"), + .target = target, + .optimize = optimize, + }); + switch (target.result.os.tag) { + .macos => { + const libc = try std.zig.LibCInstallation.findNative(.{ + .allocator = b.allocator, + .target = &target.result, + .verbose = false, + }); + c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? }); + }, + else => {}, + } + step.root_module.addImport("pty-c", c.createModule()); + }, + else => {}, } // Freetype. We always include this even if our font backend doesn't