| Commit message (Collapse) | Author | Age |
| ... | |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`|` was the only one of helix's five. The other four differ in exactly two
things — whether the selection is stdin, and where the output lands — so they
are one action carrying a `PipeBehavior` rather than four code paths:
| stdin is the selection, output REPLACES it (had this)
A-| stdin is the selection, output discarded shell_pipe_to
! no stdin, output inserted BEFORE each selection shell_insert_output
A-! no stdin, output appended AFTER each selection shell_append_output
Each arms the same visible tag-tail prompt with its own marker (`|`, `|-`, `!`,
`!+`) so the prompt says which one you are in — they take the same command line
and do very different things to the buffer.
Two helix rules came with them. A behaviour that sends no stdin runs the
command ONCE and every cursor gets that one answer (helix's `shell_output`
cache): ten cursors and `date` give ten identical stamps rather than ten forks
racing to produce one. And a command that put a trailing newline on a selection
which did not have one has it taken back off — that is what keeps a one-line
`| tr a-z A-Z` from becoming two lines. The existing multi-range test moved
with that rule and now pins it deliberately.
In all three writing behaviours the OUTPUT is what ends up selected, keeping
the original range's direction, so an operator can follow straight on from what
the command just produced.
`$` (`shell_keep_pipe` — drop the selections whose command exited nonzero) is
still missing: it needs a per-selection verdict and the runner's answer is
atomic. Noted in docs/helix-keys.md beside the `$` divergence already there.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two things made the selection pipe feel like it had never worked. It runs —
test/snapshots/pipe.snap drives the real binary through a pty and filters
`alpha beta` to `ALPHA BETA` — but it had no way to tell you when it did not,
and one of its failure conditions was not a failure at all.
IT NOW SAYS WHY. `runOne` read the command's stderr into memory and freed it
two lines later, unread; every caller answered a failed filter with a bare
`return`; `pipeResponse` had eight more silent exits under that. So `| trr`
(a typo), `| grep nomatch` (exit 1), `| jq .` on bad JSON — all did nothing,
said nothing, and left the text alone with no way to find out why. The runner
carries a `Failure` home instead: which selection, what became of the command,
and its own stderr. The core turns that into an `+Errors` buffer — acme's name
for output that came from the program rather than from a word anybody clicked:
| trr
exit status 127
sh: line 1: trr: command not found
An output buffer rather than the message row because the useful half of a
shell failure is the text the shell wrote, and a 256-byte row would keep the
label and throw away the reason. Focus stays with the file: `openRead` moves
`p.active` to what it opens, which is right for a Grep you asked to read and
wrong for a report you did not — you want to fix the command and press `|`
again. A host with no `pull_pipe` at all (the detached daemon, the browser,
the board) now says that too, instead of answering failure into the void.
`| head -1` NOW WORKS. `writer_context.ok` was part of the success condition,
so a command that stopped reading its stdin failed the filter even though it
had done exactly its job: `head` takes the line it wants and closes the pipe,
the write gets EPIPE, and a selection bigger than the 64 KiB pipe buffer was
enough to trigger it. helix joins its input task and ignores the result for
this reason; the exit status is the whole verdict. Also reported rather than
swallowed: the ten-second timeout, the output ceilings, and a file edited
while the filter ran — one keystroke during a slow command used to discard the
result in a way indistinguishable from the filter doing nothing.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Both acme chords read the PRIMARY range and dropped every other cursor on the
floor — the one thing a multi-cursor editor must not do with a command the user
aimed at all of them. They are also structurally outside the machinery that
would have handled it: the Enter/Tab chord is intercepted before `handleNormal`
so it never becomes an Action with a scope, and `runBuiltin` bails on
`multiOnce` anyway, because a builtin is per-keystroke rather than per-cursor.
So `chordEachSel` takes the `submitPipe` shape instead: `paneRanges` once,
forward in document order, every range's bytes COPIED before the first builtin
runs. The copy is not caution — a `Look` opens panes and an `Exec` can run a
builtin that edits or closes the pane those offsets point into, and a selection
whose text is `Del` is a legal Exec. The loop re-checks the slot and its serial
between iterations, the same guard `replaySels` makes for the same reason. With
one cursor it returns false on the first line and the old path runs untouched.
FOCUS FOLLOWS THE PRIMARY. `lookAt` sets `p.active` for every target it opens,
so `Look` over four selections used to leave you at whichever one happened to
sort last — an accident rather than an answer.
...AND A LOOK WITH NOWHERE TO PUT ITS ANSWER SAYS SO. All four slot checks in
`lookAt` were a bare `orelse return`: with one selection that merely felt like a
dead key, and with several it means "I opened nine of your fourteen and told you
nothing". They report `NoPaneSlots` now, through the channel output_pane.zig
already raises it on and a test already pins. The three openers report their own
failure too, so a file that will not open says whether it was permission, a
pipe, or size — which `look.readFile` only started distinguishing this week.
Known and left: N selections that all resolve to nothing run N searches over
one +Search buffer. Wasteful, converges, and worth its own change.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every argv refusal in main.zig was `return error.BadArgs` out of `main`, which
std prints as `error: BadArgs` with a return trace under it — the same shape a
real crash has, for the most ordinary thing a person can do. It also said
`BadArgs` and nothing about which argument, at sites that knew exactly:
pardes: no such option: --hepl
pardes: -n takes 1 or 3, not 'abc'
pardes: one file or directory at a time, and 'b' is the second
pardes: --detach and --attach are opposites: one runs the session, the
other joins one
Try 'pardes --help'.
stderr rather than the `+Errors` pane one function down, because argv is read
before a core exists and the person who mistyped a flag is looking at the
prompt they typed it into. `--attach`'s refusal already answered this way; now
all eleven do. `getcwd` failing is no longer reported as an argument problem,
and a `.url` or `@pN` positional says why a LAUNCH cannot act on it rather
than being swept into the same word as a typo.
AND `Look` ON A FIFO NO LONGER FREEZES THE EDITOR. `readFile` opened with a
plain blocking `open`, so a named pipe with no writer waited forever — inside
the keystroke that asked, with no frame, no message row and, in the tty shell,
no Ctrl-C either, because the terminal is in raw mode. It is `O_NONBLOCK` now,
the read loops answer `EAGAIN` rather than waiting, and `lseek` answering
ESPIPE — a pipe, a socket, a terminal — is refused as `NotAFile`, which the
boot pane spells "that is a pipe or a device, not a document". Without that
last part an unwritten FIFO read as EOF and opened a silent empty pane, which
says less than the hang did. The zero-size files worth streaming (procfs and
its kin) seek fine and are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A review of what this program does when the environment says no. The finding
that reframes it: there were almost NO panics on ordinary paths — the rule
already held — but there was a great deal of silence, and one case worse than
any panic.
SILENT DATA LOSS ON SAVE. `saveFile` marked the pane saved the moment it
QUEUED the effect, before any host had tried; `host_io.writeFd` returned void,
so a short or failed write was indistinguishable from a complete one; and
`writeFileBytes` returned true regardless. A save to a read-only file, or into
a directory removed under the pane, therefore cleared the tag's ` *` and posted
nothing — and `Del` makes no dirty check, so the next click threw the edits
away with the screen saying they were safe. On a full disk it was worse: the
file is already `O_TRUNC`'d when `write` fails, so the message row said `saved`
over a file that had just been emptied.
Now: `writeFd` reports, `writeFileBytes` returns WHY (`PermissionDenied`,
`NoSpaceLeft`, `ReadOnlyFilesystem`, …) including a failed `close`, which is
where write-back filesystems report at all; the core marks the pane saved
around `perform` rather than at emit, which is also where the bytes are read;
and a host that could not write calls `Pardes.saveFailed`, which puts the
reason on the message row and takes the clean mark back. That is a CALL and
not a return value because host.zig enforces, at comptime, that a `push_`
method reaching every host in a fan-out cannot have one answer — the first
attempt at this changed the signature and the compiler was right to refuse it.
TWO PANICS ON AN ORDINARY KEYSTROKE, in look.zig's number scans. `v = v * 10 +
d` over caller-supplied digits, reached from `parsePathLine` and the `@pN` scan
— which every Look, every right-click and every n/N motion runs on whatever
word is under the pointer. A hash in a log, a CSV column, any output shaped
`foo:99999999999999999999`, and the editor died with "integer overflow". Both
saturate now, the same way acmefs.zig's address parser already did; a saturated
line is refused by `file_pane.open`'s `line <= total` and a saturated pane id
by `focusPaneLine`'s `id < MAX_PANES`, so nothing addressable changes.
A BOOT FILE THAT WILL NOT OPEN joins the missing-name case in the `+Errors`
pane instead of taking the launch down: `pardes /root` resolves as a `.file`,
could not be read, and left `error: PermissionDenied` and a return trace.
`look.readFile` now says which errno it was, so the pane can say "permission
denied" rather than a word from the source code.
The tag-marker test drained no effects and passed anyway, which is exactly the
defect; it drains now.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
stays one line
Two defects in the +Errors boot, both found by adversarial re-review.
The pane was opened with `dir = ""` — copied from the board's boot buffer,
which can afford it because that platform has no filesystem — so its path came
out `/+Errors` and `paneDir` answered `/`. An output pane's directory is where
a `Grep` from it walks, where its `Newtty` spawns a shell and what its `Save`
prefills, so the boot screen rooted all three at the filesystem root, and the
one word the pane prints resolved against `/` and could never be clicked. The
launch directory rides in `Options.missing` beside the word now, and the test
asserts the pane's path rather than only its contents.
A typo INSIDE pardes stacked a second full-screen UI. The hand-off block above
resolves the word and sends it to the outer instance; `.none` sent nothing and
fell through, which was harmless while the classification below refused it and
became the one input that stacks the UI that block exists to prevent — with no
shell pane in it, so the only way out is `Del`. Its own comment said as much
and was falsified by the +Errors boot. `.none` is refused in that shell now, in
one line and without a stack trace, and the outer session is not told: `Look`
on a word naming nothing is not something to do to somebody else's session.
Also recorded, not fixed: the commonest permission case never reaches the
`.dir` arm this arm's comment defends. `look.isDir` probes with O_DIRECTORY|
O_RDONLY, so a directory you cannot read resolves as `.file` and dies in
`file_pane.open` with `error.OpenFailed` out of `main` — still a trace at a
human, and a different fault than the one fixed here.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`pardes nosuchfile` returned error.BadArgs out of nativeMain, which std prints
as `error: BadArgs` with a return trace under it — indistinguishable from a
crash, for a typo, and it left the human with no editor at all. A launch that
names something look.resolve cannot make a target of now boots one +Errors pane
filling the window, saying `file or directory not found` and the argument AS
TYPED: acme's own vocabulary for output that came from the program rather than
from a word somebody clicked, and the word rather than a resolved path because
`pardes ~/notes/tdoo.md` wants to see its own typo back.
A chdir that fails on a directory that really is one stays BadArgs. That is a
permission problem rather than a typo, and the two want different answers.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
bugs go with them
Nine read-only scouts compared every host-side concern across `src/macos.zig`,
`src/tty/tty.zig`, `src/gui/gui.zig` and `src/detached/server.zig`. What they
found was not a style problem: each duplicated body had drifted, and in every
case the drift WAS a bug the users of that shell could see. So the fixes and
the deduplication are the same change.
**One PATH, adopted before the first fork.** LaunchServices hands a bundle
launchd's environment, whose `PATH` is `/usr/bin:/bin:/usr/sbin:/sbin`. Every
pty shell, `|` filter and language server the app forked inherited it, so
`yazi` in `/opt/homebrew/bin` was absent from a Dock launch and present in the
identical binary run from a terminal — the "it worked briefly" window was
simply the sessions started from a shell. `shell_bin.adoptSystemPath` composes
`/etc/paths` then `/etc/paths.d/*` in the order `path_helper` reads them,
deduplicating on first occurrence, and runs once at startup in all four native
hosts. It APPENDS: an entry already present keeps its position, so running it
over a real session cannot demote a mise shim behind `/usr/bin` and silently
change which `node` runs. A `PATH` that was configured is left byte-for-byte
alone; only one nobody configured is repaired. `prepareForFork` folds that
adoption together with the prompt-rc staging and the `BASH_SILENCE_DEPRECATION_WARNING`
setenv the five hand-copied prefork sites had between them — `server.zig` had
none of it, which is why every detached pane opened with Apple's zsh banner.
**The LSP protocol client never worked on macOS.** It opened its control
socket with `libc.SOCK.CLOEXEC`; Zig defines that constant for Linux and
Darwin answers `socketpair` with `EPROTONOSUPPORT`, so the call failed before
any fork, `ensure` returned `error.NoServer`, and every row in the spec table
— rust-analyzer, clangd, gopls — was unreachable in every macOS build. The
in-process ZLS backend kept answering, which is what made it read as "only Zig
is supported". It is a plain socket plus `fcntl(FD_CLOEXEC)` now, the route
`fuse.zig:943` and `nested.zig:95` already took for the same reason. The
snapshot suite that covered this path had never run natively on a Mac: the
harness targets defaulted to x86_64-linux.
**One LSP host worker.** `src/lsp_host.zig` is the snapshot, the worker body
and the job lifetime that `tty.zig` and `gui.zig` carried verbatim — `gui.zig`
said so in a comment — and that `macos.zig` did not carry at all: `lsp` and
`pipe` were absent from its `Host.VTable`, so the core answered its own empty
answer, `SPC l i` rendered a blank panel and a `|` filter silently did
nothing. All three shells share the module, and the AppKit host implements
both effects. Its status sink is now REGISTERED as well as defined, so
unsolicited server news reaches the message row instead of nowhere.
**The animation clock measures time.** `pardes_animation_tick` advanced one
scene frame per callback and published `frame_count / 60`, so scene time was a
count of callbacks rather than elapsed seconds — and `AppDelegate` re-armed
`asyncAfter(.now() + 0.016)` only after the previous frame's work had
finished, making the true period 16 ms plus all of it. Motion ran at about
three quarters of wall clock and unevenly. The tick now spends measured
monotonic time in whole `frame_ns` steps and banks the remainder, so a late
callback advances two frames instead of stretching one; `spendTickTime` is
that arithmetic as a pure function with its own tests and no display attached.
On macOS 14+ the animating run is one `CADisplayLink` phase-locked to vsync
rather than a chain rebuilt after every frame; macOS 13 keeps the old chain.
**Three more single definitions.** `panel_animation.paintOrder` is the
moving-then-opening-then-closing composite order as a rule the core applies
once in `Pardes.render` — `macos.zig` was re-sorting an already-sorted list.
`selection_pipe.Tasks` is the bounded in-flight pipe table `tty.zig` and
`gui.zig` each declared. `boxContains` was a fourth copy of the half-open cell
test and is now an alias of `Box.contains`.
**A filtered terminal stops asking libm per cell.** `Filter`'s legibility
stage called `RGB.contrast` for every painted cell, and that ends in
`std.math.pow` up to six times, re-deriving a ratio against a background that
had not moved; the existing memo cache covered the palette reduction beside it
and never this. The indexed path's input is a `u8`, so all 256 answers are
enumerated once per pass — after the default roles are fixed, before the first
cell is read — and what a cell names becomes an array index. Only truecolour
still reduces. ReleaseFast, 190x56, Tracy: recolour 3.09 ms -> 0.130 ms,
frame 3.37 ms -> 0.299 ms. The comptime luminance table is pinned to
`RGB.luminance` and `RGB.contrast` by exact-equality test over every channel
value and all 65 536 palette pairs, because the decision is a threshold
comparison where one ULP is a different colour. A `filterInit` Tracy zone
records the part that is still per-pass: 2.9 us warm against a 117 us pass,
which is the measurement that says not to cache it across frames.
Released as 0.0.2. `build.zig.zon` carries the version into `pardes --version`
and into the `Changelog` pane through `@embedFile`, so the entries above open a
`## 0.0.2` section and `## 0.0.1` closes with the tagline work of the parent
commit.
Two bugs here were mine, caught by review rather than by me: a double free in
the macOS pipe drain arm (`Msg.free` already owns the response) that segfaulted
the app on the first `|`, and a proposed `getRowAndCell` optimisation that
targeted 2 of 43 draw samples while the contrast math beside it took 12 — and
would not have compiled. The profile that justified it was a Debug build, which
`build.zig:1160` already documents as ~5x slower than release.
Native and -Dplatform=macos suites: 0 failures. All targets build with Tracy on
and off; the shipped release binary contains no `___tracy_emit_zone_begin`.
App reinstalled, signature verified, dmg regenerated, launched with 0 crash
reports; installed binaries verified byte-identical to a fresh build.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
effects that compile
Three things this shell had its own copy of, and in each case the fix is that
it stops having one.
**The tagline band.** A pane tag draws at `gui_tagline_font_percent` of the
body face and the band it sits on shrinks with it, while the grid row stays
body-sized — so something has to decide where the shorter band sits in the
taller row. This shell decided by centring, always, which is precisely the case
`config.gui_topbar_pane_border_px` exists to prevent: the topbar's unused
half-band meets the first pane tag's unused half-band and the window background
shows through the seam. The strip is as wide as the bands are short — on a
20-pixel cell, 4 physical pixels at the default 82%, 10 at 50%, 14 at 30% — so
it grew as the tagline face shrank and read as "the tagline is wrong on the mac"
rather than as one missing rule. The rule is `pardes.taglineBandOffset` in the
core now and both pixel hosts call it: row zero bottom-aligned, the first
pane-tag row top-aligned, the two joined by `gui_topbar_pane_border_px` in the
theme's scrollbar-track colour, every row between centred, and a `Tagbottom`
band on the final row flush with the window edge — with the sub-cell strip
beneath it painted in that band's own colour, because the core grid holds only
whole cells and a window is any height it likes. `pardes_tagline_band_offset`,
`pardes_topbar_pane_border_px` and `pardes_topbar_pane_border_rgb` carry it over
the C ABI as PHYSICAL pixels: the host multiplies its points by the backing
scale going in and divides coming out, which is the snapping `Metrics` already
does for the cell, and is what keeps a one-pixel rule one pixel instead of a
two-pixel smear.
**The watch.** `file_watch.zig` was one mark/reconcile transaction over
`inotify`, so the tty shell, the SDL window and the detached daemon all watched
nothing off Linux: an edit made outside pardes never reached the pane, and a PDF
replaced on disk kept rendering the old inode. It is the same transaction over
two kernels now — `init`, `wait`, `stop`, `drain`, `markDir` and `unmarkDir` are
still the whole of it, and the hosts wait on a kqueue and poll it exactly as
they did the old descriptor. A macOS mark is TWO filters, because a kqueue
directory filter reports its entries changing and never a write to a file
already inside it: the parent mark follows rename-over saves, `markFile` catches
in-place writes, and `remarkFile` re-arms the file filter once a rename has moved
the inode. That is the same pair the AppKit host's DispatchSources already used
for the same reason. Directory marks are deduplicated here by device and inode,
because each `EVFILT_VNODE` filter needs a descriptor of its own and inotify did
that deduplication itself; `stop` and `drain` wake through the one `EVFILT_USER`
filter, since a kqueue cannot simply be read the way an inotify descriptor can.
**The effects.** The three `crt.ci.metal` entry points are
`extern "C" [[stitchable]]`. `CIKernel.kernels(withMetalString:)` compiles that
source at runtime, looks for stitchable functions, and rejects the WHOLE source
with "cannot find a valid stitchable Metal function in the source" when it finds
none — so `ScenePostprocessor.init?` returned nil and every scene effect and
panel transition silently degraded to the plain CoreText draw. The
`effect_sources.zig` test pins the exact spelling of all three, and
`draw-effect` in the e2e suite catches the degradation rather than the spelling.
Beside them, the offscreen harness owes the core a PRESENTATION. Its window is
borderless and never ordered front, so AppKit runs no display cycle and
`pardes_frame_presented` — whose only caller is `draw(_:)` — never fired. The
core holds pointer gestures inert while a layout mutation has not reached a
backend, which for an unpresenting harness is the rest of the script: the first
pane a script opened silently killed every later click, drag and Look. So
`readFrame` presents what it just rendered, into a bitmap nobody reads.
`PARDES_CHROME` also looks under `/Applications`, where a browser's executable
lives inside an application bundle and never on `PATH`. The macOS goldens are
regenerated; docs/macos.md, config.md, detached.md, web.md and the design PDF
follow.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The seam grows a second backend: src/lsp/lsp_client.zig speaks JSON-RPC to
child language servers — rust-analyzer, clangd, gopls, tsserver, pyright are
rows in a spec table — while the in-process ZLS analyser keeps .zig. One
reader thread per server owns the socket, routes responses to a mailbox
under the conn mutex (monotonic condvar), answers server-to-client requests,
feeds the diagnostics store, and narrates $/progress and state changes
through a status sink both native shells post to the transient message row:
"rust-analyzer: cargo check 88% 955/1083" lands where a save narrates, with
the same clock. Chatty progress is throttled and deduplicated; settled
states always land, which is also what makes the goldens deterministic.
Nothing wedges and nothing healthy dies: waits are deadline-bounded, a
timeout cancels and returns no rows, three consecutive timeouts restart the
server ONLY while it is idle (an indexing server is narrating its own
excuse), spawn and handshake failures back off 10s to 2min, a crash shortly
after ready counts as a failure, and only a missing binary disables a spec.
PARDES_LSP_{RS,C,GO,TS,PY} override binaries; empty disables; the snapshot
harness pins RS to test/lspmock.zig and empties the rest.
Mutating answers really mutate now: the @put record beside rename @edit
carries per-range text, so = applies the formatter (both backends) and a
same-file WorkspaceEdit rename applies atomically, one undo step, narrated
("renamed 2 range(s)"); a multi-file rename previews as rows instead of
half-applying. Malformed responses fail closed: coordinates validated not
clamped, one bad TextEdit poisons the whole edit set, poison frames kill
the connection instead of buffering forever, decoded control bytes reject a
uri, hierarchy items too deep to reserialize are skipped.
Four kinds helix does not have, on SPC l: c/C incoming/outgoing calls (rows
are call sites), t/T super/subtypes. Pull diagnostics (3.17) preferred when
advertised. Help gains a language-keys footer for the motions no builtin
row could carry; lsp.rel and look.grep now share one path-shortening rule.
zig build lspprobe drives the seam from the CLI (comma-separated kinds share
one server); measured against a 1083-crate workspace warm: gd 26ms, gr 213
rows 165ms, incoming calls 212 sites 197ms, document symbols 670 rows 347ms.
docs/lsp.md tells the whole story; lsp-evaluation.md gets an addendum.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Step 4 of the 9P chain (docs/9p.typ 12.4, docs/registry.typ 9P-15/16/17/4/5).
src/9p.zig is a base 9P2000 codec and a SANS-IO server: it never touches a
descriptor, takes no allocator, starts no thread, and builds for
wasm32-freestanding and riscv32-freestanding. That is what lets the same code
serve a unix socket here and a UART on the board later.
Server(comptime fs: type) duck-typed on fs.Req/fs.Reply/fs.Reply.Attr, so it
never imports acmefs and acmefs never learns 9P
init{ in, out, root } the caller owns the buffers; msize is derived
retry/next/reply the three fs_service.Transport ops, by name
push/output/wrote/hangup bytes in, bytes out, partial writes supported
next() is a PUMP, not one-message-one-request: a 3-element Twalk is three
lookups, Topen|OTRUNC is a setattr then an open, Tversion is none at all.
Decisions that were open and are now taken, each recorded in the file:
* qid.version is ALWAYS 0, which makes Linux set P9L_DIRECT and skip its
cache -- the 9P equivalent of the FOPEN_DIRECT_IO fuse.zig relies on.
* Every Rread is clamped to the client's count. An over-long one is a hard
-EIO in Linux, not a truncation.
* Rerror carries Linux's exact strerror text (registry 9P-4 option A), so a
mount recovers the errno instead of ESERVERFAULT. Asserted as literals,
because a typo there is 'Unknown error 526' on every mount.
* `.` and `..` are resolved BY THE SERVER. Under FUSE the kernel does it
and acmefs says so; 9P has no kernel, and forwarding `..` as a lookup
would break every client that normalises a path.
* Topen checks the perm bits itself. Under FUSE the kernel enforced them;
over 9P nobody is above the server, and `errors` would have been readable.
* Tcreate and Tremove are Rerror: `new/` creates a pane on WALK, so the
capability exists and is not spelled Tcreate.
THE INTEGRATION BUG, which was not in the protocol: the daemon's push_fs_reply
sent every reply to the FUSE mount, whose park table has no 9P tag, so it
dropped it -- Tversion worked (no core involved) and Tattach hung forever. That
is exactly the 'no routing origin for the 9P descriptor' cell in the layering
table of docs/9p.typ. Session.fs_origin now carries the transport that asked.
Proved with plan9port against a live daemon serving BOTH transports at once:
9p ls / and /1, read index/ctl/tag, write /1/body, stat, a walk through
/1/../index, pane creation through `new/body`, and the two refusals arriving as
strings -- 'permission denied' and 'No such file or directory' -- confirmed on
the raw wire as Rerror text rather than numbers. A write over 9P reads back
through FUSE and a write through FUSE reads back over 9P.
msize 8192, 34,072 bytes per connection (Server 9,488 + in 8,192 + out 16,384,
out being two msize so that every reply is infallible), four connections.
zig build unit-test: 468 tests before, 503 after.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Step 3 of the 9P chain (docs/9p.typ 12.3, docs/registry.typ 9P-8). Nothing here
is about 9P: it lands in the FUSE-served tree and any later transport inherits it.
A script could write into a terminal that already existed and read its rendered
scrollback. It could not START one, RESIZE one or SIGNAL one. Two of those were
already effects the core emits, so `exec` and `winsize` are existing
capabilities acquiring a name; only `sig` is new, and it brings the one new
host method, `push_pty_signal`.
pty/ctl winsize <cols> <rows> | sig INT|TERM|HUP|QUIT|KILL | exec
one verb per line, validate-all then apply-all, EINVAL applies
nothing -- `writeCtl`'s shape and `writeCtl`'s reason
pty/status cols, rows, tty-taken as three %11d fields
pty/data write is input to the process; read is the RAW output stream,
gated on a reader count so a pane nobody reads costs one branch
A pane that is not a terminal has no pty/ at all: the lookup is ENOENT and
readdir does not list it.
`PaneFile` is an enum(u4) and this takes it from 11 values to 15. ONE REMAINS.
That is also why pty/ is a DIRECTORY and not three more flat names -- a
subdirectory costs one value and buys its own namespace, so `ctl` and `data`
did not have to be renamed.
Two things the core does not know, and which are therefore not invented: a
child's EXIT STATUS (a shell's death is `Event.eof`, which removes the pane,
so there is no directory left to read it in) and RAW/COOKED (the core never
sets a termios; the mode belongs to the program on the far side).
Verified live against a daemon: pty/ appears only on the terminal pane; a
`winsize 0 24` and a `sig SIGINT` are refused; a bad verb beside a good one
applies neither; `echo pty-works` written to pty/data runs in the shell and its
output reaches the body; and a blocking read of pty/data returns the raw stream,
OSC 133 marks and all. fs-bench unchanged and still zero allocations.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Esc stops recentring
## A terminal row's ANSI colours survive being edited
The loudest colour bug this editor had: one keystroke anywhere in a coloured shell row turned EVERY
column of it grey. `EditAnchors` anchored a buffer line only when it was BYTE-IDENTICAL to the shell
row it stood over, so a single differing byte dropped the whole row's colour projection. Worst shape
is invisible: append past the pane's right edge, where the text is clipped, and the row looks the
same and only its colour goes.
Anchoring is byte-level now. An edit leaves the row's own bytes at both ends, and being the same
bytes they keep the same colours; only what was typed has no cell under it, so only that takes none.
Live, on real `fastfetch`: a 32-column blue run split into 6 + 26 around one typed character.
Three defects underneath it, all found by machinery rather than by reading:
* A JOIN removes a buffer line while the buffer's covered span grows, so `lines == covered` and both
aligned guesses — Nth line over the Nth covered row, and the same counted from the bottom —
resolved to the SAME wrong row. Every untouched row below a join went plain. Anchoring is now a
streaming monotone matching: one shell-row cursor that only ever moves forward, advanced once per
buffer line, linear in the buffer where the version before it was quadratic.
* An EMPTY line is not evidence. Splitting a row makes one, it equals every blank row in the span,
and left free to look ahead it claimed the blank row below the last output and took every coloured
row in between out of reach of the lines that owned them.
* Reflow under a scrolled viewport. `PageList.getTopLeft(.viewport)` returns the viewport pin
verbatim, x and all, while `PageList.pin` forces x to 0 — so after a reflow remapped a tracked pin
into the middle of a row, the text pass dumped row 0 from that column while the colour pass paired
the fragment with the row's FIRST cells. Row 0 wore its left half's colours until the pane snapped
back to live output. `bodyText` dumps from column zero now, which is also what ghostty's own
renderer draws.
Also here: DECSCNM (reverse video) was silently dropped whenever `tty_filter` was off, because the
raw path resolved a `.none` colour by role and never consulted the mode.
The test that found the first two is the one worth keeping: random editing against an ABSOLUTE
oracle — every row's own text names the colour it must have — because the differential oracle it
replaced was blind by construction. It skipped the edited row, which is the row the user is
complaining about.
## Esc returns to a pane without moving its view
Esc in body normal mode runs `Last`, "the pane you were in before this one", and that went through
`focusPaneLine`, which recentred a file on the target line unconditionally. So returning to a buffer
repainted the whole screen to show a line that was already on it.
`focusPaneLine` takes a landing now: `.center` for the three callers going somewhere you have not
been (a look target, a path a pane already holds, `@pN:LINE:COL`), `.keep` for Esc. `.keep` leaves
the view alone and lets `ensureCursorVisible` — which already existed and already scrolls by the
minimum into the `scroll_off` band — be the only thing that may move anything.
Not `line = 0`, which `focusPaneLine` already understands as "focus and touch nothing": a background
pane's view can move while you are away, because the wheel scrolls the pane under the POINTER and a
resize reveals no cursor, so the recorded cursor plus a minimal nudge is what actually gets you back.
Ctrl-o and Ctrl-i keep centring, and the asymmetry is structural rather than arbitrary: `Last` only
ever CROSSES panes, so the pane it lands on already holds the view you left it with, while `jumpBy`
can land in the SAME pane, where a long in-file jump would arrive on the very top or bottom row with
`scroll_off` lines of context on one side. Helix splits the same pair the same way — its jumplist
centres, its buffer switch does not.
One deliberate consequence: under `.keep` a PDF's page is not restored AT ALL, because a page reveal
IS that pane's view and a reveal of the page you are already on still snaps `document_scroll_y` to
that page's start, discarding where you had read to. When something moved the pane while you were
away — the wheel again — Esc leaves it where the wheel left it, and Ctrl-o is how you reach the
recorded page.
## host_io.zig: the machine-local half of a host, once
`host.zig` is the seam. The part of the answer that is identical on every host with an operating
system under it — fork a pane's shell, put bytes on a disk — was written FOUR times: in tty.zig,
gui.zig, macos.zig and detached/server.zig. What those copies had in common says what they were for:
all four were missing FD_CLOEXEC on the pty master, so in every shell pardes has shipped, a program
in one pane could read another pane's terminal.
One copy now, and the wire got smaller for it: `ServerMsg.spawn` is gone. A frontend never asked the
server to fork anything — the server has an operating system under it and forks through `host_io`
like every other host — and `decodeClient` lost the scratch buffer that message needed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
board cap on one screen
## The wire is the effect stream, not a new protocol
`pardes --detach` leaves a core running with no terminal; `pardes --attach` is a frontend that owns
a terminal and a socket and nothing else. N frontends on one core all look at the same screen —
`screen -x`, not N sessions.
The codec (`src/detached/wire.zig`) carries exactly one `Event` or one `Host.VTable` call per
message. That is not a coincidence and it is why there is no third vocabulary to keep in step: the
core's IO seam was already a struct of function pointers with plain-data arguments, so a socket is
a legal implementation of it. `nested.zig`'s socket could not be reused — it carries a builtin
command line, and a command line cannot carry a frame.
ARCHITECTURE-NEUTRAL on purpose, not as decoration. The frontend on the far end may be
riscv32-freestanding on the ESP32-P4 while the core is x86_64 Linux, so every field is an explicit
little-endian fixed width and no message is a blit of a native struct. A protocol that only works
between two builds of the same compiler would have thrown away the one frontend that motivated it.
## The board comes in; its toolchain stays out
`src/p4.zig` becomes `src/esp32p4.zig`, and the pardes half of `../05-zig-p4` — the vaxis-over-
serial runner, the UART editor terminal, the keystroke rescue ring, the on-die test suite — moves
into `src/esp32p4/`. `build.zig.zon` gains `.zig_p4 = .{ .path = "../05-zig-p4" }`, so
`zig build -Dplatform=esp32p4 -Desp32p4-firmware` builds, flashes, monitors and self-tests the
board from this repo's `build.zig`.
The DIVISION is the point. What moved is what only pardes wants: the runner that drives a pardes
core over a serial line. What stayed is everything a second project would also want — the HAL, the
register/radio/oracle layers, the linker script, `_start`. `zig_p4` declares no dependencies of its
own and its `build()` early-returns when it is not the root package, so this costs the package
graph exactly zero packages and the editor's own builds nothing at all.
## limits.zig: nine forgettable places become one budget
Nine `platform == .esp32p4` capacity tests lived in nine files. They were never nine decisions —
they are ONE decision, how much memory this build may spend, taken nine times where no reader could
see the total. `src/limits.zig` puts the whole budget on one screen with every cap named against
what it is measured against, derived from two booleans.
The payoff is testability on a machine that is not the board: the caps are ordinary comptime values,
so a host build can be compiled against the board's numbers and the parking, eviction and clamping
paths a 240 KiB core takes get exercised by the normal test suite instead of only over a UART.
## A bare `zig build`
`zig build` with no arguments now builds the tty and GUI binaries and installs them into
`~/.local/bin`, and says so once on stdout with the flag that overrides it. The old default built
one binary into `zig-out` — a path nothing on a `PATH` ever looks at, which made "build it" and
"use it" two different commands for no reason.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
the P4
## Gpio
`Gpio 33` flips one pad and answers on the message row with what it did:
GPIO 33: 0->1
GPIO 33: 1->0
Bare `Gpio` draws the header instead, because the first question about a header is which pins it
has. The pin number is DECIMAL and it is the only literal in board_memory.zig that is - every
other one is an address, and addresses come off datasheets and linker maps that print hex, which
is why that file made everything hex two commits ago. A GPIO number is not an address, it is part
of a NAME: the schematic says GPIO47, the datasheet's pin table says 47, and `Gpio 20` meaning pin
32 would be a trap laid for the one argument anybody types from memory.
## The toggle is the host's, not the editor's
New `Host.VTable.pull_gpio_toggle`, and a `GpioFn` in the p4 ABI (hence version 2), rather than
board_memory reaching for GPIO_OUT the way `Poke` two functions above it would happily do.
Writing that register is not the job. A pad has to be pointed at the GPIO function in the IO MUX,
routed in the GPIO matrix, given drive strength and an input buffer with its pulls cleared, and
only then driven - four register files behind a per-pin table. That code already exists in
`05-zig-p4/src/hal/gpio.zig`, it is the same `configureOutput` the blink demo has always used, and
its register numbers are checked against ESP-IDF's own headers on the die by `zig build diff`. A
second copy inside the editor object would be a second copy under no test, and getting it wrong on
a pin that boots as something else is how you lose the console you are typing on.
Reported levels are the OUTPUT bits, before and after, because that is what a toggle means: the
level this board is driving. A pad's input buffer on an unconnected header pin reads the air.
## JP1, read off the schematic rather than remembered
The diagram is the vendor's own wiring, from sheet 2 "Expand IO" of
`01-esp32p4-m3/docs/JC-ESP32P4-M3_schematic.pdf` - the only document that carries this mapping. The
specification PDF's "Interface Description" page turned out to be a marketing render, and there is
no board user guide; the chip datasheet has a package pinout, which is not a header.
That sheet is a 872x1168 raster (`pdfimages -list` - the PDF embeds no vectors, so rendering it
larger adds nothing), and at that size the rows around pin 14 are genuinely ambiguous by eye. So
the mapping came from the drawing's geometry instead: thirteen wires leave each side of the symbol,
a net wire runs ~100 px to its label and a power stub ~21 px. Pin 8's wire is 21 px, which is what
identifies it as unconnected rather than as the first of the GPIO4x labels - the reading that had
GPIO47 one row higher and shorted GPIO45 to the ground bracket.
Cross-checked against a second source that has been in the tree all along: `05-zig-p4/build.zig`
documents `-Dled=20` as "JP1 pin 17", and GPIO20 lands on pin 17 here. Both facts are asserted in
the test, so the diagram cannot drift from either.
## Peek, Poke, Hexdump and Gpio are now the P4 build's alone
`board_memory.enabled` was `os.tag == .freestanding and !isWasm()`, on the argument that these
words are a property of having no operating system rather than a product configuration, and that a
predicate spelled out of `builtin` cannot drift the way a hand-maintained enum can.
Tidy, and it answered the wrong question. A word only exists if some shell offers it, and the
shells are the platforms. `Gpio` settles it beyond argument: its whole content is one board's
header, and a second freestanding port would need its own pinout rather than inheriting this one.
"Bare metal" was never the requirement, "this board" was, and the two only looked identical
because there is currently one of them. The old predicate's real work was excluding wasm -
`freestanding` too, where an address is an offset into a linear memory the engine owns - and naming
`p4` excludes it by construction instead of by a term somebody has to keep remembering. The target
is now the witness rather than the gate.
Absent means not compiled: the tty binary contains no `+Gpio`, no `+Hexdump`, no `ES_I2C_SDA` and
no `MisalignedAddress`.
## The boot buffer's lines are checked, not eyeballed
Three times now a line in that tour has been one or two characters too long for a 56-column grid,
and every time it was found by reading the die's screen - the expensive way to measure a string
literal. The text is a named `boot_buffer` with a test over it, six lines came down to fit with
margin, and the tour gained `Gpio`.
Tests: the pinout's width, its thirteen aligned pin rows, GPIO20-on-17 and pin-8-unconnected; the
decimal-versus-hex distinction; every boot-buffer line. Full suite green - unit-test, snap 95/95,
hxdiff 481/0, hxparity 561/0, image-harness, pdf-harness, mupdf-check - and tty, p4, gui,
p4 at 80x24, p4 with the fade forced on. On the die `p4-bench --check` is 5/5, the fifth being a
new one: three `Gpio 33` runs must report 0->1, 1->0, 0->1, because the alternation is the only
oracle a hardcoded string could not fake.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A theme change moves the anchored chrome palette - taglines, boxes, line numbers, scroll
bars - from the old colors to the new ones over ten display frames. On a screen that
repaints in microseconds that is a short legible transition, and it is why the code exists:
a palette that teleports reads as a glitch.
On a 115200 serial line it is not a fade. Each of the ten steps recolors every anchored
cell, so the diff finds the whole chrome dirty and spends a frame's worth of wire on it, ten
times over, with nothing else on screen to look at. Measured on the die, one `NextColor`:
fade on 12,593 bytes 1,097 ms of saturated wire
fade off 2,425 bytes 215 ms
A second of the editor talking to itself about a color, on the one transport where a second
is noticeable, for a gradient nobody can watch arrive at 11.5 KB/s.
## Comptime, so the code is not there
`ChromeAnimation` now selects between `animation.Transition` and a new `animation.Immediate`
- the same interface with the animation taken out, a value that is only ever what it was
last set to. That is what makes `ChromeTheme.interpolate` unreachable, and unreachable is
what makes it absent: the flashed image drops 2,336 bytes, and the object 13,180.
A bool tested at runtime would have kept every one of those bytes and still paid the
branch. It also would have needed a second meaning bolted onto `animate_theme_changes`,
whose job is the startup window and nothing else; that field is untouched here.
The option is `-Dtheme-animation`, defaulting to off for `p4` and on everywhere else, and it
is an option rather than a platform test because "is a frame expensive" is a property of the
transport: a P4 driven over something faster than a UART would want the fade back, and
`-Dtheme-animation=true` gives it to them.
## What was checked
`Immediate` is new code with one contract worth pinning, and it is the one a caller could
get wrong: it must arrive at the SAME palette a completed fade arrives at. An endpoint that
differed by a rounding step would make the option a change of colors rather than a change of
how long they take. Tested against a fully advanced `Transition` in `animation.zig`.
Full suite: unit-test, snap 95/95, hxdiff 481/0, hxparity 561/0, image-harness, pdf-harness,
mupdf-check. Builds: tty, p4, gui, and tty/gui with the fade forced off. On the die the
canonical verifier reports the screen IDENTICAL across both arms - the workload contains no
theme change, so this is the check that ordinary rendering was not perturbed - and
`p4-bench --check` stays 4/4.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
## Eight bytes a row on the P4
`hexdump -C`'s sixteen needs 79 columns: ten for the address, forty-eight of hex, a gap,
and eighteen of ASCII gutter. The board drives 56 columns of which seven go to the line
numbers, so every row wrapped onto a second display line and the columns stopped lining
up - which is the entire value of the layout. Eight fits in 46 and keeps every property
that matters, including a gap at the halfway mark, because the eye counts in fours and
eights rather than in sixteens.
Verified on the die:
0x40000020 32 54 cd ab 00 00 00 00 |2T......|
0x40000030 30 2e 31 00 00 00 00 00 |0.1.....|
That is the app descriptor: 0xABCD5432 and the version string, read out of flash by a
command typed with no 0x on either argument.
## The boot buffer is shorter, and its addresses are named
The first draft opened with four lines of prose explaining that there is no operating
system. True, unhelpful, and it cost a third of a fourteen-row window before the first
command. One header line earns its place; the rest of the screen is addresses.
The two LP registers at the end are now named, because they are named in ESP-IDF's own
headers and the names are the interesting part: 0x5011002c is LP_SYSTEM_REG_LP_STORE0, a
general-purpose retention register that holds what you put in it, and 0x501101a4 is
LP_SYSTEM_REG_RNG_DATA, the hardware random generator. Between them they demonstrate the
whole point of a volatile read - one address gives back what was written, the other never
gives the same answer twice:
Poke 5011002c deadbeef -> 0x5011002c: wrote 0xdeadbeef, reads 0xdeadbeef
Peek 5011002c -> 0x5011002c: 0xdeadbeef
Peek 501101a4 -> 0x501101a4: 0x0b099791
Peek 501101a4 -> 0x501101a4: 0xfc97f3b7
All four run on the die, all with bare hex. Peek and Poke had not been tested there before
this - only Hexdump had, which I had let stand as though it covered all three.
snap 95/95, hxdiff 481/0, hxparity 561/0, unit-test, tty/p4/gui.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
the bus
## Hex, always
Base-0 parsing accepted `0x4ff40000` and `1341390848` and refused a bare `4ff40000`, on
the grounds that guessing between hex and decimal would let one typo address somewhere
else entirely. The reasoning was sound and the conclusion was still wrong: the ambiguity
it guarded against is not a real one. Every address anybody has ever typed at these three
words is hex - it came off a datasheet, a linker map, or a previous dump's own output, all
of which print hex - so the base was never in doubt, and demanding `0x` on every one of
them was a toll on the common case to protect a case that does not arise.
The COUNTS go with them, and that is the part worth saying out loud rather than leaving as
a surprise: `Hexdump 4ff40000 100` shows 0x100 bytes, which is 256, not one hundred. One
rule for every literal beats two rules that each fit their own argument better, because
the second kind has to be remembered at the moment you are concentrating on something
else. What these words PRINT is hex too now, clamp notes included, so a number can go back
in where it came out.
## And the board boots into somewhere worth looking
The empty output buffer was honest and useless. The three words that make this port
interesting all take an address, and a board's address space is precisely the thing you
cannot guess - so the boot buffer is now a tour of it: the image's own rodata and code in
flash, the firmware's data and the editor's heap in L2MEM, the mask ROM, UART0, the
systimer, GPIO_OUT and an IO_MUX pad, and one harmless Poke.
Every address comes from this repository rather than from memory, which is what makes them
worth trusting: the flash and RAM figures are the linker script's own ORIGINs in
`05-zig-p4/build.zig`, and the peripheral bases are the `DR_REG_*` values `05-zig-p4/src/hal`
uses. Each command sits alone on its line because an argument list ends at the last
argument - a trailing comment would be `ExtraArgument` - so the notes go above the lines
they describe. Lines are kept inside 48 columns because the first draft wrapped every one
of them at the 56-column grid, which reads like a bug.
Verified on the die: the buffer renders one line per line, and putting the cursor on
`Hexdump 40000020 60`, selecting with `x` and pressing Tab opens a dump whose first bytes
are `32 54 cd ab` - 0xABCD5432, the ESP app-descriptor magic - with the version string
right behind it. Bare hex, no prefix, reading real flash.
snap 95/95, hxdiff 481/0, hxparity 561/0, unit-test, tty/p4/gui.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A keystroke on the ESP32-P4 cost 17.0 ms and the goal is 4. Profiling the core in
that board's exact configuration - 40x12, tree-sitter disabled, via `zig build perf
-Dtree-sitter=disabled -- --cols 40 --rows 12 --only small` - named the cost, and it
was Unicode machinery answering questions about the letter `y`.
Four changes, each a fast path guarded so that non-ASCII text takes exactly the road
it took before.
`modal.graphemeStart` was 21.5% of a keystroke, the single largest item. It iterates
graphemes FROM THE START of the text with the full UAX #29 break state machine until
it passes the offset, and the render path calls it once per visible row with a column
offset - so the cost followed the cursor's distance along its line. That is the shape
measured on the die, where inserting at column 320 of a fixed 320-character line cost
7.8 ms more than inserting at column 0 of the same line. In UAX #29 every ASCII
scalar is its own cluster with ONE exception, GB3 (CR joined to LF); every other rule
that could extend a cluster - Extend, ZWJ, SpacingMark, Prepend, Regional_Indicator -
is spelled with non-ASCII scalars. So an ASCII byte whose predecessor is also ASCII,
and not that CR-LF pair, IS a boundary. O(1), and sound rather than approximate.
`Surface.print` then became the largest at 26.2%: per character it took a UTF-8
length, a decode, a FRESHLY CONSTRUCTED grapheme iterator, a slice validation and a
width lookup, to conclude that `y` is one cell. Printable ASCII followed by ASCII
takes none of that now. Same guard, same reason.
`file_pane.graphemeDisplayWidth` was 6.9%, essentially all of it asking `gwidth`
about ASCII. Bounded to 0x20..0x7e on purpose: DEL and the C0 controls are not one
printable cell and `gwidth` stays the authority on them.
`modal.lineSlice` searched for "\n" with the generic substring search where a memchr
does; it is called once per visible row per frame.
Measured at the P4's geometry and configuration, on the host: render 55 -> 12 us,
key-down 483 -> 24 us, key-right 327 -> 13 us, edit-char 205 -> 46 us. On the die,
the per-character cost of a keystroke fell from 54.3 to 6.9 us - 7.9x - and a
keystroke at a 160-character line from 25.56 ms to 15.36 ms.
## The shadow grid, and why it is static
`src/p4.zig`'s `present` copied all 480 cells into vaxis every frame, which measured
6.75 ms on the die - 57% of a keystroke - and was paid whether or not anything
changed: a second render with nothing new cost the same as the first. vaxis diffs its
own grid, but only after being told every cell, and being told is the expensive part.
So `present` now keeps the previous Surface and tells vaxis only what moved.
`Cell.visuallyEqual` is the right comparison and already existed. Copy: 6.75 -> 1.45 ms.
The grid lives in `.bss`, sized by `max_cols` x `max_rows` at comptime, and that is
not a micro-optimisation. The first version allocated it from the editor's heap; on a
board whose 384 KiB is nearly spoken for, that is exactly the kind of change that
works and then breaks something else three steps away.
`shadow_grid` is a comptime A/B switch, kept deliberately. With it false, `present`
behaves as it did before - clear and write every cell - which is the reference any
measurement should be compared against, and the way to tell a rendering bug from a
rendering difference. It earned its keep immediately: the two paths were run against
the same 19-step workload on the die - inserts, deletes, motions that move the
modified-marker, a line outgrowing the viewport, backspaces that shrink it - and the
reconstructed screens are byte-identical.
## Verification
`snap` 95/95 scripts, `hxdiff` 481 cases 0 mismatches, `hxparity` 561 cases 0
mismatches, `unit-test`, `image-harness`, `pdf-harness`, `mupdf-check`, and tty / p4 /
gui all build. The rendering changes are exactly the sort that pass a latency
benchmark while corrupting a screen, so the snapshot parity suite is the one that
matters here and it is unchanged.
`test/perf.zig` gains `--cols`/`--rows`/`--only`. The screen's shape is one of the
things that table exists to hold constant, and 40x12 is not a scaled guess at the
board - it is the board. `--only` exists because under `perf record` one 63 ms cell on
the largest fixture swamps every sample from the case being asked about.
## Found, not fixed
`vx.resize` fails on this board: a runtime geometry change hits its allocation
failure path, restores the previous size and returns, so 80 bytes go out where 1,392
should. Verified independent of everything above - it reproduces with `shadow_grid`
false. The board therefore has one geometry for the life of a session, which is why
the staleness test above compares two firmwares rather than resizing one.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`-Dplatform=p4 -Dtarget=riscv32-freestanding` emits a single freestanding OBJECT
exporting a seven-function C ABI, not an executable. The board's toolchain
(../05-zig-p4) owns `_start`, the linker script and the UART driver and links this
in. The seam is bytes rather than types, so neither side can accidentally depend
on the other's internals, and a signature that drifts fails at link time.
The serial line is the whole of the I/O. `src/p4.zig` drives vaxis unchanged over
it: the renderer is a byte writer and `queryTerminalSend` is a byte writer, so the
terminal emulator on the host answers the capability handshake and the firmware
sees a real terminal. Measured going out over the wire on attach: alt screen,
in-band resize, cursor report, kitty keyboard, kitty graphics, DA1.
THREE WORDS EXIST ONLY HERE. `src/board_memory.zig` implements `Peek`, `Poke` and
`Hexdump`, gated on `builtin.os.tag == .freestanding and !isWasm()` - derived from
the TARGET, because they are a property of running with no OS under you rather
than a product option, and because wasm is freestanding too and is exactly what
must be excluded: in a browser an address is an offset into the linear memory this
editor's own heap lives in. Every access goes through `*allowzero volatile`: a
peripheral register is not memory, and address 0 is an ordinary unmapped address
on this bus. One 4 KiB cap per command, set by the console rather than the memory -
an unbounded dump would wedge the only console the board has for eleven hours.
Measured on ESP32-P4 rev v1.3 silicon, driven from a host terminal:
Peek 0x501101a4 0x0e63ce71, then 0xaeaa6919 on a second read - the
RNG register, so the volatile loads are not folded
Poke 0x5011002c 0xdeadbeef LP_STORE0; a later Peek returned 0xdeadbeef
Hexdump 0x5011002c 32 16 bytes a row, hex columns and an ASCII gutter
Peek 0x50110001 `peek: MisalignedAddress` on the message row
That last line is the one that matters. A misaligned 32-bit access traps, and a
trap in firmware is a watchdog reset that takes the session with it, so the check
that turns it into a message is the reason the file is hand-written rather than a
generic reader.
BARE METAL BOOTS AN EMPTY OUTPUT BUFFER. Every other boot layout in `init` makes a
shell, and on this platform that is not a preference but an impossibility: nothing
to fork, no pty to give a terminal pane. Booting one anyway produced precisely what
that describes - a pane whose tag ends in `Filter`, no gutter, no buffer, and every
keystroke vanishing into the Fallback's silent pty. An output buffer is also what
the platform's own words want, since Peek, Poke and Hexdump each fill one.
Sized for the board rather than for a desktop:
* `allocators.zig` gains a p4 tier that is ALL fallback - every capacity is zero,
so each arena spills immediately to the 384 KiB heap the firmware hands over,
and no megabyte-shaped static reservation lands in `.bss`.
* `source_manifest.zig`'s allowlist is EMPTY on p4. The table is ~0.95 MiB of
rodata against a 1.5 MiB flash partition; the firmware's filesystem is the
serial host's, through the Host vtable.
* The grid is clamped and the clamp is measured, not guessed: every cell is paid
for four times (vaxis Screen + InternalScreen, pardes Surface + previous_cells),
so 40x12 fits and 80x24 exhausts the heap during `Pardes.init`.
* `Vaxis.resize` deinits both screens before allocating replacements, so a failed
resize leaves vaxis rendering nothing. The p4 shell keeps the previous geometry
on failure instead of leaving a half-applied one.
Also here: `output_pane_integration_test.zig` had an exhaustive switch over
`Platform` that adding `.p4` left unhandled, which broke `zig build unit-test`
outright - the native test binary is the one consumer no platform build compiles.
346 tests pass again.
|
| | |
|
| |
|
|
| |
takes a path argument
|
| |
|
|
| |
walk wrapped rows
|
| |
|
|
| |
optional methods
|
| |
|
|
| |
backends agree
|
| | |
|
| | |
|
| |
|
|
| |
docs
|
| |
|
|
| |
additions, unit tests
|
| |
|
|
| |
over grid cells
|
| |
|
|
| |
+ snapshot refresh
|
| | |
|
| |
|
|
| |
snapshots
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
glslc is the one build input that wants a tool a stock machine does not have,
and it is also the input that changes least often: eight GLSL files that have
outlived several rewrites of everything around them. Asking every machine that
wants to run the SDL shell for shaderc is the wrong trade.
The SPIR-V is now COMMITTED, under shaders/prebuilt/, and -Dprebuilt-shaders
embeds that copy instead of shelling out. The default stays the honest one --
compile the shaders that are actually in the tree -- because the flag trades a
dependency for a freshness problem: with it on, the .glsl sources are not build
inputs at all, so editing one changes nothing.
`zig build shaders` is the other half, and it is deliberately independent of
-Dplatform: it recompiles every shader and writes the result back into the
tracked directory, so whoever changes a shader refreshes the cache on a machine
that has the compiler and commits the diff. `jj diff shaders/prebuilt` after it
is the freshness check -- empty means the cache was already current.
The shader list is also spelled once now (gui_shaders): the eight embeds, the
eight glslc runs and the refresh step all read it, so adding a shader is a name
there plus the @embedFile in gui.zig, not three edits in two places.
Verified: -Dplatform=gui -Dprebuilt-shaders builds with glslc absent from PATH,
and image-harness passes on that binary -- real SDL GPU pipelines built from the
committed SPIR-V, 512 source pixels read back. The default gui build still runs
the eight glslc steps; tty runs none. The committed bytes are identical to a
fresh glslc run, and `zig build shaders` is idempotent.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The documentation had gone stale in the ordinary way -- claims that were true
when they were written and that nothing since had been obliged to re-read.
Some of them were load-bearing.
THE TUTOR. It still said there is no multi-cursor, that NextColor cycles three
themes, and that its practice blocks "are also run as unit tests (generated
from this file by tutor_gen)" -- a tool that appears nowhere in the tree, and
nothing anywhere parses a `# keys:` block. Left alone, that claim is what
makes the next wrong block survive.
Three of those blocks WERE wrong, and all three for one reason: since the
helix motion model landed, w/e/f/t SELECT the range they cross, so `i` after
one inserts at the SELECTION'S START. `w i Z esc` on "foo bar" gives
"Zfoo bar", not the "foo Zbar" the file promised. They were written against a
vim reading of the same keys. Every block in the file has now been run through
`zig build hxdiff` against the real core and matches byte for byte, and the
trap itself is written down in 3.3 rather than left to be rediscovered.
The tutor gains a PART 4 for everything added since it was written -- PDF
panes, the in-process ZLS backend, themes and fonts, the startup file -- and
PART 3 gains counts (and which keys ignore one), f/F/t/T, the whole g table
(bare `G` is a no-op; `ge` is the START of the last line), multiple cursors
and the s/S regex pair, `m`, `]`/`[`, `|`, insert mode, and all fifty leader
paths.
THE REST. design.typ's line table claimed 7,626 lines against a real 38,048,
and its rows did not sum to its own total; its Event/Effect boundary contract
-- the part a shell author writes against -- named four variants that do not
exist and omitted fourteen that do. lsp.md's probe count. config.md's
theme-name rules, which as written could not reach a zed theme at all.
helix-keys.md's Skipped section, holding five families that have since landed.
macos.md's menu bar, undocumented, along with sixteen other claims. web.md on
what the browser build can actually do.
SOURCE COMMENTS that had rotted alongside them: `tag_normal` is a space, not
the `•` its own comment describes; Wrap is ON by default, not off; a FontSel
row is SELECTED by n and RUN by Tab, not run by n; the SPC paths in lsp.zig
lost their `l` group prefix when the language group moved; and the
differential suites are 481 and 561 cases, not 360 and 440.
TWO THINGS FOUND BY DOCUMENTING THEM, both left standing and written down
rather than papered over. Typing `[^\n]` at an s/S prompt panics: the live
preview compiles every prefix, and `[^\` indexes an empty slice in mvzr's
parseCharSet. Both the tutor and a waiver recommended that pattern as the
workaround for `.` matching a newline; they now say what it costs and what
would make it sayable. And `Exec` is a builtin, so an `Exec` line in the
startup config types that command into a shell before the first frame -- the
tutor said nothing in that file is ever sent to one.
Nine adversarial reviews over two rounds, each with the hxdiff harness to
execute what it doubted. The second round exists because the first round's
fixes needed checking too, and it caught three regressions of my own -- one of
them a probe count I had "corrected" away from the truth.
Verified: unit-test, snap 87/87, hxdiff 481/0, hxparity 561/0, mupdf-check.
docs/design.pdf regenerated. The tutor's first seventeen lines are byte-
identical, which is what tutor.golden pins.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three changes that all turned out to be the same shape -- a feature that
worked in one direction, or for one pane kind, and quietly did not in the
others.
CLIPBOARD. Every register write emitted set_clipboard, so deleting one
character threw away whatever the desktop was holding; multi-cursor yank took
the join's early return and emitted nothing at all, so the same key reached
the clipboard on one cursor and not on two. Nothing could READ the clipboard:
the SDL shell had no SDL_GetClipboardText anywhere in it, and the tty shell
never asked for OSC 52, so `p` from another application was dead in both.
Now it is helix's split. y/d/c/p/P/R and the acme chords are the DEFAULT
REGISTER and nothing else; the system clipboard is five words on helix's own
letters -- SPC y, SPC Y, SPC p, SPC P, SPC R -- spelled as builtins so they
land in Help and are executable like every other verb. The one exception is
the tag `y` chord, which still mirrors out because a tag is always insert, so
SPC cannot be pressed there, and copying the path out is the whole point of
the chord.
Reading is a new read_clipboard effect answered by an ordinary Event.paste, so
the round trip is honest about being one: SDL and NSPasteboard answer inside
the same drain, the browser answers a promise, and a terminal answers over
OSC 52 or -- far more often -- refuses. A refused read is a paste that does
not happen, and the request dies at the next keystroke rather than landing
minutes late in whatever pane is focused by then.
The tty shell also enables BRACKETED PASTE now and coalesces
paste_start..paste_end into one event. Before this a paste arrived as a flood
of individual key presses: plausible in insert mode, and in normal mode every
pasted character ran as a command.
n/N. They stepped the armed results buffer and immediately Looked each row, so
you could not walk past a hit without opening it. They are a MOTION now:
select the next look-able text, open nothing, and let Enter decide. What they
step is the largest whitespace-delimited run look.resolve can act on
(look.lookableSpan, wrapper punctuation peeled), over a RING of panes -- every
pane that has performed a look, most recent first, then the output buffers
that have not, newest first, and only if both are empty the pane in front of
you. N is the exact inverse of n, computed rather than remembered: both
directions ask the same question about the same spans and compare against the
column the walk parks on, so x presses one way and x back land exactly where
you started, pane boundaries and the ring's seam included.
A ring rather than a list with two ends because a shell's cursor sits at the
prompt, below everything it has printed, so a walk that could not come round
would have nowhere to go on the very first press -- which is the case n/N were
written for.
One motion everywhere, no pane-kind or buffer-kind special case. The only
thing a buffer may change is the GRAIN of what a step selects, and it does it
with one flag rather than a branch: output_pane.Traits.commands (renamed from
`executes`, which named one reader's behaviour rather than the fact) makes a
row select WHOLE, because a ThemeSel line is a word to run and has no path
inside it to pick out. `]d`/`[d` are not n/N -- they are helix's diagnostic
motions, their job is to ARRIVE, and they still reach searchStep.
THE TTY PROMPT. Leaving raw tty blanked the prompt row, and the command you
had typed at that prompt shares the row, so it went too -- a shell out of tty
read as output only. OSC 133 marks the row CELL by cell, so the two are
separable: config.tty_blank = .prompt cuts the prompt's own columns and leaves
the command, left-hugged at column 0 in line with the output under it rather
than in a bay of blanks. .prompt_and_input is the old behaviour, kept.
Because the row is now something you can put a cursor in, enterTty adds the
hidden prompt width back before asking ghostty to walk the shell's own cursor
to it -- the modal column on a cut row is short by exactly that much.
Verified: unit-test 186/186 (nine new), snap 87/87 (new ttyprompt.snap),
hxdiff 481 and hxparity 561 with 0 mismatches, tty and gui both build. And
against the real binaries rather than the harness: in a pty, SPC y emits OSC
52 carrying exactly the selection while plain y emits nothing, SPC p issues
the read and pastes the reply, and a bracketed paste of "dd..." inserts text
instead of deleting two lines. In a real SDL window, SPC y then SPC p round
trips through the system clipboard while the default register holds different
text. Setting tty_blank back to .prompt_and_input reproduces all 86 old
goldens byte for byte.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every keystroke in a shell pane rebuilt the motion surface from scratch:
shellRows dumped ghostty's WHOLE history+active grid, split it, blanked the
prompt rows and handed back slices into the scratch arena, which the next
update threw away. A pane sitting on a multi-megabyte agent transcript paid an
O(scrollback) dump per press of `j`, and paid it once or twice per key, since
flatSurface then rebuilt the same rows joined by '\n' beside it.
The dump is now memoized against the pane it was built for (term_pane.RowsCache
on Pardes.shell_rows), gpa-owned rather than scratch-arena because the whole
point is to outlive the update that built it. One entry, not a table: the
surface is built for the pane the cursor is in, and a second pane asking would
only double a multi-megabyte buffer for a slot it is about to lose again. A
pane that is not the live one is answered from the arena as before.
The lifetime rule is the part that would have rotted silently, so it is one
rule and it is written down: `rows` is handed out to callers, so everything
that notices the entry has gone bad — output arrived, the grid reflowed, the
pane died, another pane wants the slot — only marks it `stale`, and the
buffers are freed in exactly two places, `sweep` at the TOP of an update
before any handler can be holding them, and `reset` when the editor goes away.
Nothing frees mid-update. dropPane clears the pointer immediately though: a
freed pane's address comes back from the allocator as a different pane, and an
entry still naming it would answer for the wrong grid.
Two things fall out of having the join already:
- flatSurface returns the memo's `text` verbatim when the lines it was handed
are the cached rows untouched, instead of rebuilding the join.
- paneCursorLines returns `rows` directly when there is no edit buffer, where
it used to copy the array one slice at a time to produce exactly what it was
given.
One bug on the way past, in the same function: an EMPTY edit buffer writes one
line but modal.lineCount("") is 0, so `ls` was sized one short of what the
loop writes — the same floor the paste site needs. Killing a whole line
(`A<C-u>`, `d%`) on a buffer covering the last row made that a length of zero.
And test/perf.zig grows the axis that would have caught this: a terminal
scoreboard beside the file one, three scrollback fixtures (64 KiB, 1 MiB,
8 MiB — half the ceiling) against render / output / resize-rows / resize-cols /
key-down / edit-char, sharing the existing text and JSON reports and the
--base comparison. resize-cols and resize-rows are both there because a COLUMN
change reflows every page in the list and a row change does not.
Measured on that table: key-down is 142 / 630 / 636 us across the three
fixtures — flat from 1 MiB to 8 MiB, which is the dump being gone, and render
flat at ~110 us throughout. What remains of key-down's step at 1 MiB is the
linear scan indexOf refuses to index for a terminal; that is now a ponytail
waiver naming its own price (615 us against 140 us) and the threading through
paneOff/panePos/paneLineStart it would cost, to be done the day 0.6 ms shows
up next to something anybody can feel.
|
| |\
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
theming, and mupdf -Djpx
Three commits off 38e9919 (macos-app@upstream) merged into main's ghostty bump.
No textual conflicts, and two things the merge needed:
- nested.zig asked libc for fstatat. Darwin has it; on linux std.c declares it
`void` (glibc hides it behind a versioned symbol std cannot name), so the tty
build stopped at 'type void not a function'. statNoFollow keeps fstatat on
darwin and asks statx on linux for the same three fields, which is what this
file did before the branch generalized it to both platforms.
- .DS_Store rode along with a797a1a. Deleted, and .gitignore now says so.
linux: snap 86/86, unit-test, image-harness and mupdf-check green. nested.zig
also type-checks for aarch64-macos.
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The AppKit shell now draws what the core renders, follows the theme without a
relaunch, and builds into something you can hand to someone.
- Pixel attachments. Surface.images was dropped on the floor here, so a PDF
pane showed nothing at all: native_images is now set, pardes_image_s carries
the geometry the core already clipped, and PardesView keeps one CGImage per
(serial, page, revision) so scrolling costs a draw and not a decode. Image
panes get real pixels instead of the petscii fallback.
- Themes take hold live. pardes_tick never advanced the chrome animation, so
every tagline kept the previous theme's colours until the next launch and
the 16 ms re-pump spun for the rest of the session. pardes_theme_bg retires
the hand-agreed #121212 and drives the window background and the titlebar
appearance; a theme with no background of its own now gets a transparent
window over an NSVisualEffectView.
- The cell snaps to whole DEVICE pixels rather than whole points. Monaco
advances 8.4014pt at 14, so ceiling to 9 spaced every column 7.1% wider than
the face was drawn for.
- The dial is one notch per 10 degrees instead of 20, and a release keeps
turning in proportion to how hard it was thrown -- ramping up from zero at
the floor, so a slow twist coasts not a little but not at all.
- A file dropped on the grid is a click plus Look, so it opens beside the pane
it was dropped on. No drop concept was added to the core.
- The titlebar follows the focused pane: proxy icon, filename, and the dirty
dot. File.saved_revision is the watermark that last one needed.
- Config (SPC f c) prints the resolved startup config path.
- build.zig assembles, signs and packages the bundle itself; build-app.sh is
gone. -Dmacos-identity= takes a Developer ID, macos-dmg makes the image, and
the icon is Glenda.
|
| | | |
|
| |/
|
|
|
|
|
| |
- build.zig.zon: bump ghostty ba38b493 -> 82e53e3f (translate-c backport, fixes 404 on cold cache), update hash
- src/pardes.zig: adapt Terminal.init/resize to new std.Io signatures; fix display-column conversion for tabs and clicks past EOL (fileRawDisplayCol/fileRawAtDisplay)
- test/e2e_harness.zig: adapt to new ghostty_vt signatures
- snap suite 86/86 green
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|