summaryrefslogtreecommitdiff
path: root/src/pardes.zig
Commit message (Collapse)AuthorAge
*-. Merge: 9P rounds 7-8 + layout files + loose ends + G3 cursorGabriel Schneider29 hours
|\ \
| | * G3: the focused cursor glides a jump on the Motion flavour's springs, and blinksGabriel Schneider29 hours
| |/ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The core owns the motion, as Lift: animation.CursorGlide keeps the focused cursor's four corners on springs (leading corners at the flavour's pace x2.4, trailing ones x follow, so a jump smears and collapses; a long jump bows by the flavour's arc), sampled at each frame's own time. A step of a cell or less, insert mode, a focus change and any scroll (a file's view, a terminal's output) land at once; a snap lands a glide under way too. The quad is clamped to the focused pane when drawn (never its springs). Chrome carries it on the wire (cursor_glide: bit 1 a cursor, bit 0 gliding; cursor_alpha; cursor_quad, grid cells; cursor_idle_ms): the GUI draws the quad as an overlay in the cursor's colour, at 85% (70% on the long flavours) rising to solid as it lands, and hides the cells' cursors while it does: one or the other every frame. Only the focused cursor glides. Blink (CursorBlink, on by default in pixel shells) is the shell's: from cursor_idle_ms and when the frame arrived, solid while typing and 500 ms after, 530 ms halves with 80 ms eased edges, solid after 10 s idle, and never while the window is unfocused or minimized or on a virtual clock. An edge redraws the last frame with no core render (level B, blink_redraws); an attached GUI blinks from the same field without a frame per edge. Tests: snaps a cell, glides a jump with smear and bow, arrives under a pixel by 150 ms on smooth and lands exactly, shifts with a scroll; blink holds, eases, stops; the core glides a jump (ge) frame by frame (gliding exactly while it moves, alpha rising, exact at rest), snaps a step (j) and a switch to insert mid-glide; a terminal's scrolling output lands its cursor; wire round-trip of the cursor record. Shared files: src/animation.zig, src/pardes.zig, src/config.zig (toggle CursorBlink), src/surface.zig, src/detached/wire.zig.
* | A column has a ctl, an exec and an rmdir over 9P, and Restore maps its serialGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | A script could read and write a column's tag but not act on the column. col/<serial>/ctl takes Delcol, Joincol, New and Tty, each acting on that column as from its tag; col/<serial>/exec runs a word as a middle click in the tag; rmdir col/<serial> closes an empty column and refuses one with panes (ENOTEMPTY, an err record saying why). The root ctl's refusal of Delcol and Joincol points at col/<serial>/ctl. A dump keeps each column's serial and Restore logs restoredcol <old> <new>; the dump golden gains them. Documented: Joincol's survivor, the column on the right, keeps its serial and tag. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | A desktop session holds 64 panes, and every route that finds none free says soGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | Sixteen panes was far too few for a real session. MAX_PANES is now 64 on a desktop and stays 16 on the board; the 48 more slots cost about 3.7 KB each in fixed tables (the core 1.33 to 1.45 MB, its surface 19 to 72 KB), and a pane itself (84 KB and its terminal) is allocated only when it opens. At the cap, pane/new fails with `pane/new: no pane slots: 64 max` and an err record rather than a bare ENFILE, a 9P look or exec that would open a pane fails its write with the same words and reads back empty, and New, Tty, Tutor and Alt-n say it on the message row (their ctl writes fail with it). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | A pane's last row of text is no resize handle; the grip is, and the GUI's ruleGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A press on a pane's last row began a seam drag, so the row could not be clicked or swept like the rest of the text (rsearch dragged the handle out of the way first). Now no row of text is a handle: in the GUI a press on the 2 px rule between two panes drags their seam, and anywhere the grip does, as acme's button moves a window -- dragged up or down its own column it moves the pane's top, either pane down to its tag alone (acme's coldragwin), dragged down inside itself too. The hover hint over a last row is gone; column edges resize as they did. Scripts that dragged a seam drag a grip instead and match their goldens; six goldens lost the hint or moved a cursor to the clicked text, re-recorded by name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | A send to Python's REPL runs once, the Enter after the paste, not in itGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | A one-line send (print(1)) to Python 3.14 stopped at ... and needed another Enter: its REPL takes a bracketed paste, and an Enter read in the same read as the paste is part of the paste, one line or not (checked in a real pty). Into a REPL that asked for bracketed paste, the Enters (two for an open block) now wait for its next output, its answer to the paste, or 250 ms with nothing. fs-test runs python3 in a real pty and checks a one-line exec prints at once; the docs' pty/data recipe says to send the Enter apart. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | A failed Dump or a Restore of no dump fails its write, before any warningGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | A Dump into a DumpDir it could not write, or a Restore of a file that is not a dump, answered a ctl write with rc 0 and only a message, and the Restore warned about unsaved panes before it ever looked at the file. The hosts' dump write now fails the waiting 9P write with EIO (the late failure a Save uses), naming the path and the reason in the log, and Restore parses the file before its unsaved-panes refusal, failing with `not a pardes dump`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | An answer the question does not offer is refused, and the question standsGabriel Schneider29 hours
|/ | | | | | | | | `answer z` to a question offering a and b succeeded, sent nothing and dropped the question; so did Del's `answer x`. The ctl now takes only an offered choice or -, refuses anything else with the choices it takes (`answer takes k j or -`), and leaves the question asked. Co-Authored-By: Claude Opus 5.5 <[email protected]>
*-. Merge: 9P round 6 + empty columns + render (acme theme, grips, Lift, Motion)Gabriel Schneider29 hours
|\ \
| | * Motion flavours differ by design, and drive notice drops and pane movesGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The flavours looked alike because Lift, the only motion that read them, moves a shadow a few pixels: overshoot and wind-up were sub-pixel. By design now: crisp leaves at full speed and never passes its mark; smooth (the default) eases in and out in twice crisp's time, a slide arriving in 208 ms (inside §8.1's 220); bouncy passes its mark by ~30% and settles twice; playful winds up, passes by ~40%, and stretches and squashes, text included (the user's choice). Small motions are exaggerated by the flavour's gain. Motion gains launch, gain, span and norm, and ease(u)/speed(u) for moves of fixed length. The notice drop follows ease() over its fall (alpha never behind its position) and stays inside its pane's body past its row. Panel tracks carry the flavour in Track.motion (the old padding byte; macOS header name follows); opening and moving slides, zooms and rises follow it over span x their frames; landed, a pane is exactly its target; closing keeps its own exit. A pane past its mark never takes a neighbour's click; a rising one stays in its box. Tests: each flavour's lift trajectory distinct; flavour character; exact landing bit for bit; a stretched pane's drawn cells map back; the overshoot hit rule; the vertical clip at peak; legacy tests pinned to crisp. GUI golden 18-mid-transition changes (the retuned smooth slide at frame 6). Shared files: src/animation.zig, src/Presentation.zig, src/Messages.zig, src/macos.zig, src/config.zig, src/pardes.zig tests, src/ninep/screen.zig test, docs/effects.md, docs/config.md.
| | * Lift moves at the display's rate: render-time animationGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Review (and the coordinator): Lift stepped with the core's 16 ms ticks, so a 144 Hz display held each shadow for 2-3 refreshes. The lift springs are now sampled at each frame's own time (p.now_ns), and Pardes.continuous() reports a render-time animation in motion: while it is, the pump draws every loop (a 1 ms wait takes only the input there is) and the present's vsync paces it at the display's rate. They left nextWake; the stepped, frame-indexed animations (transitions, message fades, the theme fade) and every golden are unchanged. With PARDES_TEST_CLOCK, each loop of a moving render-time animation moves the virtual clock by a display frame at PARDES_TEST_HZ (default 60), so a 144 Hz series is deterministic: a focus switch is 51 distinct frames at 144 Hz. Test: renders 6.9 ms apart give rising lifts; settled, no frames. The tty never lifts (GUI capability). Shared files touched: pardes.zig (continuous, pump), gui.zig.
| | * G1: Lift, soft elevation shadows (off by default)Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `Lift` (a GUI toggle, capability lift) lifts the focused pane on a critically damped spring (animation.Spring: closed form, about 240 ms to settle, velocity kept across retargets, settled = no frames) and floats notices. The core puts the lift on the regions (Region.lift, on the wire); the GUI casts from it: a soft shadow is decor, an erf rectangle on one quad grown by 3 sigma (decor.frag, flag 0x10000000), in the quad's own space so a track's transform carries it; darken-only, the caster's rect left alone, alpha pre-warped for linear light and IGN-dithered. Cast last in its group, so a neighbour's rails and rules darken with its text; a notice's before its rule. Strength is capped per theme so text over the darkest shadow keeps min(its contrast, 4.5) (test over the native themes). Opt-in until the focus lift/dim default is decided. Feel review material: docs/effects.md. Test-mode PARDES_TEST_SERIES keeps every captured frame. Shared files touched: pardes.zig (two fields, one nextWake line), detached/wire.zig (lift on the region), config.zig, builtins.zig (capability), gui.zig. Not touched: Messages.zig, mouse.zig, tty, host_io.
| * | A pane whose whole text fits in its rows never scrolls to keep a marginGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | The scroll margin reveals text; with none off screen there is nothing to reveal. ensureCursorVisible and a centred look both leave such a pane at its top. A longer file keeps helix's clamp (hxdiff and hxparity unchanged). Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | A document dragged beside the boot shell leaves it; BootShell replace closes ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | it as before Placement and BootShell are one kind of setting now, a word from an enum, set, flipped bare, and reported by one arm. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | An empty column shows its theme's empty_col: white in the acme theme, as ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | acme paints it Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | A column can be empty, as acme's can; new panes go where acme's ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | makenewwindow puts them Newcol makes an empty column; closing a column's last pane leaves it empty with the keyboard on its tag; Delcol and Joincol alone take a column away; the session's last pane closing quits. The +New stand-in and replaceStillborn / stillborn_joiner are gone. Every new pane goes through exec.placeNew, which follows acme's makenewwindow (active column, empty column whole, blank space, halving the biggest) or, with Placement pardes, the old rules. ColumnTags is gone: column tags are always shown. A grip drag shows acme's box cursor. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A pane's question is logged as ask, and answer on its ctl answers itGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A script could not answer the several-REPLs question, which only a key could. Every question a pane asks on its notice band (which REPL takes an exec, Del's side from the keyboard) is now logged as `ask <serial> <what> <choices>`, and `answer <choice>` on the pane's ctl answers it as that key would, `answer -` taking it back as Esc does. One function answers for a key and for the ctl. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A Python block sent to a REPL gets the second Enter that ends itGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A dogfood agent's `def` sent to python3 sat open at the continuation prompt: a block whose last line is indented is not over at one Enter, pasted into 3.13's REPL or typed into the old one (checked against python 3.14 in a real pty, both ways). A send to a Python REPL now adds that second Enter, and the pty/data recipe in the docs and the 9P skill says to. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A shell that exits under a run answers exit N, and the log says exit before delGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A dogfood agent ran `exit 3` through pty/run: the reader got ENOENT, and the log went straight to del. The hosts now read a shell's exit status at its pty's end as they do a command's; a run waiting on the line answers `exit 3` with what it printed, the log says `exit <serial> 3` before the pane's `del`, and an open run still stats after its pane is gone (cat fstats its input). Pins cloud9 f35b7ed, whose stat of an open fid names its open. Writes to a gone pane's pty/data, held open or not, fail ENOENT. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | pty/status reads the pty's size as winsize set it, on a line of its ownGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A dogfood agent wrote winsize 100 30 to pty/ctl and pty/status went on reporting the pane's grid, and the status line had no newline to end it. The size winsize sets is kept until the pane resizes and gives the pty its grid again; ctl's winsize read back and status both report it, and status ends with a newline. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A pane tag wraps onto rows and can collapse to one, as acme's doesGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | acme's frame wraps a tag at its right edge and the window grows to show every row (wind.c wintaglines); Tagup/Tagdown shrink it to one row and back. Pane tags used to be one row, scrolled sideways to the caret. Now a tag's lines wrap at its width (one cell kept for the caret past a full row) and it starts expanded; insert-mode Up on the first row collapses it, Down on the last row expands it, and Alt-Up/Alt-Down do so in either mode. A word the wrap breaks across rows is still one word to a click and a selection. Column and workspace tags keep scrolling sideways. Tests read body rows relative to the body's first row; the 41 snapshot goldens that moved were re-recorded by name after a programmatic check that each differs only by body rows shifting under the taller tags, and twelve scripts whose clicks targeted fixed rows now click the same text. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A look with a blank in its pattern addresses, file:N selects the line, and a ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | miss is said, logged and reads back empty Round 6: look calc.py:/return a/ was a word search, since a blank ended the address; calc.py:99 past the end focused the file and said nothing; and /look still read back the pane before a miss. A pattern after the colon may now hold blanks; file:N selects the line, newline included, as acme's does; a line past the end opens and focuses nothing; and every miss (no match, a bad address, a line past the end) says so, logs err <serial> look: why, keeps the selection, and leaves look reading back empty. Documented, with file:0/re/ for the first match, since acme evaluates the address from the file's dot. Five goldens change: a look at file:N now selects the line. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A key between two clicks makes them two clicks, not a double-clickGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | A double-click is two clicks with nothing between them; a click, some typing and a click at the same cell within half a second selected a word, and snapshot scripts that did that came out by timing. A key now forgets the last click. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A tag no longer marks an unsaved file with *: the grip shows itGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | The grip button now shows a pane's unsaved text in every state, as acme's modbutton does, and acme's tag has no asterisk. The computed prefix is the name alone, so tag reads, event offsets that count the path and the tag's text lose the " *". dirty, index's flag and the refuse-once guards are unchanged; a dump from before still restores its tags past the old mark. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A B1 sweep held past a pane's edge scrolls the body, as acme's frselect doesGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | Dragging a selection beyond the top or bottom of a pane stopped at the last visible row. Now, while B1 is held past the body's top or bottom (at a pane that reaches the screen's bottom, its last row, since the pointer cannot leave the screen), the body scrolls a line a tick; the sweep's start keeps its place in the text, and a sweep that scrolled ends as the text's own selection. Also here: a ctrl-click is not half a double-click (lspdebug's golden stays as it was), and a test that clicks many points in a row resets the double-click between them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | Everything a B1 chord does while B1 is down is one undo stepGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | B1-B2 then B1-B3 cut and pasted as two undo steps. acme marks the file once per B1 hold (text.c:881, textselect), so the pair is a copy. The chord now takes one undo point when it begins and its edits share it until B1 comes up. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A double-click selects as acme's does: a word, a line, a bracketed or quoted ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | text A second B1 click at the same cell selected nothing more. Now, as acme's textdoubleclick (plan9port text.c:1407), it selects up to the match of a bracket just before or after the point, the whole line at a line's start or end, the quoted text inside ' " `, or else the word, in bodies, tags and terminals in normal mode. A plain click still puts the caret on its cell. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | Column and workspace tags are one line again, as acme'sGabriel Schneider29 hours
|/ / | | | | | | | | | | acme gives a column tag one font height (cols.c:244) and its row tag one line; pardes let them grow a row per line. A newline typed, pasted, written or restored into a column or workspace tag now becomes a space, the caret and selection kept on the same characters, and the bars are one row each. A dump holding a multi-line header tag comes back with its lines joined by spaces. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | Edit runs sam's command language on a pane's body, as acme's Edit does, in ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | one undo step Over 9P a rename everywhere was a loop of addr and data writes. Edit, from a tag, a pane ctl or exec, parses sam's commands as acme's edit.c does and runs them as ecmd.c does against the text as it was, collecting the changes in order as elog.c logs them and applying them at once: addresses (the addr file's, line:col included), x y g v c a i d s p = m t, u alone, and { }. One undo step; any error, in acme's words, changes nothing, fails a ctl write and is logged. p and = print to the directory's +Errors. Left out, and said: the file commands, the pipes, \1-\9 (mvzr keeps no submatches), and changes out of sequence, which acme applies with a warning. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | A Save the host cannot do fails the 9P write that asked for it, and the log ↵Gabriel Schneider29 hours
|/ | | | | | | | names the path and why The write answered ok and the pane stayed dirty, with `save: AccessDenied` on its message row: a script saw success. The failure now says `Save <path>: <why>`, and a 9P write that waited on the save fails with EIO and that text, logged as an err record. Co-Authored-By: Claude Opus 5.5 <[email protected]>
*-. Trial merge: 9P + helix + renderGabriel Schneider29 hours
|\ \
| | * Post chain: Shadertoy passes compiled by glslc; Crt, Ripple and Glitch ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | rewritten as Shadertoy files Stage 9 of docs/render-pipeline.md. src/gui/Post.zig runs the chain over the finished frame: each pass reads the one before, ping-pong textures between, the last writes the window; an empty chain is the direct path. The prefix (shaders/post/prefix.glsl) is ghostty's uniform block, name for name and offset for offset (tested against a copy of its Uniforms, the selection colours by their GLSL names), at SDL GPU's bindings, y down. A user's file (Shader <path>) is compiled by glslc with compileGlsl's own flags on a thread of its own, taken in at the next loop step; errors name the file's own lines; a failed compile keeps the last good pipeline; glslc missing is said once. The bundled passes compile with the build, through the same prefix. Crt, Ripple and Glitch are Shadertoy files, rewritten: no barrel (input is identity, tested), everything in linear light and dithered, keyed to iTime. scene_effects, crt.zig and crt.frag.glsl are gone; the core keeps no clock for the chain. ShaderAnimation off|on|always drives redraw level A: the chain alone over the retained frame (34 us CPU a redraw, measured). Shared files touched: pardes.zig (nextWake loses the scene line, disableSceneEffects empties the chain, two tests), builtins.zig (one switch arm), ninep/ctl.zig (two switch arms), macos.zig (flags from the chain), gui.zig. Not touched: Messages.zig, mouse.zig, detached/*, host_io, tty.
| | * The GUI draws in tiers from the regions; pane chrome moves with its paneGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Stage 8 of docs/render-pipeline.md. The frame is drawn in groups: tier 0, one per track in paint order (tiers 2 and 3), tier 4 (notices, then guides and the debug box), tier 5 (bar cursors), each cells, images, decor. Decor (rules, rails, thumbs, grip marks, spines, the workspace and column rules, the bottom band, notice rules, bar cursors) is whole-pixel rects through the new decor.frag over ui.vert, so it carries its track's transition and clip. A closing pane's chrome comes from Surface.previous_regions. Deleted: taglineBaseRgb, topbarPaneBorderHeight, bottomTaglinePresent, frameChromeBg, cellBackgroundIs and the rail inference, paneGripCell's scan, transient_on, PaintPlan. One cover map (coverFrame) is marked from the layers and regions once a frame. New regions column, guide and debug; Surface.chrome is the palette, carried in wire v8 (not shipped yet), so an attached GUI draws the same chrome (test). A per-instance clip replaces the vertical transition's scissor. Goldens: 01-12 byte-identical. 13-17 differ only past the grid: the image pass left its scissor at the grid's size, cutting rule ends, rail feet and the bottom band in the leftover pixels whenever a picture was on screen. 16-debug now shows the debug box, which a context-row layer had hidden. 18-mid-transition is new: virtual clock (PARDES_TEST_CLOCK in the GUI), PanelSlide Newcol with the picture, frame 6 of 12. Also: the GUI sleeps when idle instead of polling every 16 ms, and a minimized or occluded window sleeps through animation. Tracy 'gui frame build' at 200x60: 992/1015 us median before, 989/987 us after. Shared files touched: pardes.zig (one export), detached/client.zig, detached/server.zig, detached/wire.zig (all additive), gui.zig. Not touched: Messages.zig, mouse.zig, tagline.zig, colors.zig, tty.zig, host_io.zig, dump.zig, exec.zig, panes.zig.
| | * One Layer for tags, notices, headers and bodies; a taller tag is one layer ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | of rows; wire v8 src/Layer.zig merges TagLayer and BodyLayer. `rows` (0 = no layer) and a cursor at {x, y}. A tag of N rows is ONE layer of N grid rows: tagHit answers the row as `line`, bodyHit keeps its meaning, and the per-line layer bases (TAG_LINE_LAYER_BASE, HEADER_LINE_LAYER_BASE) are gone, not aliased. Wire v8, the one bump: tag layers carry rows and cursor y, and the frame carries the placed region list. v7 and v9 peers are refused in both directions (server test over both, a new frontend test over both). web: tag_layer_value 11 = rows, 12 = cursor y; app.mjs lays every row. macOS: the Zig side compiles against Layer; pardes.h still sees one row per tag layer (accepted, the macOS shell is ignored for now). No visual change: snapshot goldens and the 17 GUI goldens byte-identical. Shared files touched: pardes.zig, Messages.zig, gui.zig, macos.zig, detached/client.zig, detached/server.zig, detached/wire.zig (plus web.zig, app.mjs, edit.zig, look.zig). Not touched: mouse.zig, tagline.zig, colors.zig, tty.zig, dump.zig, exec.zig, host_io.zig, panes.zig.
| | * Place every part of the frame once, before painting itGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | draw.place builds the Surface's region list at the head of render: the page; each pane's grip, tag rows, body, rail with its thumb, and notice chips; each column's grip and tag; the workspace tag. The painters read their geometry from it instead of working it out again: the grid's notice chips and the notice layers share one chip rectangle, the rails (file, terminal, image, PDF page) paint the placed thumb (a native PDF measures its own into the region as it places its pages), and the body, tag, notice and column layers take their viewports from their regions. A collapsed pane places only its grip and tag. The column-move rail's one-cell clip of the layers under it is now a region field set in place, not a mutation of the built layers. No behaviour change; a test checks every region against the old geometry, the painted rail and the layer viewports across tag bottom, column tags, notices, a collapsed pane and a multi-line tag.
| | * Step core animation by the shell's clock and sleep to the next wakeGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A shell now answers Host.now, its monotonic clock in ns, and the pump advances core animation to it: one .tick per whole 16 ms frame since core time last stood still. The same animation therefore takes the same time at 60, 120 and 144 Hz, over ssh and after a slow frame (the GUI's re-armed clock ran them ~25% slow at 144 Hz; the tty's post-frame sleep drifted). nextWake() lists every core animation in one place: a frame from now while anything moves, the end of the wait while something only waits (a message lingering, the look-hover delay), null when idle. Shells sleep exactly that long, and a wait is jumped to its end in one step, so an 800 ms linger costs one frame instead of fifty. An overshoot under 1.5 ms after a step is let go: at 60 and 120 Hz every display frame takes exactly one step (a 16 ms frame against a 16.67 ms vsync would otherwise double-step every 24th). The six tick drivers are gone: tty's timer thread only times the wait, interruptibly (a newer, shorter request cuts short a sleep still timing a longer one); the GUI's AnimationClock, web's JS tick bank (pardes_tick now takes rAF's timestamp), the detached server's and the board's ticks; grid mode steps a virtual clock straight to each wake. PARDES_TEST_CLOCK, set by the snapshot harness, gives tty and the detached server the same virtual clock. Every stepped frame is drawn. The old pump never drew the last frame of a fade (a .tick asks for no frame, and the fade was over by the check), so a theme switch stopped at 9/10 of the way until the next input: theme.golden, themesel.golden and the GUI's acme-light scene move to the theme's true colours, and nothing else changes. The frozen previous grid is captured only while a panel transition is chosen, not on every idle frame.
| | * Move the frame into draw.zig and the transitions out of layout.zigGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Pure moves, no behaviour change. draw.zig holds the whole core frame in order: render, the pane, tag, header and notice painters it calls, and the character-effect composition (Pardes.render stays a declaration alias). Presentation.zig is the panel presentation state as a file struct, and animation.zig the easing curves, transition kinds, tracks and boxes, the character effects' sources and the generic displayed-value transition, all of which lived in layout.zig. layout.zig keeps only layout.
| * | A selection's other ranges live out of line, made at the second range and ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | freed at one The ranges beside the primary were an inline array in every Text, which at 1024 grew each pane by about 60 KiB. They are now allocated when a second range appears (Text.reserveSels) and given back when the selection is one range again (Text.dropSels); a column's or the workspace's tag allocates from Text.gpa, any other text from its pane's. A refused tag edit restores them by copy. The Pane size budget is back to 128 KiB, and a test holds that a single cursor allocates nothing for them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | gw labels the words in view and jumps to the one whose label is typedGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | helix goto_word: the words of two or more word characters between the view's first and last lines get two-letter labels, nearest the cursor first, one forward and one back in turn (modal.jumpWords). The next two keys choose one and select it, stretching the selection to it in select mode; any other key takes the labels away. The words live on Pane.jump, and renderBody paints their labels over the body's cells in the selection colours, the one change on the render side. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | K and Alt-K keep or drop the ranges a regex matches in; Alt-: turns every ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | range forward K and Alt-K share the s/S prompt, which now carries a mode rather than a split flag: a range stays when a match starts inside it (K) or when none does (Alt-K), the primary becomes the first, and keeping none leaves the selection as it was (helix keep_or_remove_matches). Alt-: is ensure_selections_forward. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | A selection holds up to 1024 ranges on the desktop, 64 on the boardGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The limit was 64 everywhere, which helix-golf's enumerate_and_align outgrows (%s |\d+ makes 121 ranges). It is now memory.limits.selections, per platform, and the counts that hold it are u16. The ranges stay inline in each Text, so a pane grows by about 80 KiB on the desktop (three texts and the regex prompt's snapshot); the Pane size budget in Terminal.zig moves from 128 to 160 KiB and says why. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | Q records keys into a register and q types them again; . repeats the last insertGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Macro.zig keeps keys to be typed again. Q records into @ (or "<reg>) in helix's key notation until the next Q; q replays a register's keys count times through the modal handling, where Esc does not hop panes and Enter, Tab and Space neither look, execute nor open the leader. . replays the normal command that entered the last insert session once and what was typed in it count times (helix repeat_last_insert). a at the end of the text now adds the newline helix's append_mode does, which a terminal's text does not. helix-golf's invert_dictionary passes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | A tag's edited text stays alive until the next edit, so ms and md in a tag ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | read it whole setEditText freed the text a tag edit was handed once it had copied the tag's own part out of it, but every caller reads that text back to place the cursor, and surround goes on to edit it again: ms and md in a tag read freed memory. The same held for a terminal without an edit buffer and a text with no pane. The text is now kept until the next such edit (edit.retireEditText), which covers every caller at once; the callers the audit found reading it back are surround's two, the paste and replace paths, the number adjust, and the insert-mode Enter. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * | Registers hold a value per range, and "<reg> names oneGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Registers.zig replaces the one yank buffer: every register keeps a value per range, y fills them in document order, and p, P, R and insert Ctrl-r put value i at range i, repeating the last (helix paste_impl). "<reg> names the register for the next command; _ swallows, # numbers the ranges, . is each range's text, % the file's name, / the last s/S pattern, and + and * are the system clipboard through the ClipYank and ClipPaste paths. SPC y now writes + alone, as helix's does. The acme chords and a paste into a terminal take the default register joined by newlines. The msel-yank-paste waiver is gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A REPL question's answer goes to the pane its letter offered, or says that ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | one is gone The letter was looked up again when answered, so a REPL closed or rebound while the question stood could hand the text to another pane with the same letter, or drop it without a word. The question now keeps each letter's serial; the answer is that pane, still bound and running, or a message. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | Errors are said in words, and a refusal names what it takesGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | A failure read `Del k|j: InvalidArgument`, `Kill: NoSuchCommand`, `ill-formed control message`, or nothing at all. reportError now spells an error's name as words, and the refusals name their accepted forms: pty/ctl lists its verbs, a flag file takes 1 or 0, a setting word takes on or off, Del takes k, j or nothing, Repl takes - or a language. Kill with nothing to stop says so, /focus says pane, a Look that found nothing says no match, and pty/run on a command pane says it is not a shell. A ctl write's failure has room for the Repl list and is cut between words, the Repl word at a character. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A REPL's language is named and found as the syntax table finds one: aliases, ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | any case Repl took only the grammar's exact name, and a file's language was found by a case-sensitive suffix of its path, apart from the aliases a code fence already had. The aliases move to grammar_manifest with byName and byPath, which syntax, Repl and langOf share, so `Repl py` and a .PY file work. The line-by-line warning is for the python language, not an id that starts with python. Joincol's doc line was sitting on Repl. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | Focus on a closed pane goes to the pane that takes its place, never to the ↵Gabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | one focused before Dogfood closed a pane in one column and focus jumped to another column, only because that pane was focused last. Now removePane decides for every route (Del, a ctl del, a command pane's end, a 9P remove, a stillborn scratch replaced): the pane that grew into the rows, or the scratch standing in an emptied column; removeColumn gives it to the pane level with it in the column that takes the width. Closing an unfocused pane leaves focus alone. The per-caller focus lines in Del and replaceStillborn are gone, and so is prevFocus. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | Text sent to a REPL is plain lines, sent whole or not at allGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A CRLF became two line ends, the blank line ending a Python block; an ESC in the text, ESC[201~ above all, could end a bracketed paste early; and on the board, whose writes are 128 effects of 64 bytes with nothing pending, a send past 8 KB was cut and lost its closing ESC[201~ and Enter, leaving the REPL in a paste. Lines now end in LF alone, ESC and every control byte but tab and newline are dropped, and a send larger than can be written at once (Pardes.writeRoom) is refused with a message instead of cut. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* | | A command's exit is told once its output is in, by the pty's state, not a timerGabriel Schneider29 hours
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The watcher woke the host a second time 60 ms after the exit by its own clock while the grace was counted from the reap: a host busy for 10 ms missed it, and the tag said running for ever and Kill did nothing; and under load exit 0 could land before the last output. As decided, no timer: the exit is told once it is reaped and the pty says nothing is left (poll: no POLLIN, and no POLLHUP, which means the end of file is on its way behind the output), else at that end of file, checked after each chunk of output. The tty host checks inside its step so the frame shows it. The four hosts' copies are one host_io.takeExits/commandEof, which close a told command's pty at its end of file (the fd and the GUI's reader leaked when the exit came first). A finished command pane whose pty a job it left still holds is not reused, so that job is not hung up; and the reset before a reuse is SGR 0, not DECSTR, which ghostty's stream does not implement. Co-Authored-By: Claude Opus 5.5 <[email protected]>