0bcd6ad3
feat: x takes the pane off this wall and ends nothing
a73x 2026-09-03 05:20
Commit message
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 |