summaryrefslogtreecommitdiff
path: root/src
Commit message (Collapse)AuthorAge
* Make a pane's tag a Text edited with the body's own keysGabriel Schneider3 days
| | | | | | | | | | | | | | The tag was a fixed buffer with its own one-line editor, a cursor measured across the path, and a hijack of the body's mode while it was typed into. It is now the pane's second Text: its own characters after the computed prefix (path, dirty marker, PDF page), which is never stored in it, so a rename or a dirty marker never moves its cursor. Normal and insert mode are the body's, undo included; `:` moves the keyboard between body and tag, the tag remembers its cursor and starts on Save, and executing a word from it hands the keyboard back. The mouse still sees the prefix: a sweep selects across it and a click on the path drafts a new name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Give prompts their own line to type into instead of the tag's tailGabriel Schneider3 days
| | | | | | | | | | | A search, pipe or Save prompt wrote its marker and answer onto the end of the tag, so the tag's cursor, its selection, its 9P writes and a prompt abandoned by a click all shared one buffer: the abandoned text stayed in the tag and a 9P write landed inside the prompt. The answer is now its own Text on the pane, typed in insert mode and shown on the notice band where it was already drawn, and the tag never holds any of it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Give a pane's body its own Text holding the cursor, selections, mode and undoGabriel Schneider3 days
| | | | | | | | | | | acme keeps what edits a text in its Text (dat.h:171-190) and the window holds a body and a tag of that type. The cursor, the selections, the modal state and the edit-buffer undo move off Pane into Text.zig, Pane holds them as its body, and the edit and normal-mode operations take the Text they edit. Nothing changes in behaviour; this is the step that lets the tag become a second Text. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Point docs and comments at the files code moved toGabriel Schneider3 days
| | | | | | | | | | | | | Documentation only, no code change: README's reading order and layout, docs/design.typ's paragraph on where pane kinds and editor parts live, docs/helix-keys.md's code map (normalInput and the executors now in normal.zig and edit.zig, insertTab in edit.zig), docs/open-questions.md (execute and ttyForDir in exec.zig), and the comments that named pardes.zig for fold, Cell, takesCommandLine and runBuiltin (tools/gen_themes.zig, themes/helix.zig, detached/wire.zig, host_io.zig, lsp_zls.zig). docs/design.pdf is a retained fixture and is not regenerated. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Give the pipe its own fields in PipeGabriel Schneider3 days
| | | | | | | | | | | | | | Not a pure move: state moves. Pardes's `pipe_seq` and `pipe_wait` become Pipe.zig's own fields `seq` and `wait`, and Pardes embeds one as `pipe: Pipe = .{}`. pipeRequest reads only the request in flight, so it now takes `pipe: *const Pipe` and the four shells call `core.pipe.pipeRequest(id)` (their Pipe imports go away again); the other pipe functions still need the panes and keep `p: *Pardes`, writing `p.pipe.seq/wait`. Field reads in dump.zig, the shells' tests and two test files follow (`core.pipe_wait` becomes `core.pipe.wait`). No behaviour change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Split the pipe's editor side out of selection_pipe.zig into Pipe.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | Pure move, no behaviour change: the editor half of `|` that the earlier change put at the end of selection_pipe.zig (PendingPipe, pipeMarker, submitPipe, pipeRequest, pipeFailed, pipeCut, pipeOutput, pipeResponse, and the eight pipe tests with nextPipeEffect) now lives in its own Pipe.zig, so the pipe's editor state can become Pipe's own fields next. selection_pipe.zig goes back to exactly what it was before this series: the native runner and the boundary values the shells hand to their workers. The moved code names the runner's types as `selection_pipe.X` again, as it did in pardes.zig; callers change from `selection_pipe.submitPipe(p, ..)` to `Pipe.submitPipe(p, ..)` (pardes.zig and the four shells). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Give the message log to Messages as its own fieldsGabriel Schneider3 days
| | | | | | | | | | | | | Not a pure move: state moves. Pardes's three message-log fields (`messages: [message_log]LoggedMessage`, `messages_head`, `messages_len`) become the fields of Messages.zig itself (`log`, `head`, `len`), and Pardes embeds one as `messages: Messages = .{}`. messageLog reads only the ring, so it now takes `m: *const Messages` and is called `p.messages.messageLog(i)` (Output's +Messages view and the log test); logMessage still needs the panes and the 9P event log, so it keeps `p: *Pardes` and writes `p.messages.log/head/len`. No behaviour change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move saving and loading a whole editor out of pardes.zig into dump.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | Pure move, no behaviour change (acme keeps dump and load in rows.c): dumpState, restore, initFromDump and initDump go verbatim to the end of dump.zig, after the dump format they read and write. Inside dump.zig the moved code's `dump.` prefix drops, so `Pane` there is the dump record; the one editor pane it names is spelled `pardes.panes.Pane`, and its other `panes.X` references become `pardes.panes.X` because dump.zig's own tests use `panes` as a local name. The methods become free functions taking `p: *Pardes`: `p.dumpState()` becomes `dump.dumpState(p)`, `core.restore(bytes)` becomes `dump.restore(core, bytes)` and `Pardes.initFromDump(..)` becomes `dump.initFromDump(..)`, in pardes.zig, the shells, layout.zig, Terminal.zig, builtins.zig and the tests (38 receiver rewrites plus the initFromDump calls). The tag-tail restore helpers stay with the tag code. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move exec out of pardes.zig into exec.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | Pure move, no behaviour change (acme keeps this in exec.c): execute, commandText, max_exec_depth, executeBuiltinLine, applyStartupConfig, runBuiltin and applySettingBuiltin; the getarg-style operand code (withArg, PointerOperand, pointerOperand, heldSelection, chordEachSel); takesCommandLine and the terminals commands run in (spawnTty, spawnV9fsTty, spawnTtyWithMount, evictLonePristineTty, replaceStillborn, ttyForDir); placeDoc; and the save path (submitSave, saveFile, saveTo, askWrite), with the acme-chords test, go verbatim to exec.zig. The methods become free functions taking `p: *Pardes`. executeBuiltinLine is called from ~170 places as `p.executeBuiltinLine(..)`, so Pardes keeps one declaration alias for it and those call sites stay; the other 104 calls change from `p.execute(..)` to `exec.execute(p, ..)`. ninep/ctl.zig has a local named exec, so it writes `pardes.exec.execute` instead of importing the file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move looking out of pardes.zig into look.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | Pure move, no behaviour change (acme keeps this in look.c): expanding the word under a click (ExpandedWord, expandedWord, expandedSel, cursorWordSel), the n/N walk (LookFrom, lookEdge, lookStand, lookWalkPanes, lookPast, wholeRowSpan, lookSpanIn, lookWalk, landLookSpot, noteLookSource, armLookWalk), search results (Search, SearchStart, submitSearch, lookFirstHit, runSearch, searchStep, jumpResult), the look-hover preview (LookHoverWait, LookHoverPreview, FileWordSpan, PdfWordPreview, invalidateLookHover, cancelLookHover, lookHoverPane, noteLookHover, refreshLookHoverFromRaw, advanceLookHover) and lookAt with its targets (focusPaneLine, selectSpan, openPaneTarget, focusPaneByPath, clearNavigationSelection, resolveLookTarget, locationText, canonicalLookLocation, pdfLinkLocation, followPdfLink), with five tests, go verbatim to the end of look.zig after its word and target resolution. The methods become free functions taking `p: *Pardes`; their 143 call sites change from `p.lookAt(..)` to `look.lookAt(p, ..)` (tests reach them as `pardes.look.x`). Inside look.zig the moved code's `look.` prefix drops. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Look no longer announces itself on the message rowGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | A look's answer is the pane it opens or the place it jumps to; its own name on the message row was only noise over that. Look now declares `pub const quiet = true;`, like Msg, so runBuiltin skips the announcement. A registry test checks that Look is quiet and Del still announces. 23 snapshot goldens drop the "Look" chip from the message row and nothing else: chordcut find hscroll images jumps layout-open look-center look-dir look-file look-file-1col lookloaded lsp lsp-client lspcomplete lsprelpath psearch stepgrain syntax tag tagbottomimage tagnav ttylook ttytaken. Updated with --update --jobs=1 on exactly those scripts; every changed row, compared screen by screen against the old golden, differs only by the blanked chip (and its style runs), and two full snap runs after the update pass. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the mouse out of pardes.zig into mouse.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | Pure move, no behaviour change: handleMouse with dragUpdate, dragRelease, seamIdxAt, reportGesture, gestureRange, dispatchPointerBuiltin, mirrorTtySelection and chordCutPaste, the pointer hit helpers (bodyHitForPane, sameTagCell, tagColumn, sameBodyCell), chromeTarget with ChromeTarget, paneAt, the Drag state type with clampBorderCol/Row, the seam test fixtures and thirteen pointer and drag tests go verbatim to mouse.zig. The methods become free functions taking `p: *Pardes`; their 36 call sites change from `p.handleMouse(..)` to `mouse.handleMouse(p, ..)`, including web.zig's chromeTarget and macos.zig's paneAt. The Mouse event type stays in pardes.zig with the rest of the event vocabulary. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move helix normal mode out of pardes.zig into normal.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | Pure move, no behaviour change: the key-to-role matching (isPrefix, roleBindingName, normalInput), multiOnce, the selection regex prompt (startSelRegex, selRegexArmed, applySelRegex), KeyState, Replay and replaySels, handleNormal, executeNormalAction and handlePdfNormal, with two tests, go verbatim to normal.zig. The methods become free functions taking `p: *Pardes`; their 22 call sites (handleKey, the mouse, look walk, lsp, runBuiltin, body drawing and edit.zig) change from `p.handleNormal(..)` to `normal.handleNormal(p, ..)`. test/lspbench.zig's second anchor follows its needle into src/normal.zig. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move text editing out of pardes.zig into edit.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | Pure move, no behaviour change (acme keeps this in text.c): which text a pane edits and how it maps to the screen (editText, editTextEol, setEditText, paneCursorLines, paneByteAtDisplay, pinPaneCursor, flatSurface, paneWrapWidth), insert mode (enterInsert, handleInsert, insertKey, insertTab, exitInsert, clampFileCursor), the d/c/y/p edit operations with replace, case, join, indent, comment, number, textobjects and surround, undo and redo, yank/clipboard/paste (setYank, setClipboard, ClipRequest, clipRequest, typeToTty, applyPaste, clipYank), and the pointer selections as text (PointerTextSelection, pointerTextSelection, capturePointerSelection, paneText, pointerSourceLine, selectionText, spanHas, currentSelText), with four tests, go verbatim to edit.zig. The methods become free functions taking `p: *Pardes`; calls change from `p.insertKey(..)` to `edit.insertKey(p, ..)` (pardes.zig, body_layer.zig, selection_pipe.zig, builtins.zig, test/hxdiff.zig, test/perf.zig). In executeNormalAction the `.edit => |edit|` capture becomes `|op|`, since it would now shadow the edit import. test/lspbench.zig's first anchor follows its needle into src/edit.zig. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move body drawing out of pardes.zig into body_layer.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | Pure move, no behaviour change: renderBody, renderBodyLayer, paintPointerSelection, paintSourceSelection, paintTerminalSelection and bodyText go verbatim to the end of body_layer.zig, which already held the body's layer geometry and hit-testing, so the pane body as the surface shows it is one file. The file doc now says so. The methods become free functions taking `p: *Pardes`; their 16 call sites (render and renderPane, the tag painter, selection and dump code) change from `p.renderBody(..)` to `body_layer.renderBody(p, ..)`. pardes.zig gains `pub const body_layer = @import("body_layer.zig");`; pointerSourceLine, selRegexArmed and cellBounds become pub because the moved code calls them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the message row out of pardes.zig into Messages.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | Pure move, no behaviour change: the Message stamp helpers, LoggedMessage, setStatus, setMessage, showMessage, dismissMessage, dismissLine, advanceMessages, advanceLine, messagesAnimating, MessageMotion, messageFrames, messageMotion, noticeText, noticeLife, blendRgb, logMessage, messageLog, reportError, the notice painters (leaderText, noticeCols, Printed, printRight), collectNotices, and the six message tests go verbatim to Messages.zig. The methods become free functions taking `p: *Pardes`. setStatus, setMessage and reportError are called from ~170 places as `p.setMessage(..)`, so Pardes keeps three declaration aliases (`pub const setMessage = Messages.setMessage;`) and those call sites stay as they are; every other call changes from `p.x(..)` to `Messages.x(p, ..)` (46 of them). The five shells' `pardes.Pardes.Message` become `pardes.Messages.Message`. The message ring's fields stay on Pardes for now; moving them into Messages is a separate change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the pipe's editor side out of pardes.zig into selection_pipe.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | | Pure move, no behaviour change: submitPipe, pipeRequest, pipeFailed, pipeCut, pipeOutput and pipeResponse, the PendingPipe they share, pipeMarker, and the eight pipe tests with their nextPipeEffect helper go verbatim to the end of selection_pipe.zig, so the whole of `|` (runner, boundary values, prompt, request and atomic edit) is one file. Inside that file the `selection_pipe.` prefix drops; the file doc now says it holds both halves. The methods become free functions taking `p: *Pardes`: the four shells' `core.pipeRequest(id)` become `selection_pipe.pipeRequest(core, id)`, and pardes.zig's two calls change the same way. pushUndo becomes pub because the pipe's edit calls it; PendingPipe.deinit becomes pub for Pardes.deinit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move colour themes out of pardes.zig into colors.zigGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | Pure move, no behaviour change: the Theme type, the curated palettes, fold and the themes ring with its two comptime checks, themeContrast, mix, ChromeTheme, ChromeAnimation and initial_chrome, the Pardes methods that load theme files and switch themes (nextThemeFileGeneration, requestThemeFile, ThemeFileRequest, themeFileRequest, failThemeFile, loadThemeFile, finishThemeInitialization, invalidateThemeDependentRasters, setThemeIndex), and the ten theme tests go verbatim to colors.zig. The methods become free functions taking `p: *Pardes`; their call sites change from `p.setThemeIndex(i)` to `colors.setThemeIndex(p, i)` (37 of them, in pardes.zig, builtins.zig, file_watch.zig, macos.zig and two test files). pardes.zig keeps `pub const Theme/themes/native_theme_count/ ChromeTheme = colors.X;` for the shells that name them, and sync and enterTagEdit become pub because a moved test calls them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the screen types out of pardes.zig into surface.zigGabriel Schneider3 days
| | | | | | | | | | | | | | Pure move: Color, FontRole, CellStyle, Cell, AsciiDiff, PanelCellDiff, ImageCacheKey, NativePlacement, ImagePatch, ImagePlace and Surface, with their eight tests, go verbatim to the new surface.zig with the imports they use. pardes.zig keeps one `pub const X = @import("surface.zig").X;` line per type, as it already does for Pane, Rect and the tag layers, so the shells' `pardes.Cell` / `pardes.Surface` spellings do not change. Surface.print, clearRect and overlayDash become pub because render calls them from pardes.zig. surface.zig is added to pardes.zig's test block and to the embedded source list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the Pdf namespace out of panes.zig into pdf_view.zigGabriel Schneider3 days
| | | | | | | | | | | Pure move: the body of `pub const Pdf = struct { ... }` is now the file pdf_view.zig, dedented, with the imports it uses; panes.zig keeps `pub const Pdf = @import("pdf_view.zig");`, so `panes.Pdf.x` callers are unchanged. The file is not Pdf.zig because pdf.zig (the MuPDF module) sits beside it and the two names are one file on a case-insensitive disk. pdf_view.zig joins the embedded source list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the Terminal namespace out of panes.zig into Terminal.zigGabriel Schneider3 days
| | | | | | | | | | Pure move: the body of `pub const Terminal = struct { ... }` is now the file Terminal.zig, dedented, with the imports it uses; panes.zig keeps `pub const Terminal = @import("Terminal.zig");`, so `panes.Terminal.x` callers and Pane's `Terminal.State` field are unchanged. Terminal.zig joins the embedded source list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the File namespace out of panes.zig into File.zigGabriel Schneider3 days
| | | | | | | | | | Pure move: the body of `pub const File = struct { ... }` is now the file File.zig, dedented, with the imports it uses; panes.zig keeps `pub const File = @import("File.zig");`, so `panes.File.x` callers and the `File.State` field of Pane are unchanged. File.zig joins the embedded source list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the Output namespace out of panes.zig into Output.zigGabriel Schneider3 days
| | | | | | | | | | | Pure move: the body of `pub const Output = struct { ... }` is now the file Output.zig, dedented, with the imports it uses; panes.zig keeps `pub const Output = @import("Output.zig");`, so `panes.Output.x` callers are unchanged. decorateRow and RowDecoration.styleAt become pub because File calls them from another file now. Output.zig joins the embedded source list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the Image pane namespace out of panes.zig into image.zigGabriel Schneider3 days
| | | | | | | | | | | | | Pure move: the body of `pub const Image = struct { ... }` now sits at the end of image.zig, dedented, under a "the image pane" banner, so the image thing (decoding, glyph art, and the pane that shows it) is one file. Its nine `image.X` self-references lose the prefix; image.zig gains the four imports the pane code uses. panes.zig keeps `pub const Image = @import("image.zig");`, so `panes.Image.x` callers are unchanged. image.zig joins the embedded source list, since the pane code left panes.zig which was on it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Move the Mini namespace out of panes.zig into Mini.zigGabriel Schneider3 days
| | | | | | | | | | Pure move: the body of `pub const Mini = struct { ... }` is now the file Mini.zig, dedented, with the imports it uses; panes.zig keeps `pub const Mini = @import("Mini.zig");` so every `panes.Mini.x` caller is unchanged. Output.resetBody becomes pub because Mini calls it from another file now. Mini.zig joins the embedded source list in fs.zig. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Answer a held read by its cloud9 ticket, refuse a second, and say a pane ↵Gabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | shut down The held read is now kept by the Ticket cloud9's Conn.hold() gives its park and answered through Conn.answerWith(), which makes the answer only while that very park still waits, instead of walking the engine's slots by tag; pardes no longer reaches into the engine for it. A second read on an open whose read is held fails with file in use rather than sitting parked where nothing answers it, and a read that waits with no open record to hold it is logged and asserted on. A read on an event or pty/data open whose pane closed answers acme's "window shut down" (editors/acme/xfid.c:1005). The pane keeps its run's and its lock's open handles, checked through openOf on use, not record indices. Docs: lock from a shell needs a held fd, and a command that clears the screen may read as cut. Needs cloud9 zvuqvnzy (cca47d63), which adds Conn.hold, waiting and answerWith; build.zig.zon still pins 82d8152c until that is pushed and re-pinned. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* pty/run says cut when its output's start is gone, and reads only a bounded tailGabriel Schneider3 days
| | | | | | | | | | | | A clear or reset while the command ran, a start or end mark that came on the alternate screen, or a garbage end pin read as a complete answer; now any of them, like a start that scrolled out, answers exit N cut. The output is read from at most output_rows rows above its end instead of the whole history on the editor's thread (cut if that clips it), a D with no status answers exit ? rather than exit 0, and running out of memory answers error out of memory rather than cut. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A pane's ctl takes acme's lock and unlockGabriel Schneider3 days
| | | | | | | | | | | | A client doing an edit of several writes to addr and data had no way to keep another client's from landing in between. acme's window ctl takes lock and unlock for this (editors/acme/xfid.c:603-611): a qlock that blocks a second locker, owned by the fid that wrote it and given up when that fid is clunked, binding only clients that ask. pardes does the same: the open's record holds it, a second lock parks until unlock, close or the pane closing, and no other write is refused for it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Keep what each open holds in one table of open recordsGabriel Schneider3 days
| | | | | | | | | | | | | Snapshots (and /log's cursor), runs, and the reader_handle constants for event and pty/data each reused the open handle and each validated handle and node on its own. Now p.fs.opens is one table of 64 records, each the node it was opened on and a tagged union of what it holds, like lib9p's per-fid aux and acme's Fid (editors/acme/dat.h:373-385): one lookup (openOf), one release, ENFILE when full. A held read lives in its open's record, so it goes with the release. Opens that hold nothing answer handle 0 and take no record. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Keep a pane's filesystem state on the Pane instead of a slot-indexed tableGabriel Schneider3 days
| | | | | | | | | | | addr, limit, the event queue and its readers, pty/data's queue, a waiting run and the /log announcement flag lived in p.fs.panes[slot], reset when the pane went; a reused slot is how the bug class came in. acme keeps the same state on its Window (editors/acme/dat.h:226-266), so now Pane.fs holds it: announced, retired (noteRetire, which still answers a waiting run and drops the listener count first) and freed with the pane itself. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Hold a read that has nothing yet and answer it when its file has newsGabriel Schneider3 days
| | | | | | | | | | | | | | A following log, event, pty/data or a pty/run before its answer used to answer .again and wait for a wakeAll, which only the parked-write path asked for, so the band-aid had every queue push set turn.parked. Now the core keeps such a read (ctlfs.hold) and, as the turn is given up after anything that queued a record, ran a command out or closed a pane, answers it on its own connection, the way factotum answers the log reads it keeps and acme an event read. Only a read the engine still holds parked is answered, because cloud9 tells the backend nothing of a Tflush, so a flushed read spends no record. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* pty/run answers with the command's output; pin cloud9 with 9ns concurrency ↵Gabriel Schneider3 days
| | | | | | | | | | | | | | | | | | and E.BUSY The answer is now the header line (exit N, or exit N cut M when only the last 64 KiB were kept, or exit N cut when its start scrolled out of the history), then what the command printed: the screen text between its C and D marks, which the marks handler pins so scrolling keeps them. Also from review: a plain /log open honours its offset until follow is written, so tail -n and less work; whitespace-only run lines are refused. cloud9 is pinned at 82d8152c: 9ns keeps up to 32 requests in flight per mount so a waiting read no longer freezes the rest of it, and E.BUSY replaces the errno pardes spelled locally. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Make pty/run and /log streams, and take refused lines back cleanlyGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | Review fixes to pty/run and /log: - Both are streams with a per-open cursor. A shell's exec 3<>file shares one offset between write and read, so cat <&3 after echo make >&3 asked for offset 5 and got "0" instead of "exit 0"; log after follow lost its first bytes the same way. - A run is accepted only in pardes's own tagged input phase, so a nested shell's prompt (ssh, a shell with its own integration) is never taken for this shell's. - A line the shell refused is taken back so the next finds the prompt clear: bash's continuation prompt (now tagged k=c) gets Ctrl-C and the answer waits for the fresh prompt; a bash syntax error (no C, but a D) is answered at once; fish's kept line gets Ctrl-U (a Ctrl-C sent while fish redraws is lost). fish's right prompt no longer reads as typed input. - bash marks a command's start from PS0 on bash 4.4+, not a DEBUG trap, so a user's own DEBUG trap (bash-preexec, atuin) survives. bash's own job notices now show as in plain bash (ttyfollow golden). - 32 run slots; a new line on an open starts a fresh answer. Checked live in bash, fish, and fish with a right prompt. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Run a line at a terminal's prompt through pty/run and read how it endedGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | /pane/<n>/pty/run takes factotum's rpc shape: write one line on an open, read the answer on that same open. The answer is exit N once the command ended and the shell is back at a prompt; busy at once when a command runs, text is typed at the prompt, or the shell has not drawn its first prompt; error not run when the shell refused the line without running it (fish on a syntax error keeps it on the prompt, so it is taken back with Ctrl-U); error shell gone when the pane closes or its shell is replaced; error no prompt marks for a shell pardes could not instrument. The end comes from the shell's OSC 133 marks. ghostty parses D's exit status and drops it, so the stream now runs through a handler that wraps ghostty's and follows prompt -> input -> running -> done. The marks pardes injects into bash and fish carry aid=pardes and only those count, so fish 4's own marks (which doubled ours), a nested shell's, and a stray 133;D in printed output are ignored. Checked end to end against real bash and fish: false, exit 7, a syntax error, sleep, busy while running, and the pane closing mid-command. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Wake reads waiting on /log, event and pty/data; one reader per consuming ↵Gabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | file; pty/ctl reads back A read parked on a queue was retried only when some unrelated write had to wait, so a follower of /log (and a reader of event or pty/data) slept until then. Pushing a record now marks the turn parked, and giving the turn up wakes them (measured: stuck past 3 s before, 0 s after). event and pty/data consume what they read, so a second open for reading is refused with rio's "file in use" (EBUSY through 9ns); writers still get in, and pty/data queues output only for an actual reader. pty/ctl reads back "winsize C R", in the words it takes. /log fixes from review: a record longer than a read comes in pieces (a shell read loop failed on long lines), every repeated message is logged, a record bigger than the ring is cut to fit instead of emptying it, the ring is reserved at boot so recording never allocates, and panes present at boot are recorded first. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Record /log whether or not anyone reads it, with the editor's messages in itGabriel Schneider3 days
| | | | | | | | | | | | | | | /log is now one ring (64 KiB, 4 KiB on the board) that records new, del, rename and save, and a msg line for everything the editor says. An open freezes it, so cat log shows what happened lately and ends; writing follow to that same open makes reads past it wait for newer records, and a follower the ring outran reads lost N first. The message log keeps the pane's serial, not its reusable slot. A test now fails when the tree serves a file /README never mentions; it caught pty/status, now documented along with typing through pty/data. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Report each command's exit status in the shell's end-of-command markGabriel Schneider3 days
| | | | | | | | bash and fish now print OSC 133;D;<status>, which ghostty already parses; the planned /pane/<n>/run file reads it to answer when a command finished and how. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Replace a stillborn placeholder pane when a pane joins its column, and focus ↵Gabriel Schneider3 days
| | | | | | | | | | | | | the pane a directional Del grows A pane that joins a column whose only other pane is an untouched empty scratch (Newcol's, or the stand-in a closed column leaves) takes the column whole. Checked at the end of the step, since the pane that asked for the joiner is often the placeholder itself. Del k|j focuses the pane that took the rows. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Keep the arena buffers out of Debug binariesGabriel Schneider3 days
| | | | | | | | The five arena buffers are zeroed globals instead of undefined fields, so a Debug build keeps them in .bss rather than writing 160 MB of 0xAA filler into every binary (Debug pardes 839 MB -> 246 MB). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Upload only the atlas rows new glyphs were drawn into, not the whole atlasGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Any new glyph re-uploaded the whole 2048x2048 atlas: 4 MB copied into the transfer buffer and then to the texture, about half a millisecond of CPU on the frame, and the GPU's copy on top. Glyphs fill the atlas pen-style, a band of rows at a time, so a frame's new glyphs sit in a band a cell or two tall. The atlas now keeps the rows drawn into since the last upload (cachedGlyph widens them by each raster's rows: plain glyphs, ligature strips, tagline and grip glyphs alike), and uploadAtlas copies and uploads just those rows, at the same offset in the transfer buffer as in the stage; the texture keeps the rest. A reset (a font, size or tagline change) marks every row, as the texture's first upload does, so those still send the whole atlas. A test replays uploads over glyphs of every kind and a reset, and checks that no row outside the dirty rows ever differs from what the texture holds. Hidden captures of 27 layouts, including new glyphs typed and printed, font and size switches, and Ligatures off and on, are byte-identical to before. Over 10 interleaved rounds (160x50 cells), a frame that draws new glyphs spends 12-50 us uploading instead of about 0.5 ms: a terminal printing twelve new glyphs a line goes from 1.04 to 0.55 ms of renderFrame CPU on such frames (-34% over the scenario's frames, 9 of 10 rounds faster, -20% to the GPU fence), a typed glyph's frame from 0.89 to 0.38 ms, and opening a file of 600 new glyphs from 9.1 to 8.7 ms. The first frame and a font size change still upload everything and cost what they did; other frames are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Find the grid cells under layers once a frame, not by scanning every layer ↵Gabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | for each cell renderFrame skips the grid cells a body or tag layer paints itself, and it asked which those are cell by cell: for every cell, twice a frame (counting instances, then emitting them), bodyLayerAt and tagLayerIn walked all 16 body layers and all 119 tag layers. In a ReleaseFast profile of a full screen of src/pardes.zig those walks were about a third of the gui's CPU samples. coverLayers now fills each live layer's rectangle into a per-cell map once a frame, and the loops read one byte. The loops only ever asked whether some layer covers a cell, never which, so overlap order does not enter into it; the map marks exactly the cells the walks found: a tag layer only its viewport's first row, and a viewport past the grid only the grid's part of it. The loops over the layers a panel transition leaves get a second map, built only while one runs. A test holds the map to the old walk over a core's layouts (columns, stacked and collapsed panes, column tags, Tagbottom, message and leader chips) and 3000 random layer sets, some past the grid's edges or at the top of u16. Hidden captures of 27 layouts (message and prompt chips, a column grip rail, a PDF, an image, font and tagline changes), with and without leftover edge pixels, are byte-identical to before. Over 10 interleaved rounds (160x50 cells), renderFrame's CPU a frame falls from about 2.0 ms to 0.32-0.37 ms idle, scrolling, typing, under terminal spew and over a PDF (-82 to -84%, every round), and cell emission from about 1.07 ms to 0.27 ms. The first frame's CPU goes from 6.2 to 4.6 ms. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Turn ligatures off with a Ligatures setting, in the shell that shapes textGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | The SDL shell shapes words with HarfBuzz, so a font's `->` and `!=` draw as ligatures, and there was no way to have the plain glyphs back short of changing font. `Ligatures` is a toggle, on by default; off, no cell goes to the shaper and every cell draws its own glyph, exactly as a font without ligatures does. It exists only where it means something. A new `ligatures` capability, true for the gui shell alone, gates it like Font and WindowOpacity are gated: it is not a builtin elsewhere, has no leader path, and Config does not list it, rather than print a row the TTY could never change. macOS draws CoreText ligatures of its own, but nothing there reads the setting, so it stays off there. The table check that kept every toggle unconditional now lets the ligatures toggle, and only it, carry a capability, and requires that it carry `ligatures`; every other setting's rule is as it was. The gui keeps the setting beside its text caches, which were resolved under it: when the core's value changes, the per-codepoint cells (which record whether a cell is shaped) and the shaped words are dropped, and the frame the toggle asked for draws every cell again. The atlas keeps its glyphs: plain ones draw either way, and a ligature's strip is reused when it comes back. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Track Maple Mono, so the ligature tests always runGabriel Schneider3 days
| | | | | | | | | | | The GUI's ligature tests shape `->` and `==` with Maple Mono NF, the only ligature font in assets/, but the font was ignored, so a checkout without a private copy skipped the tests or ran them without their Maple half, and passed. The font is SIL OFL 1.1, whose licence is already tracked beside it, so it is committed and the tests now fail when it is missing instead of quietly passing. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Allocate from libc's malloc in every release shell, and check macOS's Debug ↵Gabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | build too The tty, gui and detached shells allocate from std.start's gpa, which is a DebugAllocator in Debug and, because we link libc, libc's malloc otherwise. The macOS shell hardcoded std.heap.smp_allocator in every mode, so its Debug build checked nothing it allocated outside memory.zig's subsystems. Which allocator a release build should use was measured rather than assumed, in ReleaseFast, with an experimental gui that chose at startup between glibc's malloc and smp_allocator, with and without memory.zig's stack-fallback buffers, plus an A/A pair; ten interleaved rounds, compared round by round. The gui frame-cost harness (idle, scroll and typing in src/pardes.zig, terminal spew, wheel-scrolling docs/design.pdf, first paint) saw every candidate within the A/A pair's noise. Twelve rounds of the allocation-heavy paths themselves, highlighting all of src/pardes.zig (305k tree-sitter allocations) and pardes-pdf-bench's MuPDF renders, split them: smp_allocator was 2 to 6% slower on MuPDF's page-sized rasters (slower in 9 to 11 of 12 rounds), no better on tree-sitter, and the stack-fallback buffers changed nothing either way. So glibc's malloc it is, with the buffers kept. macos.zig now takes a DebugAllocator in Debug, deinitialized in pardes_deinit with leaks logged the way std.start treats the others', and libc's malloc otherwise. main.zig says why init.gpa is kept. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Check each subsystem's memory under one DebugAllocator, not twoGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | In a Debug build memory.zig put each subsystem allocator (core, frame, lsp, tree-sitter, image, PDF) under a DebugAllocator of its own, which is what reports that subsystem's leaks and makes memory.deinit panic on one. Beneath it, past the fixed buffer, was the allocator the shell passed in: init.gpa, which std.process.Init already makes a DebugAllocator in Debug. Every block that spilled out of a fixed buffer was tracked, checked and given stack traces twice, and in a test both layers sat on std.testing.allocator. A Debug build now puts the page allocator beneath the subsystem DebugAllocators, so each allocation passes through exactly one. The shells' own gpa is still std.process.Init's DebugAllocator for everything else, and a release build is unchanged: the subsystems sit on the gpa, no DebugAllocator anywhere. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Allocate SDL's memory from the gui's Zig allocator, and give it all back on ↵Gabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | | the way out SDL allocated with libc malloc. runNative now installs the shared C heap over the gui's gpa with SDL_SetMemoryFunctions before SDL's first call: SDL frees with whichever functions are current, so none of its memory may predate them. SDL allocates from its own threads too, and the gpa is thread-safe in every build (a DebugAllocator in Debug, libc's malloc in release). That put SDL in a Debug build's leak report, and the report filled up: 1769 blocks at exit, because runNative never released its GPU objects, its window or SDL itself. The process was about to exit, but a report that long hides any leak that matters. runNative now releases every pipeline, texture, sampler and buffer the Gui holds, closes the gamepad, destroys the GPU device and the window, and calls SDL_Quit, all through defers. Eight 32-byte blocks are left, and they are SDL's: 3.4.4's VULKAN_INTERNAL_DestroyCommandPool never frees two arrays per command buffer (buffersUsedInPendingTransfers and texturesUsedInPendingTransfers), which SDL's main branch now frees. They go away with the next SDL; a comment at the SDL_Quit says so. The frame-cost harness and a software-present run (PARDES_SOFT_PRESENT) both run and exit cleanly in Debug. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Allocate tree-sitter's external scanners from the same Zig allocator as its ↵Gabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | runtime syntax.zig points the tree-sitter runtime at a Zig allocator, but the grammars' external scanners did not follow it. A grammar's tree_sitter/alloc.h maps ts_malloc to the runtime's current allocator only when TREE_SITTER_REUSE_ALLOCATOR is defined, and plain malloc otherwise, so every scanner's state and its arrays came from libc. Five scanners (bash, markdown, markdown_inline, python, typst) also call malloc, calloc, realloc and free by name. build.zig now compiles every grammar with TREE_SITTER_REUSE_ALLOCATOR and forces in src/tree_sitter_heap.h, which includes stdlib.h and then defines the four names as calls through ts_current_malloc and friends. A header forced in from the build rather than a patch to the grammars keeps zig-pkg pristine. No grammar archive refers to libc's allocator any more; only the runtime does, for its defaults. A scanner frees only what it allocated itself, and the syntax tests that create and destroy a parser for every grammar under a leak-checking allocator pass. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Hand every C library's allocator hook the same C heapGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | C's malloc, calloc, realloc and free over a Zig allocator were written three times: syntax.zig's for tree-sitter, pdf.zig's for MuPDF, and gui.zig's for FreeType and HarfBuzz. They are now one, src/c_heap.zig's Heap. Each hook names the allocator variable its heap reads; gui.zig exports its heap under the ui_malloc names font.c and HarfBuzz call. It is a module of its own because pdf.zig is the MuPDF module, and a file cannot belong to that module and the core's at once. A block starts with a 16-byte header holding its size and the allocator that made it, so it goes back where it came from even after the heap is repointed, as the fonts' copy did. The fonts' copy carried the 16-byte Allocator itself in a 32-byte header; here the header holds the allocator's index in a table of every allocator a heap has used (entries are published once and never change, and a heap remembers where its current one was last found). The difference is measurable: with the 32-byte header, pardes-pdf-bench's filtered page render at 96 dpi took 1.1% longer than before this change, slower in 15 of 16 pinned rounds in two separate runs, and highlighting all of src/pardes.zig (305k tree-sitter blocks) up to 0.6% longer, against an A/A pair within 0.7%. With the 16-byte header both are back within the A/A pair's noise. Two other things change. For tree-sitter and MuPDF, a block made before the allocator was repointed now goes back to the one that made it rather than through the current one. For the fonts, realloc(block, 0) now frees and returns null, as glibc's does and as the other two copies already did; neither FreeType nor HarfBuzz asks for it. The heap's test replaces the tests of the old copies. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Ease a message into its row fast first, its colour ahead of it, and out slow ↵Gabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | first A message fell in on t*t, an ease-in: it hung near its start for the first frames and was still moving at its fastest when it hit the row, a sixth of a row in its last frame. It now decelerates into place with an ease-out cubic, slide = -(1-t)^3: a quarter of the way in the first frame, and the last frames only settle it. There is no overshoot. At a row's height and the dozen frames of the 180 ms fall, a follow-through is a pixel or two, which reads as a jitter rather than a bounce. MessageFall keeps its 180 ms: the ease-out puts the chip where it is going within about 100 ms. The colour leads the motion: alpha = 1 - (1 - min(1, 2t))^2, whole by half way, so what lands is already legible. The SDL shell used to show the chip solid while it slid and fade it only on the way out; it now fades it up with the same alpha, and the rule under a chip carries the chip's fade (a new TagLayer.fade) instead of standing solid while the chip comes or goes. The dissolve was an ease-out, (1-t)^2: it lost a tenth of its colour in the first frame and spent its last frames nearly invisible. An exit eases in instead, alpha = 1 - t^2 over the same 300 ms: it lets go gently and leaves faster and faster. t is now counted to the last frame, so that frame shows it gone rather than it blinking out from 0.3%. 1 - t^3 was the other candidate; in captures it held the chip nearly untouched for half the dissolve after the linger had already held it, then dropped a sixth of its colour a frame, which reads as a pop. It stays in place: the lines stacked around it do not move, and a drift would read as it leaving its row. The test pins the shapes: a fast first step into the row, never past it, solid by half way; a dissolve whose first drop is smaller than its last and which ends at exactly nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Ask from the keyboard which neighbour a closed pane's rows go toGabriel Schneider3 days
| | | | | | | | | | | | | | | | | | | | | | | | | | Del takes a side: `Del k` gives the closed pane's rows to the nearest expanded pane above it, `Del j` to the one below, each falling back to the other side when it has none. DelAbove and DelBelow are those two lines, with no path of their own under SPC. A bare Del started from a key -- SPC d, Enter on the tag word, a row run from an output buffer -- on a pane with expanded panes both above and below asks instead of guessing. The question is a prompt like Save's or a search's (Pane.Prompt.del_side), so it is painted on the pane's notice band by the same path, and the next key answers it before any mode sees it: k or Up, j or Down, anything else keeps the pane, as does a click. Only a key press sets Pardes.can_ask, so a click, a 9P ctl or event write, a startup line, a restore and a shell exiting all close the pane at once, the rows going where layout.absorbVWeight has always sent them. A collapsed pane is not asked about (it has only a tag row to give), and collapsed neighbours are passed over (layout.expandedNeighbor, which Collapse now uses too). removePane and absorbVWeight take the recipient; every other caller passes null. Three scripts that closed a middle pane with SPC d answer k, which is where the rows went before, and their goldens are unchanged. delask.snap covers the question, Esc, j, a clicked DelBelow and a clicked Del. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>