a73x

0bcd6ad3

feat: x takes the pane off this wall and ends nothing

a73x   2026-09-03 05:20

Commit message
feat: x takes the pane off this wall and ends nothing

src/tui/wall_test_wall.zig
Old New
@@ -1311,3 +1311,36 @@ test "endKey: x on a pane that has not bound yet closes it locally, silent host
1311 tiles[1].pending = false; 1311 tiles[1].pending = false;
1312 try std.testing.expectEqual(EndKey.drop, wv.endKey(&tiles[1], 0)); 1312 try std.testing.expectEqual(EndKey.drop, wv.endKey(&tiles[1], 0));
1313 } 1313 }
1314
1315 test "removePane: the pane leaves the wall, its pump is told to detach, and nothing is asked to end" {
1316 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 120, .rows = 40 }, .is_tty = false };
1317 defer shared.tree.deinit();
1318 // The flats `relayout` cuts are the test allocator's, so the wall must
1319 // free them with it: the default is the page allocator, and a leak here
1320 // fails the whole file rather than this test.
1321 shared.flat_alloc = std.testing.allocator;
1322 defer if (shared.last_flat) |*f| f.deinit(std.testing.allocator);
1323 defer if (shared.base_flat) |*f| f.deinit(std.testing.allocator);
1324 var tiles = [_]Tile{ fixture.claimBench(&shared, 0), fixture.claimBench(&shared, 1) };
1325 var present = [_]bool{ true, true };
1326 // No pump is spawned here, so nothing will ever clear the liveness a
1327 // tile is born with and `endPumps` would wait on it forever. `removePane`
1328 // reads none of it: the detach is stored and the bell rung whatever the
1329 // pump is doing, which is why this fixture can state it rather than race.
1330 for (&tiles) |*t| t.alive.store(false, .release);
1331 try shared.tree.addFirst(0);
1332 try shared.tree.splitRight(0, 1);
1333 const w = fixture.wallAll(std.testing.allocator, &tiles, &present, &shared);
1334 shared.sel = 1;
1335
1336 wv.removePane(w, 1);
1337 try std.testing.expect(!present[1]);
1338 try std.testing.expect(present[0]);
1339 try std.testing.expect(tiles[1].detach_req.load(.acquire));
1340 // The daemon was NOT asked to end anything: the ask mailbox is empty.
1341 try std.testing.expectEqual(@as(u8, 0), tiles[1].ask.load(.acquire));
1342 try std.testing.expectEqual(@as(usize, 0), shared.sel);
1343 var buf: [128]u8 = undefined;
1344 try std.testing.expectEqualStrings("[pane removed - the session is still on its daemon]", wv.takeNotice(&shared, &buf));
1345 fixture.endPumps(&tiles);
1346 }
src/tui/wallview.zig
Old New
@@ -892,6 +892,22 @@ pub fn vanishTile(tiles: []Tile, present: []bool, shared: *Shared, i: usize, to:
892 } 892 }
893 } 893 }
894 894
895 /// `Ctrl-\ x`: this pane leaves THIS wall. The session is the daemon's and
896 /// keeps running for whoever else holds it; ending one is the picker's
897 /// job, beside the count of who else is there. The pump is told to say
898 /// goodbye (`detach_req`) so the daemon frees the slot now rather than at
899 /// a timeout, then the tile is vanished and the layout written without it.
900 pub fn removePane(w: Wall, z: usize) void {
901 if (z >= w.live.* or !w.present[z]) return;
902 const t = &w.tiles[z];
903 t.detach_req.store(true, .release);
904 ring(t);
905 vanishTile(w.liveTiles(), w.livePresent(), w.shared, z, null);
906 setNotice(w.shared, "[pane removed - the session is still on its daemon]");
907 wall_layout.relayout(w, w.shared.sel);
908 wall_layout.persist(w);
909 }
910
895 /// A stripe of the shell's own session paints into the grid it reads. 911 /// A stripe of the shell's own session paints into the grid it reads.
896 pub fn showsSelf( 912 pub fn showsSelf(
897 target: client.Target, 913 target: client.Target,
@@ -2350,43 +2366,14 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2350 } 2366 }
2351 }, 2367 },
2352 .end_session => if (z < w.live.* and present[z]) { 2368 .end_session => if (z < w.live.* and present[z]) {
2353 // `Ctrl-\ x` ends the SESSION on its daemon. The tile leaves 2369 removePane(w, z);
2354 // when the daemon's list no longer has it, not when the key 2370 // A piped wall of one has nowhere to leave the tile off:
2355 // is pressed. 2371 // scripts read the code, and the bare-chord abort says the
2356 var asked = false; 2372 // same words for the same act.
2357 switch (endKey(&tiles[z], std.time.milliTimestamp())) { 2373 if (!shared.is_tty and presentCount(present[0..live]) == 0) {
2358 .ask => |want| { 2374 exit_code = 0;
2359 tiles[z].ask.store(@intFromEnum(want), .release); 2375 exit_msg = "mux: aborted before attaching";
2360 // The pump is what carries the ask: a tile whose pump 2376 break :keys;
2361 // has ended has no session left to end either way.
2362 asked = ringLive(&tiles[z]);
2363 },
2364 .drop => {
2365 asked = true;
2366 vanishTile(tiles[0..live], present[0..live], &shared, z, null);
2367 // A piped wall of one has nowhere to leave the tile
2368 // off: scripts read the code, and the bare-chord
2369 // abort says the same words for the same act.
2370 if (!shared.is_tty and presentCount(present[0..live]) == 0) {
2371 exit_code = 0;
2372 exit_msg = "mux: aborted before attaching";
2373 break :keys;
2374 }
2375 // Two truths, and the tile knows which: one that
2376 // never attached leaves nothing behind, while one
2377 // whose pump died still has its session on its
2378 // daemon — `x` closed a tile, not a shell.
2379 setNotice(&shared, if (tiles[z].ever_up.load(.acquire))
2380 "[tile closed - the session is still on its daemon]"
2381 else
2382 "[nothing attached there yet - tile closed]");
2383 wall_layout.relayout(w, shared.sel);
2384 },
2385 .none => {},
2386 }
2387 if (!asked) {
2388 setNotice(&shared, "[no live session in that tile to end]");
2389 showRefusal(tiles[0..live], &shared, z);
2390 } 2377 }
2391 }, 2378 },
2392 // The spelling editor is the picker's, so its Enter is 2379 // The spelling editor is the picker's, so its Enter is