52e192d6
refactor: wallview's comments name the failure, not the history
a73x 2026-08-30 19:16
Commit message
docscheck.blocks
| Old | New | ||
|---|---|---|---|
| @@ -54,7 +54,7 @@ wall_test_layout.zig 2 | |||
| 54 | wall_test_picker.zig 4 | 54 | wall_test_picker.zig 4 |
| 55 | wall_test_pump.zig 9 | 55 | wall_test_pump.zig 9 |
| 56 | wall_test_wall.zig 7 | 56 | wall_test_wall.zig 7 |
| 57 | wallview.zig 63 | 57 | wallview.zig 0 |
| 58 | wasm_core.zig 5 | 58 | wasm_core.zig 5 |
| 59 | webhub_main.zig 4 | 59 | webhub_main.zig 4 |
| 60 | webhub.zig 29 | 60 | webhub.zig 29 |
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,22 +1,7 @@ | |||
| 1 | //! The CLI wall: N sessions in one terminal, the multiattach the browser hub | 1 | //! The CLI wall: N sessions in one terminal. It lists daemons (hosts.zig) |
| 2 | //! gives without a browser. The wall lists daemons (hosts.zig); a host's | 2 | //! and a host's live sessions are its tiles, polled. Every tile claims its |
| 3 | //! live sessions are its tiles, polled. | 3 | //! rectangle and focus is client-local. One thread per tile owns that tile's |
| 4 | //! | 4 | //! transport both ways, so the keyboard hands it bytes over a mailbox. |
| 5 | //! Every tile claims its rectangle: attach sends the rect, relayout resends | ||
| 6 | //! it, and focus is client-local and sends nothing on the wire. | ||
| 7 | //! | ||
| 8 | //! Tiles are never compacted: pump threads hold pointers into the tile array, | ||
| 9 | //! so a forgotten tile is a hole focus steps over (`stepPresent`). A tile | ||
| 10 | //! whose pump has ended keeps its rect and narrates on its label bar. | ||
| 11 | //! | ||
| 12 | //! One thread per tile, owning its transport BOTH directions. A `Transport` | ||
| 13 | //! is single-threaded — `service()` and `writeFrame` share QUIC state, and a | ||
| 14 | //! reconnect swaps the struct out from under the pump — so the keyboard | ||
| 15 | //! hands typed bytes over a mailbox and doorbell pipe. Threads, not one fair | ||
| 16 | //! loop: readFrame blocks and userspace buffering is invisible to poll. | ||
| 17 | //! | ||
| 18 | //! A beside cut needs VT-safe truncation of a grid row, because the | ||
| 19 | //! terminal only clips at ITS edge: `engine.dumpVtRowClipped` is it. | ||
| 20 | const std = @import("std"); | 5 | const std = @import("std"); |
| 21 | const proto = @import("term").protocol; | 6 | const proto = @import("term").protocol; |
| 22 | const client = @import("client"); | 7 | const client = @import("client"); |
| @@ -50,11 +35,8 @@ pub const resolveHost = client.resolveHost; | |||
| 50 | 35 | ||
| 51 | /// Whether this process has a screen to cut stripes on. | 36 | /// Whether this process has a screen to cut stripes on. |
| 52 | fn headless(out_fd: std.posix.fd_t) bool { | 37 | fn headless(out_fd: std.posix.fd_t) bool { |
| 53 | // Everything a wall does beyond its entry tile — other hosts, their | 38 | // A `mux` with no measurable terminal is a wall of ONE, writing exactly |
| 54 | // pollers, a saved layout — is stripes, so a `mux` with no measurable | 39 | // what the plain client wrote: everything else a wall does is stripes. |
| 55 | // terminal is a wall of ONE and writes what the plain client wrote. | ||
| 56 | // `run` asks this of the same fd as `measured == null`; `runAttach` | ||
| 57 | // asks it before there is a `run` to ask. | ||
| 58 | return interact.ttySize(out_fd) == null; | 40 | return interact.ttySize(out_fd) == null; |
| 59 | } | 41 | } |
| 60 | 42 | ||
| @@ -94,16 +76,8 @@ pub const State = enum { | |||
| 94 | /// between two turns of a pump's poll loop. | 76 | /// between two turns of a pump's poll loop. |
| 95 | pub const mailbox_max = 4096; | 77 | pub const mailbox_max = 4096; |
| 96 | 78 | ||
| 97 | /// The most tiles one wall can hold. | 79 | /// The most tiles one wall can hold. A hard bound, not a growable array: |
| 98 | /// | 80 | /// pump threads hold `*Tile` pointers, so growing would move it under them. |
| 99 | /// A hard bound rather than a growable array, because the pump threads hold | ||
| 100 | /// `*Tile` pointers for the wall's whole life: growing the array would move | ||
| 101 | /// it under them. The number is generous beside what a terminal can show | ||
| 102 | /// (`wallFloors` refuses a rect too thin for the daemon's row floor | ||
| 103 | /// under a label bar, so an 80x24 terminal caps at eight tiles) and beside | ||
| 104 | /// `max_clients = 8` per daemon. What it really | ||
| 105 | /// bounds is how much a wall of hosts is allowed to cost when the daemons | ||
| 106 | /// on it have grown many sessions. | ||
| 107 | pub const max_tiles: usize = 32; | 81 | pub const max_tiles: usize = 32; |
| 108 | 82 | ||
| 109 | /// Why a tile's pump stopped, for the keyboard that has to decide what that | 83 | /// Why a tile's pump stopped, for the keyboard that has to decide what that |
| @@ -120,47 +94,20 @@ pub const Shared = struct { | |||
| 120 | /// screen being cleared and the terminal changing hands. | 94 | /// screen being cleared and the terminal changing hands. |
| 121 | paint_mu: std.Thread.Mutex = .{}, | 95 | paint_mu: std.Thread.Mutex = .{}, |
| 122 | out_fd: std.posix.fd_t, | 96 | out_fd: std.posix.fd_t, |
| 123 | /// Whether there is a terminal here at all. | 97 | /// Whether there is a terminal here at all. Every WALL-level write is |
| 124 | /// | 98 | /// gated on it: on a pipe mux writes the grid and no terminal state. |
| 125 | /// `mux TARGET` is a wall of one tile now, and a scripted `mux` | ||
| 126 | /// with a pipe on both ends is the shape half the suite runs. On a pipe | ||
| 127 | /// it must write exactly what the plain client wrote — the grid, and no | ||
| 128 | /// terminal state whatever: no alternate screen, no raw mode, no | ||
| 129 | /// clears, nothing to undo. So every WALL-level write is gated here. | ||
| 130 | /// A wall of many is unaffected: it still refuses to run without a | ||
| 131 | /// terminal to cut stripes from, and says so. | ||
| 132 | is_tty: bool, | 99 | is_tty: bool, |
| 133 | /// The pumps' doorbell to the KEYBOARD — the mirror of `Tile.wake_w`. | 100 | /// The pumps' doorbell to the KEYBOARD — the mirror of `Tile.wake_w`. |
| 134 | /// | 101 | /// Without it a shell that exits goes unnoticed until the next keystroke. |
| 135 | /// A pump rings it when something only the keyboard can act on has | ||
| 136 | /// happened: a tile went quiet, a `sessions_reply` came back with a | ||
| 137 | /// name to focus, a detach was acknowledged. Without it the keyboard | ||
| 138 | /// sits in read(2) on a stdin that may never say anything again, and a | ||
| 139 | /// shell that exits under the focus would go unnoticed until the next | ||
| 140 | /// keystroke. | ||
| 141 | kb_r: std.posix.fd_t = -1, | 102 | kb_r: std.posix.fd_t = -1, |
| 142 | kb_w: std.posix.fd_t = -1, | 103 | kb_w: std.posix.fd_t = -1, |
| 143 | /// The focused tile's prediction counters, republished on every pass a | 104 | /// The focused tile's prediction counters. Published OUT to the driver |
| 144 | /// tile paints while it holds the terminal. | 105 | /// that owns the exit, because a pump is a detached thread that never |
| 145 | /// | 106 | /// reaches a `deinit` to print `MUX_PREDICT_STATS` itself. |
| 146 | /// `MUX_PREDICT_STATS` used to be printed by `Core.deinit`, and a wall | ||
| 147 | /// tile's Core never reaches one: its pump is a detached thread the | ||
| 148 | /// process exit kills where it stands. So the counters come OUT to the | ||
| 149 | /// driver that does own the exit, which prints them once, after the | ||
| 150 | /// terminal is back — the same line, on the same screen, in the same | ||
| 151 | /// order the plain client put it there. A struct copy under a lock the | ||
| 152 | /// focused pump is already taking on every paint. | ||
| 153 | stats: interact.PredictCounters = .{}, | 107 | stats: interact.PredictCounters = .{}, |
| 154 | /// One sentence for the next focused tile to put in its corner. | 108 | /// One sentence for the next focused tile to put in its corner. The |
| 155 | /// | 109 | /// keyboard has no painter for a LIVE tile — `Core.banner` belongs to a |
| 156 | /// Written by the keyboard under `paint_mu`, taken by the pump that | 110 | /// Core — so it leaves the message where that pump will find it. |
| 157 | /// owns the terminal. The keyboard has no painter of its own for a | ||
| 158 | /// LIVE tile — `Core.banner` belongs to a Core, and the Core that | ||
| 159 | /// matters is the one being focused ON — so the message is left where | ||
| 160 | /// that pump will find it when it claims. | ||
| 161 | /// Sized for a corner banner, not for arbitrary text: `setNotice` | ||
| 162 | /// truncates rather than allocate for a message nobody can read at | ||
| 163 | /// that width anyway. | ||
| 164 | notice: [96]u8 = undefined, | 111 | notice: [96]u8 = undefined, |
| 165 | notice_len: usize = 0, | 112 | notice_len: usize = 0, |
| 166 | /// The terminal's shape: measured at startup, and re-measured by the | 113 | /// The terminal's shape: measured at startup, and re-measured by the |
| @@ -168,17 +115,11 @@ pub const Shared = struct { | |||
| 168 | /// the thread that lays the wall out may act on it). Stripes are cut | 115 | /// the thread that lays the wall out may act on it). Stripes are cut |
| 169 | /// from it and a one-tile wall claims exactly it. | 116 | /// from it and a one-tile wall claims exactly it. |
| 170 | size: proto.Size, | 117 | size: proto.Size, |
| 171 | /// The selected tile's index — the FOCUSED tile, the one input goes to | 118 | /// The FOCUSED tile: where input goes, and whose pump holds the terminal. |
| 172 | /// and the one whose pump has claimed the terminal. Under `paint_mu` | 119 | /// Under `paint_mu` so a repaint draws the focus the keyboard has now. |
| 173 | /// rather than atomic because it is read while a label bar is being | ||
| 174 | /// drawn: a pump repainting on a frame must draw the focus the keyboard | ||
| 175 | /// has, not the one it had when the frame arrived. | ||
| 176 | sel: usize = 0, | 120 | sel: usize = 0, |
| 177 | /// Where the focused tile's last paint left the cursor on the terminal. | 121 | /// Where the focused tile's last paint left the cursor. The cursor |
| 178 | /// The cursor belongs to the focus, not to whichever pump painted last: | 122 | /// belongs to the focus: an unfocused paint's last act puts it back here. |
| 179 | /// an unfocused paint's last act is to put the cursor back here. Under | ||
| 180 | /// `paint_mu` because the painters that read and write it already stand | ||
| 181 | /// there. Home until the focused tile has painted once. | ||
| 182 | cursor: Engine.CursorPos = .{ .x = 0, .y = 0 }, | 123 | cursor: Engine.CursorPos = .{ .x = 0, .y = 0 }, |
| 183 | /// One label bar per tile when the wall holds more than one session; a | 124 | /// One label bar per tile when the wall holds more than one session; a |
| 184 | /// one-tile wall owns every row and draws no bar. Set by `relayout`, so | 125 | /// one-tile wall owns every row and draws no bar. Set by `relayout`, so |
| @@ -201,11 +142,8 @@ pub const Shared = struct { | |||
| 201 | /// `f` toggles it; relayout passes `sel` to flatten when set, so the | 142 | /// `f` toggles it; relayout passes `sel` to flatten when set, so the |
| 202 | /// focused tile takes the whole terminal and every other gets 0×0. | 143 | /// focused tile takes the whole terminal and every other gets 0×0. |
| 203 | fullscreen: bool = false, | 144 | fullscreen: bool = false, |
| 204 | /// Bumped when the terminal's contents are no longer anybody's paint — | 145 | /// Bumped when the terminal's contents are no longer anybody's paint. |
| 205 | /// a relayout re-cut the stripes, or the focus moved and the newly | 146 | /// The only thing that repaints a tile whose session has gone quiet. |
| 206 | /// focused tile owes a full repaint. Pumps compare it against what | ||
| 207 | /// they last painted at, which is the only thing that repaints a tile | ||
| 208 | /// whose session sent no new frame meanwhile. | ||
| 209 | repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), | 147 | repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), |
| 210 | /// True while the host picker's popup is on the terminal. Every paint | 148 | /// True while the host picker's popup is on the terminal. Every paint |
| 211 | /// asks: a tile redrawing its rect under the box would erase the rows | 149 | /// asks: a tile redrawing its rect under the box would erase the rows |
| @@ -221,13 +159,8 @@ pub const Shared = struct { | |||
| 221 | /// arrive with the picker already up. | 159 | /// arrive with the picker already up. |
| 222 | ask_open: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), | 160 | ask_open: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), |
| 223 | /// Where this wall answers ssh's prompts, and the binary ssh execs to | 161 | /// Where this wall answers ssh's prompts, and the binary ssh execs to |
| 224 | /// ask. Null is "ssh keeps its own prompts": a piped `mux` has no | 162 | /// ask. Null is "ssh keeps its own prompts": without `$XDG_RUNTIME_DIR` |
| 225 | /// popup to answer with, and a box with no `$XDG_RUNTIME_DIR` has | 163 | /// the socket would land on a shared /tmp, answerable by any local user. |
| 226 | /// nowhere private to put the socket — a prompt on a shared /tmp is | ||
| 227 | /// one any local user could answer. | ||
| 228 | /// | ||
| 229 | /// Named for what arrives on it, not `ask`: `Tile.ask` is the end-key's | ||
| 230 | /// own arm, and two fields called `ask` in one file are a reader's trap. | ||
| 231 | prompts: ?*askpass.Listener = null, | 164 | prompts: ?*askpass.Listener = null, |
| 232 | prompt_exe: []const u8 = "", | 165 | prompt_exe: []const u8 = "", |
| 233 | }; | 166 | }; |
| @@ -250,15 +183,9 @@ pub fn closeAsk( | |||
| 250 | prefix: *interact.PrefixFilter, | 183 | prefix: *interact.PrefixFilter, |
| 251 | picker_sel: usize, | 184 | picker_sel: usize, |
| 252 | ) void { | 185 | ) void { |
| 253 | // ONE function because the two ends had drifted: `relayout` CLEARS the | 186 | // ONE function: `relayout` CLEARS the screen, so a picker still open |
| 254 | // screen, so a picker still open under the box owes a repaint, and the | 187 | // under the box owes a repaint. The answer buffer is zeroed here and |
| 255 | // hangup path did not know it. A prompt can arrive over an open picker | 188 | // nowhere else — `feed` cannot, since the action it returned borrows it. |
| 256 | // — that is why the answer has a buffer of its own — so a notice whose | ||
| 257 | // helper hung up left `picking` true on a blank screen until the next | ||
| 258 | // key. | ||
| 259 | // | ||
| 260 | // The answer buffer goes here and nowhere else: `feed` cannot zero it, | ||
| 261 | // because the action it returned borrows it. | ||
| 262 | prefix.askClose(); | 189 | prefix.askClose(); |
| 263 | shared.ask_open.store(false, .release); | 190 | shared.ask_open.store(false, .release); |
| 264 | // `relayout` bumps `repaint_gen`, which is the only thing that redraws | 191 | // `relayout` bumps `repaint_gen`, which is the only thing that redraws |
| @@ -279,10 +206,8 @@ pub fn popupOpen(shared: *const Shared) bool { | |||
| 279 | } | 206 | } |
| 280 | 207 | ||
| 281 | /// The rect a paint used, in the only terms a click can be resolved in. | 208 | /// The rect a paint used, in the only terms a click can be resolved in. |
| 282 | /// | 209 | /// Absolute rows count from the oldest row the DAEMON retains, so a grid row |
| 283 | /// Absolute rows are counted from the oldest row the DAEMON still retains | 210 | /// alone names nothing without the history count its frame carried. |
| 284 | /// (`protocol.SelectionReply`), so a grid row alone names nothing: it has | ||
| 285 | /// to be read against the history count the same frame carried. | ||
| 286 | pub const Tile = struct { | 211 | pub const Tile = struct { |
| 287 | r: Resolved, | 212 | r: Resolved, |
| 288 | rect: layout.Rect, | 213 | rect: layout.Rect, |
| @@ -314,34 +239,17 @@ pub const Tile = struct { | |||
| 314 | /// here (under `paint_mu`) because the KEYBOARD repaints this bar too, | 239 | /// here (under `paint_mu`) because the KEYBOARD repaints this bar too, |
| 315 | /// when the focus moves, and it has no other way to know it. | 240 | /// when the focus moves, and it has no other way to know it. |
| 316 | state: State = .connecting, | 241 | state: State = .connecting, |
| 317 | /// Whether this tile's pump thread is still running. | 242 | /// Whether this tile's pump thread is still running. Only pumps answer |
| 318 | /// | 243 | /// `repaint_gen`, so a DEAD pump's bar is the one paint the keyboard |
| 319 | /// Only pumps answer `repaint_gen`, so a tile whose pump has ENDED — a | 244 | /// makes for a tile — drawing over a live one would stale it. |
| 320 | /// refused attach, a session that exited — has nobody to redraw its | ||
| 321 | /// rect after a relayout clear. The keyboard reads this and paints the | ||
| 322 | /// dead tile's label bar, which is the only paint it ever does on a | ||
| 323 | /// tile's behalf. | ||
| 324 | /// | ||
| 325 | /// Deliberately ONLY for dead pumps. A tile that is merely | ||
| 326 | /// `[reconnecting]` still has a thread that will repaint its hot | ||
| 327 | /// replica within a poll timeout, and letting the keyboard draw over | ||
| 328 | /// that would replace something true with something stale. | ||
| 329 | alive: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), | 245 | alive: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), |
| 330 | /// Whether this slot's pump thread has RETURNED. Stored as the pump's | 246 | /// Whether this slot's pump thread has RETURNED — the half of "free" |
| 331 | /// last act — after `alive`, after the bell that reads `t.shared` — and | 247 | /// that `present` cannot say. Reusing the slot without it overwrites a |
| 332 | /// it is the half of "free" that `present` cannot say: a birth that | 248 | /// `*Tile` the returning thread is still reading. |
| 333 | /// reused a vanished slot on `!present` alone would overwrite a `*Tile` | ||
| 334 | /// the returning thread is still reading. | ||
| 335 | pump_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), | 249 | pump_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), |
| 336 | /// Forgotten by `Ctrl-\ x`: off the wall, and off the wire as soon as | 250 | /// Forgotten by `Ctrl-\ x`: off the wall, and off the wire as soon as the |
| 337 | /// the pump notices. The pump's answer is to RETURN — which closes its | 251 | /// pump notices. "Remove is detach" — the session is untouched. Distinct |
| 338 | /// transport and frees the daemon slot — and nothing more: "remove is | 252 | /// from `alive`: a gone tile the user removed, a dead one still narrates. |
| 339 | /// detach", so the session itself is untouched and still there for the | ||
| 340 | /// next `mux` that asks for it. | ||
| 341 | /// | ||
| 342 | /// Distinct from `alive`, which says the pump has ENDED (refused, | ||
| 343 | /// exited, never started). A gone tile is one the user removed; a dead | ||
| 344 | /// one is still on the wall, narrating what became of it. | ||
| 345 | gone: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), | 253 | gone: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), |
| 346 | /// The doorbell. The keyboard writes one byte here to wake this tile's | 254 | /// The doorbell. The keyboard writes one byte here to wake this tile's |
| 347 | /// pump; the bytes themselves carry nothing, the mailbox does. Both | 255 | /// pump; the bytes themselves carry nothing, the mailbox does. Both |
| @@ -349,21 +257,14 @@ pub const Tile = struct { | |||
| 349 | /// ringing free to ring again. | 257 | /// ringing free to ring again. |
| 350 | wake_r: std.posix.fd_t = -1, | 258 | wake_r: std.posix.fd_t = -1, |
| 351 | wake_w: std.posix.fd_t = -1, | 259 | wake_w: std.posix.fd_t = -1, |
| 352 | /// Bytes typed at this tile while it was focused, waiting for its pump. | 260 | /// Bytes typed at this tile while it was focused. The keyboard writing |
| 353 | /// The keyboard is the only writer and it writes ONLY to the focused | 261 | /// ONLY to the focused tile is the whole enforcement of "input goes to |
| 354 | /// tile — which is the whole enforcement of "input goes to the focus". | 262 | /// the focus", so the pump sends what it finds without re-checking. |
| 355 | /// The pump therefore sends whatever it finds here without re-checking | ||
| 356 | /// the focus: bytes typed at a session belong to that session even if | ||
| 357 | /// the focus has moved on since. | ||
| 358 | in_mu: std.Thread.Mutex = .{}, | 263 | in_mu: std.Thread.Mutex = .{}, |
| 359 | in: [mailbox_max]u8 = undefined, | 264 | in: [mailbox_max]u8 = undefined, |
| 360 | in_len: usize = 0, | 265 | in_len: usize = 0, |
| 361 | /// A transport somebody else already opened, adopted instead of | 266 | /// The ENTRY tile's link, adopted instead of dialled: `mux HOST` dials on |
| 362 | /// dialled. This is the ENTRY tile's link: `mux HOST` dials on the main | 267 | /// the main thread while it still has the tty, so ssh can prompt. |
| 363 | /// thread while it still has the tty, so ssh can ask for a hostkey or a | ||
| 364 | /// password, and a dial that never came up exits with the message and | ||
| 365 | /// the code the plain client always gave. The pump `adopt`s it, which | ||
| 366 | /// is what moves its QUIC out-queue onto this thread's allocator. | ||
| 367 | pre: ?client.Transport = null, | 268 | pre: ?client.Transport = null, |
| 368 | /// Whether this tile's attach may CREATE the session it names. The | 269 | /// Whether this tile's attach may CREATE the session it names. The |
| 369 | /// daemon reads create-vs-join off the attach's size claim, so a tile | 270 | /// daemon reads create-vs-join off the attach's size claim, so a tile |
| @@ -378,22 +279,13 @@ pub const Tile = struct { | |||
| 378 | /// was the one attach failure it did not exit on. Null for tiles that | 279 | /// was the one attach failure it did not exit on. Null for tiles that |
| 379 | /// no tile created. | 280 | /// no tile created. |
| 380 | born_from: ?usize = null, | 281 | born_from: ?usize = null, |
| 381 | /// Whether a wall outlives this tile's ending. `born_from` cannot say | 282 | /// Whether a wall outlives this tile's ending. `born_from` cannot say it: |
| 382 | /// it: a picker birth onto an EMPTY wall has no anchor to name, and so | 283 | /// a picker birth onto an EMPTY wall carries the entry tile's null, and |
| 383 | /// carries the same null the entry tile does — whose ending really is | 284 | /// the entry tile's ending really is mux's — a script reads its code. |
| 384 | /// mux's, because a script reads `mux TARGET`'s code. Set false here and | ||
| 385 | /// a refused Enter on the empty wall quits to the shell instead of | ||
| 386 | /// putting its sentence on the empty-wall line. | ||
| 387 | keeps_wall: bool = false, | 285 | keeps_wall: bool = false, |
| 388 | /// Whether a link that dies before this tile ever saw state is worth | 286 | /// Whether a link that died before this tile saw state is worth retrying. |
| 389 | /// retrying. | 287 | /// False for the ENTRY tile — a bad host or a typo'd command helps nobody |
| 390 | /// | 288 | /// to retry. True for wall tiles: a wall is a thing you leave up. |
| 391 | /// False for the ENTRY tile, which keeps the plain client's rule | ||
| 392 | /// verbatim: a transport that died before any session carried none, so | ||
| 393 | /// it is a bad host, a typo'd command or a `mux d proxy` that exited, | ||
| 394 | /// and retrying any of those helps nobody (`client.lostMsg` is the | ||
| 395 | /// sentence). True for wall tiles, which retry a dead host forever on | ||
| 396 | /// purpose — a wall is a thing you leave up. | ||
| 397 | retry_cold: bool = true, | 289 | retry_cold: bool = true, |
| 398 | /// Why this tile's pump stopped, and with what code. Written BEFORE | 290 | /// Why this tile's pump stopped, and with what code. Written BEFORE |
| 399 | /// `alive` clears, so a keyboard that sees a dead tile can always read | 291 | /// `alive` clears, so a keyboard that sees a dead tile can always read |
| @@ -404,13 +296,8 @@ pub const Tile = struct { | |||
| 404 | /// state, so no lock: nothing else reads or writes it. | 296 | /// state, so no lock: nothing else reads or writes it. |
| 405 | end_seen: bool = false, | 297 | end_seen: bool = false, |
| 406 | /// A focus chord's question, posted by the keyboard for this tile's pump | 298 | /// A focus chord's question, posted by the keyboard for this tile's pump |
| 407 | /// to put on the wire. `client.SwitchIntent` as a | 299 | /// to put on the wire — `client.SwitchIntent` as a u8, taken with a swap |
| 408 | /// u8, zero being `.none`; the pump takes it with a swap, so one | 300 | /// so one keystroke asks one question. The PUMP asks: it owns the link. |
| 409 | /// keystroke asks exactly one question. | ||
| 410 | /// | ||
| 411 | /// It is the PUMP that asks because a Transport has one owning thread, | ||
| 412 | /// and the pump that answers because the reply arrives on its link — | ||
| 413 | /// the keyboard only learns the name to focus to. | ||
| 414 | ask: std.atomic.Value(u8) = std.atomic.Value(u8).init(0), | 301 | ask: std.atomic.Value(u8) = std.atomic.Value(u8).init(0), |
| 415 | /// Until when a second `Ctrl-\ x` on this tile FORCES the end. Written | 302 | /// Until when a second `Ctrl-\ x` on this tile FORCES the end. Written |
| 416 | /// by the pump from the daemon's refusal, read by the keyboard on the | 303 | /// by the pump from the daemon's refusal, read by the keyboard on the |
| @@ -435,13 +322,9 @@ pub const Tile = struct { | |||
| 435 | /// keystrokes never vanish without the wall admitting to it. | 322 | /// keystrokes never vanish without the wall admitting to it. |
| 436 | in_dropped: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), | 323 | in_dropped: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), |
| 437 | 324 | ||
| 438 | /// The keyboard re-cut the stripes and this tile's rect changed: the | 325 | /// The keyboard re-cut the stripes: the pump owes the daemon a `.resize`. |
| 439 | /// pump owes the daemon a `.resize` at its new content size. The pump | 326 | /// A plain bool under `paint_mu`, not an atomic — an atomic could be |
| 440 | /// is the transport's only writer, so relayout doorbells here and the | 327 | /// consumed apart from the rect it describes, and that pair is the point. |
| 441 | /// pump sends — the same single-writer rule the claim used to keep. | ||
| 442 | // A plain bool under `paint_mu`, not an atomic: an atomic could be | ||
| 443 | // consumed apart from the rect it describes, and that pair is the whole | ||
| 444 | // contract. `takePass` is the only reader. | ||
| 445 | resize_pending: bool = false, | 328 | resize_pending: bool = false, |
| 446 | /// The `repaint_gen` the pump's current pass was taken under; the paint | 329 | /// The `repaint_gen` the pump's current pass was taken under; the paint |
| 447 | /// sink refuses a paint once a relayout has moved past it. Under | 330 | /// sink refuses a paint once a relayout has moved past it. Under |
| @@ -467,12 +350,9 @@ pub const Tile = struct { | |||
| 467 | } | 350 | } |
| 468 | }; | 351 | }; |
| 469 | 352 | ||
| 470 | /// The keyboard thread's whole wall, as one value. `liveTiles`/ | 353 | /// The keyboard thread's whole wall, as one value. `liveTiles`/`livePresent` |
| 471 | /// `livePresent` name the prefix walk; a walk of the whole array is a walk | 354 | /// name the prefix walk; the whole array walks into uninitialised slots. |
| 472 | /// into uninitialised slots. | 355 | /// Keyboard-thread only: a pump gets a `*Tile` and a `*Shared`, never this. |
| 473 | /// | ||
| 474 | /// Keyboard-thread only, so `paint_mu`'s ownership of the tree is | ||
| 475 | /// unchanged: a pump is handed a `*Tile` and a `*Shared` and never this. | ||
| 476 | pub const Wall = struct { | 356 | pub const Wall = struct { |
| 477 | alloc: std.mem.Allocator, | 357 | alloc: std.mem.Allocator, |
| 478 | tiles: []Tile, | 358 | tiles: []Tile, |
| @@ -493,11 +373,9 @@ pub const Wall = struct { | |||
| 493 | } | 373 | } |
| 494 | }; | 374 | }; |
| 495 | 375 | ||
| 496 | /// Which tile's rectangle a zero-based terminal (row, col) falls in, or | 376 | /// Which tile's rectangle a zero-based terminal (row, col) falls in, or null |
| 497 | /// null when none does. A beside layout gives every tile the same rows, so | 377 | /// when none does — a click on a rail hits no tile and focus stays put. |
| 498 | /// the column is what separates them; a click on a rail column hits no | 378 | /// Under `paint_mu`, which is what `Tile.rect` is written under. |
| 499 | /// tile and focus stays put. Under `paint_mu` because `Tile.rect` is | ||
| 500 | /// written under it. | ||
| 501 | pub fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16, col: u16) ?usize { | 379 | pub fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16, col: u16) ?usize { |
| 502 | shared.paint_mu.lock(); | 380 | shared.paint_mu.lock(); |
| 503 | defer shared.paint_mu.unlock(); | 381 | defer shared.paint_mu.unlock(); |
| @@ -546,21 +424,13 @@ pub const EndKey = union(enum) { | |||
| 546 | 424 | ||
| 547 | /// Whether `Ctrl-\ x` has anything to ask, and what. | 425 | /// Whether `Ctrl-\ x` has anything to ask, and what. |
| 548 | pub fn endKey(t: *Tile, now: i64) EndKey { | 426 | pub fn endKey(t: *Tile, now: i64) EndKey { |
| 549 | // A pump that has ended is the only thread that would ever swap `ask` | 427 | // An ask stored on an ended pump goes nowhere, so the TILE goes instead. |
| 550 | // out again, so an ask stored on one goes nowhere for the wall's life: | 428 | // Nothing is asked of the daemon: the session keeps running and the |
| 551 | // the TILE goes instead. Nothing is asked of the daemon — a session it | 429 | // host's next list births the tile back. A pending pane has no pump. |
| 552 | // still holds keeps running, and the host's next list births the tile | ||
| 553 | // back — but a tile whose host no poll can reach (a declined prompt, a | ||
| 554 | // box that is down) was otherwise on the wall for the wall's life with | ||
| 555 | // no key that could remove it. | ||
| 556 | // A pending pane has no pump at all, so it goes the same way — and a | ||
| 557 | // pane closed that turns out to exist births back on the next list. | ||
| 558 | if (!t.alive.load(.acquire)) return .drop; | 430 | if (!t.alive.load(.acquire)) return .drop; |
| 559 | // A pump still on its FIRST dial is alive and parked in `dial`, which | 431 | // A pump on its FIRST dial is parked in `dial`, which polls `gone` and |
| 560 | // polls `gone` and never `ask`: the key would be swallowed for as long | 432 | // never `ask`: the key would be swallowed until the box came back, then |
| 561 | // as the box is down — the picker advertises `unreachable` rows as a | 433 | // end the session. Birthing onto an `unreachable` row is a designed path. |
| 562 | // place to press Enter, so that is a designed path — and would then end | ||
| 563 | // the session the moment the dial landed. | ||
| 564 | if (!t.ever_up.load(.acquire) and awaitingSession(t) != null) return .drop; | 434 | if (!t.ever_up.load(.acquire) and awaitingSession(t) != null) return .drop; |
| 565 | return .{ .ask = intentForEnd(t, now) }; | 435 | return .{ .ask = intentForEnd(t, now) }; |
| 566 | } | 436 | } |
| @@ -636,10 +506,8 @@ pub fn labelText( | |||
| 636 | } | 506 | } |
| 637 | 507 | ||
| 638 | /// The label bar: inverse, full width, `N> LABEL [state]`, truncated at the | 508 | /// The label bar: inverse, full width, `N> LABEL [state]`, truncated at the |
| 639 | /// terminal edge. Caller holds `paint_mu` — the marker and the state are | 509 | /// terminal edge. Caller holds `paint_mu`. Only called when `label_rows` is |
| 640 | /// shared state, and this is also what writes the bytes out. Only called | 510 | /// nonzero — a one-tile wall owns every row and has no bar. |
| 641 | /// when `label_rows` is nonzero: a one-tile wall owns every row and has no | ||
| 642 | /// bar to draw. | ||
| 643 | pub fn paintLabelLocked(t: *Tile) void { | 511 | pub fn paintLabelLocked(t: *Tile) void { |
| 644 | if (popupOpen(t.shared)) return; | 512 | if (popupOpen(t.shared)) return; |
| 645 | // ASCII, not an arrow glyph: this bar is byte-truncated, greppable in | 513 | // ASCII, not an arrow glyph: this bar is byte-truncated, greppable in |
| @@ -677,15 +545,9 @@ pub fn paintLabelLocked(t: *Tile) void { | |||
| 677 | } | 545 | } |
| 678 | 546 | ||
| 679 | // The bars of the tiles whose pump has ended. Caller holds `paint_mu`. | 547 | // The bars of the tiles whose pump has ended. Caller holds `paint_mu`. |
| 680 | // A dead pump answers no doorbell and no `repaint_gen`, so the keyboard is | 548 | // A dead pump answers no doorbell, so the keyboard is the only thread that |
| 681 | // the only thread left that can move such a tile's focus marker: without | 549 | // can move its focus marker. `gone` rather than `present`, because a focus |
| 682 | // this, two refused tiles make `Ctrl-\ 1` and `Ctrl-\ 2` change nothing on | 550 | // move has no `present` slice and a vanished tile's rect is a neighbour's. |
| 683 | // screen, and a live tile that gained the focus shows a second `>` beside | ||
| 684 | // a stale one. | ||
| 685 | // | ||
| 686 | // `gone` rather than `present` because a focus move has no `present` slice | ||
| 687 | // to consult; `vanishTile` stores the two together, and a vanished tile | ||
| 688 | // must not paint — its rect belongs to a neighbour now. | ||
| 689 | pub fn paintDeadBarsLocked(tiles: []Tile) void { | 551 | pub fn paintDeadBarsLocked(tiles: []Tile) void { |
| 690 | for (tiles) |*t| { | 552 | for (tiles) |*t| { |
| 691 | if (!t.alive.load(.acquire) and !t.gone.load(.acquire)) paintLabelLocked(t); | 553 | if (!t.alive.load(.acquire) and !t.gone.load(.acquire)) paintLabelLocked(t); |
| @@ -728,12 +590,9 @@ fn ringLive(t: *const Tile) bool { | |||
| 728 | return true; | 590 | return true; |
| 729 | } | 591 | } |
| 730 | 592 | ||
| 731 | // A tile's wake pipe is never closed, not even when its pump ends: the fd | 593 | // A tile's wake pipe is never closed: the fd NUMBER would be recycled, and a |
| 732 | // NUMBER would be recycled, and the pollers open a socket a second, so a | 594 | // late bell would put a stray byte into somebody's live connection instead of |
| 733 | // late bell — `setFocus` rings every tile, present or not — would put a | 595 | // getting EBADF. Two fds per tile — the process exit is the cheaper owner. |
| 734 | // stray byte into somebody's live connection instead of getting EBADF. | ||
| 735 | // Two fds per tile, CLOEXEC, at most `max_tiles` tiles: the process exit | ||
| 736 | // is a cheaper owner than the alternative is to get right. | ||
| 737 | 596 | ||
| 738 | /// `ring` in the other direction: never blocks, never reports. | 597 | /// `ring` in the other direction: never blocks, never reports. |
| 739 | pub fn ringKeyboard(shared: *const Shared) void { | 598 | pub fn ringKeyboard(shared: *const Shared) void { |
| @@ -786,14 +645,9 @@ pub fn takeNotice(shared: *Shared, out: []u8) []const u8 { | |||
| 786 | return takeNoticeLocked(shared, out); | 645 | return takeNoticeLocked(shared, out); |
| 787 | } | 646 | } |
| 788 | 647 | ||
| 789 | /// Called ONLY from the keyboard loop and ONLY while `t` is the focused tile: | 648 | /// Called ONLY from the keyboard loop and ONLY while `t` is focused: that |
| 790 | /// that restriction IS the enforcement of "an unfocused tile claims no terminal modes". | 649 | /// restriction IS "an unfocused tile claims no terminal modes". A chunk that |
| 791 | /// | 650 | /// does not fit is dropped WHOLE — a partial copy is a command nobody typed. |
| 792 | /// Mouse reports travel it unsplit — the split needs the pump's state — and | ||
| 793 | /// an unfocused tile's never arrive, since this is the focused tile only. | ||
| 794 | /// | ||
| 795 | /// A chunk that does not fit is dropped WHOLE: copying what fits splices two | ||
| 796 | /// reads into a command nobody typed; blocking wedges the wall. | ||
| 797 | pub fn sendKeys(t: *Tile, keys: []const u8) void { | 651 | pub fn sendKeys(t: *Tile, keys: []const u8) void { |
| 798 | { | 652 | { |
| 799 | t.in_mu.lock(); | 653 | t.in_mu.lock(); |
| @@ -812,28 +666,16 @@ pub fn sendKeys(t: *Tile, keys: []const u8) void { | |||
| 812 | ring(t); | 666 | ring(t); |
| 813 | } | 667 | } |
| 814 | 668 | ||
| 815 | /// Move the focus to tile `next`. Client-local: decides which pump owns the | 669 | /// Move the focus to tile `next`. Client-local: it decides which pump owns |
| 816 | /// terminal's modes, not which session the daemon hears. The outgoing tile's | 670 | /// the terminal's modes, not which session the daemon hears. The outgoing |
| 817 | /// `session_release` is written HERE, under `paint_mu`, so the handover is | 671 | /// tile's `session_release` is written HERE under `paint_mu`, because two |
| 818 | /// ordered — two pumps are two threads, and A's release and B's claim race | 672 | /// pumps are two threads and A's release races B's claim. |
| 819 | /// (measured on a real pty: A's release landed after B's claim in five of | ||
| 820 | /// six). Written before the doorbell; the released pump writes nothing | ||
| 821 | /// (`.already_written`). | ||
| 822 | /// | ||
| 823 | /// Legacy mouse reporting (1000 without 1006) spells a report `ESC [ M` | ||
| 824 | /// plus ordinary characters — column 81 is `q` (quits), column 88 is `x` | ||
| 825 | /// (forgets the focused tile). So the outgoing session's modes come off | ||
| 826 | /// here, ahead of the focus moving. The claim and release are doorbells; | ||
| 827 | /// no screen clear — every tile paints its own rect. | ||
| 828 | pub fn setFocus(tiles: []Tile, shared: *Shared, next: usize) void { | 673 | pub fn setFocus(tiles: []Tile, shared: *Shared, next: usize) void { |
| 829 | const prev = shared.sel; | 674 | const prev = shared.sel; |
| 830 | shared.paint_mu.lock(); | 675 | shared.paint_mu.lock(); |
| 831 | defer shared.paint_mu.unlock(); | 676 | defer shared.paint_mu.unlock(); |
| 832 | // The outgoing session's modes come off HERE, on the thread moving the | 677 | // The outgoing session's modes come off HERE, ahead of the doorbell that |
| 833 | // focus, ahead of the doorbell that lets the next pump claim. | 678 | // lets the next pump claim — and only when there IS a terminal to change. |
| 834 | // ...and only when there IS a terminal. A scripted `mux` on a pipe is a | ||
| 835 | // wall of one tile whose chords still work; what it must not do is | ||
| 836 | // write a mode change into a capture the plain client left clean. | ||
| 837 | if (shared.is_tty and prev != next and prev < tiles.len and tiles[prev].alive.load(.acquire)) { | 679 | if (shared.is_tty and prev != next and prev < tiles.len and tiles[prev].alive.load(.acquire)) { |
| 838 | proto.writeAllFd(shared.out_fd, interact.session_release) catch {}; | 680 | proto.writeAllFd(shared.out_fd, interact.session_release) catch {}; |
| 839 | tiles[prev].release_pending.store(true, .release); | 681 | tiles[prev].release_pending.store(true, .release); |
| @@ -895,11 +737,9 @@ pub fn paintEmptyWallLocked(shared: *Shared) void { | |||
| 895 | proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {}; | 737 | proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {}; |
| 896 | } | 738 | } |
| 897 | 739 | ||
| 898 | /// A tile leaves the wall: off the `present` roll, off the tree, and its | 740 | /// A tile leaves the wall: off the `present` roll, off the tree, and its pump |
| 899 | /// pump told to return. The ONE owner — the chord and the poll's diff both | 741 | /// told to return. The ONE owner — chord and poll diff both come here, so the |
| 900 | /// come here, which is why the focus hand-off can only be written once. | 742 | /// focus hand-off is written once. `to` overrides where the focus goes. |
| 901 | /// `to` overrides where a focused tile's focus goes; null takes the next | ||
| 902 | /// present tile. The caller re-cuts. | ||
| 903 | pub fn vanishTile(tiles: []Tile, present: []bool, shared: *Shared, i: usize, to: ?usize) void { | 743 | pub fn vanishTile(tiles: []Tile, present: []bool, shared: *Shared, i: usize, to: ?usize) void { |
| 904 | present[i] = false; | 744 | present[i] = false; |
| 905 | tiles[i].gone.store(true, .release); | 745 | tiles[i].gone.store(true, .release); |
| @@ -936,19 +776,14 @@ pub fn showsSelf( | |||
| 936 | // exported variable it cannot unset, and the refusal names unsetting as | 776 | // exported variable it cannot unset, and the refusal names unsetting as |
| 937 | // the way out — both spellings of that have to work. | 777 | // the way out — both spellings of that have to work. |
| 938 | if (es.len == 0 or en.len == 0) return false; | 778 | if (es.len == 0 or en.len == 0) return false; |
| 939 | // Both halves, so session 0 attaching to session 1 of the same daemon | 779 | // Both halves, so session 0 attaching to session 1 of one daemon works. |
| 940 | // keeps working. String equality on the path: a symlinked or relatively | 780 | // String equality: this guards a mistake, not a security boundary. |
| 941 | // spelled `--sock` for the same socket evades this, accepted, because | ||
| 942 | // this guards the mistake people make (typing `mux` in a mux shell) and | ||
| 943 | // is not a security boundary. | ||
| 944 | return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name)); | 781 | return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name)); |
| 945 | } | 782 | } |
| 946 | 783 | ||
| 947 | /// Two tiles on the same daemon, as SPELLED. Identity dedup would need an | 784 | /// Two tiles on the same daemon, as SPELLED. Identity dedup would need an |
| 948 | /// endpoint handshake the spec deliberately refuses, so the same session | 785 | /// endpoint handshake the spec refuses, so one session reached two ways is |
| 949 | /// reached as `HOST#S` and as `quic://…#S` is two tiles — and the ring | 786 | /// two tiles; the ring chords only ask their own daemon and never care. |
| 950 | /// chords, which only ever ask the daemon their own tile is already talking | ||
| 951 | /// to, never have to care. | ||
| 952 | fn sameTarget(a: client.Target, b: client.Target) bool { | 787 | fn sameTarget(a: client.Target, b: client.Target) bool { |
| 953 | if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false; | 788 | if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false; |
| 954 | return switch (a) { | 789 | return switch (a) { |
| @@ -986,13 +821,9 @@ const Doorbell = enum { fresh, kept }; | |||
| 986 | /// tiles are born in two places now — at startup, and whenever a focus chord | 821 | /// tiles are born in two places now — at startup, and whenever a focus chord |
| 987 | /// or the saved wall adds one. | 822 | /// or the saved wall adds one. |
| 988 | fn initTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize, bell: Doorbell) !void { | 823 | fn initTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize, bell: Doorbell) !void { |
| 989 | // The doorbell, before the pump that polls it exists. Non-blocking at | 824 | // The doorbell, before the pump that polls it exists. Non-blocking both |
| 990 | // both ends: the writer must never wedge the keyboard and the reader | 825 | // ends: neither side may wedge the other. A kept pipe may still hold the |
| 991 | // must never wedge the pump. | 826 | // byte that rang the departing pump; the bytes carry nothing. |
| 992 | // | ||
| 993 | // A kept pipe may still hold the byte `vanishTile` rang the departing | ||
| 994 | // pump with; the bytes carry nothing, so the new pump's first poll | ||
| 995 | // wakes once for nobody and reads its empty mailbox. | ||
| 996 | const wake: [2]std.posix.fd_t = switch (bell) { | 827 | const wake: [2]std.posix.fd_t = switch (bell) { |
| 997 | .fresh => try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }), | 828 | .fresh => try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }), |
| 998 | .kept => .{ t.wake_r, t.wake_w }, | 829 | .kept => .{ t.wake_r, t.wake_w }, |
| @@ -1025,15 +856,9 @@ pub fn spawnPump(t: *Tile) void { | |||
| 1025 | // never dials, so the sidecar cannot re-create a session. | 856 | // never dials, so the sidecar cannot re-create a session. |
| 1026 | if (t.pending) return; | 857 | if (t.pending) return; |
| 1027 | const th = std.Thread.spawn(.{}, wall_pump.pumpTile, .{t}) catch { | 858 | const th = std.Thread.spawn(.{}, wall_pump.pumpTile, .{t}) catch { |
| 1028 | // A tile with no thread is a tile nothing will ever paint — the | 859 | // A tile with no thread is one nothing will ever paint. Marked here |
| 1029 | // same hole `pumpTile`'s exit closes, reached without the pump | 860 | // so the keyboard paints its bar; `.connecting` would be a lie. The |
| 1030 | // having run at all. Marked here so the keyboard paints its bar and | 861 | // entry tile's link is already open and nobody is left to adopt it. |
| 1031 | // narrates a focus into it. `.connecting` would be a lie: nobody is | ||
| 1032 | // going to. | ||
| 1033 | // | ||
| 1034 | // The entry tile's link is already OPEN, and nobody is left to | ||
| 1035 | // adopt it: closed here, or the fd and the daemon slot it holds | ||
| 1036 | // outlive the tile by the whole run. | ||
| 1037 | if (t.pre) |*pre| { | 862 | if (t.pre) |*pre| { |
| 1038 | pre.close(); | 863 | pre.close(); |
| 1039 | t.pre = null; | 864 | t.pre = null; |
| @@ -1087,14 +912,8 @@ const Birth = struct { | |||
| 1087 | // next poll of that host births a second tile for the same session. | 912 | // next poll of that host births a second tile for the same session. |
| 1088 | host: ?usize = null, | 913 | host: ?usize = null, |
| 1089 | // Whether `r.session` is borrowed and the tile's own copies are owed. | 914 | // Whether `r.session` is borrowed and the tile's own copies are owed. |
| 1090 | // The poll finds a name in the buffer it is about to reuse and must not | ||
| 1091 | // pay for a copy — nor leave one behind — when the wall refuses the | ||
| 1092 | // tile; `r.label` is rebuilt here rather than passed. | ||
| 1093 | // | ||
| 1094 | // FALSE is an ownership claim, not an optimisation: the tile takes | 915 | // FALSE is an ownership claim, not an optimisation: the tile takes |
| 1095 | // `label` and `session` verbatim, and whichever birth takes its digit | 916 | // `label` and `session` verbatim and a later birth frees both. |
| 1096 | // back frees both with `alloc`. Hand it a literal and the abort is that | ||
| 1097 | // later birth, arbitrarily far from the caller that made the mistake. | ||
| 1098 | borrowed: bool = false, | 917 | borrowed: bool = false, |
| 1099 | }; | 918 | }; |
| 1100 | 919 | ||
| @@ -1157,12 +976,9 @@ fn birthTileOrRefuse(w: Wall, b: Birth) !usize { | |||
| 1157 | ); | 976 | ); |
| 1158 | defer flat.deinit(w.alloc); | 977 | defer flat.deinit(w.alloc); |
| 1159 | const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect; | 978 | const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect; |
| 1160 | // The real rect, not a placeholder: a creating tile puts its rect on | 979 | // The real rect, not a placeholder: the daemon refuses creates under |
| 1161 | // the first attach frame and the daemon refuses creates under | 980 | // `min_session_rows`, and a 2-row rect on a live session is destructive |
| 1162 | // min_session_rows; a joining tile doorbells a resize on its first | 981 | // under latest-wins. `label_rows` too, so `viewRows` is right at attach. |
| 1163 | // pass, and a 2-row rect on a live session is destructive under | ||
| 1164 | // latest-wins. `label_rows` is set here so `viewRows` is right from | ||
| 1165 | // the first attach; relayout re-flattens every rect and sets it again. | ||
| 1166 | w.shared.label_rows = if (new_live > 1) 1 else 0; | 982 | w.shared.label_rows = if (new_live > 1) 1 else 0; |
| 1167 | // Past every refusal: the tile is the wall's now, so the copies a pump | 983 | // Past every refusal: the tile is the wall's now, so the copies a pump |
| 1168 | // will hold for its whole life are worth making. Made BEFORE the slot | 984 | // will hold for its whole life are worth making. Made BEFORE the slot |
| @@ -1223,13 +1039,9 @@ fn addSessionTile(w: Wall, from: usize, name: []const u8, place: Place) FocusTo | |||
| 1223 | if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i }; | 1039 | if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i }; |
| 1224 | } | 1040 | } |
| 1225 | const at = birthTile(w, .{ | 1041 | const at = birthTile(w, .{ |
| 1226 | // The offer is inherited from the tile this one grew out of. Same | 1042 | // The offer is inherited: same target, so `-A` exposes nothing new, |
| 1227 | // target, so `-A` exposes nothing the user has not already exposed | 1043 | // and a chord-made tile has no command line to spell the flag on. |
| 1228 | // to that host — and a chord-made tile has no command line to spell | 1044 | // `want` borrows the caller's name — see `Birth.borrowed`. |
| 1229 | // the flag on, so not inheriting would silently end the forwarding | ||
| 1230 | // at the first session switch. | ||
| 1231 | // `want` borrows the caller's name: the wall makes its own copies | ||
| 1232 | // only if it keeps the tile — see `Birth.borrowed`. | ||
| 1233 | .r = .{ .target = target, .label = "", .session = want, .agent = w.tiles[from].r.agent }, | 1045 | .r = .{ .target = target, .label = "", .session = want, .agent = w.tiles[from].r.agent }, |
| 1234 | .from = from, | 1046 | .from = from, |
| 1235 | .place = place, | 1047 | .place = place, |
| @@ -1271,20 +1083,12 @@ pub fn closeNotice(tiles: []Tile, present: []const bool, shared: *Shared) void { | |||
| 1271 | if (pending) showRefusal(tiles, shared, z) else _ = ringLive(&tiles[z]); | 1083 | if (pending) showRefusal(tiles, shared, z) else _ = ringLive(&tiles[z]); |
| 1272 | } | 1084 | } |
| 1273 | 1085 | ||
| 1274 | /// How a tile spells itself on the wall: the wall grammar's own form, which | 1086 | /// How a tile spells itself on the wall — the form that keys the layout |
| 1275 | /// is what keys the layout sidecar's leaves. | 1087 | /// sidecar's leaves. `--via` has no form in that grammar, so it gets a label |
| 1276 | /// | 1088 | /// that is honest on a bar and is not a spelling. |
| 1277 | /// `--via` has no form in that grammar — an arbitrary command is not an | ||
| 1278 | /// address — so it gets a label that is honest on a bar and is not a | ||
| 1279 | /// spelling: `recordHost` refuses it for the same reason. Naming it anyway | ||
| 1280 | /// is better than a blank bar over a session that is really there. | ||
| 1281 | pub fn tileLabel(alloc: std.mem.Allocator, target: client.Target, name: []const u8) ![]const u8 { | 1089 | pub fn tileLabel(alloc: std.mem.Allocator, target: client.Target, name: []const u8) ![]const u8 { |
| 1282 | // Sized for the form it will actually take. `spellingCap` bounds the | 1090 | // `spellingCap` counts a `--via` operand as ZERO — the grammar has no |
| 1283 | // wall grammar's three forms and deliberately counts a `--via` | 1091 | // place to put one. A label does, and the command has no length bound. |
| 1284 | // command's operand as ZERO, because that grammar has no place to put | ||
| 1285 | // one; a label does, and the command can be any length at all (the | ||
| 1286 | // suite's is a cache path plus a socket path, which overran a cap built | ||
| 1287 | // from the grammar's). | ||
| 1288 | const cap = switch (target) { | 1092 | const cap = switch (target) { |
| 1289 | .via => |cmd| "--via ".len + cmd.len + 1 + proto.session_name_max, | 1093 | .via => |cmd| "--via ".len + cmd.len + 1 + proto.session_name_max, |
| 1290 | else => client.spellingCap(target), | 1094 | else => client.spellingCap(target), |
| @@ -1338,14 +1142,9 @@ pub fn endAction( | |||
| 1338 | ) EndAction { | 1142 | ) EndAction { |
| 1339 | const t = &tiles[ended]; | 1143 | const t = &tiles[ended]; |
| 1340 | const reason: EndReason = @enumFromInt(t.end.load(.acquire)); | 1144 | const reason: EndReason = @enumFromInt(t.end.load(.acquire)); |
| 1341 | // A chord-born tile that never got its session leaves no trace: the tile | 1145 | // A chord-born tile that never got its session leaves no trace. The |
| 1342 | // goes back where the chord was typed, with the sentence a script reads | 1146 | // sentence names the state: a daemon that answered and said no may free a |
| 1343 | // on stderr and a survivor the screen shows. The sentence names the | 1147 | // slot, while one that never answered has to be started. |
| 1344 | // state, because the two states send the user to different places: a | ||
| 1345 | // daemon that answered and said no may free a slot, while one that | ||
| 1346 | // never answered has to be started. `x` on the last local session ends | ||
| 1347 | // the local daemon, so a wall reaching for a new session on its own | ||
| 1348 | // machine is exactly the second case. | ||
| 1349 | const gone_msg: ?[]const u8 = switch (reason) { | 1148 | const gone_msg: ?[]const u8 = switch (reason) { |
| 1350 | // Whatever the tile had been: the user REFUSED this dial, and a | 1149 | // Whatever the tile had been: the user REFUSED this dial, and a |
| 1351 | // wall that retried anyway would ask the same question every two | 1150 | // wall that retried anyway would ask the same question every two |
| @@ -1364,16 +1163,10 @@ pub fn endAction( | |||
| 1364 | return .{ .vanish = .{ .back = back, .msg = msg } }; | 1163 | return .{ .vanish = .{ .back = back, .msg = msg } }; |
| 1365 | } | 1164 | } |
| 1366 | } | 1165 | } |
| 1367 | // A clean exit is noise once it is over: the tile leaves and the wall | 1166 | // A clean exit is noise once over: the tile leaves and the wall re-cuts, |
| 1368 | // re-cuts. Only when somebody is left to steer it — a piped client | 1167 | // but only while somebody is left to steer it. On a TERMINAL the LAST |
| 1369 | // whose stdin has gone has no keyboard, so its ending is mux's own. | 1168 | // tile goes the same way and the wall stays up empty; a piped `mux` is a |
| 1370 | // The back-target prefers a live pump: a dead tile's mailbox drains | 1169 | // wall of one and still exits with the shell's code, which scripts read. |
| 1371 | // nowhere, and a chord that needs a pump goes silent. | ||
| 1372 | // | ||
| 1373 | // On a TERMINAL the LAST tile goes the same way, and the wall stays up | ||
| 1374 | // empty with the picker over it: `x` ends a session, not the program, | ||
| 1375 | // and the machines are still there to birth on. A piped `mux` is a | ||
| 1376 | // wall of one and still exits with the shell's code — scripts read it. | ||
| 1377 | if (reason == .exited and stdin_open and (is_tty or presentCount(present[0..live]) > 1)) { | 1170 | if (reason == .exited and stdin_open and (is_tty or presentCount(present[0..live]) > 1)) { |
| 1378 | // Null rather than the tile that just went: `stepLive` falls back | 1171 | // Null rather than the tile that just went: `stepLive` falls back |
| 1379 | // to the selection itself when there is nothing else present, and | 1172 | // to the selection itself when there is nothing else present, and |
| @@ -1417,21 +1210,16 @@ pub fn endAction( | |||
| 1417 | .declined => .{ .code = 1, .msg = "mux: prompt declined" }, | 1210 | .declined => .{ .code = 1, .msg = "mux: prompt declined" }, |
| 1418 | }, | 1211 | }, |
| 1419 | }; | 1212 | }; |
| 1420 | // A session the PICKER started on a wall that is still there ends as a | 1213 | // A session the PICKER started ends as a session ends: the sentence goes |
| 1421 | // session ends: the sentence goes on the empty-wall line and the | 1214 | // on the empty-wall line. Off a terminal there is no wall to leave up. |
| 1422 | // machines are still there to birth on again. Off a terminal there is | ||
| 1423 | // no wall to leave standing and the code wins, and a closed stdin ends | ||
| 1424 | // mux whatever the tile was. | ||
| 1425 | if (t.keeps_wall and is_tty and stdin_open) | 1215 | if (t.keeps_wall and is_tty and stdin_open) |
| 1426 | return .{ .vanish = .{ .back = null, .msg = gone_msg orelse done.finish.msg } }; | 1216 | return .{ .vanish = .{ .back = null, .msg = gone_msg orelse done.finish.msg } }; |
| 1427 | return done; | 1217 | return done; |
| 1428 | } | 1218 | } |
| 1429 | 1219 | ||
| 1430 | /// Newly dead tiles, drained one per pass. Returns the first newly-dead | 1220 | /// Newly dead tiles, drained one per pass. Returns the first that is focused |
| 1431 | /// tile that is focused or whose reason is `.exited` — both need the | 1221 | /// or `.exited` — both need the keyboard to vanish them; the rest narrated on |
| 1432 | /// keyboard to act (vanish); the rest narrated on their own bars and need | 1222 | /// their own bars and are marked seen here. |
| 1433 | /// nothing, so they are marked seen here. The loop calls this every pass, | ||
| 1434 | /// so a batch of exits drains across passes. | ||
| 1435 | pub fn endedTile(tiles: []Tile, present: []const bool, shared: *Shared) ?usize { | 1223 | pub fn endedTile(tiles: []Tile, present: []const bool, shared: *Shared) ?usize { |
| 1436 | const z = shared.sel; | 1224 | const z = shared.sel; |
| 1437 | var hit: ?usize = null; | 1225 | var hit: ?usize = null; |
| @@ -1527,16 +1315,10 @@ pub fn runAttach( | |||
| 1527 | std.debug.print("{s}", .{f.msg}); | 1315 | std.debug.print("{s}", .{f.msg}); |
| 1528 | return f.exit; | 1316 | return f.exit; |
| 1529 | }; | 1317 | }; |
| 1530 | // The wall this attach lands on is the WHOLE wall: the host just | 1318 | // `mux HOST` is the whole wall zoomed on HOST, not a wall of one: the |
| 1531 | // dialled first, so it is tile 0 and the sidecar's anchor, and every | 1319 | // dialled host is tile 0 and the sidecar's anchor, every other listed |
| 1532 | // other listed host after it. `mux HOST` is the wall zoomed on HOST, | 1320 | // host after it. Unless there is no terminal — then the specs stop at the |
| 1533 | // not a wall of one. | 1321 | // entry host, so a piped `mux` writes what the plain client wrote. |
| 1534 | // | ||
| 1535 | // ...unless there is no terminal, which is what makes the promise | ||
| 1536 | // above true: `headless` is the same question `run` asks, asked here | ||
| 1537 | // because `otherHosts` runs before `run` exists. On a pipe the specs | ||
| 1538 | // stop at the entry host and `run` starts no poller for it, so this | ||
| 1539 | // process writes exactly what the plain client wrote. | ||
| 1540 | var arena_state = std.heap.ArenaAllocator.init(alloc); | 1322 | var arena_state = std.heap.ArenaAllocator.init(alloc); |
| 1541 | defer arena_state.deinit(); | 1323 | defer arena_state.deinit(); |
| 1542 | const arena = arena_state.allocator(); | 1324 | const arena = arena_state.allocator(); |
| @@ -1565,11 +1347,8 @@ pub fn runAttach( | |||
| 1565 | wall_host.otherHosts(arena, &specs, spelling, path, key, idle_ms); | 1347 | wall_host.otherHosts(arena, &specs, spelling, path, key, idle_ms); |
| 1566 | } | 1348 | } |
| 1567 | // The narration was owed to a person at a BARE prompt, and the wall is | 1349 | // The narration was owed to a person at a BARE prompt, and the wall is |
| 1568 | // about to take the screen. Past here fd 2 is the alternate screen, so | 1350 | // about to take the screen: past here a late line from ssh would land on |
| 1569 | // a late line from this ssh — `Connection to box closed by remote | 1351 | // the alternate screen. A piped `mux` goes on relaying to its script. |
| 1570 | // host.` at the link's death — would be the foreign writer this whole | ||
| 1571 | // rule removes. A piped `mux` goes on relaying: a script reads stderr | ||
| 1572 | // and has no paint to corrupt. | ||
| 1573 | if (!headless(std.posix.STDOUT_FILENO)) transport.narrate = false; | 1352 | if (!headless(std.posix.STDOUT_FILENO)) transport.narrate = false; |
| 1574 | return run(alloc, specs.items, .{ | 1353 | return run(alloc, specs.items, .{ |
| 1575 | .focus0 = true, | 1354 | .focus0 = true, |
| @@ -1618,10 +1397,8 @@ pub const Entry = struct { | |||
| 1618 | key: ?[]const u8 = null, | 1397 | key: ?[]const u8 = null, |
| 1619 | idle_ms: u32 = client.quic_idle_ms_default, | 1398 | idle_ms: u32 = client.quic_idle_ms_default, |
| 1620 | /// Whether this wall insists on a terminal. A wall of many does — there | 1399 | /// Whether this wall insists on a terminal. A wall of many does — there |
| 1621 | /// is nothing to cut stripes from without one, and it says so. A | 1400 | /// is nothing to cut stripes from without one. A scripted `mux TARGET` is |
| 1622 | /// scripted `mux TARGET` on a pipe does not: it is a wall of one tile | 1401 | /// a wall of one whose rect is the whole terminal, so it does not. |
| 1623 | /// whose rect is the whole terminal, and it writes exactly what the | ||
| 1624 | /// plain client wrote. | ||
| 1625 | needs_tty: bool = true, | 1402 | needs_tty: bool = true, |
| 1626 | }; | 1403 | }; |
| 1627 | 1404 | ||
| @@ -1638,13 +1415,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1638 | // The plain client's fallback, byte for byte: a piped `mux` painted an | 1415 | // The plain client's fallback, byte for byte: a piped `mux` painted an |
| 1639 | // 80x24 grid and this one has to paint the same one. | 1416 | // 80x24 grid and this one has to paint the same one. |
| 1640 | const size = measured orelse proto.Size{ .cols = 80, .rows = 24 }; | 1417 | const size = measured orelse proto.Size{ .cols = 80, .rows = 24 }; |
| 1641 | // Whether there is a TERMINAL is a different question from whether it | 1418 | // Whether there is a TERMINAL and whether it could be MEASURED are two |
| 1642 | // could be MEASURED, and the two are answered apart: `is_tty` off | 1419 | // questions, answered apart: a 1x1 pty is a real terminal reporting a |
| 1643 | // stdin, the size off an ioctl (`interact.ttySize`) that gives up below | 1420 | // size nothing can paint at. |
| 1644 | // two columns or rows. A 1x1 pty is a real terminal that reports a size | ||
| 1645 | // nothing can paint at, and the plain client took it over and painted | ||
| 1646 | // 80x24 into it. Conflating the two put that client on no alternate | ||
| 1647 | // screen at all. | ||
| 1648 | const is_tty = std.posix.isatty(stdin_fd); | 1421 | const is_tty = std.posix.isatty(stdin_fd); |
| 1649 | 1422 | ||
| 1650 | var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty }; | 1423 | var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty }; |
| @@ -1709,39 +1482,25 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1709 | var raw = o; | 1482 | var raw = o; |
| 1710 | raw.lflag.ICANON = false; | 1483 | raw.lflag.ICANON = false; |
| 1711 | raw.lflag.ECHO = false; | 1484 | raw.lflag.ECHO = false; |
| 1712 | // ISIG and IXON off because those keys belong to the SESSION, not | 1485 | // ISIG and IXON off because those keys belong to the SESSION: Ctrl-C |
| 1713 | // to mux: Ctrl-C has to reach the remote shell as a byte, and | 1486 | // must reach the remote shell as a byte. It is also why `Ctrl-\` is |
| 1714 | // Ctrl-S must not freeze the local terminal out from under a | 1487 | // the only way out — nothing else here raises a signal. |
| 1715 | // session that wanted it. It is also why `Ctrl-\` is the only way | ||
| 1716 | // out — nothing else here raises a signal any more. | ||
| 1717 | raw.lflag.ISIG = false; | 1488 | raw.lflag.ISIG = false; |
| 1718 | raw.iflag.IXON = false; | 1489 | raw.iflag.IXON = false; |
| 1719 | raw.iflag.ICRNL = false; | 1490 | raw.iflag.ICRNL = false; |
| 1720 | try std.posix.tcsetattr(stdin_fd, .FLUSH, raw); | 1491 | try std.posix.tcsetattr(stdin_fd, .FLUSH, raw); |
| 1721 | // Alternate screen, cursor hidden while no tile has painted, | 1492 | // Alternate screen, cursor hidden, autowrap off, title pushed — from |
| 1722 | // autowrap off (stripe clipping is the terminal's right edge), | 1493 | // interact's own constant, because the exit must undo what a focused |
| 1723 | // title pushed. Written from interact's own constant, because the | 1494 | // TILE added on top and only that module knows the whole list. |
| 1724 | // exit has to undo what a focused TILE can have added on top of it | ||
| 1725 | // and only that module knows the whole list. The wall no longer | ||
| 1726 | // claims the mouse itself — the focused tile's `claimTerminal` | ||
| 1727 | // does, so a click reaches the session that asked for it and a | ||
| 1728 | // click in another tile's rect moves the focus there. | ||
| 1729 | proto.writeAllFd(stdout_fd, interact.wall_setup) catch {}; | 1495 | proto.writeAllFd(stdout_fd, interact.wall_setup) catch {}; |
| 1730 | // Armed by the wall and not by any Core: the wall owns this | 1496 | // Armed by the wall and not by any Core: the wall owns this |
| 1731 | // terminal's raw mode. The KEYBOARD thread reads the process-wide | 1497 | // terminal's raw mode. The KEYBOARD thread reads the process-wide |
| 1732 | // winch flag and relayouts; no pump consumes it. | 1498 | // winch flag and relayouts; no pump consumes it. |
| 1733 | interact.watchWinch(); | 1499 | interact.watchWinch(); |
| 1734 | } | 1500 | } |
| 1735 | // From here to the key loop every step can fail — a tile array, a | 1501 | // From here to the key loop every `try` unwinds past the restore below, |
| 1736 | // doorbell pipe per tile, the `present` array — and each of them is a | 1502 | // leaving the user on an alternate screen in raw mode with no prompt. |
| 1737 | // `try` that unwinds straight past the restore at the bottom. That | 1503 | // Only that window: every ordinary exit is the teardown at the bottom. |
| 1738 | // leaves the user on an alternate screen, in raw mode, with a title | ||
| 1739 | // pushed and a wall that never appeared: no cursor, no prompt, and | ||
| 1740 | // nothing on screen to say why. | ||
| 1741 | // | ||
| 1742 | // It fires only on that window. Every ordinary way out of this function | ||
| 1743 | // is the explicit teardown below, because the function ends in | ||
| 1744 | // `std.posix.exit` rather than a return. | ||
| 1745 | errdefer if (orig) |o| { | 1504 | errdefer if (orig) |o| { |
| 1746 | proto.writeAllFd(stdout_fd, interact.wall_teardown) catch {}; | 1505 | proto.writeAllFd(stdout_fd, interact.wall_teardown) catch {}; |
| 1747 | std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {}; | 1506 | std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {}; |
| @@ -1756,12 +1515,8 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1756 | shared.sel = 0; | 1515 | shared.sel = 0; |
| 1757 | 1516 | ||
| 1758 | // ssh's prompts get somewhere to go, on a TERMINAL only: without one | 1517 | // ssh's prompts get somewhere to go, on a TERMINAL only: without one |
| 1759 | // there is no popup to answer with, so ssh keeps its own prompts and | 1518 | // there is no popup, so ssh keeps its prompts and fails them visibly. |
| 1760 | // fails them onto the row with its reason — half one's path, and a | 1519 | // `$XDG_RUNTIME_DIR` and no fallback: a shared /tmp socket would be a |
| 1761 | // visible one rather than a wait nobody can see. | ||
| 1762 | // | ||
| 1763 | // `$XDG_RUNTIME_DIR` and no fallback, for `sockpath`'s reason turned up | ||
| 1764 | // one notch: a guess lands in a shared /tmp, and a socket there is a | ||
| 1765 | // password prompt any local user could answer. | 1520 | // password prompt any local user could answer. |
| 1766 | var ask_exe_buf: [std.fs.max_path_bytes]u8 = undefined; | 1521 | var ask_exe_buf: [std.fs.max_path_bytes]u8 = undefined; |
| 1767 | if (is_tty) { | 1522 | if (is_tty) { |
| @@ -1863,13 +1618,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1863 | var over_buf: [64]u8 = undefined; | 1618 | var over_buf: [64]u8 = undefined; |
| 1864 | if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said); | 1619 | if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said); |
| 1865 | for (host_table[0..hosts_live]) |*h| { | 1620 | for (host_table[0..hosts_live]) |*h| { |
| 1866 | // Not without a terminal (`headless`). A poller here would turn | 1621 | // Not without a terminal: a poller here would turn every scripted |
| 1867 | // every scripted `mux --sock S` into a wall of whatever that daemon | 1622 | // `mux --sock S` into a wall of whatever that daemon is running. |
| 1868 | // happens to be running: measured on a piped attach to a daemon | 1623 | // Nothing to wait for when nothing was started. |
| 1869 | // holding four sessions, four attaches and a capture that opens | ||
| 1870 | // with label bars. | ||
| 1871 | // Nothing to wait for when nothing was started: `spawnPump`'s | ||
| 1872 | // catch marks `pump_done` for the same reason. | ||
| 1873 | if (measured == null) { | 1624 | if (measured == null) { |
| 1874 | h.poller_done.store(true, .release); | 1625 | h.poller_done.store(true, .release); |
| 1875 | continue; | 1626 | continue; |
| @@ -1887,12 +1638,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1887 | const hosts_path = hosts.statePath(alloc) catch null; | 1638 | const hosts_path = hosts.statePath(alloc) catch null; |
| 1888 | defer if (hosts_path) |p| alloc.free(p); | 1639 | defer if (hosts_path) |p| alloc.free(p); |
| 1889 | 1640 | ||
| 1890 | // The wall is always "in": every non-chord byte goes to the focused | 1641 | // The wall is always "in": every non-chord byte goes to the focused tile. |
| 1891 | // tile, whose own Core splits mouse reports and owns the drag. The | 1642 | // The keyboard holds back only the `Ctrl-\` prefix and hit-tests presses |
| 1892 | // keyboard holds back only the `Ctrl-\` prefix, and a local MouseFilter | 1643 | // for focus; the report bytes still go through, so the drag's Core sees them. |
| 1893 | // that parses the terminal's SGR reports to hit-test a press for | ||
| 1894 | // focus — the report bytes themselves go through to the focused tile | ||
| 1895 | // unchanged, so the Core that owns the drag sees them. | ||
| 1896 | var input: WallInput = .{}; | 1644 | var input: WallInput = .{}; |
| 1897 | // Which HOST the picker's selection rests on, kept across opens so a | 1645 | // Which HOST the picker's selection rests on, kept across opens so a |
| 1898 | // popup reopened by reflex is where it was left. Keyboard-thread | 1646 | // popup reopened by reflex is where it was left. Keyboard-thread |
| @@ -2001,11 +1749,8 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2001 | }, | 1749 | }, |
| 2002 | .vanish => |v| { | 1750 | .vanish => |v| { |
| 2003 | if (v.msg) |msg| { | 1751 | if (v.msg) |msg| { |
| 2004 | // The notice is the banner on the surviving tile's | 1752 | // Banner on the surviving tile's rect; stderr deferred |
| 2005 | // rect; stderr is deferred to after raw-mode restore | 1753 | // past the raw-mode restore. Both say the same words. |
| 2006 | // so it does not land on the alternate screen. Both | ||
| 2007 | // say the same words — a screen that names a cause | ||
| 2008 | // the exit line contradicts is worse than either. | ||
| 2009 | var nb: [128]u8 = undefined; | 1754 | var nb: [128]u8 = undefined; |
| 2010 | setNotice(&shared, noticeText(&nb, msg)); | 1755 | setNotice(&shared, noticeText(&nb, msg)); |
| 2011 | vanish_msg = msg; | 1756 | vanish_msg = msg; |
| @@ -2029,12 +1774,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2029 | }, | 1774 | }, |
| 2030 | } | 1775 | } |
| 2031 | } | 1776 | } |
| 2032 | // A box whose ssh has GONE, before a box that is arriving: the | 1777 | // A box whose ssh has GONE, before one that is arriving: the helper is |
| 2033 | // FIDO notifier's helper is SIGTERMed the moment the touch lands, | 1778 | // SIGTERMed when the touch lands, so the prompt on screen is a question |
| 2034 | // and what is on screen then is a question nobody is waiting on. | 1779 | // nobody waits on. Dismissing it by key would decline a live dial. |
| 2035 | // Closing it here rather than on a key is the whole point — the | ||
| 2036 | // user should not have to dismiss it, and dismissing it would put | ||
| 2037 | // a live dial's pid in the decline ring. | ||
| 2038 | if (input.prefix.asking) { | 1780 | if (input.prefix.asking) { |
| 2039 | if (shared.prompts) |l| { | 1781 | if (shared.prompts) |l| { |
| 2040 | if (!l.showing()) closeAsk(w, &shared, &input.prefix, picker_sel); | 1782 | if (!l.showing()) closeAsk(w, &shared, &input.prefix, picker_sel); |
| @@ -2097,16 +1839,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2097 | }, | 1839 | }, |
| 2098 | .leave => {}, | 1840 | .leave => {}, |
| 2099 | } | 1841 | } |
| 2100 | // On news, not on the tick: the rows have to follow the pollers | 1842 | // On news, not on the tick: the rows follow the pollers while the |
| 2101 | // while the popup is open, and a repaint every 100ms would rewrite | 1843 | // popup is open, but a repaint every 100ms rewrites an unchanged |
| 2102 | // a screen nothing changed on. | 1844 | // screen. A zeroed stamp is a screen the box is no longer on — |
| 2103 | // The bell too: a tile that ended relayouts under the box, and the | 1845 | // `relayout` cleared it, and nothing else would repaint the popup. |
| 2104 | // cleared screen owes the popup a repaint the pollers must not be | ||
| 2105 | // the first to give it. | ||
| 2106 | // A zeroed stamp is a screen the box is no longer on: `relayout` | ||
| 2107 | // clears it and zeroes the stamp, and a second ended tile drained on | ||
| 2108 | // a pass with no bell, no news and no key would otherwise leave the | ||
| 2109 | // popup wiped until the next list. | ||
| 2110 | var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; | 1846 | var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; |
| 2111 | const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or | 1847 | const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or |
| 2112 | winch or fds[1].revents != 0 or shared.picker_stamp == 0); | 1848 | winch or fds[1].revents != 0 or shared.picker_stamp == 0); |
| @@ -2146,11 +1882,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2146 | wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix); | 1882 | wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix); |
| 2147 | continue; | 1883 | continue; |
| 2148 | } | 1884 | } |
| 2149 | // ssh asks in sequence — a host key, then a password — and the | 1885 | // ssh asks in sequence and the next helper is already in the |
| 2150 | // next helper is already waiting in the backlog. Reopening | 1886 | // backlog: reopening BEFORE the close saves a screen that flashes |
| 2151 | // BEFORE the close saves the user a screen that flashes back to | 1887 | // back to the wall between two halves of one login. |
| 2152 | // the wall between two halves of one login; `askOpen` takes the | ||
| 2153 | // answer buffer back the way `closeAsk` would have. | ||
| 2154 | if (takeAsk(&shared, &input.prefix, &ask_prompt)) { | 1888 | if (takeAsk(&shared, &input.prefix, &ask_prompt)) { |
| 2155 | wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix); | 1889 | wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix); |
| 2156 | continue; | 1890 | continue; |
| @@ -2237,17 +1971,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2237 | } | 1971 | } |
| 2238 | continue; | 1972 | continue; |
| 2239 | } | 1973 | } |
| 2240 | // An EMPTY wall — no tile present, whether none was ever born or | 1974 | // An EMPTY wall has no `tiles[z]`, and every branch below reads one. |
| 2241 | // the last one has left — has no `tiles[z]` to command, and every | 1975 | // `live` cannot answer it — a slot high-water never falls — so the |
| 2242 | // branch below reads one. `live` cannot answer this: it is the slot | 1976 | // `present` roll is what `relayout` counted. No sidecar on the way |
| 2243 | // high-water and never falls, so it still counts a tile that | 1977 | // out: writing one would erase the layout the user last left. |
| 2244 | // vanished. The `present` roll is what `relayout` counted when it | ||
| 2245 | // decided to paint the empty line, and the screen and the keyboard | ||
| 2246 | // have to agree on which wall this is. | ||
| 2247 | // | ||
| 2248 | // No sidecar on the way out: an empty wall has no tree worth | ||
| 2249 | // remembering, and writing one would erase the layout the user | ||
| 2250 | // last left on a wall that had tiles. | ||
| 2251 | if (presentCount(present[0..live]) == 0) { | 1978 | if (presentCount(present[0..live]) == 0) { |
| 2252 | switch (cmd.action) { | 1979 | switch (cmd.action) { |
| 2253 | .detach => { | 1980 | .detach => { |
| @@ -2263,12 +1990,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2263 | } | 1990 | } |
| 2264 | continue; | 1991 | continue; |
| 2265 | } | 1992 | } |
| 2266 | // A BARE Ctrl-\ while there is no session to command is the | 1993 | // A BARE Ctrl-\ with no session to command is the user giving up on |
| 2267 | // user giving up on the wait. Judged after the filter rather | 1994 | // the wait. Judged after the filter, so `\x1c` still holding out for |
| 2268 | // than by scanning the raw bytes, because the two readings of | 1995 | // its command key stays apart from `\x1c w` asking for the wall. |
| 2269 | // the same byte have to stay apart: a `\x1c` still holding out | ||
| 2270 | // for its command key is the abort, while `\x1c w` is somebody | ||
| 2271 | // asking for the wall. | ||
| 2272 | if (cmd.action == .none and input.prefix.pending) { | 1996 | if (cmd.action == .none and input.prefix.pending) { |
| 2273 | if (awaitingSession(&tiles[z])) |waiting| { | 1997 | if (awaitingSession(&tiles[z])) |waiting| { |
| 2274 | exit_code = 0; | 1998 | exit_code = 0; |
| @@ -2285,20 +2009,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2285 | // it is queued BEFORE the focus moves — so `Ctrl-\ n` cannot | 2009 | // it is queued BEFORE the focus moves — so `Ctrl-\ n` cannot |
| 2286 | // deliver the tail of a word to the tile it is jumping to. | 2010 | // deliver the tail of a word to the tile it is jumping to. |
| 2287 | if (cmd.forward.len > 0) { | 2011 | if (cmd.forward.len > 0) { |
| 2288 | // A press in another tile's rect moves the focus there. | 2012 | // A press in another tile's rect moves the focus there; a press in |
| 2289 | // The MouseFilter parses the terminal's SGR reports to find | 2013 | // the focused tile is not intercepted at all. `Event.at` is the |
| 2290 | // the press; the report bytes themselves go through to the | 2014 | // byte offset of each report, because keys before a click belong to |
| 2291 | // focused tile unchanged, so the Core that owns the drag | 2015 | // the OLD focus and keys after it to the new. |
| 2292 | // sees them. A click that lands in the focused tile is not | ||
| 2293 | // intercepted at all — it reaches the session that asked for | ||
| 2294 | // the mouse, exactly as a focused tile's did. | ||
| 2295 | // | ||
| 2296 | // `Event.at` is the byte offset in `forward` where each report | ||
| 2297 | // sits. A read holding keys and a click in the same burst must | ||
| 2298 | // deliver the keys that came BEFORE the click to the old focus | ||
| 2299 | // and the keys after it to the new — flushing everything to the | ||
| 2300 | // final focus would reroute a keystroke the user aimed at the | ||
| 2301 | // tile they were leaving. | ||
| 2302 | const report = mouse_filter.feed(cmd.forward, &mouse_out); | 2016 | const report = mouse_filter.feed(cmd.forward, &mouse_out); |
| 2303 | var seg_start: usize = 0; | 2017 | var seg_start: usize = 0; |
| 2304 | for (report.events) |ev| { | 2018 | for (report.events) |ev| { |
| @@ -2330,11 +2044,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2330 | // that a keystroke can be for. | 2044 | // that a keystroke can be for. |
| 2331 | .ask_answer, .ask_decline => {}, | 2045 | .ask_answer, .ask_decline => {}, |
| 2332 | .detach => { | 2046 | .detach => { |
| 2333 | // The slot goes back to the daemon before this process | 2047 | // The slot goes back to the daemon before this process does. |
| 2334 | // does — the pump writes the frame, because a Transport | 2048 | // The pump writes the frame; this waits briefly for the ack, so |
| 2335 | // has one owning thread, and this waits briefly for the | 2049 | // a `mux` typed straight after finds the session free. |
| 2336 | // ack so a `mux` typed straight afterwards does not find | ||
| 2337 | // the session full of its own corpse. | ||
| 2338 | tiles[z].detach_req.store(true, .release); | 2050 | tiles[z].detach_req.store(true, .release); |
| 2339 | ring(&tiles[z]); | 2051 | ring(&tiles[z]); |
| 2340 | awaitDetach(&tiles[z], &shared); | 2052 | awaitDetach(&tiles[z], &shared); |