summaryrefslogtreecommitdiff
path: root/src
Commit message (Collapse)AuthorAge
...
* a new file splits the doc column you were last working inGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | placeDoc picked its source by SLOT INDEX — it scanned panes from 0 and took the first doc it found — so which column your file landed in depended on internal id allocation, not on where you had been. It now walks the jump stack backwards to the newest live doc pane. No new state: jumps already IS the record of where the keyboard has been, and Pane.serial is already the "this slot was reused" test. The slot-order scan stays as the fallback, deliberately: a restored dump has one jump entry, and if that pane is a shell the stack knows no doc — without the fallback the next file would claim a second column with docs already open. lookpanes moved: a file now lands under the doc that was actually last read rather than under whichever doc happened to hold the lowest pane id. New golden doccol pins the case the line exists for, and it fails at exactly one capture with the walk disabled.
* Help lists every builtin and every way to reach itGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Not a second builtin: Help already rendered "every builtin with a leader path and the key that runs it", and the ask is "every builtin and every shortcut" — the same listing with three more rows and one more column. A new builtin would have been a second renderer over a superset of the same rows, and the two would drift the first time a column moved. The prefix filter is now the degenerate case of the index rather than a separate mode: SPC h ? reads the same array with prefix "h", and the path-less builtins fall out on their own because an empty path starts with nothing. The shortcut column is folded, never typed: chordName spells a chord from Key's OWN declarations, so a new special key names itself and an unnamed non-printable codepoint is a compile error instead of garbage; shortcuts() walks the cmd columns of window_keys and jump_keys, look_cmd/exec_cmd with their keys and buttons, and topbar_str. Retarget a binding in config.zig and this re-renders. So Look reads `enter, right-click`, Back reads `C-o`, Kill reads `topbar`, and Theme reads nothing at all — which is the information. Not a picker, deliberately: n/N stepping a list holding Kill, Del and Delcol would end the session. The names are live text, so middle-click the one you want. Alt-n, Alt-c, the tty toggle and the cut/paste chords cannot appear — they are inline handlers that reach no builtin, so there is no row to put them on. Make one a struct with a cmd column beside its binding and it appears for free. Three goldens moved, all of them the +Help buffer's contents.
* themes: one file each, generated from helix and zed, picked by stepping a listGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | | | | Each theme is a .zig file of pure data and nothing enumerates them by hand — fold() walks the container's declarations, so a theme is a file and that is the whole registration. Field by field rather than a wholesale coercion, which makes a missing field a compile error that names it. tools/gen_themes.zig reads helix .toml and zed .json out of vendor/themes and emits one .zig each; build.zig reads that directory, so adding a theme is dropping a file in. Output goes to the build cache rather than the tree, so zig owns the freshness check and a deleted source cannot leave a stale theme behind. Vendored, not read from the genizah: the build stays offline. A source it cannot map is a hard error naming the file and the key, never a silently black-on-black theme. ThemeSel is the clever half. Traits gained an `executes` column, so n/N over that buffer hands the whole line to Exec instead of the leading word to Look — both arms the ordinary builtin, so a stepped row does exactly what the matching mouse button on it would. Stepping the list previews each theme live. The trait is a pane property, not an output-pane branch, so any pane whose lines are commands can opt in. Goldens: leader gains SPC t t; theme's fourth NextColor no longer wraps to helix because the ring is fifteen long. New: themesel.
* focus history is a stack of locations, and the Jumplist is that same stackGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | | One container, not two: focus_hist (a stack of pane ids rebuilt every sync) is now jumps[] + a current pointer, and the +Jumps buffer is a RENDERING of that array — nothing copies it, nothing shadows it. prevFocus, Toggleterm, Look's directory order, Back/Forward, Last and Jumplist all walk the one list. A pane id is reused, so a location that only remembered an id would retarget after a respawn: panes now carry a monotonic serial and an entry whose slot holds a different serial is dead. trackJump compacts those out and fixes the pointer in the same pass. The push rule lives in ONE place and says: a location is worth remembering when you cannot see it any more — a different pane, or more than a bodyful of rows away in the same one. So hjkl never grows the list and 100G, a search hit and a goto-definition do. Ctrl-o/Ctrl-i walk it, SPC j j toggles the last two, SPC j l lists them. Ctrl-i IS Tab on a legacy host, where the binding simply never fires and Tab-executes is untouched; kitty reports them apart. SPC j o/i work anywhere. Two goldens moved, both the SPC ? Help listing gaining four rows.
* Look and Exec are builtins like any otherGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | Their only special feature is now the keys they are assigned. `Look main.zig` typed in a tag is the same look a right click is; config.look_cmd/exec_cmd point the two buttons and Enter/Tab at them, so retargeting Enter to Grep is one line. actOnSelection — a hand-written cascade with the builtin dispatch nested inside it — is gone, and the `button` parameter it threaded through five KEYBOARD call sites went with it. New syntax, spelled once in config.zig: @`ls -la` names a command to run rather than a file to open. Word expansion takes the quoted run whole, the way acme does for its own </|/> words, so a click inside one does not hand Look the fragment `ls`. And it nests: @`Look .` unwraps, re-enters the dispatcher, and looks at the directory. Guarded at depth 8, which is unreachable today (every re-entry strips a word or a delimiter pair, so the string strictly shrinks) and exists for a future syntax that does not shrink. leader_path is now optional: a builtin may have no SPC path when its shortcut is a key and a mouse button. New golden cmdword; the other 59 unmoved.
* output panes remember the command that opened themGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | An output buffer was a file pane with a flag, and everything special about it was re-derived from outside by asking the wrong question: the buffer NAME ("+Search", "+Help"), a parallel search_kind field, and f.output predicates sprinkled through kind-agnostic code. Now it carries its Origin — the Builtin, or the lsp.Kind for the five gotos helix binds as motions rather than words — plus that command's argument, and one Traits table answers every question the scattered code used to ask: what it is called, whether n/N steps it, whether it is a doc for layout, whether Esc dismisses it, whether it saves. The name is derived FROM the command instead of being the thing that identifies it. Adding a kind of output pane is adding a row. No golden moved.
* config.zig: every binding and every piece of Look syntax in one fileGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | Retargeting a key, a mouse chord or a spelling is now an edit to src/config.zig and nothing else. Three parts in reading order: pardes's own bindings (where a reader lands), the Look/Exec syntax, then the helix keymap under a banner saying hxdiff/hxparity are differential suites against real helix, so a key moved there is a divergence and not a tweak. The bindings are data a comptime loop can walk, because the builtin index is going to walk them. Duplication this cut: is/isC/isA became one hit(key, chords) over ~132 call sites, and a binding being a LIST collapses the letter-or-arrow chains; swap_enter_tab is gone, replaced by look_key/exec_key that can point anywhere; the four focus builtins' h/j/k/l lived hardcoded in two places and is now one table. No golden moved.
* structure: backends into src/{tty,gui,lsp}, pane kinds and builtins into ↵Gabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | their own files The core now lies FLAT at src/ and every subdirectory is one backend, so a file being in no directory at all is what says it is core. Pane-kind bodies leave pardes.zig for term_pane.zig / file_pane.zig / output_pane.zig, leaving it the layout, the event/effect machine and the generic render loop. Builtins are one struct each in builtins.zig, and the enum is folded out of the file's own declaration list at comptime — a zig file IS a struct, so the list of builtins and the builtins themselves are the same text. Adding one is writing a struct. Key paths deliberately stay one table for the config pass. Pure refactor: no golden moved.
* openLink: hand URLs to the desktop opener (xdg-open / open) via double forkGabriel Schneider2026-08-01
|
* lsp: writer seam, ZLS introspection builtins, ctrl-click goto, SPC l groupGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | THE SEAM TAKES A WRITER. `lsp.query`'s `out` is a `*std.Io.Writer`, not a `*std.ArrayList(u8)`. The shell owns the buffer behind it (an `Io.Writer.Allocating`), so a backend never allocates the result, never frees it, and cannot get the allocator wrong — the invalid-free class of bug has nowhere left to live. It also deleted a parameter from five functions: they only ever took a `gpa` to allocate rows, and 0.16's unused-parameter error found every one. `lsp.row()` lost its allocator argument too. Since a Writer cannot rewind or be counted, `query` renders into a scratch Allocating first: the log wants an exact row count, and `explain` throws the rows away and prints narration in their place. INTROSPECTION. `SPC l i` (Lspinfo) and `SPC l w` (Lspwhy), in the `l` group that now holds every language command (see below). They exist because of the seam's own contract: a backend never fails loudly, which is right for an editor, but it makes a broken backend and a correct one that found nothing look identical from the outside. Every query now leaves a record — kind, file, offset, duration, row count, and THE ERROR `run` returned, which `catch {}` swallowed and which was visible nowhere. Lspinfo prints those, plus which ZLS is compiled in, which zig lib dir and whether it actually opens (the usual cause of "gd does nothing in std"), and what the backend answers versus refuses. It answers from ANY pane, including one with no file, because it is about the backend — which matters precisely when the pane you are sitting in is the problem; both shells now send status for a file-less pane. Lspwhy narrates the REAL resolution path. The trace is threaded through `goto` itself, so what it prints is the position context the analyser returned and the branch that actually stopped. A debug view that re-derives the logic beside it is one that can disagree with it. CTRL-CLICK IS gd. Mouse gained a `ctrl` field, set by both shells (SDL asked directly via GetModState rather than read off key-event bookkeeping, which a click with no prior keypress would miss). The flag rides the drag rather than firing on the press: a click does not place the modal cursor until RELEASE, so a query asked at press time would answer about wherever the cursor previously sat. A ctrl-DRAG still selects. The snapshot DSL gained a `ctrl-` button prefix (SGR bit 4, what a terminal sends and what vaxis decodes). test/snapshots/lspdebug.snap covers all three, including a PLAIN click in the same spot that must NOT jump — without it the test would pass on a bug that made every click a goto. Durations cannot live in a golden, so PARDES_LSP_NOTIME (set by the harness, like PARDES_DUMP) omits them. 58 snapshot scripts, hxdiff 360, hxparity 440, unit 46, gui build: all green. lspbench: 17/17, 0 false claims. THE WHOLE LANGUAGE GROUP LIVES UNDER SPC l. pardes keeps its own leader letters back. `SPC d` is Del again, `SPC k` is Kill, `SPC s d`/`SPC s r` are Dump/Restore and `SPC h t` is Tutor — exactly where they were before the language work touched them. The previous pass put the LSP commands on helix's bare `<space>` letters and moved pardes's builtins out of the way (Kill k->q, Del d->wc, Dump/Restore s?->f?, Tutor ht->T). That was the wrong trade. Those five are the most-pressed keys in the editor and predate the language work; an LSP command is something you reach for deliberately and can afford one keystroke more. So every LSP command keeps HELIX'S OWN LETTER and gains the `l` prefix: `<space>k` -> `SPC l k` (hover), `<space>d` -> `SPC l d` (diagnostics), r/a/h/s/S/D likewise. Nothing to re-learn but the prefix, and `Lspinfo`/ `Lspwhy` were already there. THE GOTOS ARE UNTOUCHED. `gd` `gD` `gy` `gi` `gr`, `]d`/`[d`, `]D`/`[D`, `=` and ctrl-click all stay exactly as helix has them — they never collided with anything, so there was never a reason to move them, and they are the ones you actually press mid-edit. leader.snap is restored to the pre-LSP script (its `key q` unmapped-key step works again now that Kill is back on `k`) plus one new step for `SPC l ?`. Its `SPC ?` root listing had to stop waiting on Restore: the full list grew to 33 rows and row 21 falls off the pane, so it watches an early row instead. DEPENDENCY IMPORTS NOW RESOLVE. `gd` on `@import("vaxis")` opens vaxis's root file; before, it silently did nothing while `std` worked perfectly. The asymmetry was not a wiring mistake. ZLS's uriFromImportStr answers exactly three ways: a relative `.zig`/`.zon` path from disk, `std` from `zig_lib_dir` (one directory, which we supply), and EVERY OTHER NAME only by running `zig build --build-runner` to discover the module graph. That last branch needs `zig_exe_path`, which this backend sets to null on purpose — so every dependency import returned `.none`. Confirmed twice over: in ZLS's source, and by `SPC l w` on the import string, which printed the STOP line naming exactly that branch. (The introspection builtin diagnosing its own backend on its first real outing is a decent argument for having built it.) We never needed a compiler for this: build.zig IS the module graph. It folds `root_mod.import_table` into a name -> root-source-file table at configure time and passes it as a build option; the backend consults it precisely where ZLS gave up. Correct by construction — a dependency added or renamed in build.zig cannot forget to update it — and it costs no subprocess, no build step and no runtime work. `SPC l i` now lists the table, since "is this name even importable" is the first question when a jump does nothing. Two limits, both stated in the code: a module whose root is a GENERATED file is skipped (it has no path until make() runs), and a file inside a dependency importing that dependency's OWN internal module name is still a miss — that would mean running its build.zig. TRAP: the table is folded out of root_mod.import_table, so `addOptions` had to move BELOW every `addImport` call. Attached where it was, the table is empty. TOPBAR GAINS `Help`, WHICH IS WHY `SPC ?` LOOKED BROKEN. A bare `pardes` boots straight into tty mode (main.zig: `args.len == 1`), where every printable key belongs to the shell — so SPC never reaches the leader, and `SPC ?`, the one thing that would tell you the leader exists, is exactly the thing you cannot press. Ctrl-b first and it all works; nothing was broken. But "the help is unreachable until you already know the escape hatch" is a bad answer, and there was no mouse route either: Help was the one builtin missing from the bar. Row 0 is not a pane, so a middle-click there is dispatched before any pane's mode is consulted — the word works in tty mode, which is the only reason it earns the width. APPENDED, not inserted, so every existing topbar word keeps its column and no golden's click coordinates move. test/snapshots/ttyhelp.snap pins it from a bare boot: click Help, get the list, shell still TTY at its prompt, then Ctrl-b + SPC ? for the keyboard route. All 58 goldens carry row 0, so all 58 moved. Verified mechanically that the only changes are the row-0 text and the row-0 style run (0-47 -> 0-52), plus: dump/restore record the topbar inside their .zon, and tagnav's `$`+Enter now executes `Help` rather than `Grep` because the bar's last word changed — still exactly what that step's comment claims it tests. THE DEPENDENCY FIX HAS A CEILING, NOW STATED. The module map is consulted from OUR goto handler, not from inside ZLS, so the analyser still cannot type the `vaxis` const: `gd` on `@import("vaxis")` opens the file, `gd` on `vaxis.init` finds nothing. That is now spelled out at the top of lsp_zls.zig and on moduleRoot rather than left implied, and `SPC l w` detects the case by name — if the left side of a failed field access is a known dependency it says so, instead of the generic "could not resolve". Lifting it means giving ZLS a real BuildConfig, either by letting it run the build runner (a subprocess, and with no cross-query cache that is once per keypress) or by synthesizing one into BuildFile.impl. Both are real work and neither is smuggled in. Also fixed while there: the field-access miss was only explained when ZLS returned null, but it returns an EMPTY SLICE when it typed the left side and found no such member. Both are "gd did nothing" from the outside; both are explained now.
* lsp backend: ZLS as an in-process libraryGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The seam's `query` now calls ZLS's analyser directly, on the worker thread, in this process. There is no zls binary, no subprocess, no JSON-RPC, no `initialize` handshake and no `Server` — `gd` is a function call whose answer comes back as rows. ZLS's build.zig already publishes its guts as an importable module (`b.modules.put("zls", ...)`), so this is a path dependency on the local 0.16.x checkout plus one new file, `src/lsp_zls.zig`. Construction is ZLS's own (tests/analysis_check.zig does exactly this): InternPool.init -> DiagnosticsCollection -> DocumentStore struct literal -> Analyser.init. `zig_exe_path` is null on purpose — shelling out to the compiler is the thing this backend exists to avoid — and `zig_lib_dir` is baked in from `b.graph.zig_lib_directory`, so `gd` on `std.mem.count` opens the same mem.zig the compiler used, with ZIG_LIB_DIR overriding at runtime. Offsets are `.@"utf-8"`, not ZLS's utf-16 default: `+Search` rows are byte columns and we are not on a wire. Seventeen probes, seventeen answering, no false claims, 5.9 MiB peak RSS. The features that were already Server-free are calls (hover, document symbols, code actions); the ones welded to `*Server` are reimplemented thin on top of public primitives — goto is gotoHandler minus the protocol, diagnostics is the in-process `std.zig.AstGen` branch of getAstCheckDiagnostics, references is symbolReferences' algorithm from the outside (offer every same-named identifier token back to the analyser and keep the ones that resolve to the same decl, so a shadowed local is not a false hit). What it does not do, deliberately: - Nothing is cached between queries. Each `query` builds a DocumentStore, resolves imports and throws it away, because the arena dies on return and `req.source` is a snapshot of a buffer the user is still typing into. So cold IS warm — there is no index to warm up. It is also fast enough not to need one: 124us for a local goto, 2.8ms into the stdlib, 8ms for references over a 5000-line file. A cross-query cache is a real design (a global, a mutex, an invalidation story), not a line of code, and it is the obvious next step rather than something smuggled in here. - References, rename and select-refs are THIS FILE only. Workspace-wide means loading every project file into the store and running the analyser over each; DocumentStore's own workspace iteration has the same limit (it can only see handles already loaded). Workspace symbols and workspace diagnostics DO walk the tree, because neither needs the analyser — a parse and a tree walk each. - Rename previews, format reports, code actions list. The seam hands back rows, not edits, so there is no channel through which a backend could rewrite the buffer. These answer the question the keypress asks and change nothing. - Without a zig binary, `@import("builtin")`, `@import("<pkg>")` and `@cImport` resolve to nothing — silently, which is ZLS's behaviour, not a bug introduced here. Relative imports and `std` work. - Non-.zig files answer nothing. The core does not gate the keymap by file type, so the gate is here: `gd` in a README must find nothing rather than parse prose as Zig and confidently resolve a word out of it. A whole-file report that ran and found nothing says so ("no diagnostics", "already formatted", "no code actions") rather than returning zero rows, because in this seam zero rows already means "no backend" — `lspResponse` opens nothing for an empty answer, so silence cannot also mean "checked, clean". Location queries keep the opposite rule: unresolvable is no rows. test/snapshots/lsp.snap covers the round trip end to end — gd jumping on a single result, SPC k opening +Hover, SPC s opening the +Search list that n steps, and gd on a keyword answering nothing without opening anything. The whole backend was also fuzzed at 20k queries over real, truncated and byte-smashed sources across every kind; that found two crashes (a decl's name token indexes its own file, not the requesting one, and is not necessarily an identifier at all on a half-typed line) which are fixed. emscripten does not get the backend: the web shell has no threads and no-ops the lsp effect, so it keeps the empty one the base tree shipped. DEPENDENCY: ZLS is FETCHED by the build system (build.zig.zon .url + .hash, pinned to commit 3e0d0820 on the 0.16.x branch) rather than a path dependency on the local genizah checkout, so it lands in zig-pkg/ like every other dependency and the build is reproducible from the .zon alone. Also passes -Dversion-string: ZLS's build.zig names itself by shelling out to `git describe`, and a fetched package is an extracted tarball with no .git, so every build printed a 'Failed to run git describe' warning. We pin the commit, so we already know the answer.
* lspbench: probe diagnostics and format against a BROKEN fixtureGabriel Schneider2026-08-01
| | | | Plus docs/lsp-evaluation.md: the three backends measured head to head.
* LSP seam: async execution model, helix keymap, evaluation harnessGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The base every language backend plugs into. Three parts: ASYNC. The core had no request/response shape - every effect was fire-and-forget or instantaneous. A language query is the first thing that answers later, so: Effect .lsp -> shell worker -> Event .lsp_resp. tty.zig uses io.concurrent + the vaxis queue, gui.zig a detached thread + the mutex queue it already had for ptys; web no-ops it. The worker never touches the core (path/source/arg are snapshotted into an LspJob), one query in flight identified by a monotonic id so a second press makes the first answer stale, and no rows is a legal answer. KEYMAP. Helix's, verified against its default.rs rather than recalled. gd/gD/gy/gi/gr and ]d/[d had no conflicts. The SPC letters did, so pardes's own builtins moved instead of helix's: Kill k->q, Del d->wc (closing a pane is a window op, and c is helix's own close), Dump/Restore s?->f?, Tutor ht->T. A three-exception muscle-memory map is not a map. RESULTS ARE +SEARCH ROWS. path:LINE:COL text, absolute. That is what look.zig resolves and n/N step, so one row from a goto jumps and several open a buffer - helix's multi-result picker needed no picker code. Backends supply exactly one function (lsp.query) plus a supports set and a name; the base has none on purpose. zig build lspbench scores them on the same corpus: feature matrix (trusting results, not the supports flag - a claimed-but-empty kind is reported as a false claim), cold and warm latency, peak RSS. Two snapshot scripts moved. leader.snap encoded the old key paths. chordcut.snap's last two steps clicked column 5, which lands on a FILE pane, so 'key c-b' toggled nothing and the typed text was being read as normal-mode keys - the golden recorded no TTY pane and no cat -v output anywhere. Pointing them at an actual shell makes both steps assert what their comments claim, and the tty paste chord is now covered for the first time.
* Builtins (Find, Grep) qol improvements and ui fixesGabriel Schneider2026-08-01
|
* build.zig runners and testing improvementsGabriel Schneider2026-08-01
|
* smal qol ux changesGabriel Schneider2026-08-01
|
* fixing some crashesGabriel Schneider2026-08-01
|
* k off the topmost tagline focuses the TOPBAR: row 0 is a tagline tooGabriel Schneider2026-08-01
|
* tag columns are the WHOLE rendered tag: h/j/k/l walk taglines, the path ↵Gabriel Schneider2026-08-01
| | | | selects but never edits
* an output pane is not a document: +Search/+Help split below the pane that ↵Gabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | asked, never into a column of their own placeDoc classified any file pane as a doc, so a +Search opened from a lone shell took layoutInsertColumn(0) - a whole new leftmost column, shoving every existing column sideways - and an open results list was then eligible as the split parent for a real file, stacking documents under the list. An output buffer has no file behind it and no Save; it is the result list belonging to the pane that asked for it. Now it is that on both sides of the rule: it splits below from_id whatever kind of pane that is (a source that died falls back to stackDocLeft, the same last resort a full column bar uses), and it is skipped when a real doc hunts for its split parent, so a file opened from a search row joins the docs instead of the results. Real docs are untouched: fsearch/rsearch/look-file/tutor/lookpanes all pass unchanged. psearch is the proof - its results snap was two columns (the shell pushed to x=70, its gutter run at 70-71) and is now one 140-wide column with the +Search stacked below at the same x origin.
* look resolves against every open pane's directory: clicked pane first, then ↵Gabriel Schneider2026-08-01
| | | | most-recently-focused, search only when all fail
* helix theme, now the default: near-black page, chrome one grey-ramp step off itGabriel Schneider2026-08-01
| | | | | | | | Colors lifted from the user's own helix theme (~/.config/helix/themes/pardes.toml, pinned against a real hx render through a pty). Theme gains box/box_dim so a restrained theme can turn the accent down; the hardcoded gray.* overlay greys were the dark theme's own fields spelled twice and are gone. The old default is still one NextColor away.
* trim the taglines: image renderer toggles and Colors/Crt become leader-onlyGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | The image tag spelled out `Petscii <C64|Term> Ascii` and actOnSelection matched those words by hand before the builtin dispatch. They are three real Builtins now (SPC t p/l/a), so the by-word matching is gone, the tag is a plain `img <path> Del`, and with nothing live left in it the image early-returns in curTail, enterTagEdit and the tag-click guard go too — an image tag edits like every other pane's. Colors and Crt leave the topbar for SPC t c / SPC t r: set-once display switches should not spend width in a bar you read every frame. NextColor stays, it is the one you cycle repeatedly. Both remain full builtins. The snap scripts that clicked those words now press the leader instead, and images.snap finally exercises the palette and ascii toggles for real: its old clicks at x=57 landed on the neighbouring shell's tag, so that toggle had never been covered. The test PPM grew to 512x512 with LCG noise so the ASCII bitmaps actually win cells and dropping them visibly changes the art.
* silence library std.log: ghostty-vt's unimplemented-sequence chatter was ↵Gabriel Schneider2026-08-01
| | | | painting over the UI
* window navigation builtins: SPC w h/j/k/l focus, SPC w t file<->terminalGabriel Schneider2026-08-01
| | | | | | | | | | | | Left/Down/Up/Right run the existing focusDir (now taking the pane the builtin ran on, so an executed name acts on ITS pane); the Ctrl-w prefix routes through the same four builtins instead of calling focusDir itself. Toggleterm hops between the most recently focused document and terminal off focus_hist, the MRU the core already rebuilds each sync. Ctrl-w stays: a tty-mode pane never sees SPC, so it is the only keyboard way out of one. Its snapshot step now moves focus somewhere (c-w l), which the old c-w h in that layout did not.
* Find builtin (SPC f f): fd in-core, results into +Search, n/N opens each hitGabriel Schneider2026-08-01
| | | | Save moves w -> f s to free the w group for window navigation.
* Esc in an output pane's body runs Del: +Search/+Help dismiss themselvesGabriel Schneider2026-08-01
|
* adding better search to tty modeGabriel Schneider2026-08-01
|
* Small ux improvements, : tagline focusGabriel Schneider2026-08-01
|
* docs: note native web client + WASM-only text logic in ideas.typGabriel Schneider2026-08-01
|\
| * macos fixGabriel Schneider2026-08-01
| |
* | web snapshots: add switch for saving/loading exports, update web version, ↵Gabriel Schneider2026-08-01
|/ | | | web snapshot tooling
* small ui fixesGabriel Schneider2026-08-01
|
* steam deck feeback 1: adjustmentsGabriel Schneider2026-08-01
|
* right click searchGabriel Schneider2026-08-01
|
* steam deck nice versionGabriel Schneider2026-08-01
|
* chord polish: the selection a 1-2/1-3 chord leaves behind is dismissed by ↵Gabriel Schneider2026-08-01
| | | | the next left click instead of stretching to it (explicit v/x selections keep click-extend), and in a tty-mode shell 1-3 now pastes into the pty like a terminal emulator — the click is forwarded first (SGR or X10, only when the app listens for mouse) so mouse-aware programs paste at the click point, then the register arrives bracketed (mode 2004) or with \n->\r when unbracketed; 1-2 stays inert there. chordcut.snap pins both (a cat -v probe shows the exact wire bytes); tutor updated.
* argv positionals: bare pardes boots the shell pane in tty mode, a FILE arg ↵Gabriel Schneider2026-08-01
| | | | (file:LINE works) opens it focused in the left column with a greeting terminal on the launch dir at right, a DIRECTORY arg chdirs so shells spawn there. New argv.snap; boot/restore.snap pin -n 1 to keep the classic boot.
* new panes reject a too-short split parent: if the chosen parent would leave ↵Gabriel Schneider2026-08-01
| | | | the new pane under 2 body rows, splitParent swaps in a qualifying pane (same column first), else the tallest pane anywhere; applied to Alt-n, look-dir, placeDoc and ttyForDir. New splitparent.snap.
* panes can shrink to just their tagline: border-drag clamps drop from MINH to ↵Gabriel Schneider2026-08-01
| | | | BOX_H (the tag row doubles as the re-enlarge handle; the gutter move box wins over the handle so a tag-only pane stays movable), a hidden body's pty/vt is never resized so re-enlarging restores it intact, and cursor/modal-selection paints never land on the tag row. New shrinktag.snap; tutor updated.
* 2-1 chord argument falls back to other panes' selections: when the ↵Gabriel Schneider2026-08-01
| | | | middle-drag pane has no selection of its own (kept left sel, else explicit v/x n/N), the active pane is checked first (it holds the most recently made selection), then slot order. New selarg.snap covers borrowing a kept left selection and a v selection from another pane; tutor updated.
* look focuses an already-loaded pane before any filesystem resolve: the ↵Gabriel Schneider2026-08-01
| | | | looked word (and its cwd-join) is matched against loaded file/image pane paths first, honoring :NN recentering — so the fs-less web build can focus dump-loaded panes on look, and native dedup no longer needs the file on disk. New lookloaded.snap: look at a path after rm'ing it still focuses/recenters the loaded pane.
* fix tty crash on outer-terminal resize (font-size change): SIGWINCH is ↵Gabriel Schneider2026-08-01
| | | | blocked in every thread and consumed on a dedicated sigwait thread instead of vaxis's signal handler, which locked std.Io mutexes from signal context and panicked the Io.Threaded machinery; pane shells unblock it after fork so bash/vim still see resizes. winch.snap storms WINCH from a pane (crashed pre-fix) and checks a real resize still reflows.
* acme mouse chords: 1-2 Cut / 1-3 Paste while a left selection drag holds ↵Gabriel Schneider2026-08-01
| | | | (both in one hold = Snarf/copy; the chorded left release is inert), plus 2-3 / 3-1 / 3-2 cancel an execute or look drag mid-gesture (the 2-1 argument chord already existed). chordcut.snap covers cut/copy/paste and both cancels; tutor documents the chords.
* shell panes follow new pty output only in tty mode: normal/insert hold the ↵Gabriel Schneider2026-08-01
| | | | scrolled view even when the pane isn't active (the old gate only protected the active pane; tag edits use the remembered body mode). New ttyfollow.snap: background job streams into a scrolled-up normal-mode pane without moving it, tty mode follows again.
* clicks in a shell pane never change its mode: the body-press exitTagEdit no ↵Gabriel Schneider2026-08-01
| | | | longer force-drops to normal (it only restores the mode a tag edit hijacked, remembered in pane.tag_mode), and the left-release cursor pin keeps the terminal's mode (files still click into normal). New ttyclick.snap covers tty body click, tty tag round trip, insert body click.
* helix diff testing and feature parityGabriel Schneider2026-08-01
|
* fix: every external-scanner grammar crashed pardes on parse ↵Gabriel Schneider2026-08-01
| | | | (rust/cpp/python/...; e.g. opening agave cpi.rs or any tracy .cpp), Debug AND ReleaseSafe. Root cause: clang -fsanitize=function (in zig's default C UBSan set) traps at the runtime's indirect call of the scanner because grammars declare external_scanner_create() with EMPTY PARENS — a K&R non-prototype whose type hash differs from the void*(*)(void) pointer type. The ud1 trap lands on a bogus inlined line (stack.c:746), which cost the diagnosis a detour through rr (its gdbserver dies replaying past the task exit — core dump + coredumpctl worked; ud1 0x6(%eax) = SanitizerHandler kind 6 = function_type_mismatch; scanner-less c/zig grammars never crashed). Fix per review direction: -fno-sanitize=function on the grammar TUs in build.zig — uninstrumented callees make the runtime's call-site checks skip; the rest of UBSan stays live. Second half: fatal signals (SIGILL/SEGV/BUS/FPE) never run defers and bypassed the panic hook, leaving the terminal raw after a crash — root.debug.handleSegfault override now runs vaxis.recover() before std.debug.defaultHandleSegfault, verified in a raw pty (kill -ILL $PPID: rmcup + mouse resets precede the trace). Verified: rust/cpp/python opens work with real highlighting (snapstyle: keywords/strings/comments colored), agave cpi.js 2.7k-line open fine, suite 30/30, ReleaseSafe build opens rust identically.
* insert vs normal cursor shape (all pane types) + the tag is always insert ↵Gabriel Schneider2026-08-01
| | | | mode. Surface.cursor grew a bar flag: the core sets it from the active pane mode (insert -> bar; normal/tty -> block) for both the modal/file cursor and the vt-tracking terminal cursor, and unconditionally for the tag cursor. tty shell: DECSCUSR via vaxis setCursorShape (beam for insert, the terminal default otherwise). gui shell: the insert cursor draws the left-eighth-block glyph in cell colors instead of the reverse-video block (ponytail: it hides the char under it; a dedicated quad if that ever matters). Tag editing: enterTagEdit goes straight to insert, Esc (or anything that exits the tag) restores the body normal mode; Enter looks / Tab executes the word at the cursor from inside the tag (tagChordText kept); the whole normal-mode tag machinery — handleTagKey motions, v/x selection, d/c/y/p, tagEnterInsert/tagDelete/tagYank/tagPaste — deleted as unreachable (~120 lines; multi-word tag execution stays the mouse middle-drag). tag.snap rewritten: INS on click, type/backspace straight in, Esc exit with the text persisting, Enter-look opens the file. Tutor documents it. Cursor SHAPE is not capturable by the harness (position only) — verify by eye: bar in insert/tags, block elsewhere, both shells. 30/30.
* horizontal scroll for file panes (terminals wrap at pty width — nothing to ↵Gabriel Schneider2026-08-01
| | | | scroll): pane.hscroll cuts content columns after the lineno gutter in bodyText (post edit-splice, so typed runs shift with the text), no scrollbar. Drivers: horizontal wheel (new wheel_left/right Mouse.Buttons, SGR 66/67 via vaxis on the tty, SDL wheel.x on the gui, ±4 cols, clamped at 0, right overscroll allowed) and keyboard movement — ensureCursorVisible grew the horizontal mirror of the scrolloff reconcile, so $ pulls the view right with 3 columns of margin and 0 reels it home; the wheel never moves the cursor. Shifted alongside: syntax recolor pass, modal cursor paint (hidden when off-view), vsel highlight, click->content-col mapping, cursorWordSel. Byte columns like the rest of the file-pane code. New hscroll.snap golden: wheel shift with fixed gutter, left clamp, $ margin follow, 0 home. 30/30.