422efd20
test: the picker on a real pty, and the wall that outlives its last tile
a73x 2026-08-28 19:53
Commit message
src/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -366,6 +366,9 @@ const Shared = struct { | |||
| 366 | /// the user is choosing from. Their replicas stay hot the whole time, | 366 | /// the user is choosing from. Their replicas stay hot the whole time, |
| 367 | /// and the close bumps `repaint_gen` so every rect comes back. | 367 | /// and the close bumps `repaint_gen` so every rect comes back. |
| 368 | picker_open: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), | 368 | picker_open: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), |
| 369 | /// The popup's last frame, so an unchanged one is not rewritten. | ||
| 370 | /// Keyboard-thread only, like the picker itself. | ||
| 371 | picker_stamp: u64 = 0, | ||
| 369 | }; | 372 | }; |
| 370 | 373 | ||
| 371 | /// The rect a paint used, in the only terms a click can be resolved in. | 374 | /// The rect a paint used, in the only terms a click can be resolved in. |
| @@ -736,26 +739,6 @@ fn tileBanner(t: *Tile, text: []const u8) void { | |||
| 736 | paint.paintBanner(t.shared.out_fd, t.rect.cols, shown, t.rect.top + t.shared.label_rows, t.rect.left); | 739 | paint.paintBanner(t.shared.out_fd, t.rect.cols, shown, t.rect.top + t.shared.label_rows, t.rect.left); |
| 737 | } | 740 | } |
| 738 | 741 | ||
| 739 | /// `tileBanner` undone. The keyboard paints the prompt, and on a tile whose | ||
| 740 | /// pump has died it is also the only thread that can take it off again: | ||
| 741 | /// `paintDeadBarsLocked` redraws the label BAR, one row above this one, so | ||
| 742 | /// an unerased prompt line sits there until the wall comes down. | ||
| 743 | fn tileBannerClear(t: *Tile) void { | ||
| 744 | if (!t.shared.is_tty or t.shared.picker_open.load(.acquire)) return; | ||
| 745 | var buf: [40]u8 = undefined; | ||
| 746 | // Span-bounded ECH, never `\x1b[K`: the cells past this tile's own | ||
| 747 | // width are a rail's and a neighbour's. Saved and restored around it | ||
| 748 | // like `paintBanner`, so no shell's cursor visibly jumps. | ||
| 749 | const text = std.fmt.bufPrint(&buf, "\x1b[s\x1b[{d};{d}H\x1b[0m\x1b[{d}X\x1b[u", .{ | ||
| 750 | t.rect.top + t.shared.label_rows + 1, | ||
| 751 | t.rect.left + 1, | ||
| 752 | t.rect.cols, | ||
| 753 | }) catch return; | ||
| 754 | t.shared.paint_mu.lock(); | ||
| 755 | defer t.shared.paint_mu.unlock(); | ||
| 756 | proto.writeAllFd(t.shared.out_fd, text) catch {}; | ||
| 757 | } | ||
| 758 | |||
| 759 | /// Under `paint_mu`: a clear spliced into a 64 KiB OSC 52 write eats the | 742 | /// Under `paint_mu`: a clear spliced into a 64 KiB OSC 52 write eats the |
| 760 | /// paint after it. | 743 | /// paint after it. |
| 761 | fn copySelection( | 744 | fn copySelection( |
| @@ -1916,6 +1899,9 @@ fn relayout( | |||
| 1916 | // visible pane, no bar — the plain client's byte stream. | 1899 | // visible pane, no bar — the plain client's byte stream. |
| 1917 | shared.label_rows = if (shared.fullscreen or live <= 1) 0 else 1; | 1900 | shared.label_rows = if (shared.fullscreen or live <= 1) 0 else 1; |
| 1918 | if (shared.is_tty) proto.writeAllFd(shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; | 1901 | if (shared.is_tty) proto.writeAllFd(shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; |
| 1902 | // The screen the popup was on has just been cleared, so the next paint | ||
| 1903 | // owes it however unchanged its rows are. | ||
| 1904 | if (shared.picker_open.load(.acquire)) shared.picker_stamp = 0; | ||
| 1919 | if (live == 0) { | 1905 | if (live == 0) { |
| 1920 | paintEmptyWallLocked(shared); | 1906 | paintEmptyWallLocked(shared); |
| 1921 | return; | 1907 | return; |
| @@ -2907,6 +2893,10 @@ const picker_row_max: usize = 128; | |||
| 2907 | const picker_min_cols: u16 = 24; | 2893 | const picker_min_cols: u16 = 24; |
| 2908 | const picker_min_rows: u16 = 4; | 2894 | const picker_min_rows: u16 = 4; |
| 2909 | 2895 | ||
| 2896 | /// One whole popup, written in one go: every row plus its cursor address | ||
| 2897 | /// and its two SGRs, at the table's cap of rows. | ||
| 2898 | const picker_frame_max: usize = (max_tiles + 2) * (picker_row_max + 32) + 8; | ||
| 2899 | |||
| 2910 | /// The picker's rendered rows. Rendered once per paint and read twice — | 2900 | /// The picker's rendered rows. Rendered once per paint and read twice — |
| 2911 | /// by the writer and by the tests — so the box on the screen is the box | 2901 | /// by the writer and by the tests — so the box on the screen is the box |
| 2912 | /// the claims are made about. | 2902 | /// the claims are made about. |
| @@ -3083,9 +3073,7 @@ fn pickForget( | |||
| 3083 | /// knows the picker exists: on open, on every key, and on every list that | 3073 | /// knows the picker exists: on open, on every key, and on every list that |
| 3084 | /// lands under it, so the state column is live while the user reads it. | 3074 | /// lands under it, so the state column is live while the user reads it. |
| 3085 | /// | 3075 | /// |
| 3086 | /// `line` is the spelling editor's text when `a` has it open. The notice | 3076 | /// `line` is the spelling editor's text when `a` has it open. |
| 3087 | /// wins the footer over both, because a refusal the user just earned is | ||
| 3088 | /// the one sentence the legend can wait for. | ||
| 3089 | fn paintPicker(shared: *Shared, host_table: []Host, sel: usize, line: ?[]const u8) void { | 3077 | fn paintPicker(shared: *Shared, host_table: []Host, sel: usize, line: ?[]const u8) void { |
| 3090 | if (!shared.is_tty) return; | 3078 | if (!shared.is_tty) return; |
| 3091 | // Before the lock, so a pump already inside `tilePaintBegin` blocks | 3079 | // Before the lock, so a pump already inside `tilePaintBegin` blocks |
| @@ -3114,6 +3102,9 @@ fn paintPicker(shared: *Shared, host_table: []Host, sel: usize, line: ?[]const u | |||
| 3114 | if (hi == sel) sel_row = i; | 3102 | if (hi == sel) sel_row = i; |
| 3115 | } | 3103 | } |
| 3116 | if (shown > 0 and sel_row >= shown) first = sel_row - shown + 1; | 3104 | if (shown > 0 and sel_row >= shown) first = sel_row - shown + 1; |
| 3105 | // The notice wins the footer over the legend and over the editor's own | ||
| 3106 | // line: a refusal the user just earned is the one sentence that cannot | ||
| 3107 | // wait for the next keystroke. | ||
| 3117 | var notice_buf: [96]u8 = undefined; | 3108 | var notice_buf: [96]u8 = undefined; |
| 3118 | const notice = takeNoticeLocked(shared, ¬ice_buf); | 3109 | const notice = takeNoticeLocked(shared, ¬ice_buf); |
| 3119 | const foot: []const u8 = if (notice.len > 0) | 3110 | const foot: []const u8 = if (notice.len > 0) |
| @@ -3122,31 +3113,40 @@ fn paintPicker(shared: *Shared, host_table: []Host, sel: usize, line: ?[]const u | |||
| 3122 | l | 3113 | l |
| 3123 | else | 3114 | else |
| 3124 | " Enter/c new session x forget a add Esc"; | 3115 | " Enter/c new session x forget a add Esc"; |
| 3116 | |||
| 3117 | var out: [picker_frame_max]u8 = undefined; | ||
| 3118 | var fbs = std.io.fixedBufferStream(&out); | ||
| 3119 | const wr = fbs.writer(); | ||
| 3125 | // The cursor is hidden for as long as the popup owns the screen: a | 3120 | // The cursor is hidden for as long as the popup owns the screen: a |
| 3126 | // caret left blinking in a tile says the keys are going there. | 3121 | // caret left blinking in a tile says the keys are going there. |
| 3127 | proto.writeAllFd(shared.out_fd, "\x1b[?25l") catch {}; | 3122 | wr.writeAll("\x1b[?25l") catch return; |
| 3128 | pickerLine(shared, top, left, w, " hosts"); | 3123 | pickerLine(wr, top, left, w, " hosts"); |
| 3129 | if (cramped) return; | 3124 | if (!cramped) { |
| 3130 | var r: u16 = 0; | 3125 | var r: u16 = 0; |
| 3131 | while (r < shown and first + r < body.n) : (r += 1) | 3126 | while (r < shown and first + r < body.n) : (r += 1) |
| 3132 | pickerLine(shared, top + 1 + r, left, w, body.row(first + r)); | 3127 | pickerLine(wr, top + 1 + r, left, w, body.row(first + r)); |
| 3133 | pickerLine(shared, top + height - 1, left, w, foot); | 3128 | pickerLine(wr, top + height - 1, left, w, foot); |
| 3129 | } | ||
| 3130 | const frame = fbs.getWritten(); | ||
| 3131 | // Nothing changed, nothing written. The pollers report once a second | ||
| 3132 | // per host and every one of them repaints this box; rewriting an | ||
| 3133 | // identical screen at that rate is a terminal that never goes quiet. | ||
| 3134 | const stamp = std.hash.Wyhash.hash(0, frame); | ||
| 3135 | if (stamp == shared.picker_stamp) return; | ||
| 3136 | shared.picker_stamp = stamp; | ||
| 3137 | proto.writeAllFd(shared.out_fd, frame) catch {}; | ||
| 3134 | } | 3138 | } |
| 3135 | 3139 | ||
| 3136 | /// One row of the box: reverse video, padded to the box width. The same | 3140 | /// One row of the box: reverse video, padded to the box width. The same |
| 3137 | /// chrome a label bar and a rail wear, so the popup never reads as a | 3141 | /// chrome a label bar and a rail wear, so the popup never reads as a |
| 3138 | /// session's own output. | 3142 | /// session's own output. |
| 3139 | fn pickerLine(shared: *Shared, row: u16, left: u16, w: u16, text: []const u8) void { | 3143 | fn pickerLine(wr: anytype, row: u16, left: u16, w: u16, text: []const u8) void { |
| 3140 | var buf: [picker_row_max * 2 + 64]u8 = undefined; | 3144 | wr.print("\x1b[{d};{d}H\x1b[7m", .{ row + 1, left + 1 }) catch return; |
| 3141 | var fbs = std.io.fixedBufferStream(&buf); | ||
| 3142 | const out = fbs.writer(); | ||
| 3143 | out.print("\x1b[{d};{d}H\x1b[7m", .{ row + 1, left + 1 }) catch return; | ||
| 3144 | const take = @min(text.len, @as(usize, w)); | 3145 | const take = @min(text.len, @as(usize, w)); |
| 3145 | out.writeAll(text[0..take]) catch return; | 3146 | wr.writeAll(text[0..take]) catch return; |
| 3146 | var i: usize = take; | 3147 | var i: usize = take; |
| 3147 | while (i < w) : (i += 1) out.writeByte(' ') catch break; | 3148 | while (i < w) : (i += 1) wr.writeByte(' ') catch break; |
| 3148 | out.writeAll("\x1b[0m") catch return; | 3149 | wr.writeAll("\x1b[0m") catch return; |
| 3149 | proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {}; | ||
| 3150 | } | 3150 | } |
| 3151 | 3151 | ||
| 3152 | /// For a caller already holding `paint_mu`. | 3152 | /// For a caller already holding `paint_mu`. |
| @@ -3736,6 +3736,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 3736 | // an empty wall opens it once, and an Esc there has to be able to | 3736 | // an empty wall opens it once, and an Esc there has to be able to |
| 3737 | // leave the empty line showing rather than be re-opened over. | 3737 | // leave the empty line showing rather than be re-opened over. |
| 3738 | var picker_auto = false; | 3738 | var picker_auto = false; |
| 3739 | // Whether the popup was already on the screen when this key arrived, | ||
| 3740 | // so an OPEN can be told from a keystroke inside one. | ||
| 3741 | var picker_shown = false; | ||
| 3739 | // `feed` hands a candidate held across the previous read back ahead of | 3742 | // `feed` hands a candidate held across the previous read back ahead of |
| 3740 | // this chunk, so its room is a whole chunk plus that hold. | 3743 | // this chunk, so its room is a whole chunk plus that hold. |
| 3741 | var mouse_out: [mailbox_max + interact.MouseFilter.max_held]u8 = undefined; | 3744 | var mouse_out: [mailbox_max + interact.MouseFilter.max_held]u8 = undefined; |
| @@ -3906,17 +3909,39 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 3906 | // one-line empty-wall text standing. | 3909 | // one-line empty-wall text standing. |
| 3907 | var picker_opened = false; | 3910 | var picker_opened = false; |
| 3908 | if (presentCount(present[0..live]) == 0) { | 3911 | if (presentCount(present[0..live]) == 0) { |
| 3909 | if (shared.is_tty and !input.prefix.picking and !picker_auto) { | 3912 | // Not before the opening hosts have answered (`restore_tried` |
| 3913 | // is that same moment, or 2s): a wall still dialling is not | ||
| 3914 | // known to be empty, and a popup flashed over a wall that is | ||
| 3915 | // about to have tiles is one the user never asked for. | ||
| 3916 | if (shared.is_tty and restore_tried and !input.prefix.picking and !picker_auto) { | ||
| 3910 | picker_auto = true; | 3917 | picker_auto = true; |
| 3911 | picker_opened = true; | 3918 | picker_opened = true; |
| 3919 | picker_shown = true; | ||
| 3912 | input.prefix.picking = true; | 3920 | input.prefix.picking = true; |
| 3913 | picker_sel = pickerNearest(host_table[0..hosts_live], picker_sel); | 3921 | picker_sel = pickerNearest(host_table[0..hosts_live], picker_sel); |
| 3914 | } | 3922 | } |
| 3915 | } else picker_auto = false; | 3923 | } else if (picker_auto) { |
| 3924 | // A tile arrived under a popup NOBODY asked for: the wall has | ||
| 3925 | // something to show again, so it gets the screen back. One the | ||
| 3926 | // user opened stays open — they are choosing, and a session | ||
| 3927 | // being born on another host is not an answer to that. | ||
| 3928 | picker_auto = false; | ||
| 3929 | input.prefix.picking = false; | ||
| 3930 | picker_shown = false; | ||
| 3931 | shared.picker_open.store(false, .release); | ||
| 3932 | // The screen under the box is about to be redrawn, so the next | ||
| 3933 | // open owes a paint however identical its rows are. | ||
| 3934 | shared.picker_stamp = 0; | ||
| 3935 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | ||
| 3936 | } | ||
| 3916 | // On news, not on the tick: the rows have to follow the pollers | 3937 | // On news, not on the tick: the rows have to follow the pollers |
| 3917 | // while the popup is open, and a repaint every 100ms would rewrite | 3938 | // while the popup is open, and a repaint every 100ms would rewrite |
| 3918 | // a screen nothing changed on. | 3939 | // a screen nothing changed on. |
| 3919 | if (input.prefix.picking and !input.prefix.prompting and (picker_opened or host_news or winch)) | 3940 | // The bell too: a tile that ended relayouts under the box, and the |
| 3941 | // cleared screen owes the popup a repaint the pollers must not be | ||
| 3942 | // the first to give it. | ||
| 3943 | if (input.prefix.picking and !input.prefix.prompting and | ||
| 3944 | (picker_opened or host_news or winch or fds[1].revents != 0)) | ||
| 3920 | paintPicker(&shared, host_table[0..hosts_live], picker_sel, null); | 3945 | paintPicker(&shared, host_table[0..hosts_live], picker_sel, null); |
| 3921 | if (fds[0].revents == 0) continue; | 3946 | if (fds[0].revents == 0) continue; |
| 3922 | 3947 | ||
| @@ -3938,14 +3963,17 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 3938 | // Typed AT the session, ahead of the chord, in the same read. | 3963 | // Typed AT the session, ahead of the chord, in the same read. |
| 3939 | if (cmd.forward.len > 0 and z < live and present[z]) sendKeys(&tiles[z], cmd.forward); | 3964 | if (cmd.forward.len > 0 and z < live and present[z]) sendKeys(&tiles[z], cmd.forward); |
| 3940 | var birth_at: ?usize = null; | 3965 | var birth_at: ?usize = null; |
| 3966 | // Opening on a focused tile pre-selects that tile's host: the | ||
| 3967 | // machine the user is already on is the one they mean. Judged | ||
| 3968 | // on the popup's own state, not on `.pick_open`, because one | ||
| 3969 | // read can open the picker and its editor together. | ||
| 3970 | if (!picker_shown) { | ||
| 3971 | if (z < live and present[z]) { | ||
| 3972 | if (tiles[z].host) |hi| picker_sel = hi; | ||
| 3973 | } | ||
| 3974 | picker_sel = pickerNearest(host_table[0..hosts_live], picker_sel); | ||
| 3975 | } | ||
| 3941 | switch (cmd.action) { | 3976 | switch (cmd.action) { |
| 3942 | // The machine the user is already on is the one they mean. | ||
| 3943 | .pick_open => { | ||
| 3944 | if (z < live and present[z]) { | ||
| 3945 | if (tiles[z].host) |hi| picker_sel = hi; | ||
| 3946 | } | ||
| 3947 | picker_sel = pickerNearest(host_table[0..hosts_live], picker_sel); | ||
| 3948 | }, | ||
| 3949 | .pick_move => |d| picker_sel = pickerStep(host_table[0..hosts_live], picker_sel, d), | 3977 | .pick_move => |d| picker_sel = pickerStep(host_table[0..hosts_live], picker_sel, d), |
| 3950 | .pick_select => |row| { | 3978 | .pick_select => |row| { |
| 3951 | if (pickerAt(host_table[0..hosts_live], row - 1)) |hi| picker_sel = hi; | 3979 | if (pickerAt(host_table[0..hosts_live], row - 1)) |hi| picker_sel = hi; |
| @@ -3988,6 +4016,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 3988 | }, | 4016 | }, |
| 3989 | else => {}, | 4017 | else => {}, |
| 3990 | } | 4018 | } |
| 4019 | picker_shown = input.prefix.picking; | ||
| 3991 | if (input.prefix.picking) { | 4020 | if (input.prefix.picking) { |
| 3992 | var line_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; | 4021 | var line_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; |
| 3993 | const line: ?[]const u8 = if (input.prefix.prompting) | 4022 | const line: ?[]const u8 = if (input.prefix.prompting) |
| @@ -4000,6 +4029,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 4000 | // and bumps `repaint_gen`, which is the only thing that | 4029 | // and bumps `repaint_gen`, which is the only thing that |
| 4001 | // redraws a tile whose session said nothing meanwhile. | 4030 | // redraws a tile whose session said nothing meanwhile. |
| 4002 | shared.picker_open.store(false, .release); | 4031 | shared.picker_open.store(false, .release); |
| 4032 | // The screen under the box is about to be redrawn, so the next | ||
| 4033 | // open owes a paint however identical its rows are. | ||
| 4034 | shared.picker_stamp = 0; | ||
| 4035 | picker_shown = false; | ||
| 4003 | if (birth_at) |at| | 4036 | if (birth_at) |at| |
| 4004 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, at) | 4037 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, at) |
| 4005 | else | 4038 | else |
| @@ -7239,36 +7272,6 @@ test "restore: focus_out maps the saved focus through the spelling match" { | |||
| 7239 | } | 7272 | } |
| 7240 | } | 7273 | } |
| 7241 | 7274 | ||
| 7242 | test "tileBannerClear: the prompt's row is erased inside the tile, never past it" { | ||
| 7243 | // A tile whose pump has died gets no claim to repaint over a banner, | ||
| 7244 | // so the keyboard takes its own prompt line back off the screen. The | ||
| 7245 | // erase is a span bounded by the tile's rect: line-wide would take the | ||
| 7246 | // rail and the neighbour's cells with it. | ||
| 7247 | const pipe = try std.posix.pipe(); | ||
| 7248 | defer std.posix.close(pipe[0]); | ||
| 7249 | var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; | ||
| 7250 | shared.label_rows = 1; | ||
| 7251 | var t = Tile{ | ||
| 7252 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | ||
| 7253 | .rect = .{ .top = 2, .left = 5, .rows = 5, .cols = 10 }, | ||
| 7254 | .shared = &shared, | ||
| 7255 | .idx = 0, | ||
| 7256 | .wake_r = -1, | ||
| 7257 | .wake_w = -1, | ||
| 7258 | }; | ||
| 7259 | tileBannerClear(&t); | ||
| 7260 | std.posix.close(pipe[1]); | ||
| 7261 | |||
| 7262 | var out: [256]u8 = undefined; | ||
| 7263 | const n = try std.posix.read(pipe[0], &out); | ||
| 7264 | const got = out[0..n]; | ||
| 7265 | // The same corner `tileBanner` writes into, and exactly as many cells | ||
| 7266 | // wide as the tile. | ||
| 7267 | try std.testing.expect(std.mem.indexOf(u8, got, "\x1b[4;6H") != null); | ||
| 7268 | try std.testing.expect(std.mem.indexOf(u8, got, "\x1b[10X") != null); | ||
| 7269 | try std.testing.expect(std.mem.indexOf(u8, got, "\x1b[K") == null); | ||
| 7270 | } | ||
| 7271 | |||
| 7272 | test "tileBanner: a prompt wider than its pane shows the tail inside the pane" { | 7275 | test "tileBanner: a prompt wider than its pane shows the tail inside the pane" { |
| 7273 | // A tile that painted a whole long spelling would run across the rail | 7276 | // A tile that painted a whole long spelling would run across the rail |
| 7274 | // into its neighbour, so the banner is bounded by the tile's rect and | 7277 | // into its neighbour, so the banner is bounded by the tile's rect and |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -177,8 +177,8 @@ done | |||
| 177 | # one of those and adds a convergence point would be pinning a fact every | 177 | # one of those and adds a convergence point would be pinning a fact every |
| 178 | # leg above already establishes. | 178 | # leg above already establishes. |
| 179 | 179 | ||
| 180 | [ "$OK_COUNT" = "84" ] || { | 180 | [ "$OK_COUNT" = "85" ] || { |
| 181 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 84 —" | 181 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 85 —" |
| 182 | echo " a scenario was added (update the pin) or silently lost" | 182 | echo " a scenario was added (update the pin) or silently lost" |
| 183 | exit 1 | 183 | exit 1 |
| 184 | } | 184 | } |
test/e2e_09_hosts.sh
| Old | New | ||
|---|---|---|---|
| @@ -606,6 +606,11 @@ ok "hosts rm takes the daemon off the wall and ends nothing" | |||
| 606 | # what the ordinary road prints, and on a wall with no session that | 606 | # what the ordinary road prints, and on a wall with no session that |
| 607 | # sentence is a lie about the user's shells. | 607 | # sentence is a lie about the user's shells. |
| 608 | # | 608 | # |
| 609 | # ...and a third, ahead of both: the empty wall OPENS THE PICKER by itself. | ||
| 610 | # A blank screen is no place to act from, and the list of machines is the | ||
| 611 | # only thing left to act on. Esc there leaves the one-line text standing — | ||
| 612 | # once, so the popup does not come straight back over the answer. | ||
| 613 | # | ||
| 609 | # The socket is one nothing has ever bound, and deliberately NOT the | 614 | # The socket is one nothing has ever bound, and deliberately NOT the |
| 610 | # default path: a listed LOCAL daemon is auto-started, which would give | 615 | # default path: a listed LOCAL daemon is auto-started, which would give |
| 611 | # this wall the very tile it is asserting the absence of. | 616 | # this wall the very tile it is asserting the absence of. |
| @@ -625,6 +630,9 @@ set +e | |||
| 625 | XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 \ | 630 | XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 \ |
| 626 | --out "$OUT.emcap" --err "$OUT.emcap.err" -- "$MUX" > "$OUT.empc" 2>&1 <<'EOF' | 631 | --out "$OUT.emcap" --err "$OUT.emcap.err" -- "$MUX" > "$OUT.empc" 2>&1 <<'EOF' |
| 627 | expect the wall is empty 20000 | 632 | expect the wall is empty 20000 |
| 633 | expect Enter/c new session 20000 | ||
| 634 | send \x1b | ||
| 635 | settle 500 15000 | ||
| 628 | send \x1cd | 636 | send \x1cd |
| 629 | waitexit 1000 | 637 | waitexit 1000 |
| 630 | EOF | 638 | EOF |
| @@ -641,4 +649,162 @@ EMLAST=$(grep -v '^[[:space:]]*$' "$OUT.emcap.err" | tail -1) | |||
| 641 | echo "e2e FAIL: empty wall: leaving an empty wall said" | 649 | echo "e2e FAIL: empty wall: leaving an empty wall said" |
| 642 | echo " '$EMLAST', want 'mux: left the wall'" | 650 | echo " '$EMLAST', want 'mux: left the wall'" |
| 643 | cat "$OUT.emcap.err"; exit 1; } | 651 | cat "$OUT.emcap.err"; exit 1; } |
| 644 | ok "an empty wall says so, and Ctrl-\\ d leaves it at once rather than detaching nothing" | 652 | # The Esc, judged on the GRID and not on the stream: the popup's rows are |
| 653 | # in the capture either way, and only the render says what the user was | ||
| 654 | # still looking at when the wall came down. | ||
| 655 | "$RENDER" --cols 80 --rows 24 < "$OUT.emcap" > "$OUT.emgrid" || { | ||
| 656 | echo "e2e FAIL: empty wall: render oracle failed"; cat "$OUT.emgrid"; exit 1; } | ||
| 657 | grep -q "the wall is empty" "$OUT.emgrid" || { | ||
| 658 | echo "e2e FAIL: empty wall: Esc did not leave the one-line text:" | ||
| 659 | cat "$OUT.emgrid"; exit 1; } | ||
| 660 | grep -q "Enter/c new session" "$OUT.emgrid" && { | ||
| 661 | echo "e2e FAIL: empty wall: the picker came back over the answer:" | ||
| 662 | cat "$OUT.emgrid"; exit 1; } | ||
| 663 | ok "an empty wall opens the picker, Esc leaves the one line, and Ctrl-\\ d leaves at once" | ||
| 664 | |||
| 665 | # ---- the picker: the wall shows sessions, the popup shows machines ------ | ||
| 666 | # | ||
| 667 | # `Ctrl-\ s` over a wall of two daemons. Everything a host needs doing to | ||
| 668 | # happens here and nowhere else, so the four keys are asserted in one run | ||
| 669 | # and each against an oracle outside the terminal: | ||
| 670 | # | ||
| 671 | # * Enter births on the SELECTED host — the daemon's own session count, | ||
| 672 | # read back with `muxd stats`, says which machine took it. A birth | ||
| 673 | # driven off a tile could only ever have landed on the focus's daemon. | ||
| 674 | # * `x` forgets a host: the file loses the line and the host's SHELL is | ||
| 675 | # still there, asked of the OS with `kill -0` on the pid the shell | ||
| 676 | # itself printed. "remove is detach" — `hosts rm` ends nothing. | ||
| 677 | # * `a` puts it back, so the file is two lines again. | ||
| 678 | # * Esc closes, and the keys after it reach the focused session — the | ||
| 679 | # popup ate every byte until then, which is what stops a `j` meant for | ||
| 680 | # the rows from landing in somebody's editor. | ||
| 681 | # | ||
| 682 | # Its own daemons and its own state home: the legs above stop theirs, and | ||
| 683 | # a hosts file this leg edits from inside must be nobody else's. | ||
| 684 | PKSOCK1="${TMPDIR:-/tmp}/muxd-e2e-pick1-$$.sock" | ||
| 685 | defer_sock "$PKSOCK1" | ||
| 686 | PKSOCK2="${TMPDIR:-/tmp}/muxd-e2e-pick2-$$.sock" | ||
| 687 | defer_sock "$PKSOCK2" | ||
| 688 | PKSTATE="${TMPDIR:-/tmp}/mux-e2e-pick-state-$$" | ||
| 689 | defer_rm "$PKSTATE" | ||
| 690 | start_daemon "$PKSOCK1" "$OUT.pk1.d" "picker daemon 1 never bound" --shell /bin/sh | ||
| 691 | PK1PID=$DPID | ||
| 692 | start_daemon "$PKSOCK2" "$OUT.pk2.d" "picker daemon 2 never bound" --shell /bin/sh | ||
| 693 | PK2PID=$DPID | ||
| 694 | |||
| 695 | # Daemon 1's shell prints its own pid: the forget below claims the SESSION | ||
| 696 | # outlives the wall line, and only a pid the shell itself reported can be | ||
| 697 | # asked of the process table afterwards. | ||
| 698 | pipe_mux "$OUT.pk1a" "$OUT.pk1a.err" env XDG_STATE_HOME="$PKSTATE" timeout 40 \ | ||
| 699 | "$MUX" --sock "$PKSOCK1" | ||
| 700 | pipe_send 'printf "pk-shell-%%s\\n" $$\n' | ||
| 701 | await_out "$OUT.pk1a" "pk-shell-[0-9]" "picker: daemon 1's shell never printed its pid" | ||
| 702 | pipe_detach | ||
| 703 | wait_grid "$PKSOCK1" "pk-shell-[0-9]" "picker: daemon 1's shell pid" | ||
| 704 | # The shell's ECHO of the command comes first and carries the prefix with | ||
| 705 | # no digits behind it, so the pattern demands at least one. | ||
| 706 | PKSHELL=$(grep -o 'pk-shell-[0-9][0-9]*' "$OUT.pk1a" | head -1 | cut -d- -f3) | ||
| 707 | [ -n "$PKSHELL" ] || { | ||
| 708 | echo "e2e FAIL: picker: never read daemon 1's shell pid:"; cat "$OUT.pk1a"; exit 1; } | ||
| 709 | kill -0 "$PKSHELL" 2>/dev/null || { | ||
| 710 | echo "e2e FAIL: picker: daemon 1's shell $PKSHELL is not running before the leg"; exit 1; } | ||
| 711 | |||
| 712 | # Daemon 2's marker, printed before this wall exists: what proves the tile | ||
| 713 | # on the far side of the popup is really daemon 2's and not an echo. | ||
| 714 | pipe_mux "$OUT.pk2a" "$OUT.pk2a.err" env XDG_STATE_HOME="$PKSTATE" timeout 40 \ | ||
| 715 | "$MUX" --sock "$PKSOCK2" | ||
| 716 | pipe_send 'printf "pk-two-%%s\\n" mark\n' | ||
| 717 | await_out "$OUT.pk2a" "pk-two-mark" "picker: daemon 2's marker never reached the client" | ||
| 718 | pipe_detach | ||
| 719 | wait_grid "$PKSOCK2" "pk-two-mark" "picker: daemon 2's marker" | ||
| 720 | |||
| 721 | mkdir -p "$PKSTATE/mux" | ||
| 722 | printf -- '--sock %s\n--sock %s\n' "$PKSOCK1" "$PKSOCK2" > "$PKSTATE/mux/hosts" | ||
| 723 | no_saved_tree "$PKSTATE" | ||
| 724 | |||
| 725 | PKBEFORE=$("$MUXD" stats --sock "$PKSOCK2" | tr ' ' '\n' | grep -c '^session$') | ||
| 726 | set +e | ||
| 727 | XDG_STATE_HOME="$PKSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 728 | --out "$OUT.pkcap" --err "$OUT.pkcap.err" -- \ | ||
| 729 | "$MUX" > "$OUT.pkpc" 2>&1 <<EOF | ||
| 730 | expect pk-two-mark 20000 | ||
| 731 | settle 700 20000 | ||
| 732 | send \x1cs | ||
| 733 | expect Enter/c new session 15000 | ||
| 734 | send 2 | ||
| 735 | settle 400 15000 | ||
| 736 | send \r | ||
| 737 | expect 3> 20000 | ||
| 738 | settle 700 20000 | ||
| 739 | send \x1cs | ||
| 740 | expect Enter/c new session 15000 | ||
| 741 | send 1 | ||
| 742 | settle 400 15000 | ||
| 743 | send x | ||
| 744 | settle 900 20000 | ||
| 745 | send \x1cs | ||
| 746 | expect Enter/c new session 15000 | ||
| 747 | send a--sock\x20 | ||
| 748 | settle 300 15000 | ||
| 749 | send $PKSOCK1\r | ||
| 750 | settle 700 15000 | ||
| 751 | send \x1b | ||
| 752 | settle 700 20000 | ||
| 753 | send printf 'pk-after-%s\n' esc\n | ||
| 754 | expect pk-after-esc 15000 | ||
| 755 | send \x1cd | ||
| 756 | waitexit 10000 | ||
| 757 | EOF | ||
| 758 | RC=$? | ||
| 759 | set -e | ||
| 760 | [ "$RC" -eq 0 ] || { | ||
| 761 | echo "e2e FAIL: picker: ptyclient leg exited $RC:" | ||
| 762 | cat "$OUT.pkpc"; echo "--- stderr ---"; cat "$OUT.pkcap.err"; exit 1; } | ||
| 763 | |||
| 764 | # Both spellings really were on the popup — the rows are the hosts file's | ||
| 765 | # daemons, and a box that listed one of two would still have satisfied the | ||
| 766 | # footer the `expect` waited on. | ||
| 767 | grep -qa -- "$PKSOCK1" "$OUT.pkcap" || { | ||
| 768 | echo "e2e FAIL: picker: daemon 1 was never a row:"; cat "$OUT.pkpc"; exit 1; } | ||
| 769 | grep -qa -- "$PKSOCK2" "$OUT.pkcap" || { | ||
| 770 | echo "e2e FAIL: picker: daemon 2 was never a row:"; cat "$OUT.pkpc"; exit 1; } | ||
| 771 | |||
| 772 | # The birth's oracle is the DAEMON's count, not the screen: row 2 was | ||
| 773 | # selected, so daemon 2 is the one that gained a session. | ||
| 774 | PKAFTER=$("$MUXD" stats --sock "$PKSOCK2" | tr ' ' '\n' | grep -c '^session$') | ||
| 775 | [ "$PKAFTER" -eq "$((PKBEFORE + 1))" ] || { | ||
| 776 | echo "e2e FAIL: picker: daemon 2 holds $PKAFTER sessions, want $((PKBEFORE + 1))" | ||
| 777 | "$MUXD" stats --sock "$PKSOCK2"; "$MUXD" stats --sock "$PKSOCK1"; exit 1; } | ||
| 778 | |||
| 779 | # The forget: the line left the file and came back, and the SESSION never | ||
| 780 | # noticed either. `kill -0` on the shell's own pid, never the daemon's word | ||
| 781 | # for it. | ||
| 782 | kill -0 "$PKSHELL" 2>/dev/null || { | ||
| 783 | echo "e2e FAIL: picker: forgetting daemon 1 killed its shell $PKSHELL"; exit 1; } | ||
| 784 | grep -qxF -- "--sock $PKSOCK1" "$PKSTATE/mux/hosts" || { | ||
| 785 | echo "e2e FAIL: picker: a re-added host is not back in the file:" | ||
| 786 | cat "$PKSTATE/mux/hosts"; exit 1; } | ||
| 787 | [ "$(grep -c . "$PKSTATE/mux/hosts")" = "2" ] || { | ||
| 788 | echo "e2e FAIL: picker: hosts file is not exactly two lines after x then a:" | ||
| 789 | cat "$PKSTATE/mux/hosts"; exit 1; } | ||
| 790 | |||
| 791 | # Esc gave the keyboard back. The needle is looked for in the DAEMONS, so | ||
| 792 | # this says which session the keys reached and not merely that something | ||
| 793 | # painted: the focus was on a daemon-2 tile when the forget took daemon | ||
| 794 | # 1's tiles off the wall. | ||
| 795 | PKWHERE="" | ||
| 796 | for _pk in 0 1; do | ||
| 797 | if dump_session "$PKSOCK2" "$_pk" 2>/dev/null | grep -q "pk-after-esc"; then PKWHERE="$_pk"; fi | ||
| 798 | done | ||
| 799 | [ -n "$PKWHERE" ] || { | ||
| 800 | echo "e2e FAIL: picker: the keys after Esc reached no session on daemon 2:" | ||
| 801 | cat "$OUT.pkpc"; exit 1; } | ||
| 802 | if dump_session "$PKSOCK1" 0 2>/dev/null | grep -q "pk-after-esc"; then | ||
| 803 | echo "e2e FAIL: picker: the keys went to the host the forget took off the wall:" | ||
| 804 | cat "$OUT.pkpc"; exit 1 | ||
| 805 | fi | ||
| 806 | assert_stopped "$PKSOCK1" "$PK1PID" "picker" "$OUT.pk1stop" | ||
| 807 | PK1PID="" | ||
| 808 | assert_stopped "$PKSOCK2" "$PK2PID" "picker" "$OUT.pk2stop" | ||
| 809 | PK2PID="" | ||
| 810 | ok "the picker births on the host a digit names, forgets a host without ending it, adds one back, and gives the keyboard back" | ||
test/e2e_10_agent.sh
| Old | New | ||
|---|---|---|---|
| @@ -98,6 +98,13 @@ send ssh-add -l\n | |||
| 98 | expect $FP48 15000 | 98 | expect $FP48 15000 |
| 99 | settle 400 15000 | 99 | settle 400 15000 |
| 100 | send exit\n | 100 | send exit\n |
| 101 | # On a TERMINAL the last tile's exit leaves an EMPTY WALL — `x` ends a | ||
| 102 | # session, never mux — and the picker opens over it. Esc closes the popup | ||
| 103 | # and `d` is what leaves. A piped mux still exits with the shell's code. | ||
| 104 | settle 700 15000 | ||
| 105 | send \x1b | ||
| 106 | settle 300 15000 | ||
| 107 | send \x1cd | ||
| 101 | waitexit 10000 | 108 | waitexit 10000 |
| 102 | EOF | 109 | EOF |
| 103 | RC=$? | 110 | RC=$? |
| @@ -414,6 +421,17 @@ expect $FPA 20000 | |||
| 414 | send echo flip-now\n | 421 | send echo flip-now\n |
| 415 | expect $FPB 25000 | 422 | expect $FPB 25000 |
| 416 | send exit\n | 423 | send exit\n |
| 424 | # Empty wall, then the picker: see the first agent leg — and `expect` on | ||
| 425 | # the popup's own legend, never a settle. The picker opens on the host | ||
| 426 | # POLL (up to a second after the last session dies) while a settle fires a | ||
| 427 | # fixed time after the last output byte; a picker that opened between the | ||
| 428 | # settle and the `\x1b` would eat the `d` that follows (every byte is the | ||
| 429 | # popup's while it is up) and this leg would fail at waitexit on a change | ||
| 430 | # nobody made. | ||
| 431 | expect Enter/c new session 20000 | ||
| 432 | send \x1b | ||
| 433 | settle 300 20000 | ||
| 434 | send \x1cd | ||
| 417 | waitexit 20000 | 435 | waitexit 20000 |
| 418 | EOF | 436 | EOF |
| 419 | FLIPAPID=$! | 437 | FLIPAPID=$! |
| @@ -432,6 +450,15 @@ send echo b-ready\n | |||
| 432 | expect flip-now 30000 | 450 | expect flip-now 30000 |
| 433 | send ssh-add -l\n | 451 | send ssh-add -l\n |
| 434 | expect $FPB 20000 | 452 | expect $FPB 20000 |
| 453 | # A's `exit` ends the session both clients hold, so B's tile goes too and | ||
| 454 | # B is left on the same empty wall with the same picker over it. The | ||
| 455 | # legend and not a settle, doubly so here: B is a passive observer, so its | ||
| 456 | # screen is already quiet when A types `exit` and a duration could expire | ||
| 457 | # before the transition this waits for has happened at all. | ||
| 458 | expect Enter/c new session 20000 | ||
| 459 | send \x1b | ||
| 460 | settle 300 20000 | ||
| 461 | send \x1cd | ||
| 435 | waitexit 20000 | 462 | waitexit 20000 |
| 436 | EOF | 463 | EOF |
| 437 | RCB=$? | 464 | RCB=$? |
test/e2e_11_select.sh
| Old | New | ||
|---|---|---|---|
| @@ -396,6 +396,15 @@ send S=$(date +%s%N); ssh-add -l; R=$?; echo mu""te2=$R ms=$(( ($(date +%s%N)-S) | |||
| 396 | expect mute2= 15000 | 396 | expect mute2= 15000 |
| 397 | settle 400 15000 | 397 | settle 400 15000 |
| 398 | send exit\n | 398 | send exit\n |
| 399 | # The last tile's exit leaves an empty wall with the picker over it on a | ||
| 400 | # terminal: Esc closes the popup, `d` leaves. The popup's own legend is | ||
| 401 | # what this waits for — a settle measures silence, and the picker opens on | ||
| 402 | # the host poll, so a duration can expire in the window where the popup is | ||
| 403 | # about to eat the `d`. | ||
| 404 | expect Enter/c new session 15000 | ||
| 405 | send \x1b | ||
| 406 | settle 300 15000 | ||
| 407 | send \x1cd | ||
| 399 | waitexit 10000 | 408 | waitexit 10000 |
| 400 | EOF | 409 | EOF |
| 401 | RC=$? | 410 | RC=$? |
test/e2e_13_birth.sh
| Old | New | ||
|---|---|---|---|
| @@ -39,14 +39,19 @@ SPDIR="${TMPDIR:-/tmp}/mux-e2e-sp-shim-$$" | |||
| 39 | defer_rm "$SPDIR" | 39 | defer_rm "$SPDIR" |
| 40 | SPINLOG="$SPDIR/dials" | 40 | SPINLOG="$SPDIR/dials" |
| 41 | 41 | ||
| 42 | # ---- Ctrl-\ : adds a HOST by spelling --------------------------------- | 42 | # ---- the picker's `a` adds a HOST by spelling -------------------------- |
| 43 | # | 43 | # |
| 44 | # `mux hosts add` typed from inside. A spelling names a DAEMON: the line | 44 | # `mux hosts add` typed from inside. A spelling names a DAEMON: the line |
| 45 | # goes into the hosts file, a poller starts, and that daemon's sessions | 45 | # goes into the hosts file, a poller starts, and that daemon's sessions |
| 46 | # become tiles — the prompt cannot conjure a session, only ask a machine | 46 | # become tiles — the editor cannot conjure a session, only ask a machine |
| 47 | # what it is running. A bad spelling is a notice and nothing else (the | 47 | # what it is running. A bad spelling is a notice and nothing else (the |
| 48 | # file does not grow). Esc eats the line: the shell never sees it, and the | 48 | # file does not grow). Esc eats the line: the shell never sees it, and |
| 49 | # next keys reach the session again. | 49 | # after the popup closes the next keys reach the session again. |
| 50 | # | ||
| 51 | # The editor lives INSIDE the picker (`Ctrl-\ s`, then `a`), so its Esc | ||
| 52 | # goes back to the rows and a second Esc is what closes the popup. That is | ||
| 53 | # also why the whole spelling round happens under ONE `\x1c s`: an `s` | ||
| 54 | # typed at an open picker closes it. | ||
| 50 | # | 55 | # |
| 51 | # The prompt echoes what is typed, so the added host's witness is not its | 56 | # The prompt echoes what is typed, so the added host's witness is not its |
| 52 | # label on the capture (the echo would match) but daemon B's OWN SHELL's | 57 | # label on the capture (the echo would match) but daemon B's OWN SHELL's |
| @@ -97,19 +102,21 @@ XDG_STATE_HOME="$PRSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ | |||
| 97 | "$MUX" > "$OUT.prpc" 2>&1 <<EOF | 102 | "$MUX" > "$OUT.prpc" 2>&1 <<EOF |
| 98 | expect pr-origin 20000 | 103 | expect pr-origin 20000 |
| 99 | settle 700 20000 | 104 | settle 700 20000 |
| 100 | send \x1c:--sock\x20 | 105 | send \x1csa--sock\x20 |
| 101 | settle 300 5000 | 106 | settle 300 5000 |
| 102 | send $SOCK63\r | 107 | send $SOCK63\r |
| 103 | expect pr-bmark 20000 | 108 | settle 400 15000 |
| 104 | settle 1000 20000 | 109 | send ax#bad name\r |
| 105 | send \x1c:x#bad name\r | ||
| 106 | expect [bad host 10000 | 110 | expect [bad host 10000 |
| 107 | settle 500 15000 | 111 | settle 400 15000 |
| 108 | send \x1c:-A nosuchhost.invalid\r | 112 | send a-A nosuchhost.invalid\r |
| 109 | expect FlagLikeTarget 10000 | 113 | expect FlagLikeTarget 10000 |
| 110 | settle 500 15000 | 114 | settle 400 15000 |
| 111 | send \x1c:zzz\x1b | 115 | send azzz\x1b |
| 112 | settle 500 15000 | 116 | settle 400 15000 |
| 117 | send \x1b | ||
| 118 | expect pr-bmark 20000 | ||
| 119 | settle 700 20000 | ||
| 113 | send printf 'pr-after-%s\n' esc\n | 120 | send printf 'pr-after-%s\n' esc\n |
| 114 | expect pr-after-esc 10000 | 121 | expect pr-after-esc 10000 |
| 115 | settle 500 15000 | 122 | settle 500 15000 |
| @@ -119,7 +126,7 @@ EOF | |||
| 119 | RC=$? | 126 | RC=$? |
| 120 | set -e | 127 | set -e |
| 121 | [ "$RC" -eq 0 ] || { | 128 | [ "$RC" -eq 0 ] || { |
| 122 | echo "e2e FAIL: prompt: ptyclient leg exited $RC (did \\x1c: add the tile?):" | 129 | echo "e2e FAIL: prompt: ptyclient leg exited $RC (did the picker's a add the tile?):" |
| 123 | cat "$OUT.prpc" "$OUT.prcap.err"; exit 1; } | 130 | cat "$OUT.prpc" "$OUT.prcap.err"; exit 1; } |
| 124 | # The oracle, not the byte stream: a banner LEFT on the screen is invisible | 131 | # The oracle, not the byte stream: a banner LEFT on the screen is invisible |
| 125 | # to a grep of the emitted bytes — the `: zzz` that painted is in the | 132 | # to a grep of the emitted bytes — the `: zzz` that painted is in the |
| @@ -146,11 +153,11 @@ grep -q -- "-A nosuchhost.invalid \[" "$OUT.prgrid" && { | |||
| 146 | grep -aq ": --sock " "$OUT.prcap" || { | 153 | grep -aq ": --sock " "$OUT.prcap" || { |
| 147 | echo "e2e FAIL: prompt: the prompt line never painted its echo:" | 154 | echo "e2e FAIL: prompt: the prompt line never painted its echo:" |
| 148 | cat "$OUT.prpc"; exit 1; } | 155 | cat "$OUT.prpc"; exit 1; } |
| 149 | # Daemon B is really on this wall: its session got a stripe of its own, | 156 | # Daemon B is really on this wall: its session got a tile of its own, |
| 150 | # naming B's socket, and the focus never left daemon A — the keys typed | 157 | # naming B's socket, and the focus never left daemon A — the keys typed |
| 151 | # after the Esc are in A's session and in nothing of B's. | 158 | # after the Esc are in A's session and in nothing of B's. |
| 152 | # B's own shell's output, on THIS wall's terminal. Not the bar: two | 159 | # B's own shell's output, on THIS wall's terminal. Not the bar: two |
| 153 | # stripes over 100 columns cut a socket path in half, and `labelText` | 160 | # tiles over 100 columns cut a socket path in half, and `labelText` |
| 154 | # spends what is left on the state word. The marker cannot be an echo — | 161 | # spends what is left on the state word. The marker cannot be an echo — |
| 155 | # B printed it before this process existed. | 162 | # B printed it before this process existed. |
| 156 | grep -qa "pr-bmark" "$OUT.prcap" || { | 163 | grep -qa "pr-bmark" "$OUT.prcap" || { |
| @@ -181,7 +188,7 @@ D63PID="" | |||
| 181 | assert_stopped "$SOCK63" "$D64PID" "prompt B" "$OUT.prbstop" | 188 | assert_stopped "$SOCK63" "$D64PID" "prompt B" "$OUT.prbstop" |
| 182 | D64PID="" | 189 | D64PID="" |
| 183 | rm -rf "$PRSTATE" | 190 | rm -rf "$PRSTATE" |
| 184 | ok "Ctrl-\\ : adds a host by spelling: its sessions become tiles, recorded, refusals narrated, Esc eats the line" | 191 | ok "the picker's a adds a host by spelling: its sessions become tiles, recorded, refusals narrated, Esc eats the line" |
| 185 | 192 | ||
| 186 | 193 | ||
| 187 | # --------------------------------------------------------------------------- | 194 | # --------------------------------------------------------------------------- |