summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/config.zig11
-rw-r--r--src/host_io.zig66
-rw-r--r--src/main.zig122
-rw-r--r--src/ninep/ctl.zig11
-rw-r--r--src/pardes.zig16
-rw-r--r--test/fs.py51
6 files changed, 265 insertions, 12 deletions
diff --git a/src/config.zig b/src/config.zig
index 86951e27..5a9cf070 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -81,6 +81,7 @@ pub const leader_path = paths: {
.Placement = "cp",
.BootShell = null,
.LookWord = null,
+ .Pager = null,
.Debug = "td",
.Msg = null,
.Colors = "tc",
@@ -754,13 +755,18 @@ pub const Runtime = struct {
/// next place in that pane, wrapping; `list`, what pardes did before,
/// lists every place in a +Search pane (look.zig, lookWord).
look_word: LookWord = .search,
+ /// What a terminal's shell pages through (host_io.ChildEnv): `pardes`,
+ /// PAGER and GIT_PAGER set to `pardes -`, a +Pager pane, where the
+ /// user's environment set neither; `off`, the environment as it is.
+ pager: Pager = .pardes,
pub const ShaderAnimation = enum { off, on, always };
pub const Placement = enum { acme, pardes };
pub const BootShell = enum { keep, replace };
pub const LookWord = enum { search, list };
+ pub const Pager = enum { pardes, off };
/// A setting that is one of a few words, each an enum field of Runtime.
- pub const Choice = enum { placement, boot_shell, look_word };
+ pub const Choice = enum { placement, boot_shell, look_word, pager };
/// shadow: a soft drop shadow. rim: a light hairline along the top of
/// the focused pane's tag rows. auto: a shadow on a light page; on a
@@ -1025,6 +1031,7 @@ pub const Runtime = struct {
.{ .word = "Placement", .action = .{ .choice = .placement }, .doc = "Where a new pane goes: acme, as makenewwindow does, or pardes, the older rules." },
.{ .word = "BootShell", .action = .{ .choice = .boot_shell }, .doc = "A document dropped where a lone untouched shell stands: keep the shell, or replace it." },
.{ .word = "LookWord", .action = .{ .choice = .look_word }, .doc = "A plain word looked at: search selects its next place in the pane, list lists every place in a +Search." },
+ .{ .word = "Pager", .action = .{ .choice = .pager }, .doc = "What a terminal's commands page through: pardes, a +Pager pane (pardes -), or off, the environment's own; for terminals started after it." },
};
pub fn find(name: []const u8) ?Setting {
@@ -1699,6 +1706,7 @@ pub const Runtime = struct {
.placement => "pardes",
.boot_shell => "replace",
.look_word => "list",
+ .pager => "off",
},
.lift => "rim",
.motion => "playful",
@@ -1768,6 +1776,7 @@ pub const Runtime = struct {
.placement => "Placement pardes\n",
.boot_shell => "BootShell replace\n",
.look_word => "LookWord list\n",
+ .pager => "Pager off\n",
},
.lift => "Lift rim\n",
.motion => "Motion playful\n",
diff --git a/src/host_io.zig b/src/host_io.zig
index 8ddb0c90..4ebac9ee 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1255,7 +1255,12 @@ pub const ChildEnv = struct {
return line[0 .. std.mem.indexOfScalar(u8, line, '=') orelse line.len];
}
- pub fn build(slots: *Slots) ?[*:null]const ?[*:0]const u8 {
+ /// Which pager a child is given where the user's environment names none:
+ /// a command pane's one-shot shell `cat` (its output is a pane already),
+ /// a terminal's shell `pardes -` (a +Pager pane) unless `Pager off`.
+ pub const Paging = enum { command, terminal, terminal_off };
+
+ pub fn build(slots: *Slots, paging: Paging) ?[*:null]const ?[*:0]const u8 {
var n: usize = 0;
var i: usize = 0;
while (libc.environ[i]) |entry| : (i += 1) {
@@ -1268,9 +1273,15 @@ pub const ChildEnv = struct {
slots[n] = entry;
n += 1;
}
- // No pager to wait in: a pty/run or a command pane's output is read
- // whole, and less would hold it at its first screen for ever. Only
- // where the inherited environment chose none.
+ // No pager to wait in (less holds a run at its first screen for
+ // ever): a command pane's output is read whole, through cat; a
+ // terminal's goes to a +Pager pane. Only where the inherited
+ // environment chose none.
+ const defaults: []const [*:0]const u8 = switch (paging) {
+ .command => &cat_pagers,
+ .terminal => pardesPagers(),
+ .terminal_off => &.{},
+ };
for (defaults) |default| {
const set = for (libc.environ[0..i]) |entry| {
if (std.mem.eql(u8, name(entry.?), name(default))) break true;
@@ -1288,8 +1299,27 @@ pub const ChildEnv = struct {
return @ptrCast(slots);
}
- /// Set for a pane's child only when the user's environment has none.
- const defaults = [_][*:0]const u8{ "PAGER=cat", "GIT_PAGER=cat" };
+ const cat_pagers = [_][*:0]const u8{ "PAGER=cat", "GIT_PAGER=cat" };
+
+ /// `PAGER='<this pardes> -'`, its own path so no PATH lookup can find
+ /// another; plain `pardes -` where the path cannot be read or quoted.
+ fn pardesPagers() []const [*:0]const u8 {
+ const S = struct {
+ var pager: [std.fs.max_path_bytes + 16:0]u8 = undefined;
+ var git_pager: [std.fs.max_path_bytes + 16:0]u8 = undefined;
+ var entries: [2][*:0]const u8 = undefined;
+ var ready = false;
+ };
+ if (!S.ready) {
+ var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const n = if (comptime builtin.os.tag == .linux) libc.readlink("/proc/self/exe", &exe_buf, exe_buf.len) else -1;
+ const exe: []const u8 = if (n > 0 and @as(usize, @intCast(n)) < exe_buf.len and std.mem.indexOfScalar(u8, exe_buf[0..@intCast(n)], '\'') == null) exe_buf[0..@intCast(n)] else "pardes";
+ S.entries[0] = if (std.fmt.bufPrintSentinel(&S.pager, "PAGER='{s}' -", .{exe}, 0)) |t| t.ptr else |_| "PAGER=pardes -";
+ S.entries[1] = if (std.fmt.bufPrintSentinel(&S.git_pager, "GIT_PAGER='{s}' -", .{exe}, 0)) |t| t.ptr else |_| "GIT_PAGER=pardes -";
+ S.ready = true;
+ }
+ return &S.entries;
+ }
};
test "the child environment replaces the launcher's terminal identity exactly once" {
@@ -1314,7 +1344,24 @@ test "the child environment replaces the launcher's terminal identity exactly on
defer _ = unsetenv("PARDES_CHILD_ENV_PROBE");
var slots: ChildEnv.Slots = undefined;
- const envp = ChildEnv.build(&slots) orelse return error.EnvironmentTooLarge;
+ // Where the user named no pager: a terminal's shell pages into a
+ // +Pager pane (`pardes -`), or, Pager off, as the environment says; a
+ // command pane's through cat. A pager the user named is kept.
+ for ([_]ChildEnv.Paging{ .terminal, .terminal_off }) |paging| {
+ var shell_slots: ChildEnv.Slots = undefined;
+ const shell_envp = ChildEnv.build(&shell_slots, paging) orelse return error.EnvironmentTooLarge;
+ var said: usize = 0;
+ var j: usize = 0;
+ while (shell_envp[j]) |entry| : (j += 1) {
+ const line = std.mem.span(entry);
+ if (std.mem.startsWith(u8, line, "PAGER=") and std.c.getenv("PAGER") == null) {
+ try std.testing.expect(paging == .terminal and std.mem.endsWith(u8, line, " -"));
+ said += 1;
+ }
+ }
+ if (std.c.getenv("PAGER") == null) try std.testing.expectEqual(@as(usize, if (paging == .terminal) 1 else 0), said);
+ }
+ const envp = ChildEnv.build(&slots, .command) orelse return error.EnvironmentTooLarge;
var terms: usize = 0;
var colorterms: usize = 0;
var probes: usize = 0;
@@ -1442,7 +1489,10 @@ pub fn forkShell(
// not call setenv, whose malloc can deadlock against a pty reader thread
// that held the heap when the fork took its snapshot.
var env_slots: ChildEnv.Slots = undefined;
- const envp = ChildEnv.build(&env_slots);
+ const paging: ChildEnv.Paging = if (one_line != null)
+ .command
+ else if (core) |c| (if (c.settings.pager == .off) .terminal_off else .terminal) else .terminal;
+ const envp = ChildEnv.build(&env_slots, paging);
const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
// The child says here why it could not become the shell (its chdir or
// its exec failed, as for a script whose interpreter is not there): the
diff --git a/src/main.zig b/src/main.zig
index 9dce559f..8818645e 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -46,11 +46,13 @@ pub const debug = if (is_emscripten) struct {} else struct {
};
const help_text =
- \\Usage: pardes [options] [file|dir]
+ \\Usage: pardes [options] [file|dir|-]
\\
\\With no arguments the shell pane starts in tty mode. A FILE argument
\\(optionally file:LINE) opens it alone, filling the window; a DIRECTORY
- \\argument becomes the cwd shells spawn in.
+ \\argument becomes the cwd shells spawn in. `-` reads stdin, its terminal
+ \\escapes stripped, into a +Pager pane: inside a pardes pane the outer
+ \\session's, returning at once (a terminal's PAGER), else a new editor's.
\\
\\Options:
\\ --tty start as one shell pane already in tty mode
@@ -98,6 +100,95 @@ const help_text =
\\
;
+/// Stdin to its end, its terminal escapes stripped (a pager's input is
+/// coloured for a terminal; a text pane shows text), at most a file's
+/// limit, a longer one cut there and said so at its end.
+fn readStdinText(gpa: std.mem.Allocator) ![]u8 {
+ const limit = pardes.memory.limits.max_file_bytes;
+ var raw: std.ArrayList(u8) = .empty;
+ var chunk: [64 * 1024]u8 = undefined;
+ var cut = false;
+ while (true) {
+ const n = std.c.read(0, &chunk, chunk.len);
+ if (n < 0) {
+ if (std.posix.errno(n) == .INTR) continue;
+ return error.ReadFailed;
+ }
+ if (n == 0) break;
+ const got: usize = @intCast(n);
+ if (raw.items.len + got > limit) {
+ try raw.appendSlice(gpa, chunk[0 .. limit - raw.items.len]);
+ cut = true;
+ break;
+ }
+ try raw.appendSlice(gpa, chunk[0..got]);
+ }
+ var text = try stripEscapes(gpa, raw.items);
+ if (cut) {
+ const said = "\n[pardes: the rest of stdin is cut: a pane holds at most a file's limit]\n";
+ text = try std.mem.concat(gpa, u8, &.{ text, said });
+ }
+ return text;
+}
+
+/// `bytes` without terminal escapes: CSI (`ESC [` ... a final byte, SGR
+/// colour among them), OSC (`ESC ]` ... BEL or `ESC \`), any other
+/// two-byte `ESC x`, and a man page's overstrike (`x BS x`, `_ BS x`).
+pub fn stripEscapes(gpa: std.mem.Allocator, bytes: []const u8) ![]u8 {
+ var out = try std.ArrayList(u8).initCapacity(gpa, bytes.len);
+ var i: usize = 0;
+ while (i < bytes.len) {
+ const c = bytes[i];
+ if (c == 0x1b and i + 1 < bytes.len) {
+ const kind = bytes[i + 1];
+ i += 2;
+ if (kind == '[') {
+ while (i < bytes.len and !(bytes[i] >= 0x40 and bytes[i] <= 0x7e)) i += 1;
+ i += 1;
+ } else if (std.mem.indexOfScalar(u8, "()*+", kind) != null) {
+ i += 1; // a charset's name after it
+ } else if (kind == ']') {
+ while (i < bytes.len) : (i += 1) {
+ if (bytes[i] == 0x07) {
+ i += 1;
+ break;
+ }
+ if (bytes[i] == 0x1b and i + 1 < bytes.len and bytes[i + 1] == '\\') {
+ i += 2;
+ break;
+ }
+ }
+ }
+ continue;
+ }
+ // Overstrike: the byte before a backspace is struck over.
+ if (c == 0x08) {
+ if (out.items.len > 0) _ = out.pop();
+ i += 1;
+ continue;
+ }
+ out.appendAssumeCapacity(c);
+ i += 1;
+ }
+ return out.toOwnedSlice(gpa);
+}
+
+test "stripEscapes takes out colour, other escapes and overstrike, and keeps the text" {
+ const gpa = std.testing.allocator;
+ const cases = [_][2][]const u8{
+ .{ "\x1b[33mcommit abc\x1b[m\n", "commit abc\n" },
+ .{ "a\x1b]8;;http://x\x1b\\link\x1b]8;;\x07b", "alinkb" },
+ .{ "N\x08NA\x08AM\x08ME\x08E _\x08u", "NAME u" },
+ .{ "plain\ttext\n", "plain\ttext\n" },
+ .{ "\x1b(Bdone", "done" },
+ };
+ for (cases) |c| {
+ const got = try stripEscapes(gpa, c[0]);
+ defer gpa.free(got);
+ try std.testing.expectEqualStrings(c[1], got);
+ }
+}
+
/// A forwarding failure in words, never an error's name: the ones this
/// file raises said outright, any other as its name's words (`NotFound`,
/// `not found`), a dial's as Messages says it.
@@ -369,7 +460,8 @@ fn nativeMain(init: std.process.Init) !void {
} else if (std.mem.eql(u8, a, "--version")) {
try std.Io.File.stdout().writeStreamingAll(init.io, version_text);
return;
- } else if (a.len > 0 and a[0] != '-' and positional == null) {
+ } else if (a.len > 0 and (a[0] != '-' or std.mem.eql(u8, a, "-")) and positional == null) {
+ // `-` is stdin, as for cat and vim: a text to show.
session_only = false;
positional = a;
} else if (a.len == 0) {
@@ -440,6 +532,24 @@ fn nativeMain(init: std.process.Init) !void {
// itself, which is what a right click in that pane would have been.
var look_buf: [64]u8 = undefined;
const look = try std.fmt.bufPrint(&look_buf, "/pane/{d}/look", .{parent.serial});
+ // `pardes -`: stdin read to its end, a +Pager pane of the session
+ // made of it (as acme's `B` has no stdin, through pane/new, a name
+ // and the body), and back at once: the PAGER a pane's shell gets.
+ if (positional) |word| if (std.mem.eql(u8, word, "-")) {
+ const text = readStdinText(arena) catch |err| Refuse.with(init.io, "stdin", err);
+ const made = ninep_io.Client.read(arena, parent.dial, "/pane/new", "/pane/new") catch |err| Refuse.with(init.io, "-", err);
+ const serial = std.fmt.parseInt(u32, std.mem.trim(u8, made, " \n"), 10) catch |err| Refuse.with(init.io, "-", err);
+ var cwd_buf: [4096]u8 = undefined;
+ const cwd = if (std.c.getcwd(&cwd_buf, cwd_buf.len)) |c| std.mem.span(@as([*:0]u8, @ptrCast(c))) else "/";
+ var path_buf: [64]u8 = undefined;
+ var line_buf: [4200]u8 = undefined;
+ const name_line = std.fmt.bufPrint(&line_buf, "{s}/+Pager\n", .{std.mem.trimEnd(u8, cwd, "/")}) catch "/+Pager\n";
+ ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/name", .{serial}), name_line) catch |err| Refuse.with(init.io, "-", err);
+ if (text.len > 0) ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/body", .{serial}), text) catch |err| Refuse.with(init.io, "-", err);
+ // Shown, not edited: nothing to ask about at Exit.
+ ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/ctl", .{serial}), "clean\n") catch {};
+ return;
+ };
const typed_word = positional orelse {
var tag_buf: [64]u8 = undefined;
const tag = try std.fmt.bufPrint(&tag_buf, "/pane/{d}/tag", .{parent.serial});
@@ -530,6 +640,12 @@ fn nativeMain(init: std.process.Init) !void {
if (wait) waitForDel(init.io, arena, parent.dial, path, made_serial);
return;
}
+ // `pardes -` outside a session: an editor whose first pane holds stdin,
+ // as `vim -` does; the terminal is /dev/tty, never stdin.
+ if (positional) |a| if (std.mem.eql(u8, a, "-")) {
+ opts.stdin_text = readStdinText(arena) catch |err| badArgs(init.io, "stdin: {s}", .{@errorName(err)});
+ positional = null;
+ };
if (positional) |a| {
const target = @import("look.zig").parsePathLine(a);
if (std.mem.eql(u8, target.path, "/n") or std.mem.startsWith(u8, target.path, "/n/") or
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index e0c4b21b..87339b70 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1812,6 +1812,17 @@ test "a look at a file it may not read fails the write naming the file" {
try testing.expectEqualStrings(expected, refused.reply.ename);
}
+test "Pager is a setting of the root ctl: pardes by default, off taken, anything else refused with its choices" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ try testing.expect(std.mem.indexOf(u8, rd(p, root_ctl, 0, 1 << 16).bytes, "Pager pardes\n") != null);
+ try testing.expectEqual(Status.ok, wr(p, root_ctl, "Pager off\n").reply.status);
+ try testing.expectEqual(config.Runtime.Pager.off, p.settings.pager);
+ try testing.expect(std.mem.indexOf(u8, rd(p, root_ctl, 0, 1 << 16).bytes, "Pager off\n") != null);
+ try testing.expectEqualStrings("bad value in control message; takes pardes, off \"Pager\"", wr(p, root_ctl, "Pager less\n").reply.ename);
+}
+
test "a refused Del written back through event fails that write, EIO, with its err" {
const gpa = testing.allocator;
const p = try withFile(gpa, "text\n");
diff --git a/src/pardes.zig b/src/pardes.zig
index 47ebf8fa..aa2c252c 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4180,6 +4180,8 @@ pub const Boot = enum {
document,
/// a Look that found nothing: reported, not opened
missing,
+ /// `pardes -`: stdin's text in a +Pager pane, as `vim -` shows it
+ stdin,
/// the firmware's boot buffer, the only pane a board has
embedded,
/// the bare tty startup: one shell in raw tty mode, with an empty text
@@ -4196,6 +4198,7 @@ pub const Boot = enum {
if (opts.boot) |named| return named;
if (opts.file != null) return .document;
if (opts.missing != null) return .missing;
+ if (opts.stdin_text != null) return .stdin;
if (comptime platform == .esp32p4) return .embedded;
if (opts.tty_only) return .tty_shell;
// ponytail: 1 and 3 are the only shell counts anyone asked for
@@ -4215,6 +4218,8 @@ pub const Options = struct {
file: ?[]const u8 = null,
file_line: usize = 0,
missing: ?struct { word: []const u8, dir: []const u8 } = null,
+ /// `pardes -`: stdin, its escapes stripped, for a +Pager pane at boot.
+ stdin_text: ?[]const u8 = null,
tty_toggle: u21 = config.tty_toggle_default,
/// load a dump of another instance instead of spawning shells (acme -l)
load_path: ?[]const u8 = null,
@@ -4626,6 +4631,17 @@ pub const Pardes = struct {
_ = try panes.Output.open(p, 0, missing.dir, .errors, "", content);
p.oneColumn();
},
+ .stdin => {
+ const content = try gpa.dupe(u8, opts.stdin_text.?);
+ errdefer gpa.free(content);
+ const dir = p.sessionDir();
+ const shown = try panes.Output.open(p, 0, dir, .{ .cmd = .New }, "", content);
+ // Named +Pager, as `pardes -` inside a session names its pane.
+ const named = try std.fs.path.join(gpa, &.{ dir, "+Pager" });
+ gpa.free(shown.file.?.path);
+ shown.file.?.path = named;
+ p.oneColumn();
+ },
.embedded => {
const content = try p.gpa.dupe(u8, boot_buffer);
errdefer p.gpa.free(content);
diff --git a/test/fs.py b/test/fs.py
index 23647b45..282cca8b 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -479,6 +479,22 @@ 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 paging command run through pty/run does not wait in less: the
+ # terminal's PAGER is `pardes -`, and its text is a +Pager pane.
+ if shutil.which('git'):
+ repo = root / 'pagedrepo'
+ repo.mkdir()
+ git = ['git', '-C', str(repo), '-c', 'user.name=p', '-c', 'user.email=p@x']
+ subprocess.run(git + ['init', '-q'], check=True, env=dict(os.environ, HOME=str(root)))
+ (repo / 'f').write_text('x\n')
+ subprocess.run(git + ['add', 'f'], check=True, env=dict(os.environ, HOME=str(root)))
+ subprocess.run(git + ['commit', '-qm', 'paged-commit-subject'], check=True, env=dict(os.environ, HOME=str(root)))
+ assert run(client, term, b'git -C pagedrepo log\n').startswith(b'exit 0\n')
+ pager = [r.split()[0].decode() for r in client.read('/index').splitlines() if r.split()[3:4] == [f'{repo}/+Pager'.encode()]] # git -C runs it in the repo
+ assert len(pager) == 1, client.read('/index')
+ assert b'paged-commit-subject' in client.read(f'/pane/{pager[0]}/body')
+ assert b'\x1b' not in client.read(f'/pane/{pager[0]}/body')
+ client.remove(f'/pane/{pager[0]}')
# A pty is not given one row (it loses the prompt's input mark,
# and the shell would read busy for ever): refused, and said.
try:
@@ -1421,6 +1437,17 @@ def test(binary, quic=False):
# A pane address the session has not is refused in words, exit
# 1, no pane made for a file of that name; one it has is looked
# at. A name with a newline is said in words, not an error name.
+ # `pardes -` (a terminal's PAGER): stdin, its escapes stripped, in a
+ # +Pager pane of the session, clean, and back at once.
+ fed = subprocess.run([binary, '-'], cwd=root, env=env, input=b'\x1b[1mbold\x1b[0m text\n',
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
+ assert fed.returncode == 0, fed.stderr
+ made_now, = serials() - before
+ assert client.read(f'/pane/{made_now}/name') == f'{root}/+Pager\n'.encode()
+ assert client.read(f'/pane/{made_now}/body') == b'bold text\n'
+ row = [r for r in client.read('/index').splitlines() if r.split()[0] == str(made_now).encode()][0]
+ assert row.split()[2] == b'0', row # clean: nothing to ask at Exit
+ client.remove(f'/pane/{made_now}')
# A new name in a directory there but not writable: refused now,
# not a pane that only fails at Save.
shut = root / 'shut-dir'
@@ -1450,6 +1477,30 @@ def test(binary, quic=False):
assert orphan.poll() is None
assert orphan.wait(timeout=5) == 1
+ # `pardes -` outside a session: an editor whose first pane is a
+ # +Pager of stdin, as `vim -`; here a detached one, to read it.
+ outside_env = {k: v for k, v in os.environ.items() if not k.startswith('PARDES_')}
+ outside_env.update(HOME=str(root), XDG_RUNTIME_DIR=str(root), PARDES_NOTIME='1')
+ boot = subprocess.Popen([binary, '--detach=stdinboot', '-'], cwd=root, env=outside_env,
+ stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
+ start_new_session=True)
+ try:
+ boot.stdin.write(b'\x1b[32mfrom stdin\x1b[m\nsecond\n')
+ boot.stdin.close()
+ sock = root / 'pardes-9p-stdinboot.sock'
+ deadline = time.monotonic() + 10
+ while not sock.exists():
+ assert boot.poll() is None, boot.stderr.read()
+ assert time.monotonic() < deadline, 'the stdin boot never listened'
+ time.sleep(.05)
+ with Client(str(sock)) as booted:
+ first = booted.read('/index').split(b'\n')[0].split()
+ assert first[3] == f'{root}/+Pager'.encode(), first
+ assert booted.read(f'/pane/{first[0].decode()}/body') == b'from stdin\nsecond\n'
+ finally:
+ boot.terminate()
+ boot.wait(timeout=10)
+
# Killed with SIGTERM or SIGHUP, a tty editor puts its terminal
# back (main screen, cooked mode) before it dies: a nested one
# killed in a pane leaves that pane usable.