Compare commits
10 commits
737236db01
...
df5c426383
| Author | SHA1 | Date | |
|---|---|---|---|
| df5c426383 | |||
| 5f9b7b7c13 | |||
| 21b7995393 | |||
| 29c3424913 | |||
| 0a37c2b05b | |||
| 57aae0d45c | |||
| fc78e8cf02 | |||
| a35edeaa9b | |||
| 94f6b342fa | |||
| 632a7c4453 |
8 changed files with 197 additions and 53 deletions
10
build.zig
10
build.zig
|
|
@ -392,6 +392,13 @@ pub fn build_exe(
|
|||
},
|
||||
});
|
||||
|
||||
const argv_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/argv.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "cbor", .module = cbor_mod },
|
||||
},
|
||||
});
|
||||
|
||||
const lsp_config_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/lsp_config.zig"),
|
||||
.imports = &.{
|
||||
|
|
@ -660,6 +667,7 @@ pub fn build_exe(
|
|||
.{ .name = "project_manager", .module = project_manager_mod },
|
||||
.{ .name = "syntax", .module = syntax_mod },
|
||||
.{ .name = "text_manip", .module = text_manip_mod },
|
||||
.{ .name = "argv", .module = argv_mod },
|
||||
.{ .name = "Buffer", .module = Buffer_mod },
|
||||
.{ .name = "keybind", .module = keybind_mod },
|
||||
.{ .name = "shell", .module = shell_mod },
|
||||
|
|
@ -709,6 +717,7 @@ pub fn build_exe(
|
|||
exe.root_module.addImport("cbor", cbor_mod);
|
||||
exe.root_module.addImport("config", config_mod);
|
||||
exe.root_module.addImport("text_manip", text_manip_mod);
|
||||
exe.root_module.addImport("argv", argv_mod);
|
||||
exe.root_module.addImport("Buffer", Buffer_mod);
|
||||
exe.root_module.addImport("tui", tui_mod);
|
||||
exe.root_module.addImport("thespian", thespian_mod);
|
||||
|
|
@ -759,6 +768,7 @@ pub fn build_exe(
|
|||
check_exe.root_module.addImport("cbor", cbor_mod);
|
||||
check_exe.root_module.addImport("config", config_mod);
|
||||
check_exe.root_module.addImport("text_manip", text_manip_mod);
|
||||
check_exe.root_module.addImport("argv", argv_mod);
|
||||
check_exe.root_module.addImport("Buffer", Buffer_mod);
|
||||
check_exe.root_module.addImport("tui", tui_mod);
|
||||
check_exe.root_module.addImport("thespian", thespian_mod);
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@
|
|||
.hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/neurocyte/libvaxis?ref=main#e83e3f871786fab577bb26e2e4800dd7e9bf4390",
|
||||
.hash = "vaxis-0.5.1-BWNV_Ke0CQCnpGN5qbzPOFD1V_oBgcIWd1O0PrBYmTMa",
|
||||
.url = "git+https://github.com/neurocyte/libvaxis?ref=main#9c8e0a7d61c75ea6aee3b69761acfb36899709e6",
|
||||
.hash = "vaxis-0.5.1-BWNV_LC4CQDtTTjkRwC90yjbqAtv2AnDiRpU0e8c_BtF",
|
||||
},
|
||||
.zeit = .{
|
||||
.url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88",
|
||||
|
|
|
|||
33
src/argv.zig
Normal file
33
src/argv.zig
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Write a `[]const []const u8` argv array as a space-separated command string.
|
||||
/// Args that contain spaces are wrapped in double-quotes.
|
||||
/// Writes nothing if argv is null or empty.
|
||||
pub fn write(writer: *std.Io.Writer, argv: ?[]const []const u8) error{WriteFailed}!usize {
|
||||
const args = argv orelse return 0;
|
||||
var count: usize = 0;
|
||||
for (args, 0..) |arg, i| {
|
||||
if (i > 0) {
|
||||
try writer.writeByte(' ');
|
||||
count += 1;
|
||||
}
|
||||
const needs_quote = std.mem.indexOfScalar(u8, arg, ' ') != null;
|
||||
if (needs_quote) {
|
||||
try writer.writeByte('"');
|
||||
count += 1;
|
||||
}
|
||||
try writer.writeAll(arg);
|
||||
count += arg.len;
|
||||
if (needs_quote) {
|
||||
try writer.writeByte('"');
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Return the display length of an argv array rendered by write_argv.
|
||||
pub fn len(argv: ?[]const []const u8) usize {
|
||||
var discard: std.Io.Writer.Discarding = .init(&.{});
|
||||
return write(&discard.writer, argv) catch return 0;
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ gutter_width_maximum: usize = 8,
|
|||
enable_terminal_cursor: bool = true,
|
||||
enable_terminal_color_scheme: bool = false,
|
||||
terminal_scrollback_size: u16 = 500,
|
||||
terminal_on_exit: TerminalOnExit = .hold_on_error,
|
||||
enable_sgr_pixel_mode_support: bool = true,
|
||||
enable_modal_dim: bool = true,
|
||||
highlight_current_line: bool = true,
|
||||
|
|
@ -248,3 +249,9 @@ pub const AgeFormat = enum {
|
|||
short,
|
||||
long,
|
||||
};
|
||||
|
||||
pub const TerminalOnExit = enum {
|
||||
hold_on_error,
|
||||
close,
|
||||
hold,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ const file_type_config = @import("file_type_config");
|
|||
const text_manip = @import("text_manip");
|
||||
const write_string = text_manip.write_string;
|
||||
const write_padding = text_manip.write_padding;
|
||||
const argv = @import("argv");
|
||||
const builtin = @import("builtin");
|
||||
const RGB = @import("color").RGB;
|
||||
|
||||
|
|
@ -22,9 +23,9 @@ pub fn list(allocator: std.mem.Allocator, writer: *std.io.Writer, tty_config: st
|
|||
for (file_type_config.get_all_names()) |file_type_name| {
|
||||
const file_type = try file_type_config.get(file_type_name) orelse unreachable;
|
||||
max_language_len = @max(max_language_len, file_type.name.len);
|
||||
max_langserver_len = @max(max_langserver_len, args_string_length(file_type.language_server));
|
||||
max_formatter_len = @max(max_formatter_len, args_string_length(file_type.formatter));
|
||||
max_extensions_len = @max(max_extensions_len, args_string_length(file_type.extensions));
|
||||
max_langserver_len = @max(max_langserver_len, argv.len(file_type.language_server));
|
||||
max_formatter_len = @max(max_formatter_len, argv.len(file_type.formatter));
|
||||
max_extensions_len = @max(max_extensions_len, argv.len(file_type.extensions));
|
||||
}
|
||||
|
||||
try tty_config.setColor(writer, .yellow);
|
||||
|
|
@ -43,59 +44,42 @@ pub fn list(allocator: std.mem.Allocator, writer: *std.io.Writer, tty_config: st
|
|||
try tty_config.setColor(writer, .reset);
|
||||
try writer.writeAll(" ");
|
||||
try write_string(writer, file_type.name, max_language_len + 1);
|
||||
try write_segmented(writer, file_type.extensions, ",", max_extensions_len + 1, tty_config);
|
||||
{
|
||||
const exts = file_type.extensions orelse &.{};
|
||||
var ext_len: usize = 0;
|
||||
for (exts, 0..) |ext, i| {
|
||||
if (i > 0) {
|
||||
try writer.writeByte(',');
|
||||
ext_len += 1;
|
||||
}
|
||||
try writer.writeAll(ext);
|
||||
ext_len += ext.len;
|
||||
}
|
||||
try tty_config.setColor(writer, .reset);
|
||||
try write_padding(writer, ext_len, max_extensions_len + 1);
|
||||
}
|
||||
|
||||
if (file_type.language_server) |language_server|
|
||||
try write_checkmark(writer, bin_path.can_execute(allocator, language_server[0]), tty_config);
|
||||
|
||||
try write_segmented(writer, file_type.language_server, " ", max_langserver_len + 1, tty_config);
|
||||
const len = try argv.write(writer, file_type.language_server);
|
||||
try tty_config.setColor(writer, .reset);
|
||||
try write_padding(writer, len, max_langserver_len + 1);
|
||||
|
||||
if (file_type.formatter) |formatter|
|
||||
try write_checkmark(writer, bin_path.can_execute(allocator, formatter[0]), tty_config);
|
||||
|
||||
try write_segmented(writer, file_type.formatter, " ", null, tty_config);
|
||||
_ = try argv.write(writer, file_type.formatter);
|
||||
try tty_config.setColor(writer, .reset);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn args_string_length(args_: ?[]const []const u8) usize {
|
||||
const args = args_ orelse return 0;
|
||||
var len: usize = 0;
|
||||
var first: bool = true;
|
||||
for (args) |arg| {
|
||||
if (first) first = false else len += 1;
|
||||
len += arg.len;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
fn write_checkmark(writer: anytype, success: bool, tty_config: std.io.tty.Config) !void {
|
||||
try tty_config.setColor(writer, if (success) .green else .red);
|
||||
if (success) try writer.writeAll(success_mark) else try writer.writeAll(fail_mark);
|
||||
}
|
||||
|
||||
fn write_segmented(
|
||||
writer: anytype,
|
||||
args_: ?[]const []const u8,
|
||||
sep: []const u8,
|
||||
pad: ?usize,
|
||||
tty_config: std.io.tty.Config,
|
||||
) !void {
|
||||
const args = args_ orelse return;
|
||||
var len: usize = 0;
|
||||
var first: bool = true;
|
||||
for (args) |arg| {
|
||||
if (first) first = false else {
|
||||
len += 1;
|
||||
try writer.writeAll(sep);
|
||||
}
|
||||
len += arg.len;
|
||||
try writer.writeAll(arg);
|
||||
}
|
||||
try tty_config.setColor(writer, .reset);
|
||||
if (pad) |pad_| try write_padding(writer, len, pad_);
|
||||
}
|
||||
|
||||
fn setColorRgb(writer: anytype, color: u24) !void {
|
||||
const fg_rgb_legacy = "\x1b[38;2;{d};{d};{d}m";
|
||||
const rgb = RGB.from_u24(color);
|
||||
|
|
|
|||
|
|
@ -1014,7 +1014,13 @@ const cmds = struct {
|
|||
vt.focus();
|
||||
}
|
||||
}
|
||||
pub const focus_terminal_meta: Meta = .{ .description = "Terminal" };
|
||||
pub const focus_terminal_meta: Meta = .{ .description = "Open terminal" };
|
||||
|
||||
pub fn close_terminal(self: *Self, _: Ctx) Result {
|
||||
if (self.get_panel_view(terminal_view)) |_|
|
||||
try self.toggle_panel_view(terminal_view, .disable);
|
||||
}
|
||||
pub const close_terminal_meta: Meta = .{ .description = "Close terminal" };
|
||||
|
||||
pub fn close_find_in_files_results(self: *Self, _: Ctx) Result {
|
||||
if (self.file_list_type == .find_in_files)
|
||||
|
|
|
|||
|
|
@ -138,11 +138,10 @@ fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void {
|
|||
} else {
|
||||
tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
||||
project_manager.add_task(entry.label) catch {};
|
||||
const run_cmd = switch (activate) {
|
||||
.normal => "run_task",
|
||||
.alternate => "run_task_in_terminal",
|
||||
};
|
||||
tp.self_pid().send(.{ "cmd", run_cmd, .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
||||
(switch (activate) {
|
||||
.normal => tp.self_pid().send(.{ "cmd", "run_task", .{entry.label} }),
|
||||
.alternate => tp.self_pid().send(.{ "cmd", "run_task_in_terminal", .{ entry.label, "hold" } }),
|
||||
}) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ const cbor = @import("cbor");
|
|||
const command = @import("command");
|
||||
const vaxis = @import("renderer").vaxis;
|
||||
const shell = @import("shell");
|
||||
const argv = @import("argv");
|
||||
const config = @import("config");
|
||||
|
||||
const Plane = @import("renderer").Plane;
|
||||
const Widget = @import("Widget.zig");
|
||||
|
|
@ -22,6 +24,7 @@ const Self = @This();
|
|||
const widget_type: Widget.Type = .panel;
|
||||
|
||||
const Terminal = vaxis.widgets.Terminal;
|
||||
const TerminalOnExit = config.TerminalOnExit;
|
||||
|
||||
allocator: Allocator,
|
||||
plane: Plane,
|
||||
|
|
@ -51,8 +54,11 @@ pub fn create_with_args(allocator: Allocator, parent: Plane, ctx: command.Contex
|
|||
errdefer env.deinit();
|
||||
|
||||
var cmd_arg: []const u8 = "";
|
||||
var on_exit: TerminalOnExit = tui.config().terminal_on_exit;
|
||||
const argv_msg: ?tp.message = if (ctx.args.match(.{tp.extract(&cmd_arg)}) catch false and cmd_arg.len > 0)
|
||||
try shell.parse_arg0_to_argv(allocator, &cmd_arg)
|
||||
else if (ctx.args.match(.{ tp.extract(&cmd_arg), tp.extract(&on_exit) }) catch false and cmd_arg.len > 0)
|
||||
try shell.parse_arg0_to_argv(allocator, &cmd_arg)
|
||||
else
|
||||
null;
|
||||
defer if (argv_msg) |msg| allocator.free(msg.buf);
|
||||
|
|
@ -68,7 +74,7 @@ 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 "bash");
|
||||
try argv_list.append(allocator, env.get("SHELL") orelse "/bin/sh");
|
||||
}
|
||||
|
||||
// Use the current plane dimensions for the initial pty size. The plane
|
||||
|
|
@ -77,7 +83,7 @@ pub fn create_with_args(allocator: Allocator, parent: Plane, ctx: command.Contex
|
|||
const cols: u16 = @intCast(@max(80, plane.dim_x()));
|
||||
const rows: u16 = @intCast(@max(24, plane.dim_y()));
|
||||
|
||||
if (global_vt == null) try Vt.init(allocator, argv_list.items, env, rows, cols);
|
||||
if (global_vt == null) try Vt.init(allocator, argv_list.items, env, rows, cols, on_exit);
|
||||
|
||||
const self = try allocator.create(Self);
|
||||
errdefer allocator.destroy(self);
|
||||
|
|
@ -154,6 +160,19 @@ pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
|
|||
.mods = @bitCast(modifiers),
|
||||
.text = if (text.len > 0) text else null,
|
||||
};
|
||||
if (self.vt.process_exited) {
|
||||
if (keypress == input.key.enter) {
|
||||
self.vt.process_exited = false;
|
||||
self.restart() catch |e|
|
||||
std.log.err("terminal_view: restart failed: {}", .{e});
|
||||
tui.need_render(@src());
|
||||
return true;
|
||||
}
|
||||
if (keypress == input.key.escape) {
|
||||
tp.self_pid().send(.{ "cmd", "close_terminal", .{} }) catch {};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self.vt.vt.scrollToBottom();
|
||||
self.vt.vt.update(.{ .key_press = key }) catch |e|
|
||||
std.log.err("terminal_view: input failed: {}", .{e});
|
||||
|
|
@ -176,6 +195,10 @@ pub fn unfocus(self: *Self) void {
|
|||
}
|
||||
|
||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||
if (global_vt) |*vt| if (vt.process_exited) {
|
||||
vt.deinit(allocator);
|
||||
global_vt = null;
|
||||
};
|
||||
if (self.focused) tui.release_keyboard_focus(Widget.to(self));
|
||||
self.commands.unregister();
|
||||
self.plane.deinit();
|
||||
|
|
@ -194,7 +217,8 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool {
|
|||
while (self.vt.vt.tryEvent()) |event| {
|
||||
switch (event) {
|
||||
.exited => |code| {
|
||||
self.show_exit_message(code);
|
||||
self.vt.process_exited = true;
|
||||
self.handle_child_exit(code);
|
||||
tui.need_render(@src());
|
||||
},
|
||||
.redraw, .bell => {},
|
||||
|
|
@ -256,6 +280,17 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
fn handle_child_exit(self: *Self, code: u8) void {
|
||||
switch (self.vt.on_exit) {
|
||||
.hold => self.show_exit_message(code),
|
||||
.hold_on_error => if (code == 0)
|
||||
tp.self_pid().send(.{ "cmd", "close_terminal", .{} }) catch {}
|
||||
else
|
||||
self.show_exit_message(code),
|
||||
.close => tp.self_pid().send(.{ "cmd", "close_terminal", .{} }) catch {},
|
||||
}
|
||||
}
|
||||
|
||||
fn show_exit_message(self: *Self, code: u8) void {
|
||||
var msg: std.Io.Writer.Allocating = .init(self.allocator);
|
||||
defer msg.deinit();
|
||||
|
|
@ -266,11 +301,30 @@ fn show_exit_message(self: *Self, code: u8) void {
|
|||
if (code != 0)
|
||||
w.print(" with code {d}", .{code}) catch {};
|
||||
w.writeAll("]\x1b[0m\r\n") catch {};
|
||||
// Re-run prompt
|
||||
const cmd_argv = self.vt.vt.cmd.argv;
|
||||
if (cmd_argv.len > 0) {
|
||||
w.writeAll("\x1b[0m\x1b[2mPress enter to re-run '") catch {};
|
||||
_ = argv.write(w, cmd_argv) catch {};
|
||||
w.writeAll("'\x1b[0m\r\n") catch {};
|
||||
}
|
||||
var parser: pty.Parser = .{ .buf = .init(self.allocator) };
|
||||
defer parser.buf.deinit();
|
||||
_ = self.vt.vt.processOutput(&parser, msg.written()) catch {};
|
||||
}
|
||||
|
||||
fn restart(self: *Self) !void {
|
||||
// Kill the old pty actor if still alive
|
||||
if (self.vt.pty_pid) |pid| {
|
||||
pid.send(.{"quit"}) catch {};
|
||||
pid.deinit();
|
||||
self.vt.pty_pid = null;
|
||||
}
|
||||
// Re-spawn the child process and a fresh pty actor
|
||||
try self.vt.vt.spawn();
|
||||
self.vt.pty_pid = try pty.spawn(self.allocator, &self.vt.vt);
|
||||
}
|
||||
|
||||
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
|
||||
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
|
||||
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
|
||||
|
|
@ -319,8 +373,10 @@ const Vt = struct {
|
|||
app_fg: ?[3]u8 = null,
|
||||
app_bg: ?[3]u8 = null,
|
||||
app_cursor: ?[3]u8 = null,
|
||||
process_exited: bool = false,
|
||||
on_exit: TerminalOnExit,
|
||||
|
||||
fn init(allocator: std.mem.Allocator, argv: []const []const u8, env: std.process.EnvMap, rows: u16, cols: u16) !void {
|
||||
fn init(allocator: std.mem.Allocator, cmd_argv: []const []const u8, env: std.process.EnvMap, rows: u16, cols: u16, on_exit: TerminalOnExit) !void {
|
||||
const home = env.get("HOME") orelse "/tmp";
|
||||
|
||||
global_vt = .{
|
||||
|
|
@ -328,11 +384,12 @@ const Vt = struct {
|
|||
.env = env,
|
||||
.write_buf = undefined, // managed via self.vt's pty_writer pointer
|
||||
.pty_pid = null,
|
||||
.on_exit = on_exit,
|
||||
};
|
||||
const self = &global_vt.?;
|
||||
self.vt = try Terminal.init(
|
||||
allocator,
|
||||
argv,
|
||||
cmd_argv,
|
||||
&env,
|
||||
.{
|
||||
.winsize = .{ .rows = rows, .cols = cols, .x_pixel = 0, .y_pixel = 0 },
|
||||
|
|
@ -389,6 +446,8 @@ const pty = struct {
|
|||
parser: Parser,
|
||||
receiver: Receiver,
|
||||
parent: tp.pid,
|
||||
err_code: i64 = 0,
|
||||
sigchld: ?tp.signal = null,
|
||||
|
||||
pub fn spawn(allocator: std.mem.Allocator, vt: *Terminal) !tp.pid {
|
||||
const self = try allocator.create(@This());
|
||||
|
|
@ -407,6 +466,7 @@ const pty = struct {
|
|||
|
||||
fn deinit(self: *@This()) void {
|
||||
std.log.debug("terminal: pty actor deinit (pid={?})", .{self.vt.cmd.pid});
|
||||
if (self.sigchld) |s| s.deinit();
|
||||
self.fd.deinit();
|
||||
self.parser.buf.deinit();
|
||||
self.parent.deinit();
|
||||
|
|
@ -423,6 +483,10 @@ const pty = struct {
|
|||
std.log.debug("terminal: pty initial wait_read failed: {}", .{e});
|
||||
return tp.exit_error(e, @errorReturnTrace());
|
||||
};
|
||||
self.sigchld = tp.signal.init(std.posix.SIG.CHLD, tp.message.fmt(.{"sigchld"})) catch |e| {
|
||||
std.log.debug("terminal: SIGCHLD signal init failed: {}", .{e});
|
||||
return tp.exit_error(e, @errorReturnTrace());
|
||||
};
|
||||
tp.receive(&self.receiver);
|
||||
}
|
||||
|
||||
|
|
@ -448,6 +512,25 @@ const pty = struct {
|
|||
return tp.exit_normal();
|
||||
},
|
||||
};
|
||||
} else if (try m.match(.{ "fd", "pty", "read_error", tp.extract(&self.err_code), tp.more })) {
|
||||
// thespian fires read_error with EPOLLHUP when the child exits cleanly.
|
||||
// Treat it the same as EIO: reap the child and signal exit.
|
||||
const code = self.vt.cmd.wait();
|
||||
std.log.debug("terminal: read_error from fd (err={d}), process exited with code={d}", .{ self.err_code, code });
|
||||
self.vt.event_queue.push(.{ .exited = code });
|
||||
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
||||
return tp.exit_normal();
|
||||
} else if (try m.match(.{"sigchld"})) {
|
||||
// SIGCHLD fires when any child exits. Check if it's our child.
|
||||
if (self.vt.cmd.try_wait()) |code| {
|
||||
std.log.debug("terminal: child exited (SIGCHLD) with code={d}", .{code});
|
||||
self.vt.event_queue.push(.{ .exited = code });
|
||||
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
||||
return tp.exit_normal();
|
||||
}
|
||||
// Not our child (or already reaped) - re-arm the signal and continue.
|
||||
if (self.sigchld) |s| s.deinit();
|
||||
self.sigchld = tp.signal.init(std.posix.SIG.CHLD, tp.message.fmt(.{"sigchld"})) catch null;
|
||||
} else if (try m.match(.{"quit"})) {
|
||||
std.log.debug("terminal: pty exiting: received quit", .{});
|
||||
return tp.exit_normal();
|
||||
|
|
@ -462,7 +545,19 @@ const pty = struct {
|
|||
|
||||
while (true) {
|
||||
const n = std.posix.read(self.vt.ptyFd(), &buf) catch |e| switch (e) {
|
||||
error.WouldBlock => break,
|
||||
error.WouldBlock => {
|
||||
// No more data right now. Check if the child already exited -
|
||||
// on Linux a clean exit may not make the pty fd readable again
|
||||
// (no EPOLLIN), it just starts returning EIO on the next read.
|
||||
// Polling here catches that case before we arm wait_read again.
|
||||
if (self.vt.cmd.try_wait()) |code| {
|
||||
std.log.debug("terminal: child exited (detected via try_wait) with code={d}", .{code});
|
||||
self.vt.event_queue.push(.{ .exited = code });
|
||||
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
||||
return error.InputOutput;
|
||||
}
|
||||
break;
|
||||
},
|
||||
error.InputOutput => {
|
||||
const code = self.vt.cmd.wait();
|
||||
std.log.debug("terminal: read EIO, process exited with code={d}", .{code});
|
||||
|
|
@ -515,6 +610,16 @@ const pty = struct {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for child exit once more before sleeping in wait_read.
|
||||
// A clean exit with no final output will never make the pty fd readable,
|
||||
// so we must detect it here rather than waiting forever.
|
||||
if (self.vt.cmd.try_wait()) |code| {
|
||||
std.log.debug("terminal: child exited (pre-wait_read check) with code={d}", .{code});
|
||||
self.vt.event_queue.push(.{ .exited = code });
|
||||
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
||||
return error.InputOutput;
|
||||
}
|
||||
|
||||
self.fd.wait_read() catch |e| switch (e) {
|
||||
error.ThespianFileDescriptorWaitReadFailed => {
|
||||
std.log.debug("terminal: wait_read failed: {} (pid={?})", .{ e, self.vt.cmd.pid });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue