a73x

src/tui/wallview.zig

Ref:   Size: 129.9 KiB   History

//! The CLI wall: N sessions in one terminal. It lists daemons (hosts.zig)
//! and a host's live sessions are its tiles, polled. Every tile claims its
//! rectangle and focus is client-local. One thread per tile owns that tile's
//! transport both ways, so the keyboard hands it bytes over a mailbox.
const std = @import("std");
const proto = @import("term").protocol;
const client = @import("client");
const hosts = @import("client").hosts;
const handoff = @import("client").handoff;
const askpass = @import("client").askpass;
const spawn = @import("spawn");
const proxy = @import("proxy");
const grid_mod = @import("term").grid;
const paint = @import("paint.zig");
// Counters ride out through `Shared` because a detached pump never reaches
// a `Core.deinit`.
// The chord table and the prediction hooks a focused tile shares with the
// client: one interaction core, not a second copy (interact.zig).
const interact = @import("interact.zig");
const layout = @import("client").layout;
// Where a per-wall socket lives, asked of the row that owns the answer for
// the daemon socket too: one spelling of the runtime directory, not a
// second getenv beside it.
const sockpath = @import("sockpath");
const wall_host = @import("wall_host.zig");
const wall_layout = @import("wall_layout.zig");
const wall_picker = @import("wall_picker.zig");
const wall_pump = @import("wall_pump.zig");
const Host = wall_host.Host;
const PickerAuto = wall_picker.PickerAuto;
const Resolved = wall_host.Resolved;

// `wallview` is this module's face: mux_main reaches these through
// the root, wherever inside the module they now live.
pub const HostSpec = client.HostSpec;
pub const resolveHost = client.resolveHost;

/// Whether this process has a screen to cut stripes on.
fn headless(out_fd: std.posix.fd_t) bool {
    // A `mux` with no measurable terminal is a wall of ONE, writing exactly
    // what the plain client wrote: everything else a wall does is stripes.
    return interact.ttySize(out_fd) == null;
}

pub const State = enum {
    /// A pending pane: nothing has dialed and nothing will until the
    /// host's own list names its session. `.connecting` would be a lie.
    waiting,
    /// A pending pane whose host's poll failed: the picker row's word,
    /// worn on the pane, so the wall itself says which setup is dark.
    @"unreachable",
    /// A pending pane whose REACHABLE host answered without its session:
    /// the host said no, not nothing. The pane stands — its rect is the
    /// user's saved layout, not the daemon's state — until Enter re-creates
    /// the session in place or `x` dismisses it. Reversible like
    /// `unreachable`: the tile stays pending, so a list that names the
    /// session later binds it.
    gone,
    connecting,
    up,
    reconnecting,
    exited,
    refused,
    /// The user pressed Esc at ssh's prompt. A state and not a notice: the
    /// tile stays on the wall wearing the reason, the way a lost one does,
    /// and the wall it is on is still there to birth from again.
    declined,

    pub fn word(s: State) []const u8 {
        return switch (s) {
            .waiting => "waiting",
            .@"unreachable" => "unreachable",
            .gone => "session ended",
            .connecting => "connecting",
            .up => "up",
            .reconnecting => "reconnecting",
            .exited => "exited",
            .declined => "prompt declined",
            .refused => "refused",
        };
    }
};

/// The mailbox: how many typed bytes can wait for a pump to put them on
/// the wire. One stdin read's worth, which is all a keyboard can produce
/// between two turns of a pump's poll loop.
pub const mailbox_max = 4096;

/// The most tiles one wall can hold. A hard bound, not a growable array:
/// pump threads hold `*Tile` pointers, so growing would move it under them.
/// DEFINED FROM the layout's bound rather than beside it: the file is the
/// wall, both fronts read it, and two spellings of 32 agreeing by comment
/// is a drift waiting for whoever raises one of them.
pub const max_tiles: usize = client.layout.max_leaves;

/// Why a tile's pump stopped, for the keyboard that has to decide what that
/// means for the WALL. `State` narrates the same events on a label bar;
/// this is the machine-readable half, and they are separate because a cold
/// loss has no label word of its own and an exit carries a code.
pub const EndReason = enum(u8) { none, exited, taken, refused, lost, no_thread, declined };

pub const Shared = struct {
    running: std.atomic.Value(bool) = std.atomic.Value(bool).init(true),
    /// Serializes every write to the terminal. Held (and never released)
    /// at teardown, so no stripe paints across the restore, and held
    /// across a focus move, so no stripe paints into the gap between the
    /// screen being cleared and the terminal changing hands.
    paint_mu: std.Thread.Mutex = .{},
    out_fd: std.posix.fd_t,
    /// Whether there is a terminal here at all. Every WALL-level write is
    /// gated on it: on a pipe mux writes the grid and no terminal state.
    is_tty: bool,
    /// Where this wall is written, or null for a wall that persists nothing
    /// (a piped `mux`, a test). Set once by `run` on a terminal. The ONE
    /// gate every save reads, so a test points it at a file and proves what
    /// an operation wrote rather than that a save was called.
    layout_path: ?[]const u8 = null,
    /// This client's own version, what a host's stated version is judged
    /// against for the bar's drift word. Empty means unknown, which judges
    /// nobody — see `driftWord`. Set once before any pump exists, read-only
    /// after, so it rides `Shared` without a lock.
    own_version: []const u8 = "",
    /// The pumps' doorbell to the KEYBOARD — the mirror of `Tile.wake_w`.
    /// Without it a shell that exits goes unnoticed until the next keystroke.
    kb_r: std.posix.fd_t = -1,
    kb_w: std.posix.fd_t = -1,
    /// The focused tile's prediction counters. Published OUT to the driver
    /// that owns the exit, because a pump is a detached thread that never
    /// reaches a `deinit` to print `MUX_PREDICT_STATS` itself.
    stats: interact.PredictCounters = .{},
    /// One sentence for the next focused tile to put in its corner. The
    /// keyboard has no painter for a LIVE tile — `Core.banner` belongs to a
    /// Core — so it leaves the message where that pump will find it.
    notice: [96]u8 = undefined,
    notice_len: usize = 0,
    /// The terminal's shape: measured at startup, and re-measured by the
    /// KEYBOARD thread on a SIGWINCH (the flag is process-wide, and only
    /// the thread that lays the wall out may act on it). Stripes are cut
    /// from it and a one-tile wall claims exactly it.
    size: proto.Size,
    /// The FOCUSED tile: where input goes, and whose pump holds the terminal.
    /// Under `paint_mu` so a repaint draws the focus the keyboard has now.
    sel: usize = 0,
    /// Where the focused tile's last paint left the cursor. The cursor
    /// belongs to the focus: an unfocused paint's last act puts it back here.
    cursor: grid_mod.CursorPos = .{ .x = 0, .y = 0 },
    /// The container tree that owns every tile's rect. The keyboard thread
    /// mutates it under `paint_mu` — the same single-writer rule as `sel`
    /// — and relayout flattens it to read rects.
    tree: layout.Tree = layout.Tree.init(std.heap.page_allocator),
    /// The last flatten result, kept so `focus_dir` can read adjacency
    /// without re-flattening under the keyboard's `paint_mu` hold.
    /// Relayout replaces it (freeing the old one); the keyboard thread
    /// is the sole reader, under the same ownership as `tree`.
    last_flat: ?layout.Flat = null,
    /// The non-fullscreen flatten, kept so `focus_dir` can answer from
    /// the base layout while fullscreened — the fullscreen flat gives
    /// every non-focused tile a 0×0 rect, so adjacency on it is null.
    base_flat: ?layout.Flat = null,
    flat_alloc: std.mem.Allocator = std.heap.page_allocator,
    /// `f` toggles it; relayout passes `sel` to flatten when set, so the
    /// focused tile takes the whole terminal and every other gets 0×0.
    fullscreen: bool = false,
    /// Bumped when the terminal's contents are no longer anybody's paint.
    /// The only thing that repaints a tile whose session has gone quiet.
    repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
    /// True while the host picker's popup is on the terminal. Every paint
    /// asks: a tile redrawing its rect under the box would erase the rows
    /// the user is choosing from. Their replicas stay hot the whole time,
    /// and the close bumps `repaint_gen` so every rect comes back.
    picker_open: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    /// The popup's last frame: its hash, so an unchanged one is not
    /// rewritten, and the box it wrote, so a SHORTER one clears the rows it
    /// no longer covers. Keyboard-thread only, like the picker itself.
    picker_frame: PickerFrame = .{},
    /// The picker's end two-step, per host and name: a second `x` on the SAME
    /// row inside the window forces, any other row is a first press.
    /// Keyboard-thread only, like the picker itself.
    pick_end: PickEnd = .{},
    /// True while ssh's prompt box is on the terminal. `picker_open`'s
    /// twin, for its reason and at the same gates — and a second flag
    /// rather than a mode of the first, because a prompt ARRIVES and can
    /// arrive with the picker already up.
    ask_open: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    /// Where this wall answers ssh's prompts, and the binary ssh execs to
    /// ask. Null is "ssh keeps its own prompts": without the runtime
    /// directory `sockpath.runtimeDir` names, the socket would land on a
    /// shared /tmp, answerable by any local user.
    prompts: ?*askpass.Listener = null,
    prompt_exe: []const u8 = "",

    /// One label bar per tile on a tty, however many tiles there are: the
    /// bar is what names a session on screen, and hiding it when one tile
    /// was visible left a solo or fullscreened session anonymous. A piped
    /// wall owns every row and draws none — its byte stream is a script's
    /// input, and a bar in it would be bytes the session never wrote.
    /// Derived, never assigned: `viewRows`, `core.row_off` and the layout
    /// floors (`wall_layout.floorsOf`) all read this one function, so no
    /// site can drift from the rule the way three assignment copies could.
    pub fn labelRows(s: *const Shared) u16 {
        return @intFromBool(s.is_tty);
    }
};

/// What the popup left on the screen, as the next paint needs it: the hash
/// of the bytes, and the box those bytes covered. ONE value rather than two
/// fields, because the two facts are invalidated together — every place that
/// says the frame is no longer on the screen (a close, a relayout under it,
/// the ssh prompt box painting over it) clears this whole struct, and a site
/// that zeroed only the hash would leave the next paint erasing a rect that
/// now belongs to a tile. `stamp` 0 is "nothing of the box is on the
/// screen", and the rect means nothing then.
pub const PickerFrame = struct {
    stamp: u64 = 0,
    top: u16 = 0,
    left: u16 = 0,
    rows: u16 = 0,
    cols: u16 = 0,
};

/// The picker's `x`, which is a two-step the client only REMEMBERS: the
/// daemon refuses the first press with a count, and the second inside the
/// window says the user meant it. Keyed by host AND name because the same
/// session name lives on two daemons, and because a press that moved to
/// another row is a question about another session — it must be a first
/// press, not a force inherited from the row above.
pub const PickEnd = struct {
    host: usize = 0,
    name: client.SessionName = .{},
    until: i64 = 0,
    pub fn armedFor(self: *const PickEnd, host: usize, name: []const u8, now: i64) bool {
        return now < self.until and self.host == host and std.mem.eql(u8, self.name.slice(), name);
    }
    pub fn arm(self: *PickEnd, host: usize, name: []const u8, until: i64) void {
        self.host = host;
        self.name = client.SessionName.of(name);
        self.until = until;
    }
    pub fn clear(self: *PickEnd) void {
        self.until = 0;
    }
};

/// Opens the prompt box on whatever ssh is asking, if anything is. False
/// when there is nothing to show or a box is already up: `take` is the
/// transition, so a doorbell rung twice opens one popup.
fn takeAsk(shared: *Shared, prefix: *interact.PrefixFilter, out: *askpass.Prompt) bool {
    if (prefix.asking) return false;
    const l = shared.prompts orelse return false;
    if (!l.take(out)) return false;
    prefix.askOpen(out.kind);
    return true;
}

/// The prompt box gives the terminal back — whichever end ended it.
pub fn closeAsk(
    w: Wall,
    shared: *Shared,
    prefix: *interact.PrefixFilter,
    picker_sel: usize,
    picker_row: usize,
) void {
    // ONE function: `relayout` CLEARS the screen, so a picker still open
    // under the box owes a repaint. The answer buffer is zeroed here and
    // nowhere else — `feed` cannot, since the action it returned borrows it.
    prefix.askClose();
    shared.ask_open.store(false, .release);
    // `relayout` bumps `repaint_gen`, which is the only thing that redraws
    // a tile whose session said nothing while the box was up.
    wall_layout.relayout(w, shared.sel);
    if (!prefix.picking) return;
    var foot: [interact.PrefixFilter.prompt_max + 4]u8 = undefined;
    const back = wall_picker.pickerRepaint(&foot, prefix, true);
    wall_picker.paintPicker(w, picker_sel, .{ .level = prefix.pick_level, .row = picker_row }, back.line);
}

/// The host picker gives the terminal back — the key that closed it, or the
/// auto-open deciding the wall no longer needs it. `shown` is the keyboard
/// loop's own mirror of `prefix.picking`, cleared here so the two cannot
/// disagree about whose screen it is. `birth_at` is the tile a picker Enter
/// just made: the focus goes there, re-cutting on the way.
fn closePicker(
    w: Wall,
    prefix: *interact.PrefixFilter,
    shown: *bool,
    birth_at: ?usize,
) void {
    // ONE function: the close owes a `relayout`, which CLEARS the screen the
    // popup was painted over and bumps `repaint_gen` — the only thing that
    // redraws a tile whose session said nothing while the box was up.
    prefix.picking = false;
    // The level goes home with the box, whichever end closed it: a popup
    // reopened after an auto-close must be a HOST list, and only the keys
    // that close it themselves put the level back.
    prefix.pick_level = .hosts;
    shown.* = false;
    w.shared.picker_open.store(false, .release);
    // The screen under the box is about to be redrawn, so the next open owes
    // a paint however identical its rows are, and owes no clear for a box
    // the redraw has already taken off the screen.
    w.shared.picker_frame = .{};
    if (birth_at) |at|
        focusAnswer(w, true, at)
    else
        wall_layout.relayout(w, w.shared.sel);
}

/// Whether a popup owns the terminal.
pub fn popupOpen(shared: *const Shared) bool {
    // Every tile paint asks: a rect redrawn under either box would erase
    // what the user is reading, and the close bumps `repaint_gen` to bring
    // the rects back.
    return shared.picker_open.load(.acquire) or shared.ask_open.load(.acquire);
}

/// The rect a paint used, in the only terms a click can be resolved in.
/// Absolute rows count from the oldest row the DAEMON retains, so a grid row
/// alone names nothing without the history count its frame carried.
pub const Tile = struct {
    r: Resolved,
    rect: layout.Rect,
    shared: *Shared,
    /// This tile's place in the wall: what the focus and the `1`-`9`
    /// jump are indices into.
    idx: usize,
    /// Which host on the wall this tile belongs to, as an index into the
    /// run's host table. Null for a tile no host poll owns, which the poll
    /// therefore never vanishes.
    host: ?usize = null,
    /// Whether this tile has ever had state from its session — the pump's
    /// `.up`. A CREATING tile that has not is a session the daemon does not
    /// have YET, not one it has dropped: the poll that races a chord-born
    /// tile's attach must not vanish the tile it is racing.
    ever_up: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    /// One list already failed to name this tile's session while its pump
    /// was still alive. Keyboard-thread only, like the diff that sets it.
    missed_once: bool = false,
    /// A pane seeded from the layout sidecar: a rect and a name, no pump.
    /// Keyboard-thread only; `bindTile` is the one clear.
    pending: bool = false,
    /// This tile's interaction core, set by its OWN pump thread once the
    /// core exists and dereferenced only inside that pump's paint-end hook.
    /// No lifetime beyond the pump's scope: the core is freed before the
    /// pump returns, and no other thread reads this.
    core: ?*interact.Core = null,
    /// The state its label last narrated. Owned by the pump but stored
    /// here (under `paint_mu`) because the KEYBOARD repaints this bar too,
    /// when the focus moves, and it has no other way to know it.
    state: State = .connecting,
    /// Whether this tile's pump thread is still running. Only pumps answer
    /// `repaint_gen`, so a DEAD pump's bar is the one paint the keyboard
    /// makes for a tile — drawing over a live one would stale it.
    alive: std.atomic.Value(bool) = std.atomic.Value(bool).init(true),
    /// Whether this slot's pump thread has RETURNED — the half of "free"
    /// that `present` cannot say. Reusing the slot without it overwrites a
    /// `*Tile` the returning thread is still reading.
    pump_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    /// Forgotten by `Ctrl-\ x`: off the wall, and off the wire as soon as the
    /// pump notices. "Remove is detach" — the session is untouched. Distinct
    /// from `alive`: a tile the user removed, a dead one still narrates.
    removed: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    /// The doorbell. The keyboard writes one byte here to wake this tile's
    /// pump; the bytes themselves carry nothing, the mailbox does. Both
    /// ends are non-blocking, which is what makes a bell that is already
    /// ringing free to ring again.
    wake_r: std.posix.fd_t = -1,
    wake_w: std.posix.fd_t = -1,
    /// Bytes typed at this tile while it was focused. The keyboard writing
    /// ONLY to the focused tile is the whole enforcement of "input goes to
    /// the focus", so the pump sends what it finds without re-checking.
    in_mu: std.Thread.Mutex = .{},
    in: [mailbox_max]u8 = undefined,
    in_len: usize = 0,
    /// The ENTRY tile's link, adopted instead of dialled: `mux HOST` dials on
    /// the main thread while it still has the tty, so ssh can prompt.
    pre: ?client.Transport = null,
    /// Whether this tile's attach may CREATE the session it names. The
    /// daemon reads create-vs-join off the attach's size claim, so a tile
    /// that only joins what a host's list already has must attach at 0x0;
    /// the entry tile and a chord-born tile carry the rect.
    creates: bool = false,
    /// How a chord-born tile enters the tree when its session name arrives.
    /// Set by the keyboard with `.ask`, read by the answer handler.
    pending_place: Place = .beside_focus,
    /// Where a chord-created tile came from, so a REFUSED one has somewhere
    /// to put the focus back — the plain client's `.refused` recovery, which
    /// was the one attach failure it did not exit on. Null for tiles that
    /// no tile created.
    born_from: ?usize = null,
    /// Whether a wall outlives this tile's ending. Read in exactly one
    /// case, the tail of `endAction`: the LAST pane of a wall on a terminal
    /// ending for something other than a clean exit — a refused, lost or
    /// thread-less dial. Two panes reach that line for opposite reasons and
    /// `born_from` is null for both. The entry tile is also the one tile
    /// whose `retry_cold` is false, so that field COULD tell the two apart
    /// today — but it answers a different question (is a link that died
    /// before any state arrived worth redialling), and hanging the wall's
    /// survival off it would mean one edit to the retry rule silently
    /// changes what a refused `mux TARGET` does. This flag says the one
    /// thing it is named for and nothing else reads it.
    /// A picker birth onto an empty wall is one session saying no, and
    /// the wall the user still has hosts on stands. The ENTRY tile is this
    /// `mux`: its refusal is the program's, message and exit code and all,
    /// which is what `mux TARGET` on a full daemon has to say. Deleting the
    /// flag was tried and cannot work — with it gone both cases take
    /// whichever of the two answers is written, and one of them is wrong.
    keeps_wall: bool = false,
    /// Whether a link that died before this tile saw state is worth retrying.
    /// False for the ENTRY tile — a bad host or a typo'd command helps nobody
    /// to retry. True for wall tiles: a wall is a thing you leave up.
    retry_cold: bool = true,
    /// The host's version-drift word as the keyboard last applied it, empty
    /// when there is none to say. A `paint_mu` field like `state`: written
    /// by the keyboard in `wall_host.applyHostList`, read wherever the bar
    /// is painted — the pump paints its own bar, and a pending pane's is
    /// painted by the keyboard, so the word has to live where both look.
    drift: [drift_max]u8 = undefined,
    drift_len: usize = 0,
    /// Why this tile's pump stopped, and with what code. Written BEFORE
    /// `alive` clears, so a keyboard that sees a dead tile can always read
    /// a reason for it.
    end: std.atomic.Value(u8) = std.atomic.Value(u8).init(@intFromEnum(EndReason.none)),
    code: std.atomic.Value(u8) = std.atomic.Value(u8).init(0),
    /// Whether the keyboard has already acted on this tile's end. Keyboard
    /// state, so no lock: nothing else reads or writes it.
    end_seen: bool = false,
    /// A focus chord's question, posted by the keyboard for this tile's pump
    /// to put on the wire — `client.SwitchIntent` as a u8, taken with a swap
    /// so one keystroke asks one question. The PUMP asks: it owns the link.
    ask: std.atomic.Value(u8) = std.atomic.Value(u8).init(0),
    /// Until when a second `Ctrl-\ X` on this tile FORCES the end. Written
    /// by the pump from the daemon's refusal, read by the keyboard on the
    /// next press — atomic because those are two threads, and a torn i64 is
    /// a force nobody asked for.
    end_armed_until: std.atomic.Value(i64) = std.atomic.Value(i64).init(0),
    /// The pump's answer: the name the ring (or `nextFreeName`) landed on.
    /// An EMPTY name is the honest "nowhere to go" — a one-session daemon,
    /// or a reply that does not name the session we are standing in.
    ans_mu: std.Thread.Mutex = .{},
    ans: client.SessionName = .{},
    ans_ready: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    /// `Ctrl-\ d`: the keyboard wants this tile's slot handed back to the
    /// daemon before the process dies, rather than left for the socket's
    /// death to be noticed. Only the pump may write the frame, so the
    /// keyboard asks and waits briefly for the ack.
    detach_req: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    detach_ack: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    /// A chunk this tile's mailbox had no room for, and so never sent.
    /// Sticky until the pump next gets input out, which is the moment the
    /// news stops being current; the label bar narrates it meanwhile, so
    /// keystrokes never vanish without the wall admitting to it.
    in_dropped: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),

    /// The keyboard re-cut the stripes: the pump owes the daemon a `.resize`.
    /// A plain bool under `paint_mu`, not an atomic — an atomic could be
    /// consumed apart from the rect it describes, and that pair is the point.
    resize_pending: bool = false,
    /// The `repaint_gen` the pump's current pass was taken under; the paint
    /// sink refuses a paint once a relayout has moved past it. Under
    /// `paint_mu` like the rect, and for the same reason: it describes it.
    pass_gen: u64 = 0,
    /// Focus moved onto this tile: its pump owes a `claimTerminal` (the
    /// session's mouse modes and side channels). Doorbell-driven so the
    /// claim runs on the pump thread, the one that owns the transport and
    /// the Core.
    claim_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    /// Focus moved off this tile: its pump owes a `releaseTerminal` to drop
    /// the claim it took when it was focused. `.already_written`, because
    /// the keyboard wrote the session's release under `paint_mu` before
    /// doorbelling — the handover is ordered, not eventual.
    release_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),

    pub fn viewRows(t: *const Tile) u16 {
        return t.rect.rows -| t.shared.labelRows();
    }

    pub fn viewCols(t: *const Tile) u16 {
        return t.rect.cols;
    }
};

/// The keyboard thread's whole wall, as one value. `liveTiles`/`livePresent`
/// name the prefix walk; the whole array walks into uninitialised slots.
/// Keyboard-thread only: a pump gets a `*Tile` and a `*Shared`, never this.
pub const Wall = struct {
    alloc: std.mem.Allocator,
    tiles: []Tile,
    present: []bool,
    /// The high-water mark of slots ever used, not the tile count: a reused
    /// digit is already inside it, and it never falls.
    live: *usize,
    shared: *Shared,
    hosts: []Host = &.{},

    /// The slots ever used; the array past it is uninitialised.
    pub fn liveTiles(self: Wall) []Tile {
        return self.tiles[0..self.live.*];
    }

    pub fn livePresent(self: Wall) []bool {
        return self.present[0..self.live.*];
    }
};

/// Which tile's rectangle a zero-based terminal (row, col) falls in, or null
/// when none does — a click on a rail hits no tile and focus stays put.
/// Under `paint_mu`, which is what `Tile.rect` is written under.
pub fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16, col: u16) ?usize {
    shared.paint_mu.lock();
    defer shared.paint_mu.unlock();
    for (tiles, present) |*t, p| {
        if (!p) continue;
        if (row >= t.rect.top and row < t.rect.top + t.rect.rows and
            col >= t.rect.left and col < t.rect.left + t.rect.cols) return t.idx;
    }
    return null;
}

/// The keyboard loop's state: just the prefix filter.
const WallInput = struct {
    prefix: interact.PrefixFilter = .{},
};

/// The next tile in `present` after `sel`, wrapping, or null when none is
/// left. `sel` itself is the answer when it is the only one present, which
/// is what makes a one-tile wall's `j` a no-op rather than a null.
pub fn stepPresent(present: []const bool, sel: usize, forward: bool) ?usize {
    return stepWhere(present, null, sel, forward);
}

/// One lap of the ring from `sel` in `forward`'s direction, answering the
/// first index that qualifies. `tiles` narrows the answer to a tile whose
/// pump is still running; null takes any present tile. Exactly one lap, so
/// `sel` itself is the last index tried and never skipped.
fn stepWhere(present: []const bool, tiles: ?[]const Tile, sel: usize, forward: bool) ?usize {
    const n = present.len;
    if (n == 0 or sel >= n) return null;
    var i = sel;
    var seen: usize = 0;
    while (seen < n) : (seen += 1) {
        i = if (forward) (i + 1) % n else (i + n - 1) % n;
        if (!present[i]) continue;
        if (tiles) |ts| if (!ts[i].alive.load(.acquire)) continue;
        return i;
    }
    return null;
}

/// How long a refusal leaves the second press armed — `Ctrl-\ X` on a tile,
/// `x` on a picker row. Long enough to read the
/// count and decide, short enough that the decision is about the session on
/// the screen and not one the user has since walked away from.
pub const end_arm_ms: i64 = 3000;

/// Which press this is. `Ctrl-\ x` removes a pane and asks the daemon
/// nothing (`removePane`); `Ctrl-\ X` is the end key, and this is the
/// intent its keypress posts to the tile's pump. The arming behind it is
/// written by the pump from the daemon's answer, through `onEndReply`.
pub fn intentForEnd(t: *const Tile, now: i64) client.SwitchIntent {
    // The client never DECIDES to force: the daemon is the only side that
    // knows who else is attached, so this only remembers being told to ask
    // twice.
    return if (now < t.end_armed_until.load(.acquire)) .end_force else .end;
}

/// An accepted end DISARMS: the window must not outlive its session.
pub fn onEndReply(t: *Tile, r: proto.EndReply, now: i64) void {
    t.end_armed_until.store(if (r.accepted) 0 else now + end_arm_ms, .release);
}

/// `n`/`p`: the next TILE, or null when there is nowhere to go.
pub fn walkTiles(present: []const bool, sel: usize, forward: bool) ?usize {
    // Over `present`, which is what the eyes are looking at: the wall is
    // every host's sessions, and the daemon's session ring was only ever
    // the local host's slice of it.
    const to = stepPresent(present, sel, forward) orelse return null;
    // Its own tile is not a move — a focus that "moves" to itself releases
    // the terminal and re-claims it for nothing.
    return if (to == sel) null else to;
}

/// Like `stepPresent` but prefers a tile whose pump is still alive. Falls
/// back to `stepPresent` when no live tile remains, so a wall of dead
/// narrators still has somewhere to put the cursor. `tiles` must outlive
/// the call; only `present` is used for the fallback.
fn stepLive(tiles: []const Tile, present: []const bool, sel: usize, forward: bool) ?usize {
    return stepWhere(present, tiles, sel, forward) orelse stepPresent(present, sel, forward);
}

/// The state is remembered on the tile: the keyboard repaints this bar
/// with no frame, and only on a tty — a piped wall has no bar to paint.
pub fn paintLabel(t: *Tile, state: State) void {
    t.shared.paint_mu.lock();
    defer t.shared.paint_mu.unlock();
    t.state = state;
    if (t.shared.labelRows() != 0) paintLabelLocked(t);
}

/// The longest drift word: "daemon " + a capped version + " stale".
pub const drift_max = "daemon ".len + proto.sessions_meta_version_max + " stale".len;

/// The bar's version-drift word, composed from what the host's daemon said
/// about itself and what this client is. Says only what DIFFERS: a version
/// unequal to our own, a replaced image, both, or — the common case —
/// nothing at all. An unknown own version withholds the version half
/// rather than calling every daemon drifted; staleness needs no comparison
/// and is said regardless.
pub fn driftWord(
    buf: *[drift_max]u8,
    own_version: []const u8,
    meta: ?proto.SessionsMeta,
) []const u8 {
    const m = meta orelse return "";
    // Re-capped here rather than trusted: the parser bounds what it yields,
    // but this function's contract is `drift_max`, not its caller's manners.
    const ver = m.version[0..@min(m.version.len, proto.sessions_meta_version_max)];
    const differs = own_version.len != 0 and !std.mem.eql(u8, own_version, ver);
    if (differs and m.stale)
        return std.fmt.bufPrint(buf, "daemon {s} stale", .{ver}) catch "";
    if (differs)
        return std.fmt.bufPrint(buf, "daemon {s}", .{ver}) catch "";
    if (m.stale) return "daemon stale";
    return "";
}

/// Bounded by `cols` AND by `buf`: `cols` alone overran past 250 columns.
pub fn labelText(
    buf: []u8,
    cols: u16,
    marker: []const u8,
    label: []const u8,
    status: []const u8,
) []const u8 {
    // Everything the format puts around the label: the leading space, the
    // marker, " [", the status word, "]".
    const overhead = marker.len + status.len + 4;
    // The state survives truncation, the label doesn't: a wall whose bars
    // read `...scratchpad` instead of `[reconnecting]` narrates nothing.
    const label_max = @min(@as(usize, cols) -| overhead -| 1, buf.len -| overhead);
    const cut = label[0..@min(label.len, label_max)];
    // Unreachable by the bound above, and an empty bar rather than
    // undefined bytes if that bound is ever broken.
    const text = std.fmt.bufPrint(buf, " {s}{s} [{s}]", .{ marker, cut, status }) catch buf[0..0];
    return text[0..@min(text.len, cols)];
}

/// The label bar: inverse, full width, `N> LABEL [state]`, truncated at the
/// terminal edge. Caller holds `paint_mu`. Only called when `labelRows` is
/// nonzero — a piped wall owns every row and has no bar.
pub fn paintLabelLocked(t: *Tile) void {
    if (popupOpen(t.shared)) return;
    // ASCII, not an arrow glyph: this bar is byte-truncated, greppable in
    // a capture, and must not depend on a font. The unfocused marker is
    // the same width, so labels do not shift as the focus moves.
    const arrow: []const u8 = if (t.shared.sel == t.idx) "> " else "  ";
    // Stable for the tile's whole life because `vanishTile` leaves a hole
    // rather than compacting: a digit is never SHIFTED onto a neighbour,
    // though a hole is what the next birth takes back (`freeSlot`). Past 9
    // the number still prints, though no chord reaches it.
    var marker_buf: [8]u8 = undefined;
    const marker: []const u8 = std.fmt.bufPrint(&marker_buf, "{d}{s}", .{ t.idx + 1, arrow }) catch arrow;
    // Keystrokes the mailbox had no room for are said where the eye already
    // is, beside the state that explains them: input is only ever dropped
    // by a pump that stopped reading, so the state word is the reason and
    // this is the consequence. The host's drift word rides the same bracket
    // — one place a bar says everything that is wrong, last because it is
    // the least urgent thing in it.
    var status_buf: [160]u8 = undefined;
    const word = t.state.word();
    const drift = t.drift[0..t.drift_len];
    const dropped = t.in_dropped.load(.acquire);
    const status: []const u8 = blk: {
        if (dropped and drift.len != 0)
            break :blk std.fmt.bufPrint(&status_buf, "{s}, input dropped, {s}", .{ word, drift }) catch word;
        if (dropped)
            break :blk std.fmt.bufPrint(&status_buf, "{s}, input dropped", .{word}) catch word;
        if (drift.len != 0)
            break :blk std.fmt.bufPrint(&status_buf, "{s}, {s}", .{ word, drift }) catch word;
        break :blk word;
    };
    var text_buf: [256]u8 = undefined;
    const shown = labelText(&text_buf, t.rect.cols, marker, t.r.label, status);
    var out: [1024]u8 = undefined;
    var fbs = std.io.fixedBufferStream(&out);
    const w = fbs.writer();
    w.print("\x1b[?2026h\x1b[{d};{d}H\x1b[7m{s}", .{ t.rect.top + 1, t.rect.left + 1, shown }) catch return;
    // Padded with spaces rather than \x1b[K: erase-to-EOL fills with the
    // background color, not the reverse-video attribute, on most
    // terminals — the bar would end where the text does.
    var i: usize = shown.len;
    while (i < t.rect.cols) : (i += 1) w.writeByte(' ') catch break;
    w.writeAll("\x1b[0m\x1b[?2026l") catch return;
    proto.writeAllFd(t.shared.out_fd, fbs.getWritten()) catch {};
}

// The bars of the tiles whose pump has ended. Caller holds `paint_mu`.
// A dead pump answers no doorbell, so the keyboard is the only thread that
// can move its focus marker. `removed` rather than `present`, because a focus
// move has no `present` slice and a vanished tile's rect is a neighbour's.
pub fn paintDeadBarsLocked(tiles: []Tile) void {
    for (tiles) |*t| {
        if (!t.alive.load(.acquire) and !t.removed.load(.acquire)) paintLabelLocked(t);
    }
}

/// A status line in ONE tile's own corner, bounded by its rect: a notice
/// wider than a left-hand pane must not cross the rail into its neighbour,
/// and one right-aligned to the SCREEN lands in whichever pane owns that
/// corner. The only owner of a per-tile banner, for exactly that reason.
pub fn tileBanner(t: *Tile, text: []const u8) void {
    if (!t.shared.is_tty) return;
    // The tail, so the cursor end of a long spelling is what is on screen.
    // Bounded by the paint's own cap as well as the rect: a label past it
    // is not truncated by `paintBanner`, it is dropped whole.
    const room = @min(@as(usize, t.rect.cols), paint.banner_label_max);
    const shown = if (text.len > room) text[text.len - room ..] else text;
    t.shared.paint_mu.lock();
    defer t.shared.paint_mu.unlock();
    // Under the lock, for `tilePaintBegin`'s reason: read before it, a
    // banner that lost the race for `paint_mu` lands on a box row, and
    // `picker_frame` suppresses the repaint that would repair it.
    if (popupOpen(t.shared)) return;
    paint.paintBanner(t.shared.out_fd, t.rect.cols, shown, t.rect.top + t.shared.labelRows(), t.rect.left);
}

/// Never blocks and never reports: a full pipe is a bell already ringing.
pub fn ring(t: *const Tile) void {
    _ = std.posix.write(t.wake_w, "\x00") catch {};
}

/// A CHORD's doorbell: false when nobody is listening.
fn ringLive(t: *const Tile) bool {
    // An ended pump polls nothing, so its bell is a byte into a pipe with
    // no reader and the chord would die silently. The WALL's own bells
    // still ring unconditionally — waking a pump so it can RETURN is the
    // one thing a dead one does not need.
    if (!t.alive.load(.acquire)) return false;
    ring(t);
    return true;
}

// A tile's wake pipe is never closed: the fd NUMBER would be recycled, and a
// late bell would put a stray byte into somebody's live connection instead of
// getting EBADF. Two fds per tile — the process exit is the cheaper owner.

/// `ring` in the other direction: never blocks, never reports.
pub fn ringKeyboard(shared: *const Shared) void {
    _ = std.posix.write(shared.kb_w, "\x00") catch {};
}

/// The same bell behind `askpass.Hooks`' runtime pair.
fn ringKeyboardCtx(ctx: *anyopaque) void {
    ringKeyboard(@as(*const Shared, @ptrCast(@alignCast(ctx))));
}

/// Both ends of both bells are non-blocking, which is what makes this safe
/// on a pipe nobody has rung.
pub fn drainBell(fd: std.posix.fd_t) void {
    var sink: [64]u8 = undefined;
    while (std.posix.read(fd, &sink)) |n| {
        if (n < sink.len) break;
    } else |_| {}
}

/// Leave a sentence for whichever tile owns the terminal next. Keyboard
/// thread only, and only for a focus it is about to move — the pump that
/// lands there paints it as a banner on its claim.
pub fn setNotice(shared: *Shared, text: []const u8) void {
    shared.paint_mu.lock();
    defer shared.paint_mu.unlock();
    const n = @min(text.len, shared.notice.len);
    @memcpy(shared.notice[0..n], text[0..n]);
    shared.notice_len = n;
}

/// A standing condition, into an EMPTY slot only: one notice, re-derived
/// every second per host, would erase a sentence the user just earned.
pub fn setNoticeIdle(shared: *Shared, text: []const u8) void {
    shared.paint_mu.lock();
    // Dropped between the two: both writers are the keyboard thread, so
    // nothing can take the slot in the gap.
    const held = shared.notice_len != 0;
    shared.paint_mu.unlock();
    if (!held) setNotice(shared, text);
}

/// What a gone pane can do, said where the user is looking. Under the
/// 96-byte notice buffer; `setNoticeIdle` truncates silently past it.
pub const gone_notice: []const u8 = "[session ended - Enter restarts it, x closes]";

/// The same, for an unreachable pane — the one the wall was silent about, so
/// the user pressed Enter (the gone reflex) and nothing answered. It says
/// the host is retrying on its own AND that Enter hurries it, because both
/// are true: the shared poller retries every cycle, and Enter pokes it now.
pub const unreachable_notice: []const u8 = "[host unreachable, retrying - Enter retries now, x closes]";

/// Take it, once.
pub fn takeNotice(shared: *Shared, out: []u8) []const u8 {
    // Copied out because the caller paints it after releasing the lock:
    // `Core.banner` takes `paint_mu` itself, through the sink, and the
    // sink is not reentrant.
    shared.paint_mu.lock();
    defer shared.paint_mu.unlock();
    return takeNoticeLocked(shared, out);
}

/// Called ONLY from the keyboard loop and ONLY while `t` is focused: that
/// restriction IS "an unfocused tile claims no terminal modes". A chunk that
/// does not fit is dropped WHOLE — a partial copy is a command nobody typed.
pub fn sendKeys(t: *Tile, keys: []const u8) void {
    {
        t.in_mu.lock();
        defer t.in_mu.unlock();
        if (keys.len > t.in.len - t.in_len) {
            // Narrated rather than silent: input vanishing with no
            // explanation is the worst version of this. Atomic rather than
            // guarded by `in_mu`, so the label bar can read it under
            // `paint_mu` without the two locks ever having to nest.
            t.in_dropped.store(true, .release);
        } else {
            @memcpy(t.in[t.in_len..][0..keys.len], keys);
            t.in_len += keys.len;
        }
    }
    ring(t);
}

/// Move the focus to tile `next`. Client-local: it decides which pump owns
/// the terminal's modes, not which session the daemon hears. The outgoing
/// tile's `session_release` is written HERE under `paint_mu`, because two
/// pumps are two threads and A's release races B's claim.
pub fn setFocus(tiles: []Tile, shared: *Shared, next: usize) void {
    const prev = shared.sel;
    // Scoped rather than deferred to the end of the function: the notice at
    // the tail goes through `setNoticeIdle`, which takes `paint_mu` itself,
    // and the mutex is not reentrant.
    var landed: State = .waiting;
    {
        shared.paint_mu.lock();
        defer shared.paint_mu.unlock();
        moveFocusLocked(tiles, shared, prev, next);
        // Read HERE because `state` is a `paint_mu` field a pump writes
        // through `paintLabel`, and the keyboard is not its owner.
        if (next < tiles.len) landed = tiles[next].state;
    }
    // The pane's bar names the state; this says what to do about it. Idle
    // only: a refusal or an exit sentence already on screen outranks a hint.
    switch (landed) {
        .gone => setNoticeIdle(shared, gone_notice),
        .@"unreachable" => setNoticeIdle(shared, unreachable_notice),
        else => {},
    }
}

/// The focus move itself. Caller holds `paint_mu`.
fn moveFocusLocked(tiles: []Tile, shared: *Shared, prev: usize, next: usize) void {
    // The outgoing session's modes come off HERE, ahead of the doorbell that
    // lets the next pump claim — and only when there IS a terminal to change.
    if (shared.is_tty and prev != next and prev < tiles.len and tiles[prev].alive.load(.acquire)) {
        proto.writeAllFd(shared.out_fd, interact.session_release) catch {};
        // The wall's own click capture goes straight back on, by THIS hand:
        // the incoming tile's claim used to be the re-arm, and a pump parked
        // dialling a dead host never claims — one click on an unreachable
        // pane then deafened the mouse for the whole wall. See
        // `interact.wall_mouse_capture`.
        proto.writeAllFd(shared.out_fd, interact.wall_mouse_capture) catch {};
        tiles[prev].release_pending.store(true, .release);
        ring(&tiles[prev]);
    }
    shared.sel = next;
    if (next < tiles.len) {
        tiles[next].claim_pending.store(true, .release);
        ring(&tiles[next]);
    }
    // Repaint the label bars: the focus marker moved. One bump tells every
    // pump to redraw its bar; the doorbells above make the two involved
    // tiles immediate, and the rest follow on their next poll timeout.
    _ = shared.repaint_gen.fetchAdd(1, .release);
    for (tiles) |*t| ring(t);
    // Only where there IS a bar: a piped wall's `labelRows` is zero, and
    // a bar painted there lands on a content row the tile that owns the
    // screen has already written.
    if (shared.labelRows() != 0) paintDeadBarsLocked(tiles);
}

/// Re-cut before the move: `setFocus` releases the outgoing session
/// and needs the true previous focus.
pub fn focusAnswer(w: Wall, recut: bool, to: usize) void {
    if (recut) wall_layout.relayout(w, w.shared.sel);
    setFocus(w.liveTiles(), w.shared, to);
    // Fullscreen re-flattens with the new focus so the full rect follows.
    if (w.shared.fullscreen) wall_layout.relayout(w, w.shared.sel);
}

pub const no_live_here = "[no live session here - Ctrl-\\ s picks a host]";

/// TAKEN, not read: an empty wall has no pump to paint a notice as a
/// banner, and every relayout would repaint a sentence left on this line.
pub fn emptyWallHint(shared: *Shared) []const u8 {
    if (shared.notice_len == 0) return "Ctrl-\\ s to pick a host - Ctrl-\\ d to leave";
    const said = shared.notice[0..shared.notice_len];
    shared.notice_len = 0;
    return said;
}

/// The whole screen an empty wall gets: one line, at the top, saying the
/// wall is empty and the way out. A blank terminal with no cursor reads as
/// hung, and the last `x` is precisely when the user needs to be told that
/// what they see is the answer and not a crash.
pub fn paintEmptyWallLocked(shared: *Shared) void {
    if (!shared.is_tty) return;
    var text_buf: [256]u8 = undefined;
    const shown = labelText(
        &text_buf,
        shared.size.cols,
        "",
        "the wall is empty",
        emptyWallHint(shared),
    );
    var out: [512]u8 = undefined;
    var fbs = std.io.fixedBufferStream(&out);
    fbs.writer().print("\x1b[1;1H{s}\x1b[?25h", .{shown}) catch return;
    proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {};
}

/// A tile leaves the wall: off the `present` roll, off the tree, and its pump
/// told to return. The ONE owner — chord and poll diff both come here, so the
/// focus hand-off is written once. `to` overrides where the focus goes.
pub fn vanishTile(tiles: []Tile, present: []bool, shared: *Shared, i: usize, to: ?usize) void {
    present[i] = false;
    tiles[i].removed.store(true, .release);
    ring(&tiles[i]);
    shared.tree.remove(@intCast(i));
    // Through `setFocus`, never by writing `sel`: a focus that arrives
    // without a claim leaves the new tile's pump holding no terminal — no
    // mouse modes, no prediction — until the user moves focus away and back.
    if (shared.sel == i) {
        // Nothing left to step to is not a reason to focus the tile that
        // just went: `sel` stays where it is, and the caller puts it on a
        // real tile as soon as there is one (`applyHostList`). An empty
        // wall has none, and paints its own line instead.
        if (to orelse stepPresent(present, i, true)) |next| setFocus(tiles, shared, next);
    }
}

/// `Ctrl-\ x`: this pane leaves THIS wall. The session is the daemon's and
/// keeps running for whoever else holds it; ending one is the picker's
/// job, beside the count of who else is there. The pump is told to say
/// goodbye (`detach_req`): it writes `.detach` on its way out — the last
/// thing `pumpTile` does past its loop — so the daemon frees the slot on a
/// goodbye rather than at a timeout, and the transport close behind it is
/// the fallback for a pump that ended some other way. Then the tile is
/// vanished and the layout written without it.
///
/// The sentence is set BEFORE the vanish, and the order is the whole of
/// whether the user ever reads it. `vanishTile` hands the focus on, which
/// arms the incoming pump's claim, and a claim's first act is `takeNotice`:
/// set afterwards, the notice loses that race about one press in four
/// (measured 2026-09-02) and then waits for a focus move the user has no
/// reason to make. Set first, the claim that the vanish arms is the claim
/// that paints it.
pub fn removePane(w: Wall, z: usize) void {
    if (z >= w.live.* or !w.present[z]) return;
    const t = &w.tiles[z];
    t.detach_req.store(true, .release);
    ring(t);
    setNotice(w.shared, "[pane removed - the session is still on its daemon]");
    vanishTile(w.liveTiles(), w.livePresent(), w.shared, z, null);
    wall_layout.relayout(w, w.shared.sel);
    wall_layout.persist(w);
}

/// A stripe of the shell's own session paints into the grid it reads.
pub fn showsSelf(
    target: client.Target,
    name: []const u8,
    env_sock: ?[]const u8,
    env_session: ?[]const u8,
) bool {
    // A unix socket path only: a host or quic:// target is a different
    // daemon whatever its sessions are called.
    const sock = switch (target) {
        .sock => |p| p,
        else => return false,
    };
    const es = env_sock orelse return false;
    const en = env_session orelse return false;
    // Emptied counts as unset: `MUX_SESSION=` is how a shell overrides an
    // exported variable it cannot unset, and the refusal names unsetting as
    // the way out — both spellings of that have to work.
    if (es.len == 0 or en.len == 0) return false;
    // Both halves, so session 0 attaching to session 1 of one daemon works.
    // String equality: this guards a mistake, not a security boundary.
    return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name));
}

/// Two tiles on the same daemon, as SPELLED. Identity dedup would need an
/// endpoint handshake the spec refuses, so one session reached two ways is
/// two tiles; the ring chords only ask their own daemon and never care.
fn sameTarget(a: client.Target, b: client.Target) bool {
    if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false;
    return switch (a) {
        .sock => |p| std.mem.eql(u8, p, b.sock),
        .via => |c| std.mem.eql(u8, c, b.via),
        .quic => |q| std.mem.eql(u8, q.host_port, b.quic.host_port),
        .hand => |h| std.mem.eql(u8, h.host, b.hand.host),
    };
}

pub fn presentCount(present: []const bool) usize {
    var n: usize = 0;
    for (present) |p| {
        if (p) n += 1;
    }
    return n;
}

/// The lowest slot a new tile may take back: off the wall AND its pump
/// returned. `Tile.pump_done` is why both halves are needed.
pub fn freeSlot(tiles: []const Tile, present: []const bool) ?usize {
    for (tiles, present, 0..) |*t, p, i| {
        if (!p and t.pump_done.load(.acquire)) return i;
    }
    return null;
}

/// Whether a slot arrives with a doorbell already in it. `kept` is a slot a
/// vanished tile left behind: the pipe is never closed, so digging a fresh
/// one per reuse would leak an fd pair per birth. Nothing else in a reused
/// slot survives — `initTile` overwrites the whole tile.
const Doorbell = enum { fresh, kept };

/// One tile, with the doorbell its pump polls. Split out of `run` because
/// tiles are born in two places now — at startup, and whenever a focus chord
/// or the saved wall adds one.
fn initTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize, bell: Doorbell) !void {
    // The doorbell, before the pump that polls it exists. Non-blocking both
    // ends: neither side may wedge the other. A kept pipe may still hold the
    // byte that rang the departing pump; the bytes carry nothing.
    const wake: [2]std.posix.fd_t = switch (bell) {
        .fresh => try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }),
        .kept => .{ t.wake_r, t.wake_w },
    };
    t.* = .{
        .r = r,
        .rect = s,
        .shared = shared,
        .idx = idx,
        .wake_r = wake[0],
        .wake_w = wake[1],
    };
}

/// The one maker of a pending tile: a slot whose rect the seed already
/// cut, wearing the sidecar's names, that nothing will dial. `pump_done`
/// is true because there is no thread to wait for; `bindTile` undoes it.
pub fn seedTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize, host: usize) !void {
    try initTile(t, r, s, shared, idx, .fresh);
    t.pending = true;
    t.host = host;
    t.state = .waiting;
    t.creates = false;
    t.alive.store(false, .release);
    t.pump_done.store(true, .release);
}

pub fn spawnPump(t: *Tile) void {
    // The one arming point is also the one refusal: a remembered shape
    // never dials, so the sidecar cannot re-create a session.
    if (t.pending) return;
    const th = std.Thread.spawn(.{}, wall_pump.pumpTile, .{t}) catch {
        // A tile with no thread is one nothing will ever paint. Marked here
        // so the keyboard paints its bar; `.connecting` would be a lie. The
        // entry tile's link is already open and nobody is left to adopt it.
        if (t.pre) |*pre| {
            pre.close();
            t.pre = null;
        }
        t.state = .refused;
        // Not `.refused`: nothing refused anything. `endAction` turns that
        // into "attach refused (session full?)", which would blame a daemon
        // that was never asked.
        t.end.store(@intFromEnum(EndReason.no_thread), .release);
        t.alive.store(false, .release);
        ringKeyboard(t.shared);
        // No thread was ever going to read this slot, so its digit is free
        // the moment the tile leaves the wall — without this a spawn that
        // failed would hold a slot nothing can ever reuse.
        t.pump_done.store(true, .release);
        return;
    };
    th.detach();
}

/// Where a focus chord's answer takes the focus.
const FocusTo = union(enum) {
    /// Onto this tile: one that was already on the wall (a hot replica and
    /// a local repaint — zero round trips) or one just added for it.
    moved: usize,
    /// The wall cannot hold another tile. Said rather than silently done,
    /// because the alternative — a roaming connection that claims full size
    /// without earning a tile — is a rule this spec states elsewhere.
    full,
    /// Nowhere to go; the focus stays put.
    stay,
};

/// How a chord-born tile enters the tree: `beside_focus` is the `c` chord's
/// sibling-after-focus (insert), `right_of` / `below` are the split chords.
const Place = enum { beside_focus, right_of, below };

/// One row of the birth table: what a caller owes a new tile.
const Birth = struct {
    r: Resolved,
    from: usize,
    place: Place,
    creates: bool,
    // Where a REFUSED attach hands the focus back; null for a tile no
    // tile made (the fold's).
    born_from: ?usize,
    // Whether the wall stands without it: true for a birth the PICKER made,
    // which is a session starting on a wall, not the wall itself starting.
    keeps_wall: bool = false,
    // Which host owns it. A chord-born tile inherits the focus's. Grading
    // is per host (`wall_host.ownedBy`), so a pane carrying the wrong host
    // or none is one no list ever binds, confirms or calls gone.
    host: ?usize = null,
    // Whether `r.session` is borrowed and the tile's own copies are owed.
    // FALSE is an ownership claim, not an optimisation: the tile takes
    // `label` and `session` verbatim and a later birth frees both.
    borrowed: bool = false,
};

/// The pending→live transition, in the one order that is safe: a saved pane
/// stops being a drawing and becomes a session a pump is dialling. `creates`
/// asks the daemon to MAKE the session (a revive); false attaches to one the
/// host already listed (a bind). The caller spawns the pump after.
fn wakePending(t: *Tile, creates: bool) void {
    t.shared.paint_mu.lock();
    t.pending = false;
    if (creates) t.creates = true;
    t.state = .connecting;
    t.shared.paint_mu.unlock();
    t.end_seen = false;
    // The focus may already be sitting on this pane — the saved focus a bind
    // lands under, or the pane whose Enter asked for the revive — and no
    // `setFocus` moves onto it, so the claim is what reaches the terminal.
    if (t.idx == t.shared.sel) t.claim_pending.store(true, .release);
    // False must land before the thread exists: the pump only ever stores
    // true, and `freeSlot` reads it as "no thread still holds this slot".
    t.pump_done.store(false, .release);
    t.alive.store(true, .release);
}

/// How a pending pane comes to life — the two axes every wake differs on,
/// and nothing else. `creates`: the daemon MAKES the session (a nonzero
/// attach size) rather than only attaching to one it already lists.
/// `asked`: the dial STARTS the daemon (`mux d endpoint --start`) rather
/// than reading what is already there — the field the entry attach and a
/// picker birth set, worn on a `.hand` target. Every road onto a pending
/// pane — a poll bind, a gone-pane Enter, an unreachable-pane Enter — is one
/// of these three combos, so they share one primitive and diverge only in
/// the struct.
const ReviveOpts = struct { creates: bool, asked: bool };

/// Set the dial's start-the-daemon flag where the target can carry one. A
/// `.sock`/`.quic`/`.via` target has no daemon to start over ssh, so the
/// flag has nowhere to live and the ask is a no-op — a local daemon that is
/// down is `mux d start`, not a wall pane's to boot.
fn armAsked(t: *Tile, asked: bool) void {
    if (t.r.target == .hand) t.r.target.hand.asked = asked;
}

/// The state half of a wake, split from the thread so a test can pin the
/// transitions without racing a pump.
pub fn armPane(t: *Tile, opts: ReviveOpts) void {
    armAsked(t, opts.asked);
    wakePending(t, opts.creates);
}

/// The one wake: arm the pane, then give it its pump. Every caller names its
/// intent in the struct — a bind attaches ({false,false}), a gone-pane Enter
/// re-creates on a live host ({true,false}), an unreachable-pane Enter starts
/// the daemon first and then re-creates ({true,true}). No tree edit and no
/// flatten: waking re-cuts nothing, which is the promise a seeded rect keeps.
fn revivePane(t: *Tile, opts: ReviveOpts) void {
    armPane(t, opts);
    spawnPump(t);
}

/// A pending pane the host's own list confirmed: wake it in place, attaching
/// to the session the poll just saw — never creating, never starting.
pub fn bindTile(w: Wall, i: usize) void {
    revivePane(&w.tiles[i], .{ .creates = false, .asked = false });
}

/// The one door for bytes aimed at the focused tile. A pending pane has no
/// pump, so no byte could reach a session — eaten here rather than queued
/// into `in` where they would greet the NEXT session this tile carries.
/// Enter is the pane's one verb: it wakes the pane through `revivePane`, and
/// the only thing that differs by WHY the pane was pending is whether the
/// daemon must be started first — a gone pane's host is up, an unreachable
/// pane's may be down.
pub fn keysToFocused(t: *Tile, keys: []const u8) void {
    if (t.pending and (t.state == .gone or t.state == .@"unreachable")) {
        if (std.mem.indexOfAny(u8, keys, "\r\n") != null)
            revivePane(t, .{ .creates = true, .asked = t.state == .@"unreachable" });
        return;
    }
    sendKeys(t, keys);
}

/// Every road onto a running wall — chord, fold, prompt — one body.
pub fn birthTile(w: Wall, b: Birth) ?usize {
    return birthTileOrRefuse(w, b) catch null;
}

fn birthTileOrRefuse(w: Wall, b: Birth) !usize {
    // The lowest digit a departed tile left behind, before a new one:
    // create 1 2 3, end 2, create — and the wall says 2, not 4. Reuse, not
    // renumbering: the tiles that stayed keep the digit their user learned.
    const reuse = freeSlot(w.liveTiles(), w.livePresent());
    if (reuse == null and w.live.* >= max_tiles) return error.WallFull;
    // "Does it fit" has ONE owner, and it is the tree: insert, flatten,
    // and undo the insert when flatten refuses. Row arithmetic here
    // capped every terminal at rows/3 panes however wide, because it
    // cannot see that a `.beside` cut spends columns.
    const at = reuse orelse w.live.*;
    switch (b.place) {
        .beside_focus => if (w.shared.tree.root == null)
            // A wall whose tiles all arrive from a host's list starts with
            // no tree at all; `insert` has no leaf to sit beside.
            try w.shared.tree.addFirst(@intCast(at))
        else
            try w.shared.tree.insert(@intCast(b.from), @intCast(at)),
        .right_of => try w.shared.tree.splitRight(@intCast(b.from), @intCast(at)),
        .below => try w.shared.tree.splitBelow(@intCast(b.from), @intCast(at)),
    }
    // Past the insert, so every refusal below puts the tree back exactly
    // as the caller found it.
    errdefer w.shared.tree.remove(@intCast(at));
    const flat = try w.shared.tree.flatten(
        w.alloc,
        w.shared.size.rows,
        w.shared.size.cols,
        wall_layout.floorsOf(w.shared),
        null,
    );
    defer flat.deinit(w.alloc);
    const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect;
    // The real rect, not a placeholder: the daemon refuses creates under
    // `min_session_rows`, and a 2-row rect on a live session is destructive
    // under latest-wins.
    // Past every refusal: the tile is the wall's now, so the copies a pump
    // will hold for its whole life are worth making. Made BEFORE the slot
    // is overwritten, so the last thing that can fail here still fails
    // against a slot that is exactly as the caller found it.
    var r = b.r;
    if (b.borrowed) {
        const session = try w.alloc.dupe(u8, b.r.session);
        errdefer w.alloc.free(session);
        r.session = session;
        r.label = try tileLabel(w.alloc, b.r.target, session);
    }
    // Only the copies THIS call made: a caller that owns them frees its own.
    errdefer if (b.borrowed) {
        w.alloc.free(r.session);
        w.alloc.free(r.label);
    };
    // The departed tile's copies go with its digit. Every tile owns these
    // two — `run` dupes even the entry tile's session for this — so a
    // reused slot that kept them would leak one label and one name per
    // birth for the wall's whole life.
    if (reuse != null) {
        w.alloc.free(w.tiles[at].r.session);
        w.alloc.free(w.tiles[at].r.label);
    }
    // Only a `.fresh` doorbell can fail, and it fails before the slot is
    // written, so the errdefers above are the whole unwind.
    try initTile(&w.tiles[at], r, new_rect, w.shared, at, if (reuse == null) .fresh else .kept);
    // The caller's row of the birth table, and the whole of what separates
    // the roads: a chord row inherits its target and agent and CREATES, a
    // poll row joins a session the daemon already has. The pump spawn stays
    // the caller's.
    w.tiles[at].creates = b.creates;
    w.tiles[at].born_from = b.born_from;
    w.tiles[at].keeps_wall = b.keeps_wall;
    w.tiles[at].host = b.host;
    w.present[at] = true;
    // `live` is the high-water mark of slots ever used, not the tile count:
    // a reused digit is already inside it, and growing here would walk the
    // keyboard's `liveTiles` walk off the end of the array.
    if (reuse == null) w.live.* += 1;
    return at;
}

/// A sibling is focused if it has a tile and GETS one if not — otherwise a
/// tile labelled S would paint T.
fn addSessionTile(w: Wall, from: usize, name: []const u8, place: Place) FocusTo {
    // The parent's ask does not descend. A chord is a session on a daemon
    // this wall is already attached to, so there is nothing here to start;
    // inheriting the bit would hand every descendant of a picker-born tile
    // a permission nobody asked for, for the rest of its life.
    var target = w.tiles[from].r.target;
    if (target == .hand) target.hand.asked = false;
    const want = proto.resolveName(name);
    for (w.liveTiles(), w.livePresent(), 0..) |*t, p, i| {
        if (!p) continue;
        if (!sameTarget(t.r.target, target)) continue;
        if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i };
    }
    const at = birthTile(w, .{
        // The offer is inherited: same target, so `-A` exposes nothing new,
        // and a chord-made tile has no command line to spell the flag on.
        // `want` borrows the caller's name — see `Birth.borrowed`.
        .r = .{ .target = target, .label = "", .session = want, .agent = w.tiles[from].r.agent },
        .from = from,
        .place = place,
        .creates = true,
        .born_from = from,
        .host = w.tiles[from].host,
        .borrowed = true,
    }) orelse return .full;
    spawnPump(&w.tiles[at]);
    return .{ .moved = at };
}

/// A refusal reaches the eyes that earned it: a live tile shows the notice
/// on the re-claim, a dead tile is never claimed, so the keyboard paints it
/// there and TAKES it — left in `shared` it surfaces on a later claim.
fn showRefusal(tiles: []Tile, shared: *Shared, z: usize) void {
    if (tiles[z].alive.load(.acquire)) {
        setFocus(tiles, shared, z);
        return;
    }
    var buf: [96]u8 = undefined;
    const text = takeNotice(shared, &buf);
    if (text.len > 0) tileBanner(&tiles[z], text);
}

/// What the closing picker owes the wall it hands back: the retry for a
/// claim the popup refused, and — when the popup left a sentence behind —
/// the focus move that is the only thing which makes a pump take it.
pub fn closeNotice(tiles: []Tile, present: []const bool, shared: *Shared) void {
    const z = shared.sel;
    if (z >= tiles.len or !present[z]) return;
    shared.paint_mu.lock();
    const pending = shared.notice_len != 0;
    shared.paint_mu.unlock();
    // A forget's sentence names a host that owns no tile here, so nothing
    // else on this path will ever move the focus and arm a claim: left to
    // the bare ring, `[hosts file not updated: ...]` waits for the user's
    // next focus move to be read, which may be never.
    if (pending) showRefusal(tiles, shared, z) else _ = ringLive(&tiles[z]);
}

/// How a tile spells itself on the wall — the form that keys the layout
/// sidecar's leaves. `--via` has no form in that grammar, so it gets a label
/// that is honest on a bar and is not a spelling.
pub fn tileLabel(alloc: std.mem.Allocator, target: client.Target, name: []const u8) ![]const u8 {
    // `spellingCap` counts a `--via` operand as ZERO — the grammar has no
    // place to put one. A label does, and the command has no length bound.
    const cap = switch (target) {
        .via => |cmd| "--via ".len + cmd.len + 1 + proto.session_name_max,
        else => client.spellingCap(target),
    };
    const buf = try alloc.alloc(u8, cap);
    const text = if (client.wallSpelling(buf, target, name)) |s| s else |_| switch (target) {
        .via => |cmd| std.fmt.bufPrint(buf, "--via {s}#{s}", .{ cmd, name }) catch buf[0..0],
        // Every other target has a wall spelling and the buffer is sized
        // for it, so this is unreachable in practice; an empty label beats
        // a failed attach for a bar nobody has looked at yet.
        else => buf[0..0],
    };
    // The label IS its allocation, never a prefix of one: `cap` is an upper
    // bound and a caller that frees a sub-slice of it frees the wrong size.
    // A shrink cannot move memory in any allocator this runs under, so the
    // `realloc` arm is the contract's fallback rather than a real path.
    return if (alloc.resize(buf, text.len)) buf[0..text.len] else try alloc.realloc(buf, text.len);
}

/// What a pump's ending means for the wall. Only the last present tile
/// ending can end the program: a tile that goes quiet while others remain
/// narrates itself and the wall goes on, which is what the dead-tile paint is for.
pub const EndAction = union(enum) {
    /// The wall has other tiles: the focus moves to the next present one.
    /// Its label already says what became of the ended tile.
    refocus: usize,
    /// Nothing left to look at. mux ends here.
    finish: struct { code: u8, msg: ?[]const u8 },
    /// A tile with nothing left to say leaves no trace; `msg` is the
    /// refusal's sentence, an exit says nothing. `back` is null when there
    /// is no tile left to hand the focus to — the wall stays up, empty.
    vanish: struct { back: ?usize, msg: ?[]const u8 },
};

/// One sentence in both places it is said: on stderr as `mux: ...`, and in
/// the brackets a banner wears.
pub fn noticeText(buf: *[128]u8, msg: []const u8) []const u8 {
    const prefix = "mux: ";
    const bare = if (std.mem.startsWith(u8, msg, prefix)) msg[prefix.len..] else msg;
    return std.fmt.bufPrint(buf, "[{s}]", .{bare}) catch bare;
}

/// This run stops writing the layout file, for good, and says why. Nulling
/// the path is the whole mechanism: `wall_layout.persist` reads it before
/// every save, so one call here covers every later change to the wall. The
/// three callers are the three ways a run can end up showing something the
/// file does not describe — a `--via` wall, a file this wall refused, and a
/// file it could only seat part of.
fn stopSaving(shared: *Shared, alloc: std.mem.Allocator, why: []const u8) void {
    if (shared.layout_path) |p| alloc.free(p);
    shared.layout_path = null;
    setNotice(shared, why);
}

/// The refusal, cut to what a notice holds. stderr already carried the whole
/// line before the alternate screen opened; this is the reminder that
/// survives onto the wall, so the head is what matters and the tail of a
/// long spelling is the part to lose.
pub fn refusalNotice(buf: *[96]u8, line: []const u8) []const u8 {
    const head = "[layout not saved: the layout file was refused - fix ";
    const room = buf.len - head.len - 1;
    const n = @min(line.len, room);
    return std.fmt.bufPrint(buf, "{s}{s}]", .{ head, line[0..n] }) catch
        "[layout not saved: the layout file was refused]";
}

/// Closed `stdin` ends mux: a wall nobody types at is nowhere to leave a user.
pub fn endAction(
    tiles: []Tile,
    present: []const bool,
    live: usize,
    ended: usize,
    stdin_open: bool,
    is_tty: bool,
) EndAction {
    const t = &tiles[ended];
    const reason: EndReason = @enumFromInt(t.end.load(.acquire));
    // A chord-born tile that never got its session leaves no trace. The
    // sentence names the state: a daemon that answered and said no may free a
    // slot, while one that never answered has to be started.
    const gone_msg: ?[]const u8 = switch (reason) {
        // Whatever the tile had been: the user REFUSED this dial, and a
        // wall that retried anyway would ask the same question every two
        // seconds for as long as it stayed up.
        .declined => "mux: prompt declined",
        .refused => "mux: cannot create a new session (daemon full?)",
        .lost => if (t.ever_up.load(.acquire))
            null
        else
            "mux: cannot create a new session (daemon unreachable)",
        else => null,
    };
    if (gone_msg) |msg| {
        if (t.born_from) |back| {
            if (back < live and present[back])
                return .{ .vanish = .{ .back = back, .msg = msg } };
        }
    }
    // A clean exit is noise once over: the tile leaves and the wall re-cuts,
    // but only while somebody is left to steer it. On a TERMINAL the LAST
    // tile goes the same way and the wall stays up empty; a piped `mux` is a
    // wall of one and still exits with the shell's code, which scripts read.
    if (reason == .exited and stdin_open and (is_tty or presentCount(present[0..live]) > 1)) {
        // Null rather than the tile that just went: `stepLive` falls back
        // to the selection itself when there is nothing else present, and
        // handing the focus to a vanished tile is how an empty wall would
        // end up with a pump-less claim.
        const back: ?usize = if (presentCount(present[0..live]) > 1)
            stepLive(tiles[0..live], present[0..live], ended, true)
        else
            null;
        return .{ .vanish = .{ .back = back, .msg = null } };
    }
    // A lost link may heal, and a refusal narrates: both keep
    // their rect and their bar, and the focus steps on. Same liveness
    // preference: a live neighbour can take the keyboard, a dead one
    // cannot.
    if (stdin_open and presentCount(present[0..live]) > 1)
        return .{ .refocus = stepLive(tiles[0..live], present[0..live], ended, true) orelse ended };
    const done: EndAction = .{
        .finish = switch (reason) {
            // The shell's own code, which is the whole point of the single-tile
            // case: `mux` is still what you put in a script.
            .exited => .{ .code = t.code.load(.acquire), .msg = null },
            .taken => .{ .code = 0, .msg = "mux: detached (another client attached)" },
            .refused => .{
                .code = 1,
                .msg = "mux: attach refused or no state received (session full?)",
            },
            // A transport that died before any session carried none. The
            // sentence separates a `--via` command that never ran from a link
            // that was up and went — `client.lostMsg` is where that distinction
            // is kept.
            .lost => .{ .code = 1, .msg = client.lostMsg(t.r.target, 0) },
            // A pump that could not start at all: no Core, or a dial the quit
            // interrupted. There is nothing true to add to the exit code.
            .none => .{ .code = 1, .msg = null },
            // ...and the one cause that IS knowable, which no daemon had a
            // part in.
            .no_thread => .{ .code = 1, .msg = "mux: could not start a thread for this session" },
            // Said in the user's own terms: they pressed Esc at a password
            // box, and no ssh diagnostic describes that better than they do.
            .declined => .{ .code = 1, .msg = "mux: prompt declined" },
        },
    };
    // A session the PICKER started ends as a session ends: the sentence goes
    // on the empty-wall line, and the wall stays for the next Enter. Off a
    // terminal there is no wall to leave up. Falling PAST this branch is the
    // entry tile's road, where the ending is the program's — `Tile.keeps_wall`
    // is why no other field can tell the two apart.
    if (t.keeps_wall and is_tty and stdin_open)
        return .{ .vanish = .{ .back = null, .msg = gone_msg orelse done.finish.msg } };
    return done;
}

/// Newly dead tiles, drained one per pass. Returns the first that is focused
/// or `.exited` — both need the keyboard to vanish them; the rest narrated on
/// their own bars and are marked seen here.
pub fn endedTile(tiles: []Tile, present: []const bool, shared: *Shared) ?usize {
    const z = shared.sel;
    var hit: ?usize = null;
    for (tiles, present, 0..) |*t, p, i| {
        // A pending pane is `!alive` because no pump ran, not because one
        // ended: reading it as dead would exit mux on a resumed wall.
        if (!p or t.end_seen or t.pending or t.alive.load(.acquire)) continue;
        const reason: EndReason = @enumFromInt(t.end.load(.acquire));
        // A tile that only narrates (lost / refused / taken) needs no
        // keyboard action: its pump painted its bar before it died.
        if (i != z and reason != .exited) {
            t.end_seen = true;
            continue;
        }
        if (hit == null) {
            t.end_seen = true;
            hit = i;
        }
    }
    return hit;
}

/// The bare-Ctrl-\ abort is gated on exactly this: a tile whose pump ENDED is
/// not waiting, so its `Ctrl-\ w` is the chord it looks like. The state comes
/// back too: nothing was detached from a session that never came up.
fn awaitingSession(t: *Tile) ?State {
    if (!t.alive.load(.acquire)) return null;
    t.shared.paint_mu.lock();
    defer t.shared.paint_mu.unlock();
    return switch (t.state) {
        .connecting, .reconnecting => t.state,
        else => null,
    };
}

/// Not tidiness: the daemon frees the slot when it reads the frame, and a
/// `mux` typed straight after a `Ctrl-\ d` would otherwise race its own
/// corpse for the session. Bounded so a wedged pump cannot hold the terminal
/// hostage; the process exit closes the socket either way.
pub fn awaitDetach(t: *Tile, shared: *Shared) void {
    // The pump is the only thread that sets `detach_ack`, so one that has
    // already returned can only run the wait out: 400ms of a wall that has
    // said goodbye.
    if (!t.alive.load(.acquire)) return;
    const deadline = std.time.milliTimestamp() + 400;
    while (!t.detach_ack.load(.acquire)) {
        const left = deadline - std.time.milliTimestamp();
        if (left <= 0) return;
        var fds = [_]std.posix.pollfd{
            .{ .fd = shared.kb_r, .events = std.posix.POLL.IN, .revents = 0 },
        };
        _ = std.posix.poll(&fds, @intCast(@min(left, 100))) catch return;
        drainBell(shared.kb_r);
    }
}

/// The session on this host that mux itself is running inside, if any.
pub fn selfSession(target: client.Target, env_sock: ?[]const u8, env_session: ?[]const u8) ?[]const u8 {
    const es = env_session orelse return null;
    if (!showsSelf(target, es, env_sock, env_session)) return null;
    return proto.resolveName(es);
}

/// For a caller already holding `paint_mu`.
fn takeNoticeLocked(shared: *Shared, out: []u8) []const u8 {
    const said = wall_picker.peekNoticeLocked(shared, out);
    shared.notice_len = 0;
    return said;
}

/// The DIAL is on the main thread, before any wall: ssh can want the tty.
/// The pump ADOPTS a link that is already up.
pub fn runAttach(
    alloc: std.mem.Allocator,
    target: client.Target,
    session_name: []const u8,
    key: ?[]const u8,
    idle_ms: u32,
    agent: bool,
    own_version: []const u8,
) !u8 {
    // Anything typed while the first handshake is in flight belongs to the
    // shell, so it is held rather than dropped — and handed to the tile's
    // mailbox once there is a tile to hand it to.
    var carry: std.ArrayList(u8) = .empty;
    defer carry.deinit(alloc);
    // The one dial with a person waiting on it, so the one dial that keeps
    // ssh's last line: the failure below is said in ssh's words when ssh
    // had any, and in mux's only when it did not.
    var dial: handoff.Dial = .{};
    var transport = client.Transport.open(alloc, target, &carry, std.posix.STDIN_FILENO, &dial) catch |err| {
        var buf: [client.open_err_len]u8 = undefined;
        const f = client.openFailure(&buf, target, err, dial.reason.slice());
        std.debug.print("{s}", .{f.msg});
        return f.exit;
    };
    // `mux HOST` is the whole wall zoomed on HOST, not a wall of one: the
    // dialled host is tile 0 and the sidecar's anchor, every other listed
    // host after it. Unless there is no terminal — then the specs stop at the
    // entry host, so a piped `mux` writes what the plain client wrote.
    var arena_state = std.heap.ArenaAllocator.init(alloc);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const spelling = try wall_host.hostSpelling(arena, target);
    // The ask is SPENT: the open above is the dial the user waited for,
    // and everything downstream of this spec — the poller, the grading of
    // this host's panes, the entry tile's reconnects — is the wall acting
    // on its own.
    var spec_target = target;
    if (spec_target == .hand) spec_target.hand.asked = false;
    var specs: std.ArrayList(HostSpec) = .empty;
    try specs.append(arena, .{
        .spelling = spelling,
        .target = spec_target,
        // A failure here leaves the poll on the recipe the ATTACH used: it
        // can prompt, which is worse, but a wall that refuses to open over
        // one allocation is worse still. Unasked either way — a poll that
        // could start a daemon is not a lesser evil, it is the bug.
        .poll_target = client.pollTargetFor(arena, spec_target) catch spec_target,
    });
    if (hosts.statePath(arena) catch null) |path| {
        // stderr, not a notice: this runs before `run` takes the screen.
        if (wall_host.recordHost(arena, target, spelling, path)) |err|
            std.debug.print("mux: hosts file not updated ({s}): {s}\n", .{ path, hosts.reason(err) });
        if (!headless(std.posix.STDOUT_FILENO))
            wall_host.otherHosts(arena, &specs, spelling, path, key, idle_ms);
    }
    // The narration was owed to a person at a BARE prompt, and the wall is
    // about to take the screen: past here a late line from ssh would land on
    // the alternate screen. A piped `mux` goes on relaying to its script.
    if (!headless(std.posix.STDOUT_FILENO)) transport.narrate = false;
    return run(alloc, specs.items, .{
        .focus0 = true,
        .pre = transport,
        .carry = carry.items,
        .entry_host = 0,
        .entry_session = proto.resolveName(session_name),
        .agent = agent,
        .own_version = own_version,
        .key = key,
        .idle_ms = idle_ms,
        // A scripted `mux` has pipes on both ends and is still a wall of
        // one tile whose rect is the whole terminal. Only a wall of many
        // needs a terminal.
        .needs_tty = false,
    }) catch |err| {
        transport.close();
        return err;
    };
}

/// How a wall is ENTERED. One program, two doors: `mux` opens on the wall
/// itself, `mux TARGET` opens focused on the tile it just attached to.
/// Everything after the first paint is the same machinery.
const Entry = struct {
    /// Start focused on tile 0 rather than on the wall. `mux [TARGET]`
    /// opens here.
    focus0: bool = false,
    /// Tile 0's link, already open. See `Tile.pre`.
    pre: ?client.Transport = null,
    /// Keystrokes typed while that first handshake was in flight, owed to
    /// the shell as soon as there is an attach to send them after. They go
    /// through the mailbox like everything else a user types, so they reach
    /// the session by the same path — prediction, mouse split and all.
    carry: []const u8 = "",
    /// Which host on the wall tile 0 belongs to. Null (with no `pre`) is
    /// the plain wall: every tile arrives from a host's own list.
    entry_host: ?usize = null,
    /// The session tile 0 opens on that host; empty is the default one.
    entry_session: []const u8 = "",
    /// Whether tile 0's attaches offer this client's ssh-agent (`mux -A`).
    /// Per tile, not per wall: a tile the user never named must not hand a
    /// stranger's host the keys.
    agent: bool = false,
    /// This client's own version, handed in as data because only the cli
    /// modules import `build_options`. `Shared.own_version` is set from it.
    own_version: []const u8 = "",
    /// The key and idle timeout a `quic://` host is resolved with — the
    /// ones this invocation was given.
    key: ?[]const u8 = null,
    idle_ms: u32 = client.quic_idle_ms_default,
    /// Whether this wall insists on a terminal. A wall of many does — there
    /// is nothing to cut stripes from without one. A scripted `mux TARGET` is
    /// a wall of one whose rect is the whole terminal, so it does not.
    needs_tty: bool = true,
};

/// Run the wall until it is left. Never returns on the success path — see
/// the exit at the bottom.
pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) !u8 {
    const stdin_fd = std.posix.STDIN_FILENO;
    const stdout_fd = std.posix.STDOUT_FILENO;
    const measured = interact.ttySize(stdout_fd);
    if (measured == null and entry.needs_tty) {
        std.debug.print("mux: wall needs a terminal\n", .{});
        return 2;
    }
    // The plain client's fallback, byte for byte: a piped `mux` painted an
    // 80x24 grid and this one has to paint the same one.
    const size = measured orelse proto.Size{ .cols = 80, .rows = 24 };
    // Whether there is a TERMINAL and whether it could be MEASURED are two
    // questions, answered apart: a 1x1 pty is a real terminal reporting a
    // size nothing can paint at.
    const is_tty = std.posix.isatty(stdin_fd);

    var shared = Shared{
        .out_fd = stdout_fd,
        .size = size,
        .is_tty = is_tty,
        .own_version = entry.own_version,
    };
    shared.tree = layout.Tree.init(alloc);
    shared.flat_alloc = alloc;
    // Resolved before the seed reads it and before any tile is seated: a
    // pipe has no stripes, so it has no layout worth remembering — and a
    // tree it saved would be a tree the next TERMINAL seeds over the
    // aspect rule. A state dir that cannot be named leaves this null, and
    // the wall then runs exactly as a piped one does.
    if (is_tty) shared.layout_path = hosts.layoutPath(alloc) catch null;
    defer if (shared.layout_path) |p| alloc.free(p);
    const env_sock = std.posix.getenv(proto.sock_env);
    const env_session = std.posix.getenv(proto.session_env);
    // The host table before the first flatten, because the seed matches
    // saved leaves against these spellings. Allocated at CAPACITY: a
    // poller thread holds its `*Host` for the wall's whole life, so the
    // array may never move when the picker's `a` adds a host to it.
    const host_table = try alloc.alloc(Host, max_tiles);
    var hosts_live: usize = @min(host_specs.len, max_tiles);
    for (host_table[0..hosts_live], host_specs[0..hosts_live]) |*h, spec| {
        h.* = .{
            .spec = spec,
            .shared = &shared,
            .self_name = selfSession(spec.target, env_sock, env_session),
        };
    }
    const has_entry = entry.pre != null and entry.entry_host != null;
    var entry_name: []const u8 = "";
    var entry_label: ?[]const u8 = null;
    if (has_entry) {
        // Duped, though argv outlives the process: a tile that ends frees
        // these two when its digit is taken back.
        entry_name = try alloc.dupe(u8, proto.resolveName(entry.entry_session));
        entry_label = try tileLabel(alloc, host_specs[entry.entry_host.?].target, entry_name);
    }
    // A `--via CMD` wall records nothing, and that includes its own pane.
    // `hosts.zig` refuses to write a `--via` line — an arbitrary command is
    // not an address — so no hosts table can ever hold a row for one, while
    // `tileLabel` still spells the tile `--via CMD#NAME` for its bar. Left
    // alone, the start-up `persist` writes that spelling as a leaf and the
    // NEXT start's `seedLayout` finds no host for it and refuses the WHOLE
    // file: one throwaway `--via` run costs the user every pane they
    // authored. Same rule as the trimmed seed below — a run that cannot
    // record what it is showing stops recording, before it reads the file
    // as well as before it writes one, and says so where the wall says
    // everything else.
    if (has_entry and host_specs[entry.entry_host.?].target == .via) {
        stopSaving(&shared, alloc, "[layout not saved: a --via wall is not recorded]");
    }
    // The wall starts with the cut the layout file remembers: its leaves are
    // pending panes the polls bind in place or the settle collapses, so
    // the first paint is the saved shape, not a placeholder to re-cut.
    var refused_line: [96]u8 = undefined;
    var refused_notice: [96]u8 = undefined;
    // The plan is MOVED out of this below and owned by `seed_plan` from
    // there on; nothing reads the verdict again.
    const seed_res: wall_layout.SeedResult = if (measured != null)
        wall_layout.seedSidecar(alloc, host_table[0..hosts_live], &shared, entry_label, &refused_line)
    else
        .none;
    var seed_plan: ?wall_layout.SeedPlan = switch (seed_res) {
        .plan => |pl| pl,
        else => null,
    };
    const seeded = seed_plan != null;
    switch (seed_res) {
        // A file this run could not read is a file this run must not write.
        // The wall boots on the default cut, and the entry tile's start-up
        // `persist` would then replace the user's whole authored wall with
        // the one leaf it has — the panes gone before a key was pressed,
        // and the printed line pointing at a file that no longer holds the
        // mistake it names. Nothing is written for the rest of the run; the
        // next start reads the same file and refuses it the same way.
        .refused => |line| stopSaving(&shared, alloc, refusalNotice(&refused_notice, line)),
        // The file is not wrong — this wall simply cannot seat any of it,
        // because every leaf is the session this `mux` is running inside.
        // Same rule: a run that shows none of the file does not rewrite it.
        .self_only => stopSaving(&shared, alloc, "[layout not saved: it names only this shell's own session]"),
        else => {},
    }
    // A seed that could not seat everything the file named is not this
    // run's to write back either. `persist` serializes the tree it HAS, so
    // the first save would drop the leaves this wall left out — the user's
    // other panes gone before they touched a key, and no undo. The whole
    // run stops saving instead, and says so once; the next start on a
    // terminal that fits them saves again.
    if (seed_plan) |pl| {
        if (pl.dropped > 0) {
            var why: [96]u8 = undefined;
            const fit = pl.dropped - pl.dropped_self;
            stopSaving(&shared, alloc, if (fit > 0)
                std.fmt.bufPrint(&why, "[layout not saved: terminal too small for {d} of its panes]", .{fit}) catch
                    "[layout not saved: terminal too small for it]"
            else
                "[layout not saved: it names this shell's own session]");
        }
    }
    if (seed_plan == null and has_entry) shared.tree.addFirst(0) catch return 2;
    const init_flat = shared.tree.flatten(
        alloc,
        size.rows,
        size.cols,
        wall_layout.floorsOf(&shared),
        null,
    ) catch {
        std.debug.print("mux: terminal too small\n", .{});
        return 2;
    };
    shared.last_flat = init_flat;

    // A daemon that dies mid-write must surface as a write error on that
    // tile's thread, not a process-fatal SIGPIPE.
    proxy.ignoreSigpipe();

    // Raw mode and the alternate screen only when there is a terminal to
    // put back afterwards. On a pipe both are skipped and there is nothing
    // to restore, which is what makes a scripted attach's capture identical
    // to the one the plain client produced.
    const orig: ?std.posix.termios = if (is_tty) try std.posix.tcgetattr(stdin_fd) else null;
    if (orig) |o| {
        var raw = o;
        raw.lflag.ICANON = false;
        raw.lflag.ECHO = false;
        // ISIG and IXON off because those keys belong to the SESSION: Ctrl-C
        // must reach the remote shell as a byte. It is also why `Ctrl-\` is
        // the only way out — nothing else here raises a signal.
        raw.lflag.ISIG = false;
        raw.iflag.IXON = false;
        raw.iflag.ICRNL = false;
        try std.posix.tcsetattr(stdin_fd, .FLUSH, raw);
        // Alternate screen, cursor hidden, autowrap off, title pushed — from
        // interact's own constant, because the exit must undo what a focused
        // TILE added on top and only that module knows the whole list.
        proto.writeAllFd(stdout_fd, interact.wall_setup) catch {};
        // Armed by the wall and not by any Core: the wall owns this
        // terminal's raw mode. The KEYBOARD thread reads the process-wide
        // winch flag and relayouts; no pump consumes it.
        interact.watchWinch();
    }
    // From here to the key loop every `try` unwinds past the restore below,
    // leaving the user on an alternate screen in raw mode with no prompt.
    // Only that window: every ordinary exit is the teardown at the bottom.
    errdefer if (orig) |o| {
        proto.writeAllFd(stdout_fd, interact.wall_teardown) catch {};
        std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {};
    };

    // The pumps' way of waking a keyboard asleep in read(2). Created before
    // any pump exists, because a pump that ends immediately (a refused
    // attach) rings it before the keyboard has ever polled.
    const kb = try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
    shared.kb_r = kb[0];
    shared.kb_w = kb[1];
    shared.sel = 0;

    // ssh's prompts get somewhere to go, on a TERMINAL only: without one
    // there is no popup, so ssh keeps its prompts and fails them visibly.
    // `sockpath.runtimeDir` is the one place that names the directory, so
    // the popup lands beside the daemon socket on whatever OS this is —
    // and what keeps this from being a password prompt any other local
    // user could answer is that directory's privacy, which each OS buys
    // differently. On Linux it is $XDG_RUNTIME_DIR with no fallback at
    // all, so there is no /tmp path to share. On Darwin the fallback is
    // /tmp/mux-<uid>, inside a world-writable directory, and what carries
    // the guarantee there is `sockpath.runtimeDir` refusing that name
    // unless it is a directory this uid owns at mode 0700.
    var ask_exe_buf: [std.fs.max_path_bytes]u8 = undefined;
    if (is_tty) {
        if (sockpath.runtimeDir()) |rt| {
            if (askpass.Listener.start(alloc, rt, .{
                .ctx = &shared,
                .wake = ringKeyboardCtx,
            })) |l| {
                shared.prompts = l;
                // The image that is RUNNING, resolved by `spawn.selfExe`
                // and never a PATH walk: ssh execs this as its helper, and
                // a helper found by name is whichever mux the environment
                // happens to have.
                shared.prompt_exe = spawn.selfExe(&ask_exe_buf);
            } else |_| {}
        }
    }
    // The ERROR returns between here and the keys loop, and those only:
    // this function ends in `std.posix.exit`, which reaches no defer at
    // all. The normal way out is `retire` at the tail, and the two are not
    // interchangeable — see there.
    defer if (shared.prompts) |l| l.stop();

    // Allocated at CAPACITY, not at length: the pump threads are detached
    // and hold `*Tile` for the wall's whole life, so the array may never
    // move. `live` is how much of it is real.
    const tiles = try alloc.alloc(Tile, max_tiles);
    var live: usize = 0;
    const present = try alloc.alloc(bool, max_tiles);
    @memset(present, false);
    // One value for the six the wall used to pass around. `hosts` is
    // re-sliced wherever `hosts_live` grows — the picker's `a` is the only
    // thing that does.
    var w: Wall = .{ .alloc = alloc, .tiles = tiles, .present = present, .live = &live, .shared = &shared };
    w.hosts = host_table[0..hosts_live];
    if (has_entry) {
        const hi = entry.entry_host.?;
        const target = host_specs[hi].target;
        const rect = init_flat.rectOf(0) orelse return 2;
        try initTile(&tiles[0], .{
            .target = target,
            .label = entry_label.?,
            .session = entry_name,
            .agent = entry.agent,
        }, rect, &shared, 0, .fresh);
        tiles[0].host = hi;
        tiles[0].pre = entry.pre;
        // `mux TARGET` is attach-or-create: the attach carries the rect,
        // which is what the daemon reads create-vs-join off.
        tiles[0].creates = true;
        // The entry tile keeps the plain client's cold-loss rule; see
        // `Tile.retry_cold`.
        tiles[0].retry_cold = false;
        // The handshake's keystrokes, before the pump that will send them
        // exists. Whatever will not fit is dropped whole, `sendKeys`'
        // reason: a spliced command is worse than a lost one.
        const n = @min(entry.carry.len, tiles[0].in.len);
        @memcpy(tiles[0].in[0..n], entry.carry[0..n]);
        tiles[0].in_len = n;
        present[0] = true;
        w.live.* = 1;
        // Its pump claims the terminal on its first pass. Set before
        // `spawnPump` so the claim is the first thing the pump does after
        // the dial, and the attach already carries the terminal's size.
        tiles[0].claim_pending.store(true, .release);
    }
    if (seed_plan) |*pl| {
        for (pl.panes, 0..) |mp, i| {
            const pane = mp orelse continue;
            const rect = init_flat.rectOf(@intCast(i)) orelse layout.Rect{ .top = 0, .left = 0, .rows = 0, .cols = 0 };
            try seedTile(&tiles[i], .{
                .target = host_table[pane.host].spec.target,
                .label = pane.label,
                .session = pane.session,
            }, rect, &shared, i, pane.host);
            present[i] = true;
        }
        live = pl.panes.len;
        // The saved focus, unless the user is already typing into the
        // entry tile - a focus record must not move them off it.
        if (!has_entry) shared.sel = pl.focus orelse (wall_layout.firstPresent(present[0..live]) orelse 0);
        // The names moved into the tiles above; only the array goes.
        alloc.free(pl.panes);
        seed_plan = null;
    }
    for (tiles[0..live]) |*t| spawnPump(t);
    // A wall with no tile yet paints its one line rather than nothing (a
    // blank terminal with no cursor reads as hung), and a seeded wall
    // paints its remembered cut: rails and waiting bars have no pump.
    if (w.live.* == 0 or seeded) wall_layout.relayout(w, shared.sel);
    // The entry tile is a pane the user just added to the wall, whichever
    // road seated it — `addFirst(0)` on a wall with no saved tree, or the
    // seed's own insertion beside the leaves the file named. Written here
    // and not at the exit, so `mux HOST` in one terminal is on the wall the
    // next terminal starts. A wall with no entry tile added nothing, and a
    // save there would write over a file this run may have refused to read.
    if (has_entry) wall_layout.persist(w);

    // One poller per host, all of them at once and none of them on this
    // thread: the user asked for one session and must not be held on
    // another host's ssh to see it.
    var over_buf: [64]u8 = undefined;
    if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said);
    for (host_table[0..hosts_live]) |*h| {
        // Not without a terminal: a poller here would turn every scripted
        // `mux --sock S` into a wall of whatever that daemon is running.
        // Nothing to wait for when nothing was started.
        if (measured == null) {
            h.poller_done.store(true, .release);
            continue;
        }
        const th = std.Thread.spawn(.{}, wall_host.pollHost, .{h}) catch {
            h.poller_done.store(true, .release);
            continue;
        };
        th.detach();
    }
    // Where the picker's `a` writes the host it adds. Resolved once, up
    // here: a popup is not the place to find out there is no state dir, and
    // a null path leaves the host on this wall and out of the file rather
    // than refusing it.
    const hosts_path = hosts.statePath(alloc) catch null;
    defer if (hosts_path) |p| alloc.free(p);

    // The wall is always "in": every non-chord byte goes to the focused tile.
    // The keyboard holds back only the `Ctrl-\` prefix and hit-tests presses
    // for focus; the report bytes still go through, so the drag's Core sees them.
    var input: WallInput = .{};
    // Which HOST the picker's selection rests on, kept across opens so a
    // popup reopened by reflex is where it was left. Keyboard-thread
    // state, like the focus.
    var picker_sel: usize = 0;
    // Which SESSION row the picker's second level rests on, as an index into
    // `picker_sel`'s host list. Reset on every level change rather than kept
    // like `picker_sel`: a row number means nothing on another host's list,
    // and the daemons answer while the box is open.
    var picker_row: usize = 0;
    // The prompt on screen, kept because the box is repainted on every
    // keystroke and the question does not come round again.
    var ask_prompt: askpass.Prompt = .{};
    // Whether the picker on screen is one the WALL opened, not the user:
    // an empty wall opens it once, and an Esc there has to be able to
    // leave the empty line showing rather than be re-opened over.
    var picker_auto: PickerAuto = .{};
    // Whether the popup was already on the screen when this key arrived,
    // so an OPEN can be told from a keystroke inside one.
    var picker_shown = false;
    // `feed` hands a candidate held across the previous read back ahead of
    // this chunk, so its room is a whole chunk plus that hold.
    var mouse_out: [mailbox_max + interact.MouseFilter.max_held]u8 = undefined;
    var mouse_filter: interact.MouseFilter = .{};
    // A vanish sentence deferred to the normal screen: printed after raw
    // mode is restored, alongside the exit message below.
    var vanish_msg: ?[]const u8 = null;
    // How this run ends: the code, and the one sentence that goes with it
    // on the normal screen once the terminal is back.
    var exit_code: u8 = 0;
    var exit_msg: ?[]const u8 = null;
    // The wall SETTLES once: every opening host has answered, or 2s - the
    // poll's own dial budget. It gates the picker's auto-open and nothing
    // else; only a host's own list may take a pane (`dressSilent`).
    const settle_due: i64 = std.time.milliTimestamp() + 2000;
    var settled = false;
    // Only the hosts the wall OPENED with are waited for. One added in the
    // picker arrives long after this, on a wall the user is already
    // looking at, and holding the settle for it would relay their panes
    // under their hands.
    const opening_hosts = hosts_live;
    // The aspect heuristic is the fallback for a wall with no saved tree,
    // and it decides once — the first time the wall holds two tiles.
    var oriented = false;
    // Stdin gone is not the wall gone. A piped `mux` whose script has run
    // out still has a session on the far end, exactly as the plain client
    // did — it ends when that session does.
    var stdin_open = true;
    var b: [mailbox_max]u8 = undefined;
    keys: while (true) {
        var fds = [_]std.posix.pollfd{
            .{ .fd = if (stdin_open) stdin_fd else -1, .events = std.posix.POLL.IN, .revents = 0 },
            .{ .fd = shared.kb_r, .events = std.posix.POLL.IN, .revents = 0 },
        };
        // A 100ms cap so the keyboard can check the process-wide winch
        // flag without a pump having to — `winch` is one flag and the tile
        // that would have answered it is no longer special.
        const winch = interact.winchRaised();
        // A finite timeout, not -1: the flag is a flag, and poll retries
        // on EINTR — a SIGWINCH landing mid-poll would otherwise wait for
        // the next keystroke to be noticed. 100ms is the pumps' cadence.
        _ = std.posix.poll(&fds, if (winch) 0 else 100) catch break;
        if (winch) {
            const measured2 = interact.ttySize(stdout_fd) orelse continue;
            if (measured2.cols != shared.size.cols or measured2.rows != shared.size.rows) {
                shared.paint_mu.lock();
                shared.size = measured2;
                shared.paint_mu.unlock();
                wall_layout.relayout(w, shared.sel);
            }
        }

        if (fds[1].revents != 0) {
            drainBell(shared.kb_r);
            // Answers first, ends second: a chord that has just been
            // answered may move the focus onto a tile whose end we are
            // about to read, and acting on the end of a tile nobody is
            // looking at any more is the wrong order to notice things in.
            const z = shared.sel;
            if (z < w.live.* and tiles[z].ans_ready.swap(false, .acq_rel)) {
                var name: client.SessionName = undefined;
                {
                    tiles[z].ans_mu.lock();
                    defer tiles[z].ans_mu.unlock();
                    name = tiles[z].ans;
                }
                // Tiles, not slots: a birth that took a vanished digit
                // back leaves `live` — the high-water mark — exactly where
                // it was, and a wall that read growth off it would put the
                // new tile on a screen nothing re-cut.
                const before = presentCount(present[0..live]);
                switch (addSessionTile(w, z, name.slice(), tiles[z].pending_place)) {
                    .moved => |to| {
                        const grew = presentCount(present[0..live]) > before;
                        focusAnswer(w, grew, to);
                        // The chord's birth (`.new_session`, `.split_right`,
                        // `.split_below`) lands here, one round trip after
                        // the key: the answer is what makes the tile, so this
                        // is where the wall changed. An answer that only
                        // moved the focus onto a tile the wall already had
                        // changed nothing to write.
                        if (grew) wall_layout.persist(w);
                    },
                    .full => setNotice(&shared, "[no room on the wall for another tile]"),
                    .stay => {},
                }
                wall_host.pokeHost(w.hosts, &tiles[z]);
            }
        }
        // Ends are read every pass, not only on the bell: two pumps dying
        // together share one ring, and `drainBell` clears it, so a residual
        // end would otherwise wait for a ring that may not come.
        if (endedTile(tiles[0..live], present[0..live], &shared)) |ended| {
            switch (endAction(tiles, present, w.live.*, ended, stdin_open, shared.is_tty)) {
                .refocus => |to| {
                    if (!tiles[to].alive.load(.acquire))
                        setNotice(&shared, "[focus on a dead tile - no live neighbour]");
                    setFocus(tiles[0..live], &shared, to);
                },
                .vanish => |v| {
                    if (v.msg) |msg| {
                        // Banner on the surviving tile's rect; stderr deferred
                        // past the raw-mode restore. Both say the same words.
                        var nb: [128]u8 = undefined;
                        setNotice(&shared, noticeText(&nb, msg));
                        vanish_msg = msg;
                    }
                    // An unfocused tile ending must not move the user's
                    // focus: it goes back where it was, not where the dead
                    // tile pointed — `vanishTile` moves it only if the
                    // vanished tile had it.
                    if (v.back) |back| {
                        if (ended == shared.sel and !tiles[back].alive.load(.acquire))
                            setNotice(&shared, "[focus on a dead tile - no live neighbour]");
                    }
                    vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back);
                    wall_layout.relayout(w, shared.sel);
                    // A pane whose session ended is a pane the wall no
                    // longer has: the file says so now, not at the next
                    // detach, so a crash cannot bring the dead one back.
                    wall_layout.persist(w);
                },
                .finish => |how| {
                    exit_code = how.code;
                    exit_msg = how.msg;
                    break :keys;
                },
            }
        }
        // A box whose ssh has GONE, before one that is arriving: the helper is
        // SIGTERMed when the touch lands, so the prompt on screen is a question
        // nobody waits on. Dismissing it by key would decline a live dial.
        if (input.prefix.asking) {
            if (shared.prompts) |l| {
                if (!l.showing()) closeAsk(w, &shared, &input.prefix, picker_sel, picker_row);
            }
        }
        // Every pass, not only on the bell: `endedTile`'s reason twice
        // over — one drained ring can carry two pumps' news — and a prompt
        // is an ssh already blocked on the answer.
        if (takeAsk(&shared, &input.prefix, &ask_prompt))
            wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix);
        // Ends first, lists second: a session that exited is its pump's
        // news and arrives at once, while a list is up to a poll behind.
        // Reading the list first would vanish the tile the exit code is on.
        const host_news = wall_host.applyReadyLists(w);
        if (!settled) {
            var all_reported = true;
            for (host_table[0..opening_hosts]) |*h| {
                if (!h.applied) {
                    all_reported = false;
                    break;
                }
            }
            if (all_reported or std.time.milliTimestamp() >= settle_due)
                settled = true;
        }
        if (!seeded and !oriented and presentCount(present[0..live]) > 1) {
            oriented = true;
            shared.paint_mu.lock();
            shared.tree.setRootOrient(wall_layout.rootOrient(shared.size));
            shared.paint_mu.unlock();
            wall_layout.relayout(w, shared.sel);
        }
        // An empty wall IS the picker: there is nothing else on the screen
        // to act from, and the last `x` is exactly when the user needs the
        // list of machines. Once, so the Esc that closes it leaves the
        // one-line empty-wall text standing.
        var picker_opened = false;
        // `settled` is the gate, not emptiness alone: it is the moment
        // every opening host has answered (or 2s), and a wall still dialling
        // is not known to be empty. A popup flashed over a wall that is
        // about to have tiles is one the user never asked for.
        switch (picker_auto.step(
            shared.is_tty and settled,
            presentCount(present[0..live]) == 0,
            input.prefix.picking,
            input.prefix.prompting,
        )) {
            .open => {
                picker_opened = true;
                picker_shown = true;
                input.prefix.picking = true;
                // The wall's own open is a HOST list: the level is the
                // filter's, and nothing else here would put it back.
                input.prefix.pick_level = .hosts;
                picker_row = 0;
                picker_sel = wall_picker.pickerNearest(w.hosts, picker_sel);
            },
            .close => closePicker(w, &input.prefix, &picker_shown, null),
            .leave => {},
        }
        // On news, not on the tick: the rows follow the pollers while the
        // popup is open, but a repaint every 100ms rewrites an unchanged
        // screen. A zeroed stamp is a screen the box is no longer on —
        // `relayout` cleared it, and nothing else would repaint the popup.
        // The lists move under the box: a poll that ended a session while
        // the popup was open leaves the selection past the last row, and
        // the highlight would be on nothing.
        if (input.prefix.pick_level == .sessions)
            picker_row = @min(picker_row, wall_picker.sessionCount(w.hosts, picker_sel) -| 1);
        var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined;
        const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or
            winch or fds[1].revents != 0 or shared.picker_frame.stamp == 0);
        if (repair.due)
            wall_picker.paintPicker(w, picker_sel, .{ .level = input.prefix.pick_level, .row = picker_row }, repair.line);
        if (fds[0].revents == 0) continue;

        const n = std.posix.read(stdin_fd, &b) catch break;
        if (n == 0) {
            stdin_open = false;
            // The session outlives the script that was typing at it;
            // `endAction` is what ends the run when that session does.
            continue;
        }

        const z = shared.sel;
        const cmd = input.prefix.feed(b[0..n]);
        // Over EVERYTHING, the picker included: ssh is blocked on this
        // answer, and the box that owns the keyboard is the one that
        // arrived last. Nothing is forwarded — the filter keeps every byte
        // while `asking`, which is what stops a password reaching a shell.
        if (input.prefix.asking or wall_picker.isAskAction(cmd.action)) {
            // A NOTICE takes no answer, whichever key ended it: ssh asked
            // nothing — "Confirm user presence for key ..." — and a decline
            // recorded here would park a tile whose dial is still going.
            // The box just leaves; the helper is ssh's to kill.
            const answerable = ask_prompt.kind != .notice;
            switch (cmd.action) {
                .ask_answer => |line| if (answerable) {
                    if (shared.prompts) |l| l.answer(line);
                },
                .ask_decline => if (answerable) {
                    if (shared.prompts) |l| l.decline();
                },
                else => {},
            }
            if (input.prefix.asking) {
                wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix);
                continue;
            }
            // ssh asks in sequence and the next helper is already in the
            // backlog: reopening BEFORE the close saves a screen that flashes
            // back to the wall between two halves of one login.
            if (takeAsk(&shared, &input.prefix, &ask_prompt)) {
                wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix);
                continue;
            }
            closeAsk(w, &shared, &input.prefix, picker_sel, picker_row);
            continue;
        }
        // The picker is a MODE: while it is open every key is the popup's,
        // so neither the empty wall's arms below nor the tiles' ever see
        // one — and the popup is the one thing that works on a wall with
        // no tile at all, which is why it is answered ahead of both.
        if (input.prefix.picking or wall_picker.isPickAction(cmd.action)) {
            // Typed AT the session, ahead of the chord, in the same read.
            if (cmd.forward.len > 0 and z < w.live.* and present[z]) keysToFocused(&tiles[z], cmd.forward);
            var birth_at: ?usize = null;
            // Opening on a focused tile pre-selects that tile's host: the
            // machine the user is already on is the one they mean. Judged
            // on the popup's own state, not on `.pick_open`, because one
            // read can open the picker and its editor together.
            if (!picker_shown) {
                if (z < w.live.* and present[z]) {
                    if (tiles[z].host) |hi| picker_sel = hi;
                }
                picker_sel = wall_picker.pickerNearest(w.hosts, picker_sel);
                picker_row = 0;
            }
            // The level the FILTER left, not the one the key arrived at: it
            // flips `pick_level` before returning, so `.sessions` on a
            // `.pick_enter` means the list just opened and `.hosts` means a
            // session was chosen and the popup is closing.
            const level = input.prefix.pick_level;
            switch (cmd.action) {
                .pick_move => |d| if (level == .sessions) {
                    picker_row = wall_picker.rowStep(picker_row, d, wall_picker.sessionCount(w.hosts, picker_sel));
                } else {
                    picker_sel = wall_picker.pickerStep(w.hosts, picker_sel, d);
                },
                .pick_select => |row| if (level == .sessions) {
                    picker_row = @min(row - 1, wall_picker.sessionCount(w.hosts, picker_sel) -| 1);
                } else {
                    if (wall_picker.pickerAt(w.hosts, row - 1)) |hi| picker_sel = hi;
                },
                .pick_enter => if (level == .sessions) {
                    // Enter at the host level: the session list just opened,
                    // and there is nothing to do but paint it from the top.
                    picker_row = 0;
                    // ...unless there is no host under the selection at all.
                    // An empty hosts file leaves `picker_sel` at 0 with no
                    // row there, and a level with nothing in it is a box the
                    // user has to Esc back out of for nothing.
                    if (picker_sel >= w.hosts.len or w.hosts[picker_sel].forgotten.load(.acquire)) {
                        input.prefix.pick_level = .hosts;
                        setNotice(&shared, "[no host to list sessions on - a adds one]");
                    }
                } else {
                    // Enter at the session level: the popup closed on a
                    // choice, and that session becomes a pane.
                    birth_at = wall_picker.pickAdd(w, picker_sel, picker_row);
                },
                // Back at the hosts, so the row means nothing any more.
                .pick_back => picker_row = 0,
                .pick_end => wall_picker.pickEnd(w, picker_sel, picker_row, std.time.milliTimestamp()),
                .pick_birth => birth_at = wall_picker.pickBirth(w, picker_sel),
                .pick_forget => wall_picker.pickForget(w, picker_sel, hosts_path),
                .add_tile => |spelling| {
                    // The editor adds a HOST, so its answer belongs on the
                    // host list — whichever level the `a` was typed at.
                    input.prefix.pick_level = .hosts;
                    picker_row = 0;
                    switch (wall_host.addHost(
                        alloc,
                        &shared,
                        host_table,
                        &hosts_live,
                        spelling,
                        entry.key,
                        entry.idle_ms,
                        hosts_path,
                    )) {
                        .added => |hi| {
                            // The only thing that grows `hosts_live`, so the
                            // only place the wall's own slice has to follow.
                            w.hosts = host_table[0..hosts_live];
                            const th = std.Thread.spawn(.{}, wall_host.pollHost, .{&host_table[hi]}) catch null;
                            if (th) |handle| handle.detach();
                            // The row the user just made is the row they meant.
                            picker_sel = hi;
                            picker_row = 0;
                        },
                        // The row it is already on is the row they meant too:
                        // an unchanged popup is the prompt saying nothing.
                        .listed => |hi| {
                            picker_sel = hi;
                            picker_row = 0;
                        },
                        .refused => {},
                    }
                },
                else => {},
            }
            picker_shown = input.prefix.picking;
            if (input.prefix.picking) {
                var line_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined;
                const keyed = wall_picker.pickerRepaint(&line_buf, &input.prefix, true);
                wall_picker.paintPicker(w, picker_sel, .{ .level = input.prefix.pick_level, .row = picker_row }, keyed.line);
            } else {
                // A close the USER asked for: the auto-open no longer owns
                // this popup, so the next tile to arrive cannot force-close
                // a box the user opened themselves.
                picker_auto.taken();
                closePicker(w, &input.prefix, &picker_shown, birth_at);
                // `pickBirth` wrote the new pane; the close is what moved
                // the focus onto it, and the focus is in the file too.
                if (birth_at != null) wall_layout.persist(w);
                // The focused pump may be holding a claim the popup refused;
                // it re-arms and retries on its next pass, and this is what
                // makes that pass happen now rather than within a poll.
                closeNotice(tiles[0..live], present[0..live], &shared);
            }
            continue;
        }
        // An EMPTY wall has no `tiles[z]`, and every branch below reads one.
        // `live` cannot answer it — a slot high-water never falls — so the
        // `present` roll is what `relayout` counted. No sidecar on the way
        // out: writing one would erase the layout the user last left.
        if (presentCount(present[0..live]) == 0) {
            switch (cmd.action) {
                .detach => {
                    exit_code = 0;
                    exit_msg = "mux: left the wall";
                    break :keys;
                },
                .new_session, .split_right, .split_below => {
                    setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]");
                    wall_layout.relayout(w, shared.sel);
                },
                else => {},
            }
            continue;
        }
        // A BARE Ctrl-\ with no session to command is the user giving up on
        // the wait. Judged after the filter, so `\x1c` still holding out for
        // its command key stays apart from `\x1c w` asking for the wall.
        if (cmd.action == .none and input.prefix.pending) {
            if (awaitingSession(&tiles[z])) |waiting| {
                exit_code = 0;
                exit_msg = switch (waiting) {
                    // Nothing has been detached from a session that
                    // never came up.
                    .connecting => "mux: aborted before attaching",
                    else => "mux: detached while reconnecting (session still running; run mux to reattach)",
                };
                break :keys;
            }
        }
        // What preceded the chord was typed AT the focused session, and
        // it is queued BEFORE the focus moves — so `Ctrl-\ n` cannot
        // deliver the tail of a word to the tile it is jumping to.
        if (cmd.forward.len > 0) {
            // A press in another tile's rect moves the focus there; a press in
            // the focused tile is not intercepted at all. `Event.at` is the
            // byte offset of each report, because keys before a click belong to
            // the OLD focus and keys after it to the new.
            const report = mouse_filter.feed(cmd.forward, &mouse_out);
            var seg_start: usize = 0;
            for (report.events) |ev| {
                if (ev.kind != .press) continue;
                if (rectHit(tiles[0..live], present[0..live], &shared, ev.row, ev.col)) |hit| {
                    if (hit != shared.sel) {
                        if (ev.at > seg_start)
                            keysToFocused(&tiles[shared.sel], cmd.forward[seg_start..ev.at]);
                        seg_start = ev.at;
                        setFocus(tiles[0..live], &shared, hit);
                    }
                }
            }
            keysToFocused(&tiles[shared.sel], cmd.forward[seg_start..]);
        }
        switch (cmd.action) {
            .none => {},
            // The picker's own keys: the popup answers them, never a tile.
            .pick_open,
            .pick_move,
            .pick_select,
            .pick_birth,
            .pick_forget,
            .pick_add_open,
            .pick_close,
            .pick_enter,
            .pick_back,
            .pick_end,
            => {},
            // ssh's own keys, answered above this switch for the reason
            // the picker's are: the box is the only thing on the screen
            // that a keystroke can be for.
            .ask_answer, .ask_decline => {},
            .detach => {
                // The slot goes back to the daemon before this process does.
                // The pump writes the frame; this waits briefly for the ack, so
                // a `mux` typed straight after finds the session free.
                tiles[z].detach_req.store(true, .release);
                ring(&tiles[z]);
                awaitDetach(&tiles[z], &shared);
                // No save here, and none on `.finish` either. Every change
                // to the pane set or the tree has already written the file,
                // so an exit has nothing left to record — and two terminals
                // on one device made the exit-time save actively wrong: the
                // passive one, showing a tree from before the other's edits,
                // wrote its stale copy back on the way out and undid them.
                // The focus is the only thing that goes unsaved by this, and
                // a focus is not authored.
                exit_code = 0;
                exit_msg = "mux: detached (session still running; run mux to reattach)";
                break :keys;
            },
            .wall => {
                // Nothing left to fold in: every tile the wall will ever
                // have is already here or on its way from a host's list.
                // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom.
                wall_layout.relayout(w, shared.sel);
            },
            .new_session, .split_right, .split_below => {
                // Ask the focused tile's daemon for a new session. The
                // answer arrives on the pump's transport and is posted
                // back to the keyboard (`postAnswer`), which moves the
                // focus or grows the wall.
                tiles[z].pending_place = switch (cmd.action) {
                    .split_right => .right_of,
                    .split_below => .below,
                    else => .beside_focus,
                };
                tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release);
                if (ringLive(&tiles[z])) {
                    wall_host.pokeHost(w.hosts, &tiles[z]);
                } else {
                    setNotice(&shared, no_live_here);
                    showRefusal(tiles[0..live], &shared, z);
                }
            },
            .next_session, .prev_session => {
                // Client-local, and no frame at all: the wall already knows
                // every session every host has, so asking one daemon for a
                // ring would walk its slice and skip the rest of the wall.
                if (walkTiles(present[0..live], z, cmd.action == .next_session)) |to| {
                    focusAnswer(w, false, to);
                }
            },
            .focus_dir => |d| {
                const flat = shared.base_flat orelse shared.last_flat;
                if (flat) |f| {
                    if (layout.neighbor(f, @intCast(z), wall_layout.dirOf(d))) |nb| {
                        if (nb < w.live.* and present[nb] and nb != z) {
                            if (shared.fullscreen)
                                focusAnswer(w, false, nb)
                            else
                                setFocus(tiles[0..live], &shared, nb);
                        }
                    }
                }
            },
            .fullscreen => {
                shared.fullscreen = !shared.fullscreen;
                wall_layout.relayout(w, shared.sel);
            },
            .resize => |d| {
                // Only a resize that MOVED a boundary: a key the tree
                // refused left the same tree behind, and rewriting it
                // would cost a file write per held-down arrow.
                if (wall_layout.doResize(w, z, d)) wall_layout.persist(w);
            },
            .focus => |idx| {
                if (idx > 0 and idx <= w.live.* and present[idx - 1] and idx - 1 != z) {
                    if (shared.fullscreen)
                        focusAnswer(w, false, idx - 1)
                    else
                        setFocus(tiles[0..live], &shared, idx - 1);
                }
            },
            .end_focused => if (z < w.live.* and present[z]) {
                // Asked through THIS tile's own pump, not a side connection:
                // the pump is already attached to the session, so the daemon
                // excludes it from the "others hold it" count and the answer
                // comes back on the link the tile is already reading. The
                // picker dials its own connection because the row it stands
                // on may have no pane here; a focused tile always does.
                //
                // Nothing is vanished on the keypress. The pane leaves the
                // way an ended session's pane always leaves — the shell's
                // `exit_status`, then the poll that no longer names it — so
                // a refused end costs the wall nothing.
                tiles[z].ask.store(
                    @intFromEnum(intentForEnd(&tiles[z], std.time.milliTimestamp())),
                    .release,
                );
                if (ringLive(&tiles[z])) {
                    wall_host.pokeHost(w.hosts, &tiles[z]);
                } else {
                    // No pump, so no link to ask on. A `gone` pane's session
                    // is already off its daemon and an unreachable one's
                    // cannot be reached to be ended.
                    setNotice(&shared, no_live_here);
                    showRefusal(tiles[0..live], &shared, z);
                }
            },
            .remove_pane => if (z < w.live.* and present[z]) {
                removePane(w, z);
                // A piped wall of one has nowhere to leave the tile off:
                // scripts read the code, and the bare-chord abort says the
                // same words for the same act.
                if (!shared.is_tty and presentCount(present[0..live]) == 0) {
                    exit_code = 0;
                    exit_msg = "mux: aborted before attaching";
                    break :keys;
                }
            },
            // The spelling editor is the picker's, so its Enter is
            // answered up there with the rest of the popup's keys.
            .add_tile => {},
        }
    }

    shared.running.store(false, .release);
    // The socket leaves the filesystem here rather than in the defer above,
    // which this function's `exit` never reaches. `retire` and not `stop`:
    // a detached pump may be inside `declined` on it, and a free in the
    // window before the exit is a use-after-free.
    if (shared.prompts) |l| l.retire();
    // Taken and never released: no tile paints across the restore. The
    // pump threads are detached and die with the process; joining them
    // could wait on a blocked readFrame forever.
    shared.paint_mu.lock();
    if (orig) |o| {
        // Every mode this terminal can be carrying, off — the wall's own
        // and any focused tile's session set through it. A wall left
        // through a detach chord or a session that ended must not leave a
        // terminal reporting clicks into the user's shell.
        proto.writeAllFd(stdout_fd, interact.wall_teardown) catch {};
        std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {};
    }
    if (vanish_msg) |m| std.debug.print("{s}\n", .{m});
    // The prediction stats, on the normal screen and before the exit
    // sentence — exactly where and when `Core.deinit` used to put them for
    // a plain client. Only for an ATTACH: a wall the user opened on no
    // session never printed a stats line and gains no reason to start.
    if (entry.focus0) interact.dumpPredictStats(shared.stats);
    if (exit_msg) |m| std.debug.print("{s}\n", .{m});
    // exit(), not return: returning would run the caller's frees and leak
    // checks while detached pump threads still hold pointers into `tiles`
    // and their own live transports — the window between here and process
    // death must not contain a free.
    std.posix.exit(exit_code);
}

// Reaching a file is what registers its tests; build.zig gates
// the list.
test {
    _ = @import("interact.zig");
    _ = @import("paint.zig");
    _ = @import("predict.zig");
    _ = @import("wall_test_harness.zig");
    _ = @import("wall_test_host.zig");
    _ = @import("wall_test_layout.zig");
    _ = @import("wall_test_picker.zig");
    _ = @import("wall_test_pump.zig");
    _ = @import("wall_test_wall.zig");
}