mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
291 lines
9.6 KiB
Zig
291 lines
9.6 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const windows = @import("os/main.zig").windows;
|
|
|
|
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 = if (builtin.os.tag == .windows)
|
|
WindowsPty
|
|
else
|
|
PosixPty;
|
|
|
|
/// 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 PosixPty = struct {
|
|
pub const Fd = std.os.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 = struct {
|
|
usingnamespace 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");
|
|
}),
|
|
};
|
|
};
|
|
|
|
/// The file descriptors for the master and slave side of the pty.
|
|
master: Fd,
|
|
slave: Fd,
|
|
|
|
/// 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 = undefined;
|
|
var slave_fd: Fd = undefined;
|
|
if (c.openpty(
|
|
&master_fd,
|
|
&slave_fd,
|
|
null,
|
|
null,
|
|
@ptrCast(&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);
|
|
_ = std.os.system.close(self.slave);
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Return the size of the pty.
|
|
pub fn getSize(self: Pty) !winsize {
|
|
var ws: winsize = undefined;
|
|
if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&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, @intFromPtr(&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
|
|
switch (std.os.system.getErrno(c.ioctl(self.slave, 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
|
|
std.os.close(self.slave);
|
|
std.os.close(self.master);
|
|
|
|
// TODO: reset signals
|
|
}
|
|
};
|
|
|
|
/// Windows PTY creation and management.
|
|
const WindowsPty = struct {
|
|
pub const Fd = windows.HANDLE;
|
|
|
|
// Process-wide counter for pipe names
|
|
var pipe_name_counter = std.atomic.Atomic(u32).init(1);
|
|
|
|
out_pipe: windows.HANDLE,
|
|
in_pipe: windows.HANDLE,
|
|
out_pipe_pty: windows.HANDLE,
|
|
in_pipe_pty: windows.HANDLE,
|
|
pseudo_console: windows.exp.HPCON,
|
|
size: winsize,
|
|
|
|
/// Open a new PTY with the given initial size.
|
|
pub fn open(size: winsize) !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.kernel32.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.kernel32.CreateNamedPipeW(
|
|
pipe_path_w.ptr,
|
|
windows.PIPE_ACCESS_OUTBOUND |
|
|
windows.exp.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.kernel32.GetLastError());
|
|
}
|
|
errdefer _ = windows.kernel32.CloseHandle(pty.in_pipe);
|
|
|
|
var security_attributes_read = security_attributes;
|
|
pty.in_pipe_pty = windows.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.kernel32.GetLastError());
|
|
}
|
|
errdefer _ = windows.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.kernel32.CloseHandle(pty.in_pipe_pty);
|
|
// _ = windows.kernel32.CloseHandle(pty.in_pipe);
|
|
// }
|
|
|
|
if (windows.exp.kernel32.CreatePipe(&pty.out_pipe, &pty.out_pipe_pty, null, 0) == 0) {
|
|
return windows.unexpectedError(windows.kernel32.GetLastError());
|
|
}
|
|
errdefer {
|
|
_ = windows.kernel32.CloseHandle(pty.out_pipe);
|
|
_ = windows.kernel32.CloseHandle(pty.out_pipe_pty);
|
|
}
|
|
|
|
try windows.SetHandleInformation(pty.in_pipe, windows.HANDLE_FLAG_INHERIT, 0);
|
|
try windows.SetHandleInformation(pty.in_pipe_pty, windows.HANDLE_FLAG_INHERIT, 0);
|
|
try windows.SetHandleInformation(pty.out_pipe, windows.HANDLE_FLAG_INHERIT, 0);
|
|
try windows.SetHandleInformation(pty.out_pipe_pty, windows.HANDLE_FLAG_INHERIT, 0);
|
|
|
|
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.kernel32.CloseHandle(self.in_pipe_pty);
|
|
_ = windows.kernel32.CloseHandle(self.in_pipe);
|
|
_ = windows.kernel32.CloseHandle(self.out_pipe_pty);
|
|
_ = windows.kernel32.CloseHandle(self.out_pipe);
|
|
_ = windows.exp.kernel32.ClosePseudoConsole(self.pseudo_console);
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Return the size of the pty.
|
|
pub fn getSize(self: Pty) !winsize {
|
|
return self.size;
|
|
}
|
|
|
|
/// Set the size of the pty.
|
|
pub fn setSize(self: *Pty, size: winsize) !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;
|
|
}
|
|
};
|
|
|
|
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());
|
|
}
|