summaryrefslogtreecommitdiff
path: root/src/tty/tty.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 13:08:29 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitd51e1cd8d7344738ca04578beb8731ab72235a07 (patch)
tree107f768d96b98c0e625e7ead07f6b3d38e655152 /src/tty/tty.zig
parentdc37b4ea777b7eb881a993e737457c18699c5b2c (diff)
downloadpardes-d51e1cd8d7344738ca04578beb8731ab72235a07.tar.gz
pardes-d51e1cd8d7344738ca04578beb8731ab72235a07.zip
A tty editor killed with SIGTERM or SIGHUP puts its terminal back before it dies
A nested pardes killed in a pane died with the default action. That left the pane's terminal on the alternate screen, in raw mode, with mouse and paste reporting on, so the shell under it was unusable until a reset. The tty frontend now handles both signals: it writes the resets a clean exit writes (keyboard protocol, mouse modes, focus, bracketed paste, SGR, cursor, main screen) and restores the cooked termios, all async-signal-safe. Then it dies of the same signal, since SA_RESETHAND put the default back. fs.py kills a tty session with each signal and checks for the main-screen switch and cooked mode; it fails without the handler. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/tty/tty.zig')
-rw-r--r--src/tty/tty.zig36
1 files changed, 36 insertions, 0 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index ef7f2761..1d7ff415 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1083,6 +1083,8 @@ pub fn run(init: std.process.Init, opts: pardes.Options, attach: ?[]const u8) !v
defer vx.exitAltScreen(tty.writer()) catch {};
try vx.setMouseMode(tty.writer(), true);
try vx.setBracketedPaste(tty.writer(), true);
+ Killed.arm(tty.fd.handle, tty.termios);
+ defer Killed.disarm();
if (attach != null) {
attach_end = attachSession(init, attach_name, &tty, &vx);
@@ -1119,6 +1121,40 @@ pub fn run(init: std.process.Init, opts: pardes.Options, attach: ?[]const u8) !v
}
}
+/// SIGTERM or SIGHUP: the terminal put back as a clean exit leaves it --
+/// the main screen, no mouse, paste or keyboard modes, the cursor shown,
+/// cooked termios -- and then the signal's own death. A nested editor
+/// killed in a pane must not leave that pane's terminal wedged.
+/// Async-signal-safe: write, tcsetattr, sigaction, raise.
+const Killed = struct {
+ var fd: posix.fd_t = -1;
+ var cooked: posix.termios = undefined;
+ const reset = "\x1b[<u\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1004l\x1b[?2004l\x1b[0m\x1b[?25h\x1b[?1049l";
+ const signals = [_]posix.SIG{ .TERM, .HUP };
+
+ fn arm(tty_fd: posix.fd_t, termios: posix.termios) void {
+ cooked = termios;
+ fd = tty_fd;
+ const on: posix.Sigaction = .{ .handler = .{ .handler = handle }, .mask = posix.sigemptyset(), .flags = posix.SA.RESETHAND };
+ for (signals) |sig| posix.sigaction(sig, &on, null);
+ }
+
+ fn disarm() void {
+ fd = -1;
+ const default: posix.Sigaction = .{ .handler = .{ .handler = posix.SIG.DFL }, .mask = posix.sigemptyset(), .flags = 0 };
+ for (signals) |sig| posix.sigaction(sig, &default, null);
+ }
+
+ fn handle(sig: posix.SIG) callconv(.c) void {
+ if (fd >= 0) {
+ _ = std.c.write(fd, reset, reset.len);
+ _ = std.c.tcsetattr(fd, .FLUSH, &cooked);
+ }
+ // SA_RESETHAND put the default back: the same signal, now fatal.
+ _ = std.c.raise(sig);
+ }
+};
+
fn localSession(
init: std.process.Init,
opts: pardes.Options,