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.
This commit is contained in:
Jeffrey C. Ollie
2026-03-18 16:36:18 -05:00
parent 1f89ce91d9
commit 89ae0ea6ef
9 changed files with 327 additions and 30 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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),
}
}

4
src/pty/freebsd.c Normal file
View File

@@ -0,0 +1,4 @@
#include <termios.h> // ioctl and constants
#include <libutil.h> // openpty
#include <stdlib.h> // ptsname_r
#include <unistd.h> // tcgetpgrp

5
src/pty/linux.c Normal file
View File

@@ -0,0 +1,5 @@
#define _GNU_SOURCE // ptsname_r
#include <pty.h> // openpty
#include <stdlib.h> // ptsname_r
#include <sys/ioctl.h> // ioctl and constants
#include <unistd.h> // tcgetpgrp, setsid

17
src/pty/macos.c Normal file
View File

@@ -0,0 +1,17 @@
#include <sys/ioctl.h> // ioctl and constants
#include <sys/ttycom.h> // ioctl and constants for TIOCPTYGNAME
#include <sys/types.h>
#include <unistd.h> // tcgetpgrp
#include <util.h> // openpty
#ifndef TIOCSCTTY
#define TIOCSCTTY 536900705
#endif
#ifndef TIOCSWINSZ
#define TIOCSWINSZ 2148037735
#endif
#ifndef TIOCGWINSZ
#define TIOCGWINSZ 1074295912
#endif

View File

@@ -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;

View File

@@ -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),
};
}

View File

@@ -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.