From ada81f05127506dec0dbb7af390cb43fd0da9d40 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 14:43:17 -0300 Subject: A reused command pane leaves the last program's screen modes before the next command A program that crashed out of a full screen left the command pane's emulator in the alternate screen with mouse reports, bracketed paste or a hidden cursor on, and the next command ran in that. Before the '% line' separator the pane now leaves the alternate screen, turns those modes off and soft-resets (DECSTR). The docs add that a command which clears the screen and its scrollback erases the pane's history above it. Co-Authored-By: Claude Opus 5.5 --- docs/fs.md | 5 ++++- src/Terminal.zig | 7 +++++++ src/exec.zig | 6 ++++++ src/pardes.zig | 5 +++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/fs.md b/docs/fs.md index 3efc5b36..ebc85f12 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -240,7 +240,10 @@ same tree without leaving the process. which shows its output and then `exit N` (its tag reads ` () running`, then `exit N`), and stays. The directory's next command runs in that pane once it is done, below what it showed, after a `% ` line; - one still running gets a second pane. A command pane's own exec starts + one still running gets a second pane. Before the next command the pane + leaves any alternate screen and turns off the modes a program left on + (mouse reports, bracketed paste, a hidden cursor); a command that clears + the screen and its scrollback (`clear`, ED3) erases the history above it. A command pane's own exec starts the next command there too. The log says `run ` and `exit `; `exec` reads back the command pane's serial; Kill ends its whole process group; a line is at most 1 KB. A misspelled word is a diff --git a/src/Terminal.zig b/src/Terminal.zig index cd292e5f..e204b44c 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -932,6 +932,13 @@ test "raw terminal output needs no position tracking allocations" { try std.testing.expectEqual(pins, pane.terminal.?.vt.screens.active.pages.countTrackedPins()); } +/// A full-screen program has the emulator on its alternate screen. +pub fn onAlternateScreen(pane: *const Pane) bool { + if (comptime !enabled) return false; + const state = pane.terminal orelse return false; + return state.vt.screens.active_key == .alternate; +} + pub fn promptInputReady(pane: *const Pane) bool { if (comptime !enabled) return false; const state = pane.terminal orelse return false; diff --git a/src/exec.zig b/src/exec.zig index 86dba32a..8b058a12 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -427,6 +427,12 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { pane.command_done = false; pane.command_status = null; pane.body.mode = .tty; + // What the last program left the emulator in goes first: the + // alternate screen left (only if it is there: leaving restores a + // saved cursor), mouse reports and bracketed paste off, the cursor + // shown, then a soft reset (DECSTR) for the rest. + if (panes.Terminal.onAlternateScreen(pane)) panes.Terminal.feedOutput(p, pane, "\x1b[?1049l"); + panes.Terminal.feedOutput(p, pane, "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l\x1b[?25h\x1b[!p"); var buf: [command_max + 8]u8 = undefined; const lead = if (panes.Terminal.gridCursor(pane).x != 0) "\r\n" else ""; panes.Terminal.feedOutput(p, pane, std.fmt.bufPrint(&buf, "{s}% {s}\r\n", .{ lead, line }) catch "%\r\n"); diff --git a/src/pardes.zig b/src/pardes.zig index 74554e0d..97ba534a 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1432,8 +1432,13 @@ test "a command pane shows how its command ended, and the next command there run // Kill leaves a finished one be. try std.testing.expect(p.executeBuiltinLine(0, "Kill make")); while (p.nextEffect()) |effect| if (effect == .signal_pty and effect.signal_pty.pane == dst) return error.SignalledAFinishedCommand; + // A program that crashed out of its full screen leaves the emulator + // in its modes; the next command starts clear of them. + p.update(.{ .output = .{ .pane = @intCast(dst), .bytes = "\x1b[?1049h\x1b[?2004h\x1b[?1000h" } }); + try std.testing.expect(panes.Terminal.bracketedPaste(pane)); // Done, the directory's next command runs there, below what it showed. try std.testing.expectEqual(@as(?usize, dst), exec.execute(p, 1, "make test")); + try std.testing.expect(!panes.Terminal.bracketedPaste(pane)); var respawned = false; while (p.nextEffect()) |effect| if (effect == .spawn and effect.spawn.pane == dst) { respawned = true; -- cgit v1.3