ghostty/src/Pty.zig
Mitchell Hashimoto 84b1ae9a3c set IUTF8 on the pty
This is important on Mac since the pty by default on Mac does NOT have
this enabled. Without this, attempting to read/write UTF-8 characters
in the raw pty layer would sometimes turn into '?' even though ghostty
fully supports it.
2022-11-24 10:20:18 -08:00

134 lines
3.6 KiB
Zig

//! Linux PTY creation and management. This is just a thin layer on top
//! of Linux syscalls. The caller is responsible for detail-oriented handling
//! of the returned file handles.
const Pty = @This();
const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
const fd_t = std.os.fd_t;
const c = switch (builtin.os.tag) {
.macos => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
}),
};
// 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;
/// Redeclare this winsize struct so we can just use a Zig struct. This
/// layout should be correct on all tested platforms.
const winsize = extern struct {
ws_row: u16,
ws_col: u16,
ws_xpixel: u16,
ws_ypixel: u16,
};
pub extern "c" fn setsid() std.c.pid_t;
/// The file descriptors for the master and slave side of the pty.
master: fd_t,
slave: fd_t,
/// Open a new PTY with the given initial size.
pub fn open(size: winsize) !Pty {
// Need to copy so that it becomes non-const.
var sizeCopy = size;
var master_fd: fd_t = undefined;
var slave_fd: fd_t = undefined;
if (c.openpty(
&master_fd,
&slave_fd,
null,
null,
@ptrCast([*c]c.struct_winsize, &sizeCopy),
) < 0)
return error.OpenptyFailed;
errdefer {
_ = std.os.system.close(master_fd);
_ = std.os.system.close(slave_fd);
}
// 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 Pty{
.master = master_fd,
.slave = slave_fd,
};
}
pub fn deinit(self: *Pty) void {
_ = std.os.system.close(self.master);
self.* = undefined;
}
/// Return the size of the pty.
pub fn getSize(self: Pty) !winsize {
var ws: winsize = undefined;
if (c.ioctl(self.master, TIOCGWINSZ, @ptrToInt(&ws)) < 0)
return error.IoctlFailed;
return ws;
}
/// Set the size of the pty.
pub fn setSize(self: Pty, size: winsize) !void {
if (c.ioctl(self.master, TIOCSWINSZ, @ptrToInt(&size)) < 0)
return error.IoctlFailed;
}
/// This should be called prior to exec in the forked child process
/// in order to setup the tty properly.
pub fn childPreExec(self: Pty) !void {
// Create a new process group
if (setsid() < 0) return error.ProcessGroupFailed;
// Set controlling terminal
if (c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)) < 0)
return error.SetControllingTerminalFailed;
// Can close master/slave pair now
std.os.close(self.slave);
std.os.close(self.master);
// TODO: reset signals
}
test {
var ws: winsize = .{
.ws_row = 50,
.ws_col = 80,
.ws_xpixel = 1,
.ws_ypixel = 1,
};
var pty = try 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());
}