a73x

98a07296

fix: a layout that cannot be saved says so on the notice line, not over a pane

a73x   2026-09-03 05:20

Commit message
fix: a layout that cannot be saved says so on the notice line, not over a pane

Every save runs under the alternate screen, so `saveLayoutTo`'s two
`std.debug.print` lines painted into the middle of whichever pane the
cursor was in and stayed there until a repaint. It returns its failures
instead and `persist` says `[layout not saved: <reason>]` where the wall
says everything else. The allocation failure it used to swallow is
reported the same way. `persist` is gated on `Shared.layout_path`, which
is only set on a terminal, so there is no no-terminal path left to print
from. A new test pins it: a wall whose layout path sits under a regular
file persists and finds the sentence in the notice slot.

`Tile.keeps_wall` SURVIVES, against the plan. The tail of `endAction` is
reached only by the last pane of a terminal wall ending for something
other than a clean exit, and two panes get there with a null `born_from`
for opposite reasons: a picker birth onto an empty wall is one session
saying no and the wall stands, while the entry tile is this `mux` and its
refusal is the program's message and exit code. The four assertions in
"endAction: a birth the picker made into a standing wall is refused ON
it" are the proof — the first and the fourth are byte-for-byte identical
inputs with opposite expected answers, separated by the flag alone.
Deleting it makes both take whichever answer is written. Its doc comment
now names that case and records the attempt.

The startup save the plan asked for is already in `run`, beside the seed
block, and is not duplicated here.

Comments that still described a poll which births tiles are reworded to
what the poll does now, which is grade the panes the layout named:
`endKey`, `Birth.host` and `runAttach` in wallview, `hostState` and
`pickerRepaint` in the picker, `planHostDiff` and `applyReadyLists` in
wall_host, and four test rationales.

src/tui/wall_host.zig
Old New
@@ -208,8 +208,9 @@ pub fn planHostDiff(
208 // daemon does not have yet, not one it dropped — and `pokeHost` 208 // daemon does not have yet, not one it dropped — and `pokeHost`
209 // asks for this list at exactly that moment. 209 // asks for this list at exactly that moment.
210 if (t.creates and !t.ever_up.load(.acquire)) continue; 210 if (t.creates and !t.ever_up.load(.acquire)) continue;
211 // The same walk the births came out of: a tile's own name was a name 211 // The same walk the binds came out of: a pane's name was a name when
212 // when it was born, so filtering the list cannot drop a real match. 212 // the layout seeded it or the picker made it, so filtering the list
213 // cannot drop a real match.
213 const keep = proto.sessionsHas(list, proto.resolveName(t.r.session)); 214 const keep = proto.sessionsHas(list, proto.resolveName(t.r.session));
214 if (keep) { 215 if (keep) {
215 t.missed_once = false; 216 t.missed_once = false;
@@ -299,9 +300,10 @@ pub fn applyReadyLists(w: Wall) bool {
299 for (w.hosts, 0..) |*h, hi| { 300 for (w.hosts, 0..) |*h, hi| {
300 if (!h.poll.list_ready.swap(false, .acq_rel)) continue; 301 if (!h.poll.list_ready.swap(false, .acq_rel)) continue;
301 news = true; 302 news = true;
302 // A poll already in flight when the host was forgotten still lands. 303 // A poll already in flight when the host was forgotten still lands,
303 // Applying it would re-birth the tiles the forget just took off the 304 // and the forget has already taken that host's panes off the wall:
304 // wall, one poll later. 305 // there is nothing left for its list to grade, and a slot the user
306 // emptied must not act on the wall on its way out.
305 if (h.forgotten.load(.acquire)) continue; 307 if (h.forgotten.load(.acquire)) continue;
306 applyHostList(w, hi); 308 applyHostList(w, hi);
307 h.applied = true; 309 h.applied = true;
src/tui/wall_layout.zig
Old New
@@ -216,15 +216,17 @@ pub fn loadLayout(alloc: std.mem.Allocator, path: []const u8) ?[]u8 {
216 216
217 /// `serialize` indexes spellings by leaf ID (tile index), so the array is 217 /// `serialize` indexes spellings by leaf ID (tile index), so the array is
218 /// tile-indexed: holes get "" and are never serialized (the tree dropped 218 /// tile-indexed: holes get "" and are never serialized (the tree dropped
219 /// them). A failed write prints one stderr line and returns. 219 /// them). A failure is RETURNED, never printed: every save runs while the
220 /// wall owns the alternate screen, so the caller is the only one who knows
221 /// where a sentence can safely go.
220 pub fn saveLayoutTo( 222 pub fn saveLayoutTo(
221 alloc: std.mem.Allocator, 223 alloc: std.mem.Allocator,
222 path: []const u8, 224 path: []const u8,
223 tiles: []Tile, 225 tiles: []Tile,
224 present: []const bool, 226 present: []const bool,
225 shared: *Shared, 227 shared: *Shared,
226 ) void { 228 ) !void {
227 const spellings = alloc.alloc([]const u8, tiles.len) catch return; 229 const spellings = try alloc.alloc([]const u8, tiles.len);
228 defer alloc.free(spellings); 230 defer alloc.free(spellings);
229 for (tiles, present, 0..) |*t, p, i| { 231 for (tiles, present, 0..) |*t, p, i| {
230 spellings[i] = if (p) t.r.label else ""; 232 spellings[i] = if (p) t.r.label else "";
@@ -234,13 +236,8 @@ pub fn saveLayoutTo(
234 // shared.sel is the focused tile index; serialize maps it to the 236 // shared.sel is the focused tile index; serialize maps it to the
235 // encounter index of its leaf in the depth-first walk. 237 // encounter index of its leaf in the depth-first walk.
236 const focus: ?u8 = if (shared.sel < tiles.len and present[shared.sel]) @intCast(shared.sel) else null; 238 const focus: ?u8 = if (shared.sel < tiles.len and present[shared.sel]) @intCast(shared.sel) else null;
237 shared.tree.serialize(spellings, focus, buf.writer(alloc)) catch |err| { 239 try shared.tree.serialize(spellings, focus, buf.writer(alloc));
238 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); 240 try hosts.saveBytes(path, buf.items);
239 return;
240 };
241 hosts.saveBytes(path, buf.items) catch |err| {
242 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)});
243 };
244 } 241 }
245 242
246 /// The one save path. Every change to the pane set or the tree comes 243 /// The one save path. Every change to the pane set or the tree comes
@@ -249,7 +246,15 @@ pub fn saveLayoutTo(
249 /// a wall that crashes loses nothing it committed. 246 /// a wall that crashes loses nothing it committed.
250 pub fn persist(w: Wall) void { 247 pub fn persist(w: Wall) void {
251 const path = w.shared.layout_path orelse return; 248 const path = w.shared.layout_path orelse return;
252 saveLayoutTo(w.alloc, path, w.liveTiles(), w.livePresent(), w.shared); 249 saveLayoutTo(w.alloc, path, w.liveTiles(), w.livePresent(), w.shared) catch |err| {
250 // Said on the notice line and nowhere else. A save runs on every
251 // change to the wall, all of them under the alternate screen, so a
252 // stderr line here would print into the middle of whichever pane
253 // the cursor happened to be in and stay there until a repaint.
254 var why: [96]u8 = undefined;
255 wv.setNotice(w.shared, std.fmt.bufPrint(&why, "[layout not saved: {s}]", .{@errorName(err)}) catch
256 "[layout not saved]");
257 };
253 } 258 }
254 259
255 pub const SeedPane = struct { host: usize, session: []u8, label: []u8 }; 260 pub const SeedPane = struct { host: usize, session: []u8, label: []u8 };
src/tui/wall_picker.zig
Old New
@@ -76,8 +76,8 @@ pub fn hostState(buf: []u8, h: *Host) []const u8 {
76 } 76 }
77 var n: usize = 0; 77 var n: usize = 0;
78 h.poll.list_mu.lock(); 78 h.poll.list_mu.lock();
79 // The same walk `planHostDiff` births through, so the count a row 79 // The same walk `planHostDiff` grades panes through, so the count a row
80 // advertises is the number of tiles that host can actually put on the wall. 80 // advertises is the number of sessions Enter can open on that host.
81 var it = proto.sessionsIter(h.poll.list[0..h.poll.list_len]); 81 var it = proto.sessionsIter(h.poll.list[0..h.poll.list_len]);
82 while (it.next()) |_| n += 1; 82 while (it.next()) |_| n += 1;
83 h.poll.list_mu.unlock(); 83 h.poll.list_mu.unlock();
@@ -764,7 +764,7 @@ pub fn paintPicker(w: Wall, sel: usize, view: PickerView, line: ?[]const u8) voi
764 const PickerRepaint = struct { due: bool, line: ?[]const u8 }; 764 const PickerRepaint = struct { due: bool, line: ?[]const u8 };
765 765
766 /// `prompting` is not a term: `relayout` clears the whole screen on any 766 /// `prompting` is not a term: `relayout` clears the whole screen on any
767 /// poll's birth or vanish while the box is up, and a paint skipped for the 767 /// pane a poll vanishes while the box is up, and a paint skipped for the
768 /// spelling editor leaves the user typing into an editor the screen no 768 /// spelling editor leaves the user typing into an editor the screen no
769 /// longer shows — every key still reaching it. 769 /// longer shows — every key still reaching it.
770 pub fn pickerRepaint(buf: []u8, prefix: *const interact.PrefixFilter, trigger: bool) PickerRepaint { 770 pub fn pickerRepaint(buf: []u8, prefix: *const interact.PrefixFilter, trigger: bool) PickerRepaint {
src/tui/wall_test_layout.zig
Old New
@@ -693,7 +693,7 @@ test "saveLayoutTo writes the sidecar for the present tiles, spellings verbatim"
693 }; 693 };
694 } 694 }
695 var present = [_]bool{ true, true }; 695 var present = [_]bool{ true, true };
696 wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); 696 try wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared);
697 const got = wall_layout.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; 697 const got = wall_layout.loadLayout(alloc, path) orelse return error.TestUnexpectedResult;
698 defer alloc.free(got); 698 defer alloc.free(got);
699 try std.testing.expect(std.mem.startsWith(u8, got, "mux-layout 1\nbeside 0\n")); 699 try std.testing.expect(std.mem.startsWith(u8, got, "mux-layout 1\nbeside 0\n"));
@@ -743,7 +743,7 @@ test "saveLayoutTo handles a vanished middle tile without panicking" {
743 }; 743 };
744 } 744 }
745 var present = [_]bool{ true, false, true }; 745 var present = [_]bool{ true, false, true };
746 wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); 746 try wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared);
747 const got = wall_layout.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; 747 const got = wall_layout.loadLayout(alloc, path) orelse return error.TestUnexpectedResult;
748 defer alloc.free(got); 748 defer alloc.free(got);
749 // Tile 2's label is verbatim; the hole (tile 1) is never serialized. 749 // Tile 2's label is verbatim; the hole (tile 1) is never serialized.
@@ -1001,7 +1001,7 @@ test "a wall left before its hosts answered saves the shape it was given" {
1001 const path = try std.fmt.bufPrint(&pbuf, "{s}/layout", .{tmp.path()}); 1001 const path = try std.fmt.bufPrint(&pbuf, "{s}/layout", .{tmp.path()});
1002 // Detaching inside the settle window: the pending panes' spellings go 1002 // Detaching inside the settle window: the pending panes' spellings go
1003 // back out verbatim, so a quick in-and-out does not erode the sidecar. 1003 // back out verbatim, so a quick in-and-out does not erode the sidecar.
1004 wall_layout.saveLayoutTo(alloc, path, &tiles, &present, &shared); 1004 try wall_layout.saveLayoutTo(alloc, path, &tiles, &present, &shared);
1005 const bytes = wall_layout.loadLayout(alloc, path) orelse return error.NothingSaved; 1005 const bytes = wall_layout.loadLayout(alloc, path) orelse return error.NothingSaved;
1006 defer alloc.free(bytes); 1006 defer alloc.free(bytes);
1007 try std.testing.expect(std.mem.indexOf(u8, bytes, "--sock /tmp/h0.sock#a") != null); 1007 try std.testing.expect(std.mem.indexOf(u8, bytes, "--sock /tmp/h0.sock#a") != null);
@@ -1213,6 +1213,55 @@ test "persist: a birth and a vanish each write the layout, and no layout_path wr
1213 try std.testing.expect(std.mem.indexOf(u8, second, "#0") == null); 1213 try std.testing.expect(std.mem.indexOf(u8, second, "#0") == null);
1214 } 1214 }
1215 1215
1216 test "persist: a save that fails says so on the notice line, where the wall says everything else" {
1217 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1218 defer arena.deinit();
1219 const alloc = arena.allocator();
1220 var tmp = try TmpDir.make();
1221 defer tmp.cleanup();
1222 var path_buf: [std.fs.max_path_bytes]u8 = undefined;
1223 // A path whose parent is a FILE. `hosts.saveBytes` makes missing
1224 // directories, so a merely absent one would be created and the save
1225 // would succeed; this one fails at the write, past every check
1226 // `persist` itself makes.
1227 const blocker = try std.fmt.bufPrint(&path_buf, "{s}/blocker", .{tmp.path()});
1228 (try std.fs.cwd().createFile(blocker, .{})).close();
1229 var path_buf2: [std.fs.max_path_bytes]u8 = undefined;
1230 const path = try std.fmt.bufPrint(&path_buf2, "{s}/blocker/layout", .{tmp.path()});
1231
1232 var shared: Shared = undefined;
1233 fixture.stoppedWall(alloc, &shared);
1234 shared.size = .{ .cols = 120, .rows = 40 };
1235 var tiles: [wv.max_tiles]Tile = undefined;
1236 var present = [_]bool{false} ** wv.max_tiles;
1237 var live: usize = 0;
1238 defer fixture.endPumps(tiles[0..live]);
1239 var hosts_table = [_]Host{fixture.testHost(&shared, "--sock /a", "/a")};
1240 const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &hosts_table);
1241 shared.layout_path = path;
1242 const at = wv.birthTile(w, .{
1243 .r = .{ .target = hosts_table[0].spec.target, .label = "", .session = "0" },
1244 .from = 0,
1245 .place = .beside_focus,
1246 .creates = false,
1247 .born_from = null,
1248 .host = 0,
1249 .borrowed = true,
1250 }).?;
1251 tiles[at].alive.store(false, .release);
1252
1253 wall_layout.persist(w);
1254 // Not on stderr and not `std.debug.print`: by the time any save runs the
1255 // wall owns the alternate screen, so the only place a sentence can go is
1256 // the slot every other refusal goes into.
1257 var buf: [96]u8 = undefined;
1258 const said = wv.takeNotice(&shared, &buf);
1259 if (!std.mem.startsWith(u8, said, "[layout not saved: ")) {
1260 std.debug.print("notice was: {s}\n", .{said});
1261 return error.FailedSaveSaidNothing;
1262 }
1263 }
1264
1216 test "seed: a plan that leaves leaves behind counts them apart - the shell's own stripe and the panes that would not fit" { 1265 test "seed: a plan that leaves leaves behind counts them apart - the shell's own stripe and the panes that would not fit" {
1217 const alloc = std.testing.allocator; 1266 const alloc = std.testing.allocator;
1218 var shared: Shared = undefined; 1267 var shared: Shared = undefined;
src/tui/wall_test_picker.zig
Old New
@@ -518,8 +518,8 @@ test "pickerRepaint: a screen cleared under the spelling editor still owes a pai
518 try std.testing.expect(!closed.due); 518 try std.testing.expect(!closed.due);
519 try std.testing.expectEqual(@as(?[]const u8, null), closed.line); 519 try std.testing.expectEqual(@as(?[]const u8, null), closed.line);
520 520
521 // The editor is open and a poll on some OTHER host births or vanishes a 521 // The editor is open and a poll on some OTHER host vanishes a pane:
522 // tile: `relayout` clears the whole screen and zeroes the stamp, which 522 // `relayout` clears the whole screen and zeroes the stamp, which
523 // is the trigger. Skipping the paint here leaves the user typing into an 523 // is the trigger. Skipping the paint here leaves the user typing into an
524 // editor that is not on the screen, and every key still goes to it. 524 // editor that is not on the screen, and every key still goes to it.
525 f.prompting = true; 525 f.prompting = true;
@@ -549,7 +549,7 @@ test "hostState: the row counts the sessions the wall could show, not the runs i
549 try std.testing.expectEqualStrings("3 sessions", wall_picker.hostState(&buf, &h)); 549 try std.testing.expectEqualStrings("3 sessions", wall_picker.hostState(&buf, &h));
550 550
551 // A row saying `4 sessions` beside three tiles is the row lying about 551 // A row saying `4 sessions` beside three tiles is the row lying about
552 // the wall: `planHostDiff` births through `validSessionName`, so the 552 // the wall: `planHostDiff` grades through `validSessionName`, so the
553 // count reads the reply through the same filter. 553 // count reads the reply through the same filter.
554 const with_junk = "a\nb\n" ++ ("x" ** (proto.session_name_max + 1)) ++ "\nc\n"; 554 const with_junk = "a\nb\n" ++ ("x" ** (proto.session_name_max + 1)) ++ "\nc\n";
555 @memcpy(h.poll.list[0..with_junk.len], with_junk); 555 @memcpy(h.poll.list[0..with_junk.len], with_junk);
src/tui/wall_test_wall.zig
Old New
@@ -342,8 +342,9 @@ test "birthTile: a vanished digit is taken back, and not before its pump returne
342 .wake_r = -1, 342 .wake_r = -1,
343 .wake_w = -1, 343 .wake_w = -1,
344 }; 344 };
345 // Every other tile is born the way a host's poll births one, so the 345 // Every other tile is born the way every live birth road spells it —
346 // copies a reuse has to free are the real owned ones. 346 // `Birth.borrowed`, so the tile takes its own copies — and those are the
347 // copies a reuse has to free.
347 defer for (tiles[1..], present[1..]) |*t, p| { 348 defer for (tiles[1..], present[1..]) |*t, p| {
348 if (!p) continue; 349 if (!p) continue;
349 alloc.free(t.r.session); 350 alloc.free(t.r.session);
@@ -677,8 +678,11 @@ test "endAction: a birth the picker made into a standing wall is refused ON it,
677 wv.endAction(&tiles, &one, 3, 0, true, true), 678 wv.endAction(&tiles, &one, 3, 0, true, true),
678 ); 679 );
679 680
680 // The entry tile of `mux TARGET` is the wall, and its refusal is mux's: 681 // The entry tile of `mux TARGET` IS this mux, so its refusal is the
681 // a script reads the code. 682 // program's: the sentence and the code. Byte for byte the inputs of the
683 // first block above — same reason, same terminal, same lone tile, same
684 // null `born_from` — and only the flag separates the two answers, which
685 // is the whole argument for keeping it.
682 fixture.endBench(&tiles, &shared, 0, .refused, 0, null); 686 fixture.endBench(&tiles, &shared, 0, .refused, 0, null);
683 try std.testing.expectEqual( 687 try std.testing.expectEqual(
684 EndAction{ .finish = .{ .code = 1, .msg = "mux: attach refused or no state received (session full?)" } }, 688 EndAction{ .finish = .{ .code = 1, .msg = "mux: attach refused or no state received (session full?)" } },
@@ -1302,9 +1306,10 @@ test "endKey: x on a pane that has not bound yet closes it locally, silent host
1302 tiles[1].alive.store(false, .release); 1306 tiles[1].alive.store(false, .release);
1303 tiles[1].state = .waiting; 1307 tiles[1].state = .waiting;
1304 // A pending pane names no session to end, so `x` takes the TILE. The 1308 // A pending pane names no session to end, so `x` takes the TILE. The
1305 // pane the user closed that turns out to exist births back on the next 1309 // pane the user closed stays closed — no list puts one back — and its
1306 // list; a pane on a silent host is otherwise unremovable for the wall's 1310 // session, if the host turns out to have one, is the picker's to open
1307 // life, which is what makes `unreachable` a place to press a key. 1311 // again; a pane on a silent host is otherwise unremovable for the
1312 // wall's life, which is what makes `unreachable` a place to press a key.
1308 if (wv.endKey(&tiles[1], 0) != .drop) return error.WaitingPaneRefusedTheKeyThatCloseIt; 1313 if (wv.endKey(&tiles[1], 0) != .drop) return error.WaitingPaneRefusedTheKeyThatCloseIt;
1309 tiles[1].state = .@"unreachable"; 1314 tiles[1].state = .@"unreachable";
1310 if (wv.endKey(&tiles[1], 0) != .drop) return error.SilentHostsPaneCannotBeClosed; 1315 if (wv.endKey(&tiles[1], 0) != .drop) return error.SilentHostsPaneCannotBeClosed;
src/tui/wallview.zig
Old New
@@ -366,9 +366,17 @@ pub const Tile = struct {
366 /// was the one attach failure it did not exit on. Null for tiles that 366 /// was the one attach failure it did not exit on. Null for tiles that
367 /// no tile created. 367 /// no tile created.
368 born_from: ?usize = null, 368 born_from: ?usize = null,
369 /// Whether a wall outlives this tile's ending. `born_from` cannot say it: 369 /// Whether a wall outlives this tile's ending. Read in exactly one
370 /// a picker birth onto an EMPTY wall carries the entry tile's null, and 370 /// case, the tail of `endAction`: the LAST pane of a wall on a terminal
371 /// the entry tile's ending really is mux's — a script reads its code. 371 /// ending for something other than a clean exit — a refused, lost or
372 /// thread-less dial. Two panes reach that line for opposite reasons and
373 /// `born_from` is null for both, so nothing else on the tile separates
374 /// them. A picker birth onto an empty wall is one session saying no, and
375 /// the wall the user still has hosts on stands. The ENTRY tile is this
376 /// `mux`: its refusal is the program's, message and exit code and all,
377 /// which is what `mux TARGET` on a full daemon has to say. Deleting the
378 /// flag was tried and cannot work — with it gone both cases take
379 /// whichever of the two answers is written, and one of them is wrong.
372 keeps_wall: bool = false, 380 keeps_wall: bool = false,
373 /// Whether a link that died before this tile saw state is worth retrying. 381 /// Whether a link that died before this tile saw state is worth retrying.
374 /// False for the ENTRY tile — a bad host or a typo'd command helps nobody 382 /// False for the ENTRY tile — a bad host or a typo'd command helps nobody
@@ -529,8 +537,9 @@ pub const EndKey = union(enum) {
529 /// Whether `Ctrl-\ x` has anything to ask, and what. 537 /// Whether `Ctrl-\ x` has anything to ask, and what.
530 pub fn endKey(t: *Tile, now: i64) EndKey { 538 pub fn endKey(t: *Tile, now: i64) EndKey {
531 // An ask stored on an ended pump goes nowhere, so the TILE goes instead. 539 // An ask stored on an ended pump goes nowhere, so the TILE goes instead.
532 // Nothing is asked of the daemon: the session keeps running and the 540 // Nothing is asked of the daemon: the session keeps running there, and
533 // host's next list births the tile back. A pending pane has no pump. 541 // no list puts it back on this wall — the picker's session rows are the
542 // way back onto one (`wall_picker.pickAdd`). A pending pane has no pump.
534 if (!t.alive.load(.acquire)) return .drop; 543 if (!t.alive.load(.acquire)) return .drop;
535 // A pump on its FIRST dial is parked in `dial`, which polls `removed` and 544 // A pump on its FIRST dial is parked in `dial`, which polls `removed` and
536 // never `ask`: the key would be swallowed until the box came back, then 545 // never `ask`: the key would be swallowed until the box came back, then
@@ -1097,8 +1106,9 @@ const Birth = struct {
1097 // Whether the wall stands without it: true for a birth the PICKER made, 1106 // Whether the wall stands without it: true for a birth the PICKER made,
1098 // which is a session starting on a wall, not the wall itself starting. 1107 // which is a session starting on a wall, not the wall itself starting.
1099 keeps_wall: bool = false, 1108 keeps_wall: bool = false,
1100 // Which host owns it. A chord-born tile inherits the focus's, or the 1109 // Which host owns it. A chord-born tile inherits the focus's. Grading
1101 // next poll of that host births a second tile for the same session. 1110 // is per host (`wall_host.ownedBy`), so a pane carrying the wrong host
1111 // or none is one no list ever binds, confirms or calls gone.
1102 host: ?usize = null, 1112 host: ?usize = null,
1103 // Whether `r.session` is borrowed and the tile's own copies are owed. 1113 // Whether `r.session` is borrowed and the tile's own copies are owed.
1104 // FALSE is an ownership claim, not an optimisation: the tile takes 1114 // FALSE is an ownership claim, not an optimisation: the tile takes
@@ -1457,7 +1467,10 @@ pub fn endAction(
1457 }, 1467 },
1458 }; 1468 };
1459 // A session the PICKER started ends as a session ends: the sentence goes 1469 // A session the PICKER started ends as a session ends: the sentence goes
1460 // on the empty-wall line. Off a terminal there is no wall to leave up. 1470 // on the empty-wall line, and the wall stays for the next Enter. Off a
1471 // terminal there is no wall to leave up. Falling PAST this branch is the
1472 // entry tile's road, where the ending is the program's — `Tile.keeps_wall`
1473 // is why no other field can tell the two apart.
1461 if (t.keeps_wall and is_tty and stdin_open) 1474 if (t.keeps_wall and is_tty and stdin_open)
1462 return .{ .vanish = .{ .back = null, .msg = gone_msg orelse done.finish.msg } }; 1475 return .{ .vanish = .{ .back = null, .msg = gone_msg orelse done.finish.msg } };
1463 return done; 1476 return done;
@@ -1571,9 +1584,9 @@ pub fn runAttach(
1571 const arena = arena_state.allocator(); 1584 const arena = arena_state.allocator();
1572 const spelling = try wall_host.hostSpelling(arena, target); 1585 const spelling = try wall_host.hostSpelling(arena, target);
1573 // The ask is SPENT: the open above is the dial the user waited for, 1586 // The ask is SPENT: the open above is the dial the user waited for,
1574 // and everything downstream of this spec — the poller, the tiles the 1587 // and everything downstream of this spec — the poller, the grading of
1575 // host's own list births, the entry tile's reconnects — is the wall 1588 // this host's panes, the entry tile's reconnects — is the wall acting
1576 // acting on its own. 1589 // on its own.
1577 var spec_target = target; 1590 var spec_target = target;
1578 if (spec_target == .hand) spec_target.hand.asked = false; 1591 if (spec_target == .hand) spec_target.hand.asked = false;
1579 var specs: std.ArrayList(HostSpec) = .empty; 1592 var specs: std.ArrayList(HostSpec) = .empty;