diff --git a/build.zig.zon b/build.zig.zon index 7966bf3..e68bf0d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -30,8 +30,8 @@ .hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D", }, .vaxis = .{ - .url = "git+https://github.com/neurocyte/libvaxis?ref=main#31e54f7d16c16b3f1a4aaf99966dadb9e5ca7a0f", - .hash = "vaxis-0.5.1-BWNV_DzhCQDZI3Vt_DRwcmmEd_jrTyNLGPKpnOPoHQ3-", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#398f890e9015576673a9767d6d44877c1da34cfc", + .hash = "vaxis-0.5.1-BWNV_PQdCgBo6zTDEPmwNeTk9TM_qY0FjHWOx60YRYUB", }, .zeit = .{ .url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88", diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index 460eafc..026ee22 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const tp = @import("thespian"); @@ -74,7 +75,11 @@ pub fn create_with_args(allocator: Allocator, parent: Plane, ctx: command.Contex try argv_list.append(allocator, arg); } } else { - try argv_list.append(allocator, env.get("SHELL") orelse "/bin/sh"); + const default_shell = if (builtin.os.tag == .windows) + env.get("COMSPEC") orelse "cmd.exe" + else + env.get("SHELL") orelse "/bin/sh"; + try argv_list.append(allocator, default_shell); } // Use the current plane dimensions for the initial pty size. The plane @@ -503,7 +508,11 @@ const Vt = struct { }; var global_vt: ?Vt = null; -const pty = struct { +// Platform-specific pty actor: POSIX uses tp.file_descriptor + SIGCHLD, +// Windows uses tp.file_stream with IOCP overlapped reads on the ConPTY output pipe. +const pty = if (builtin.os.tag == .windows) pty_windows else pty_posix; + +const pty_posix = struct { const Parser = Terminal.Parser; const Receiver = tp.Receiver(*@This()); @@ -698,3 +707,107 @@ const pty = struct { }; } }; + +/// Windows pty actor: reads ConPTY output pipe via tp.file_stream (IOCP overlapped I/O). +/// Exit detection relies on error 109 (ERROR_BROKEN_PIPE) from the read stream, +/// which fires when the child process exits and the ConPTY closes the pipe. +const pty_windows = struct { + const Parser = Terminal.Parser; + const Receiver = tp.Receiver(*@This()); + + allocator: std.mem.Allocator, + vt: *Terminal, + stream: tp.file_stream, + parser: Parser, + receiver: Receiver, + parent: tp.pid, + + pub fn spawn(allocator: std.mem.Allocator, vt: *Terminal) !tp.pid { + const self = try allocator.create(@This()); + errdefer allocator.destroy(self); + // tp.file_stream.init takes a *anyopaque (Win32 HANDLE) + const stream = try tp.file_stream.init("pty_out", vt.ptyOutputHandle()); + self.* = .{ + .allocator = allocator, + .vt = vt, + .stream = stream, + .parser = .{ .buf = try .initCapacity(allocator, 128) }, + .receiver = Receiver.init(pty_receive, self), + .parent = tp.self_pid().clone(), + }; + return tp.spawn_link(allocator, self, start, "pty_actor"); + } + + fn deinit(self: *@This()) void { + std.log.debug("terminal: pty actor (windows) deinit", .{}); + self.stream.deinit(); + self.parser.buf.deinit(); + self.parent.deinit(); + self.allocator.destroy(self); + } + + fn start(self: *@This()) tp.result { + errdefer self.deinit(); + self.stream.start_read() catch |e| { + std.log.debug("terminal: pty stream start_read failed: {}", .{e}); + return tp.exit_error(e, @errorReturnTrace()); + }; + tp.receive(&self.receiver); + } + + fn pty_receive(self: *@This(), _: tp.pid_ref, m: tp.message) tp.result { + errdefer self.deinit(); + + var bytes: []const u8 = ""; + var err_code: i64 = 0; + var err_msg: []const u8 = ""; + + if (try m.match(.{ "stream", "pty_out", "read_complete", tp.extract(&bytes) })) { + // Got output data from the child - process it, then arm next read. + if (bytes.len == 0) { + // Zero bytes = EOF on the pipe = child exited + const code = self.vt.cmd.wait(); + std.log.debug("terminal: ConPTY pipe EOF, process exited with code={d}", .{code}); + self.vt.event_queue.push(.{ .exited = code }); + self.parent.send(.{ "terminal_view", "output" }) catch {}; + return tp.exit_normal(); + } + defer self.parent.send(.{ "terminal_view", "output" }) catch {}; + switch (self.vt.processOutput(&self.parser, bytes) catch |e| { + std.log.debug("terminal: processOutput error: {}", .{e}); + return tp.exit_normal(); + }) { + .exited => { + std.log.debug("terminal: processOutput returned .exited", .{}); + return tp.exit_normal(); + }, + .running => {}, + } + // Re-arm the read for next chunk + self.stream.start_read() catch |e| { + std.log.debug("terminal: pty stream re-arm failed: {}", .{e}); + return tp.exit_normal(); + }; + } else if (try m.match(.{ "stream", "pty_out", "read_error", 109, tp.extract(&err_msg) })) { + // ERROR_BROKEN_PIPE (109) = child process has exited and ConPTY closed the pipe. + const code = self.vt.cmd.wait(); + std.log.debug("terminal: ConPTY pipe broken (child exited), code={d}", .{code}); + self.vt.event_queue.push(.{ .exited = code }); + self.parent.send(.{ "terminal_view", "output" }) catch {}; + return tp.exit_normal(); + } else if (try m.match(.{ "stream", "pty_out", "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) { + // Other read error - treat as unexpected exit + std.log.debug("terminal: ConPTY read error: {d} {s}", .{ err_code, err_msg }); + const code = self.vt.cmd.wait(); + self.vt.event_queue.push(.{ .exited = code }); + self.parent.send(.{ "terminal_view", "output" }) catch {}; + return tp.exit_normal(); + } else if (try m.match(.{"quit"})) { + std.log.debug("terminal: pty actor (windows) received quit", .{}); + return tp.exit_normal(); + } else { + std.log.debug("terminal: pty actor (windows) unexpected message", .{}); + return tp.unexpected(m); + } + } +};