From c6bba6208fa0b7e40150e4aff76d55ed5c116b94 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 14 Apr 2026 22:29:38 -0500 Subject: [PATCH] vt example compiles but does not work correctly --- examples/vt.zig | 35 ++++------- src/widgets/terminal/Command.zig | 100 ++++++++++++++++-------------- src/widgets/terminal/Pty.zig | 37 ++++++----- src/widgets/terminal/Terminal.zig | 99 +++++++++++++++-------------- 4 files changed, 143 insertions(+), 128 deletions(-) diff --git a/examples/vt.zig b/examples/vt.zig index fc13149..44d6866 100644 --- a/examples/vt.zig +++ b/examples/vt.zig @@ -9,33 +9,23 @@ const Event = union(enum) { pub const panic = vaxis.panic_handler; -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer { - const deinit_status = gpa.deinit(); - //fail test; can't try in defer as defer is executed after we return - if (deinit_status == .leak) { - std.log.err("memory leak", .{}); - } - } - const alloc = gpa.allocator(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const alloc = init.gpa; var buffer: [1024]u8 = undefined; - var tty = try vaxis.Tty.init(&buffer); + var tty: vaxis.Tty = try .init(io, &buffer); const writer = tty.writer(); - var vx = try vaxis.init(alloc, .{}); + var vx = try vaxis.init(io, alloc, init.environ_map, .{}); defer vx.deinit(alloc, writer); - var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; - try loop.init(); + var loop: vaxis.Loop(Event) = .init(io, &tty, &vx); try loop.start(); defer loop.stop(); try vx.enterAltScreen(writer); try vx.queryTerminal(writer, 1 * std.time.ns_per_s); - var env = try std.process.getEnvMap(alloc); - defer env.deinit(); const vt_opts: vaxis.widgets.Terminal.Options = .{ .winsize = .{ @@ -45,15 +35,16 @@ pub fn main() !void { .y_pixel = 0, }, .scrollback_size = 0, - .initial_working_directory = env.get("HOME") orelse @panic("no $HOME"), + .initial_working_directory = init.environ_map.get("HOME") orelse @panic("no $HOME"), }; - const shell = env.get("SHELL") orelse "bash"; + const shell = init.environ_map.get("SHELL") orelse "bash"; const argv = [_][]const u8{shell}; var write_buf: [4096]u8 = undefined; var vt = try vaxis.widgets.Terminal.init( + io, alloc, &argv, - &env, + init.environ_map, vt_opts, &write_buf, ); @@ -62,9 +53,9 @@ pub fn main() !void { var redraw: bool = false; while (true) { - std.Thread.sleep(8 * std.time.ns_per_ms); + try io.sleep(.fromMilliseconds(8), .real); // try vt events first - while (vt.tryEvent()) |event| { + while (try vt.tryEvent()) |event| { redraw = true; switch (event) { .bell => {}, @@ -74,7 +65,7 @@ pub fn main() !void { .pwd_change => {}, } } - while (loop.tryEvent()) |event| { + while (try loop.tryEvent()) |event| { redraw = true; switch (event) { .key_press => |key| { diff --git a/src/widgets/terminal/Command.zig b/src/widgets/terminal/Command.zig index 0cd24f7..22cd1d4 100644 --- a/src/widgets/terminal/Command.zig +++ b/src/widgets/terminal/Command.zig @@ -5,6 +5,7 @@ const builtin = @import("builtin"); const Pty = @import("Pty.zig"); const Terminal = @import("Terminal.zig"); +const linux = std.os.linux; const posix = std.posix; argv: []const []const u8, @@ -14,22 +15,21 @@ working_directory: ?[]const u8, // Set after spawn() pid: ?std.posix.pid_t = null, -env_map: *const std.process.EnvMap, +env_map: *const std.process.Environ.Map, pty: Pty, -pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void { +pub fn spawn(self: *Command, io: std.Io, allocator: std.mem.Allocator) !void { var arena_allocator = std.heap.ArenaAllocator.init(allocator); defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - - const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null); - for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; - - const envp = try createEnvironFromMap(arena, self.env_map); - - const pid = try std.posix.fork(); + const pid = pid: { + const rc = linux.fork(); + break :pid switch (linux.errno(rc)) { + .SUCCESS => rc, + else => return error.ForkError, + }; + }; if (pid == 0) { // we are the child _ = std.os.linux.setsid(); @@ -39,20 +39,39 @@ pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void { if (posix.system.ioctl(self.pty.tty.handle, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError; // set up io - try posix.dup2(self.pty.tty.handle, std.posix.STDIN_FILENO); - try posix.dup2(self.pty.tty.handle, std.posix.STDOUT_FILENO); - try posix.dup2(self.pty.tty.handle, std.posix.STDERR_FILENO); - - self.pty.tty.close(); - if (self.pty.pty.handle > 2) self.pty.pty.close(); - - if (self.working_directory) |wd| { - try std.posix.chdir(wd); + { + const rc = linux.dup2(self.pty.tty.handle, std.posix.STDIN_FILENO); + switch (linux.errno(rc)) { + .SUCCESS => {}, + else => return error.Dup2Failed, + } + } + { + const rc = linux.dup2(self.pty.tty.handle, std.posix.STDOUT_FILENO); + switch (linux.errno(rc)) { + .SUCCESS => {}, + else => return error.Dup2Failed, + } + } + { + const rc = linux.dup2(self.pty.tty.handle, std.posix.STDERR_FILENO); + switch (linux.errno(rc)) { + .SUCCESS => {}, + else => return error.Dup2Failed, + } } + self.pty.tty.close(io); + if (self.pty.pty.handle > 2) self.pty.pty.close(io); + + // if (self.working_directory) |wd| { + // try std.posix.chdir(wd); + // } // exec - const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp); - _ = err catch {}; + std.process.replace(io, .{ + .argv = self.argv, + .environ_map = self.env_map, + }) catch {}; } // we are the parent @@ -75,15 +94,18 @@ pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void { return; } -fn handleSigChild(_: c_int) callconv(.c) void { - const result = std.posix.waitpid(-1, 0); - - Terminal.global_vt_mutex.lock(); - defer Terminal.global_vt_mutex.unlock(); - if (Terminal.global_vts) |vts| { - var vt = vts.get(result.pid) orelse return; - vt.event_queue.push(.exited); - } +fn handleSigChild(_: posix.SIG) callconv(.c) void { + var status: u32 = undefined; + const rc = linux.waitpid(-1, &status, 0); + const pid: i32 = switch (linux.errno(rc)) { + .SUCCESS => @intCast(rc), + else => return, + }; + + Terminal.global_vt_mutex.lock(Terminal.global_io) catch return; + defer Terminal.global_vt_mutex.unlock(Terminal.global_io); + var vt = Terminal.global_vts.get(pid) orelse return; + vt.event_queue.push(.exited) catch {}; } pub fn kill(self: *Command) void { @@ -97,21 +119,7 @@ pub fn kill(self: *Command) void { /// hash map plus options. fn createEnvironFromMap( arena: std.mem.Allocator, - map: *const std.process.EnvMap, + map: *const std.process.Environ.Map, ) ![:null]?[*:0]u8 { - const envp_count: usize = map.count(); - - const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null); - var i: usize = 0; - - { - var it = map.iterator(); - while (it.next()) |pair| { - envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }, 0); - i += 1; - } - } - - std.debug.assert(i == envp_count); - return envp_buf; + return try map.createPosixBlock(arena, .{}); } diff --git a/src/widgets/terminal/Pty.zig b/src/widgets/terminal/Pty.zig index d9f4138..2a77765 100644 --- a/src/widgets/terminal/Pty.zig +++ b/src/widgets/terminal/Pty.zig @@ -5,23 +5,24 @@ const std = @import("std"); const builtin = @import("builtin"); const Winsize = @import("../../main.zig").Winsize; +const linux = std.os.linux; const posix = std.posix; -pty: std.fs.File, -tty: std.fs.File, +pty: std.Io.File, +tty: std.Io.File, /// opens a new tty/pty pair -pub fn init() !Pty { +pub fn init(io: std.Io) !Pty { switch (builtin.os.tag) { - .linux => return openPtyLinux(), + .linux => return openPtyLinux(io), else => @compileError("unsupported os"), } } /// closes the tty and pty -pub fn deinit(self: Pty) void { - self.pty.close(); - self.tty.close(); +pub fn deinit(self: Pty, io: std.Io) void { + self.pty.close(io); + self.tty.close(io); } /// sets the size of the pty @@ -36,24 +37,30 @@ pub fn setSize(self: Pty, ws: Winsize) !void { return error.SetWinsizeError; } -fn openPtyLinux() !Pty { - const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); - errdefer posix.close(p); +fn openPtyLinux(io: std.Io) !Pty { + const pty = try std.Io.Dir.openFileAbsolute(io, "/dev/ptmx", .{ + .mode = .read_write, + .allow_ctty = false, + }); + errdefer pty.close(io); // unlockpt var n: c_uint = 0; - if (posix.system.ioctl(p, posix.T.IOCSPTLCK, @intFromPtr(&n)) != 0) return error.IoctlError; + if (posix.system.ioctl(pty.handle, posix.T.IOCSPTLCK, @intFromPtr(&n)) != 0) return error.IoctlError; // ptsname - if (posix.system.ioctl(p, posix.T.IOCGPTN, @intFromPtr(&n)) != 0) return error.IoctlError; + if (posix.system.ioctl(pty.handle, posix.T.IOCGPTN, @intFromPtr(&n)) != 0) return error.IoctlError; var buf: [16]u8 = undefined; const sname = try std.fmt.bufPrint(&buf, "/dev/pts/{d}", .{n}); std.log.debug("pts: {s}", .{sname}); - const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); + const tty = try std.Io.Dir.openFileAbsolute(io, sname, .{ + .mode = .read_write, + .allow_ctty = false, + }); return .{ - .pty = .{ .handle = p }, - .tty = .{ .handle = t }, + .pty = pty, + .tty = tty, }; } diff --git a/src/widgets/terminal/Terminal.zig b/src/widgets/terminal/Terminal.zig index 36d61d7..38da161 100644 --- a/src/widgets/terminal/Terminal.zig +++ b/src/widgets/terminal/Terminal.zig @@ -43,21 +43,24 @@ pub const InputEvent = union(enum) { key_press: vaxis.Key, }; -pub var global_vt_mutex: std.Thread.Mutex = .{}; -pub var global_vts: ?std.AutoHashMap(i32, *Terminal) = null; +pub var global_io: std.Io = undefined; +pub var global_io_initialized: bool = false; +pub var global_vt_mutex: std.Io.Mutex = .init; +pub var global_vts: std.AutoHashMapUnmanaged(i32, *Terminal) = .empty; pub var global_sigchild_installed: bool = false; +io: std.Io, allocator: std.mem.Allocator, scrollback_size: u16, pty: Pty, -pty_writer: std.fs.File.Writer, +pty_writer: std.Io.File.Writer, cmd: Command, -thread: ?std.Thread = null, +thread: ?std.Io.Future(void) = null, /// the screen we draw from front_screen: Screen, -front_mutex: std.Thread.Mutex = .{}, +front_mutex: std.Io.Mutex = .init, /// the back screens back_screen: *Screen = undefined, @@ -65,7 +68,7 @@ back_screen_pri: Screen, back_screen_alt: Screen, // only applies to primary screen scroll_offset: usize = 0, -back_mutex: std.Thread.Mutex = .{}, +back_mutex: std.Io.Mutex = .init, // dirty is protected by back_mutex. Only access this field when you hold that mutex dirty: bool = false, @@ -79,22 +82,27 @@ working_directory: std.ArrayList(u8) = .empty, last_printed: []const u8 = "", -event_queue: Queue = .{}, +event_queue: Queue, /// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the /// screen pub fn init( + io: std.Io, allocator: std.mem.Allocator, argv: []const []const u8, - env: *const std.process.EnvMap, + env: *const std.process.Environ.Map, opts: Options, write_buf: []u8, ) !Terminal { + if (!global_io_initialized) { + global_io = io; + global_io_initialized = true; + } // Verify we have an absolute path if (opts.initial_working_directory) |pwd| { if (!std.fs.path.isAbsolute(pwd)) return error.InvalidWorkingDirectory; } - const pty = try Pty.init(); + const pty = try Pty.init(io); try pty.setSize(opts.winsize); const cmd: Command = .{ .argv = argv, @@ -108,15 +116,17 @@ pub fn init( try tabs.append(allocator, col); } return .{ + .io = io, .allocator = allocator, .pty = pty, - .pty_writer = pty.pty.writerStreaming(write_buf), + .pty_writer = pty.pty.writerStreaming(io, write_buf), .cmd = cmd, .scrollback_size = opts.scrollback_size, .front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows), .back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size), .back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows), .tab_stops = tabs, + .event_queue = .init(io), }; } @@ -125,25 +135,23 @@ pub fn deinit(self: *Terminal) void { self.should_quit = true; pid: { - global_vt_mutex.lock(); - defer global_vt_mutex.unlock(); - var vts = global_vts orelse break :pid; + global_vt_mutex.lock(self.io) catch break :pid; + defer global_vt_mutex.unlock(self.io); if (self.cmd.pid) |pid| - _ = vts.remove(pid); - if (vts.count() == 0) { - vts.deinit(); - global_vts = null; + _ = global_vts.remove(pid); + if (global_vts.count() == 0) { + global_vts.deinit(self.allocator); } } self.cmd.kill(); - if (self.thread) |thread| { + if (self.thread) |*thread| { // write an EOT into the tty to trigger a read on our thread const EOT = "\x04"; - _ = self.pty.tty.write(EOT) catch {}; - thread.join(); + self.pty.tty.writeStreamingAll(self.io, EOT) catch {}; + thread.await(self.io); self.thread = null; } - self.pty.deinit(); + self.pty.deinit(self.io); self.front_screen.deinit(self.allocator); self.back_screen_pri.deinit(self.allocator); self.back_screen_alt.deinit(self.allocator); @@ -156,29 +164,26 @@ pub fn spawn(self: *Terminal) !void { if (self.thread != null) return; self.back_screen = &self.back_screen_pri; - try self.cmd.spawn(self.allocator); + try self.cmd.spawn(self.io, self.allocator); self.working_directory.clearRetainingCapacity(); if (self.cmd.working_directory) |pwd| { try self.working_directory.appendSlice(self.allocator, pwd); } else { - const pwd = std.fs.cwd(); - var buffer: [std.fs.max_path_bytes]u8 = undefined; - const out_path = try std.os.getFdPath(pwd.fd, &buffer); + const pwd: std.Io.Dir = .cwd(); + const out_path = try pwd.realPathFileAlloc(self.io, ".", self.allocator); try self.working_directory.appendSlice(self.allocator, out_path); } { // add to our global list - global_vt_mutex.lock(); - defer global_vt_mutex.unlock(); - if (global_vts == null) - global_vts = std.AutoHashMap(i32, *Terminal).init(self.allocator); + try global_vt_mutex.lock(self.io); + defer global_vt_mutex.unlock(self.io); if (self.cmd.pid) |pid| - try global_vts.?.put(pid, self); + try global_vts.put(self.allocator, pid, self); } - self.thread = try std.Thread.spawn(.{}, Terminal.run, .{self}); + self.thread = try self.io.concurrent(Terminal.run, .{self}); } /// resize the screen. Locks access to the back screen. Should only be called from the main thread. @@ -190,8 +195,8 @@ pub fn resize(self: *Terminal, ws: Winsize) !void { ws.rows == self.front_screen.height) return; - self.back_mutex.lock(); - defer self.back_mutex.unlock(); + try self.back_mutex.lock(self.io); + defer self.back_mutex.unlock(self.io); self.front_screen.deinit(self.allocator); self.front_screen = try Screen.init(self.allocator, ws.cols, ws.rows); @@ -206,7 +211,7 @@ pub fn resize(self: *Terminal, ws: Winsize) !void { pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !void { if (self.back_mutex.tryLock()) { - defer self.back_mutex.unlock(); + defer self.back_mutex.unlock(self.io); // We keep this as a separate condition so we don't deadlock by obtaining the lock but not // having sync if (!self.mode.sync) { @@ -231,8 +236,8 @@ pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !v } } -pub fn tryEvent(self: *Terminal) ?Event { - return self.event_queue.tryPop(); +pub fn tryEvent(self: *Terminal) !?Event { + return try self.event_queue.tryPop(); } pub fn update(self: *Terminal, event: InputEvent) !void { @@ -249,12 +254,16 @@ pub fn get_pty_writer(self: *Terminal) *std.Io.Writer { return &self.pty_writer.interface; } -fn reader(self: *const Terminal, buf: []u8) std.fs.File.Reader { - return self.pty.pty.readerStreaming(buf); +fn reader(self: *const Terminal, buf: []u8) std.Io.File.Reader { + return self.pty.pty.readerStreaming(self.io, buf); } /// process the output from the command on the pty -fn run(self: *Terminal) !void { +fn run(self: *Terminal) void { + self._run() catch {}; +} + +fn _run(self: *Terminal) !void { var parser: Parser = .{ .buf = try .initCapacity(self.allocator, 128), }; @@ -265,10 +274,10 @@ fn run(self: *Terminal) !void { while (!self.should_quit) { const event = try parser.parseReader(&reader_.interface); - self.back_mutex.lock(); - defer self.back_mutex.unlock(); + try self.back_mutex.lock(self.io); + defer self.back_mutex.unlock(self.io); - if (!self.dirty and self.event_queue.tryPush(.redraw)) + if (!self.dirty and try self.event_queue.tryPush(.redraw)) self.dirty = true; switch (event) { @@ -673,7 +682,7 @@ fn run(self: *Terminal) !void { 0 => { self.title.clearRetainingCapacity(); try self.title.appendSlice(self.allocator, osc[semicolon + 1 ..]); - self.event_queue.push(.{ .title_change = self.title.items }); + try self.event_queue.push(.{ .title_change = self.title.items }); }, 7 => { // OSC 7 ; file:// @@ -693,7 +702,7 @@ fn run(self: *Terminal) !void { } else enc[i]; try self.working_directory.append(self.allocator, b); } - self.event_queue.push(.{ .pwd_change = self.working_directory.items }); + try self.event_queue.push(.{ .pwd_change = self.working_directory.items }); }, else => log.info("unhandled osc: {s}", .{osc}), } @@ -708,7 +717,7 @@ inline fn handleC0(self: *Terminal, b: ansi.C0) !void { .NUL, .SOH, .STX => {}, .EOT => {}, // we send EOT to quit the read thread .ENQ => {}, - .BEL => self.event_queue.push(.bell), + .BEL => try self.event_queue.push(.bell), .BS => self.back_screen.cursorLeft(1), .HT => self.horizontalTab(1), .LF, .VT, .FF => try self.back_screen.index(), -- 2.51.2