summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md5
-rw-r--r--src/ninep/pty.zig7
-rw-r--r--src/pardes.zig5
-rw-r--r--test/fs.py7
4 files changed, 20 insertions, 4 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 5c2486f4..9ba9c0e4 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -818,8 +818,9 @@ terminal (a REPL, `less`) takes none: write to `pty/data` for it --
which is also when the third field of `pty/status` reads 1. `pty/status` is
one line, three right-aligned fields and a newline: the pty's columns and
rows, then busy (0 or 1). The size, and `pty/ctl`'s `winsize` read back, is
-what `winsize C R` last set, until the pane itself resizes and gives the pty
-its grid again. A line written
+what `winsize C R` last set (R of 1 is taken as 2: a one-row pty loses
+its prompt's mark and would read busy for ever), until the pane itself
+resizes and gives the pty its grid again. A line written
before a new terminal's shell has drawn its first prompt is not busy: it
waits for that prompt (a respawn meanwhile keeps it waiting for the new
shell's) and is sent then, so the first command a script gives a fresh
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index 046a456a..4009af49 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -28,6 +28,9 @@ fn dimension(word: []const u8) ?u16 {
return if (n == 0 or n > winsize_max) null else n;
}
+/// The fewest rows a winsize write gives a terminal's pty.
+pub const pty_rows_min = 2;
+
/// As the root ctl's `size` (4096 a side).
const winsize_max = 4096;
const e_winsize_range = std.fmt.comptimePrint("invalid winsize: out of range, 1x1 to {d}x{d}", .{ winsize_max, winsize_max });
@@ -71,7 +74,9 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool {
switch (v) {
.winsize => {
const cols = dimension(words.next() orelse return false) orelse return false;
- const rows = dimension(words.next() orelse return false) orelse return false;
+ // Two rows at least, as a pane is laid out: a one-row pty
+ // loses its prompt's input mark and reads busy for ever.
+ const rows = @max(pty_rows_min, dimension(words.next() orelse return false) orelse return false);
if (words.next() != null) return false;
if (!apply) return true;
p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } });
diff --git a/src/pardes.zig b/src/pardes.zig
index dd9d91c8..6341de91 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -6596,7 +6596,10 @@ pub const Pardes = struct {
pane.tag_top = @intCast(@min(top, lines - pane.tag_rows));
} else pane.tag_top = 0;
const cols = @max(1, r.w -| config.GUTTER);
- const rows = @max(1, r.h -| pane.tag_rows); // the tag steals the top rows
+ // The tag steals the top rows. The pty is what the pane shows,
+ // even one row dragged short: a taller pty would put its cursor
+ // below the pane (only winsize keeps two, ctlfs.pty).
+ const rows = @max(1, r.h -| pane.tag_rows);
// A terminal's pty follows its pane down to a row, whatever the
// resize; only a folded one keeps the size it had, and one not
// placed yet the grid it was made with.
diff --git a/test/fs.py b/test/fs.py
index 8d16d718..45fdbc38 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -435,6 +435,13 @@ def run_file(binary):
assert run(client, term, b'fi\n') == b'error not run\n' # a syntax error runs nothing
assert run(client, term, b"echo 'open\n") == b'error not run\n' # nor does an open quote
assert run(client, term, b'true\n') == b'exit 0\n' # ...and the shell is clear after
+ # A pty asked for one row gets two: one loses the prompt's input
+ # mark, and the shell would read busy for ever.
+ client.write(f'/pane/{term}/pty/ctl', b'winsize 20 1\n')
+ time.sleep(.5)
+ assert run(client, term, b'true\n') == b'exit 0\n'
+ assert client.read(f'/pane/{term}/pty/ctl').split()[2] == b'2', client.read(f'/pane/{term}/pty/ctl')
+ client.write(f'/pane/{term}/pty/ctl', b'winsize 80 24\n')
with Client(address) as other:
slow = []
waiter = threading.Thread(target=lambda: slow.append(run(other, term, b'sleep 1\n')), daemon=True)