aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_layout.py (follow)
Commit message (Collapse)AuthorAge
* sl0ppty: the middle of a border is the button, not all of ituser2026-08-18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Clicking a pane's border to split toward it is the right gesture and was the wrong target. A border is the longest thing on the screen and it sits between two things you did mean to click -- a title, a gap, the pane beyond it -- so reaching a cell too far changed the layout, and the top row was worse than the sides: clicking a pane's header to focus it split it upward. So the target shrank. Each side carries a *handle* of three to seven cells in the middle, and only the handle splits; the rest of the edge is the *rim*, which arms the guide and does nothing when pressed. Three is the floor the pane buttons already settled on for the reason written there -- a one-cell target is a thing you miss with a mouse -- and seven is the ceiling, because a third of a tall pane's border is most of that border again and brings the accidents back with it. Hover now answers two questions in two stages, because they are two questions. Anywhere on an edge, the edge goes heavy and the handle thickens by a cell into the pane: that says where the button is, which is the thing you can no longer infer. On the handle, the dashed boundary and the arrow as well: that says what the click will do. The hint follows the same split, so sweeping an edge neither draws a boundary nor offers a split you cannot get from where the pointer is. Three things this had to get right. **The thickened handle is registered, not just painted.** The guide adds the hit for the inner strip in the frame that draws it, and reads the outer rect back from the hit list rather than recomputing it, so there is one opinion about where the handle is (D6). A handle you can see and cannot click would be worse than either mistake alone. **The top row cannot put its handle in the middle.** The buttons own its right and a centred title -- the default -- owns exactly the middle. The title winning is the existing rule, and it is what keeps a double-click rename from splitting twice on its way, so the handle is placed last, in the widest run of cells the row has left and nearest the middle among equals. A row with nothing left offers no upward split; its other three edges are unaffected. Each handle cell is then painted only if the hit list still agrees it belongs to the handle, so a block never lands on a pane's name. **The row keeps the drag and loses the split.** `title:<id>` takes no side now and `title:<id>:t` -- the handle -- carries one, which is why the press path reads the side out of the action instead of assuming it. Press-and-hold skips the dwell on a handle, where a press means something, rather than anywhere on an edge. Verified: `make retest`, 1801 checks, all green. Nine of them are new or rewritten: the two-stage guide, the caption belonging to the handle, the rim splitting nothing on any of the four sides, the top row's handle still splitting upward on a titled pane, and the rim of the top row not. Tests read the handle's cell out of the hit list (`Snapshot.handle`/`.rim`) rather than deriving the geometry, so a test cannot agree with a broken formula. Also checked by hand on a live session, which is where the placement rule earned itself: hovering the rim draws the heavy edge and the block and no dashed line, hovering the handle adds the line and the arrow, three clicks on pane headers split nothing, and on a pane titled `sl0ppty` the top handle lands to the right of the title and left of the buttons -- `drag to move · click to split up` at x=42, `drag to move` either side of it.
* sl0ppty: ruff format the Pythonuser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The bumpy one, again. Every `.py` outside `vendor/` plus the three `contrib/` scripts that are Python with no extension: 60 files, 6,714 lines rewritten, no behaviour changed. Nothing here is worth reading -- the next commit adds it to `.git-blame-ignore-revs` so `git blame` walks past it. It is a bigger reformat than the C was, 60% of the lines against 17%, and for a reason worth writing down rather than tuning away: ruff and black break a call that does not fit onto one argument per line, where this code aligned the continuation under the opening paren. The test suite is thousands of `check("some prose", cond, detail)` calls, so nearly every one of them changed shape and the suite grew 3,308 lines. Line length was the one lever that moved it -- 120 columns would have been a 40% reformat and no growth at all -- and it was left at 88, because picking an unusual width to dodge a one-time diff is paying forever to avoid paying once. Import sorting came with it. Two files had `import os, pty, uuid, select, struct, sys, termios, time, fcntl, signal` on one line and now have ten. Verified after, not before, because a formatter that alters behaviour would be worse than none: - `make retest`, 1801 checks green -- and this reformats the suite that does the checking, including `harness.py`, which every one of those files imports. - `make test-live`, the tty tests the fast suite skips. They matter here more than usual: `live_input.py` and `live_m0.py` are two of the files whose comma-imports were split, and nothing in the fast suite would have noticed if that broke them. - All 60 files still compile, `make fmt` twice in a row changes nothing the second time, and `ruff check --select I` has nothing left to say.
* sl0ppty: a pane has to clear the floor in both directionsuser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two panes stacked on top of each other could be squeezed to four columns wide and nothing collapsed, at any width. The fit check lived only on split nodes, and a split node only ever asks about the dimension it divides: a row split asks about height and never about width. A leaf was asked nothing at all. So for a stacked layout there was no node in the tree responsible for noticing that its panes had become unusably narrow — the check existed, it was just looking the other way. A leaf is the only node that can see both of its own sides, so it now checks both. The floor is per pane and the same in every arrangement, which is what made this a hole rather than a missing feature. That immediately made resizing dangerous, which is the more interesting half. Dragging a divider one cell too far could push a pane under the floor and collapse the entire tab into a list — a drastic answer to a nudge, and it looked like the session falling over rather than a limit being reached. So the transfer stops at the floor instead, the way every other resizable thing stops. The clamp went into transfer_weight rather than into the caller, because the mouse reaches it through drag_edge and the keyboard through resize_focus, and my first attempt clamped only the mouse. A limit that one of two paths respects is worse than none: it looks fixed until you use the other hand. It also has to round the weight *up*. The weight that buys exactly `floor_` cells is a fraction, and rounding it down buys one cell fewer — which is precisely the cell that puts the pane under the floor. The first version rounded down and collapsed anyway, one drag later than before. Two tests changed with real content rather than arithmetic: one used a seventy-six by eight screen whose panes were four rows tall, which is under the floor and should always have collapsed, and one asserted a squeezed pane stayed "at least 3 columns" when it now stops at 24 exactly.
* sl0ppty: a floor under splitting, and an arrow saying which wayuser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two changes to the same affordance. A border stops being a button below `min_split cols=32 rows=8`. This is a different question from min_pane, which is where the layout gives up and collapses a pane: two twenty-five column panes technically fit and are not worth having, so the number that decides whether to *offer* a split is larger than the one that decides whether a pane can survive. It is clamped to never go below min_pane, because refusing to offer what the layout would collapse anyway is the floor under the floor. The test is the layout's own arithmetic rather than an approximation of it: the same floor, the same gap, and the same child count the split would create — which is one more sibling when the parent already splits that way and two when it does not. The guide's standing claim is that drawing and the layout cannot disagree, and a guide promising a split that immediately collapsed would be exactly that. The keyboard obeys the same floor, with a toast. A rule only the mouse followed would mean the same session splits or refuses depending on how you asked, and whether there is room cannot depend on that. The control API and layout files are *not* bound by it: a script asking for a pane is declaring what it wants rather than being offered something, and D6's responsive collapse is what catches it there. That distinction was not free — app_split_pane went through the same helper as the keybinding, so the API was silently bound too until a test noticed, and the interactive path is now its own function. The guide also gained an arrow on the dashed line, pointing at the half the new pane will take. The line already said where the boundary lands and left which side to be inferred from which border you happened to be touching. Pointers for left and right rather than the matching triangles: U+25C0 and U+25B6 carry emoji presentation and are widely rendered double-width, while screen_text books every chrome glyph as exactly one cell — so one that drew as two would shift the rest of the row. U+25B2 is already the scroll indicator, so the vertical pair is known good here. Raising a floor invalidates every test that quietly relied on the old one, and nine of them did: sessions sized for a world where a sixty-column terminal could be split into columns. They are sized for the rule now, and the two that were really about *layout* collapse rather than the affordance now split over the API, which is the path that still means "do it anyway".
* sl0ptty: the border is the button — click an edge to split toward itsl0p2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The per-pane '+' is gone. It was one glyph for a verb with four directions, so it always split into columns; it taxed every frame three columns of title forever; and there were N of them on screen for one verb. Clicking a pane's border now splits toward that border: the side you click is the side the new pane appears on. Hovering arms the edge (it goes heavy) and draws a dashed line exactly where the new boundary would land, so the gesture explains itself and an idle frame stays quiet. ╭──────────────────── shell ─────────────────────╮ ┃ ╎ │ ┃ ╎ │ hovering the left ╰────────────────────────────────────────────────╯ border │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌│ hovering the bottom ╰━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╯ border The top border keeps its other job: a click splits upward, a drag moves the pane. A press that never moves is a click, which is a distinction the drag machine already had to make. The guide has to be painted after the pane's content, not with the frame: the dashed line crosses the terminal's own cells, and the first version was drawn underneath them and was invisible. Two old tests changed rather than the code, both because the gesture set is better now: 'beside the split button' is a drag handle, and 'dropping a pane on itself does nothing' is a click, which splits. The replacement asserts what actually needs asserting — a drag that wanders and returns changes nothing. 387 checks green (360 headless + 27 live).
* sl0ptty: M8 — pane sizes as weights, resize and drag-to-reordersl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Sizes are weights now, so an even split is equal weights and resizing is not a special case of anything: the layout pass is still the same pure function of the tree and the rect, and a resized layout survives a collapse/expand cycle without anyone writing code for that. Keyboard: C-a shift-H/J/K/L (and shift+arrows) move the boundary in the direction pressed, whichever side of it the focused pane is on, which is what a person means by wider. A pane can be squeezed but never squeezed out. Mouse: one drag machine, two verbs, both starting from the hit list so neither can disagree with what is on screen. - a frame's top row is a drag handle: drop a pane on another to swap them, with the drop target highlighted while you drag; - the gap between two panes is the boundary: drag it to resize. The gap was dead space until now, and its hit rect is derived from the rects the children were just given rather than recomputed from the config — the same rule that keeps the split button honest. Any keystroke ends a drag. A release can go missing (the pointer leaves the terminal, a client detaches mid-drag) and a mouse wedged in a state the user cannot see is the worst outcome available. One test needed updating rather than the code: the frame's top row is now the drag handle, so the cell beside the split button reports title: rather than focus:. That is the intent, and the hit list said so immediately. 304 checks green (277 headless + 27 live).
* sl0ptty: M5 — responsive collapse, status bar, pane findersl0p2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | D6 as promised: layout(tree, rect) recomputed every frame, and a split that cannot give every child a floor (24 cols / 6 rows) collapses *itself* into a stack — one expanded child, every sibling a one-row header. No swap layouts, no remembered position, no synthesized anything. Three properties fall out of the design rather than being coded: - the expanded child is the one holding focus, so focusing a collapsed pane expands it: from the finder, by clicking its header, or by walking into it; - a hidden pane is never resized, so its program does not reflow off-screen; - a narrow/wide cycle returns the exact rects it started with, and a pane added afterwards is not stacked. That last one is the fork's 'panes stacked after a responsive collapse/expand cycle' bug, asserted directly. It cannot regress here without the layout function growing memory it does not have. A collapsed header shows the pane's title and its OSC 5577 status, so a stacked agent is still legible — the monitored strip, without a 'rail' mode. Below the size where headers fit, a node shows only the focused subtree: at 12x4 you get one pane instead of rects that do not fit on the screen. Every size from 200x60 down to 12x4 is asserted for overlap and containment. Status bar: tabs, a +tab button, live pane count, and the held prefix shown as a mode. The button is '+tab', not '+', because the frame already has a '+' for splitting — two verbs sharing a glyph is a UI bug the fork shipped, noticed, and wrote down; the collision showed up here as two hit-list tests failing. Pane finder (C-a f): substring filter over title, purpose and tab name across every tab, keyboard and mouse, painted last so its rows win the hit list. It is how you reach a pane the screen has collapsed away, which is tested. Deferred to M6: drag-to-reorder, which wants the same drag-state machine as pane resizing and should be built with it. 213 checks green (186 headless + 27 live).
* sl0ptty: M1 — layout tree, splits, focus, frames, and the hit list in usesl0p2026-08-13
app_t owns the tree, focus, the prefix and what a key means; the interactive client and the headless driver are now both thin front ends over it, so there is exactly one implementation of every command. Layout is the pure function D6 promised: layout(tree, rect) recomputed every frame, remainder spread across children so nothing drifts, and splitting in a direction that already exists grows that split rather than nesting — three splits give three equal columns, not 1/2 + 1/4 + 1/4. Chrome: rounded frames, aspect-corrected gap, centred titles. The split button is budgeted BEFORE the title and hit-tested from the rect it was drawn in, which is the fix the fork needed four attempts to land. Every painted element registers (rect, action) as it is painted; a click is a lookup. Mouse into a pane is translated to pane-local coordinates. Three bugs the harness caught, none of which unit tests would have: - one click on the split button split twice: press *and* release both fired. Chrome now acts on press; forwarding to a pane still gets every event. - callbacks were registered as &fn, but the option is pointer-typed, so the stored 'callback' was a stack address. Any OSC 2 or DSR jumped into a dead frame. Now a hard segfault turned into two regression tests. - a keystroke that changed nothing still emitted a cursor hide/show pair. The cursor is hidden lazily now, so an idle frame costs zero bytes. 96 checks green in 6s (40 unit, 23 screen, 33 layout), plus 9 live.