| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A 9P round trip on a local Unix socket cost 9.7 ms against the SDL shell and
0.758 ms against the terminal one. The server was not slow and the wake was
not broken: instrumenting the path showed every request waking the loop early
(wakes=210, woke_early=212, timed_out=38 over 250 ticks) and being answered on
that same pass. The cost was that `pump` answers 9P at one point in a loop
that then renders and presents unconditionally, so a client's next request
landed while the main thread was blocked on the display, and each round trip
therefore cost a whole frame. The frame rate was governing something that has
nothing to do with drawing.
`Pardes.needs_frame` starts true, is set by every event except a tick with
nothing animating and a filesystem request that only reads, and is cleared
once a frame is presented. `pump` returns before render and present when it is
false and nothing is animating. An idle editor answering reads now draws
nothing at all.
9P read_fid, one RPC: gui 9.7 ms -> 0.056 ms (173x)
tty 0.758 ms -> 0.062 ms (12x)
Verified the shells still paint rather than going quiet: the rendered screen
carries the opened file, a write through 9P redraws within the frame, and
`fs-discovery-test` passes over the real wire. Suite unchanged at 778/783 with
the two pre-existing crashes.
Also from the adversarial review of the previous commits:
`pardes --tty FILE` silently discarded the file, and `--tty MISSING` silently
discarded the error pane. main.zig named the boot layout before the positional
was resolved, and naming one short-circuits `Boot.of`. The choice now happens
after the argument is known, and only when there is no file and no missing
word. macos.zig names the same layout, so the app no longer boots a different
one from the terminal and SDL shells.
`pre_close_last_pane_tail` was transcribed from the NEW default rather than the
old one, so the upgrade path it was added for did not exist: a workspace dumped
before the tagline reorder came back with the old default welded on as a custom
tail. It is now the string it claims to be.
A pane two rows tall lost its message and, worse, its prompt and the cursor
with it. The notice cap keeps the LAST notices now, because the prompt is last
and a prompt you cannot see is one you type into blind.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A pane's message, its pending leader chord and a prompt waiting for input all
wanted the same row above the tagline. The prompt won it, the leader drew over
whatever was there on the right, and renderBodyLayer recomputed "is anything
down there" inline to reserve a single row. Three claimants, one row, and two
places deciding.
Pane.Notices is that decision in one place: a short ordered list of the lines
a pane is showing, rebuilt every frame by collectNotices from the state that
owns each one. The body layer reserves exactly notices.len rows, the way it
already reserves rows for sticky context headers, and the paint pass walks the
list and gives each line a row of its own, stacked upward from the tagline.
Nothing stores a second copy of the truth, so a line that goes away is simply
not added next frame and the rest close the gap. One notice lands on exactly
the row the message always had, and the prompt stays nearest the tagline so it
keeps its cursor.
Last also learned what to do when the jumplist is empty. It used to walk the
jumps and, finding nothing, do nothing at all -- which is the ordinary case
for a pane that opened beside this one and was never focused, such as the text
pane the bare tty layout puts under the shell. It now falls back to
neighbourPane: the next pane down the column, wrapping, and any other live
pane failing that. Alternating with the neighbour is what Last is for.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
| |\ |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Pty children are exec'd with their own TERM/COLORTERM/TERM_PROGRAM instead of
inheriting a .app launch's empty environment, and ttyTaken finally answers on
darwin — libproc walks the tty's foreground process group — so Escape reaches
the child and Exec stops believing every pane sits at its prompt. The
occupancy suite runs on both platforms now.
The workspace tag row moves into the native menu bar as a Builtins menu.
-Dworkspace-tag (default off for -Dplatform=macos, on everywhere else) drives
it, and Pardes.topBarHeight replaces the TOPBAR_H constant so the core stops
reserving the row.
The view pins every variable-font axis to the file's own default (Maple Mono
came up Thin otherwise), shapes ligatures, carries per-shape pointer cursors,
and draws the look-hover affordance as refracted glass. Tag rows fill edge to
edge, with the anchor box painted back on top of that fill and its mode glyph
centred on the same square. Theme accents re-saturated across the set.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The 9P tree stops being a command language wearing a filesystem. /new
created a pane as a side effect of a *read*; it is now Tcreate in /pane,
with Tremove to close, which cloud9's engine has always supported and the
editor never declared: tree.zig now says
`features = .{ .create = true, .remove = true }`. Eleven pane ctl verbs
become files that can be read as well as written -- dot, limit, dirty,
mark, scroll, look, exec -- leaving ctl with `get`, the one verb no file
would say better. Root /ctl splits into a read-only /status and the
/look and /exec files whose write IS the click. stat carries real sizes
where it used to answer 0, and qid versions track a pane's revision, so a
client can poll for change without re-reading the body.
Commit a624a56 moved raw-tty keys to an early-return branch that knew only
Ctrl-B and bare Escape, and in the same edit deleted the paste branch below
it. That cost Shift-Escape (the unconditional way out of tty mode) and both
paste chords: Ctrl-V and Ctrl-Shift-V reached the child as keystrokes, so an
agent CLI running in a pane took Ctrl-V for its image-paste binding and
answered "No image found in clipboard". Both are restored, with tests.
Nested detection was not subtly broken but deleted: 60367d8 removed
nested.zig's process-ancestry walk and left "am I inside pardes" derived from
PARDES_FORWARD_LOOK, which read "0" both for --nested and for "the listener
did not come up". PARDES_PID now answers that question on its own, checked
with kill(pid, 0); PARDES_9P and PARDES_PANE answer how to reach it; the flag
is gone. The posted-9P registry also self-heals now -- a session that aborts
cannot unlink its own socket, so posting sweeps entries whose target refuses
a connection, symlinks only and on a definite ECONNREFUSED only.
Elsewhere: tty scrolling is sticky-bottom, following new output only from
the last row, with typing and entering raw mode snapping back to live; the
boot layouts are a Boot enum instead of a chain of ifs, and the bare tty
startup (Boot.tty, which main.zig names) opens an empty text pane under the
shell while tests keep Boot.tty_shell; builtins announce themselves on the
message row under a Verbose setting that is on by default; Config prints
each setting the way you would type it back, so WindowOpacity 70 rather than
"WindowOpacity: 70%"; LocationsConfig opens its window only when called bare;
every tagline puts the word that closes the thing last, and a column now
outlives its panes -- closing the last one leaves an empty pane, and only
Delcol, newly on the column tagline, takes the column away.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
src/9p.zig shrinks to an 88-line alias: the engine and the backend contract
(Req, Reply/ReplyWith, Op, Status, Attr, E, error strings) now come from
cloud9.fs. The editor's limits become cloud9.fs.Options values; the ESP32-P4
board backend drops its own copies of the contract types. No behaviour
change; fs-bench still allocates nothing per request. cloud9 re-pinned to
the commit that carries the engine.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The served tree loses the self/ level: /index /ctl /new /log /screen
/listeners /pane/<n>/... /os, with /src only in -Dembed-sources=true builds
(default off, on for esp32p4). ctl speaks the editor's own language with two
lowercase verbs, look TEXT and exec TEXT, plus acme's addr verbs; the new/
factory directory becomes one clone file; cons is gone (exec Msg); name and
sel are files; stats report real lengths, modes and mtimes; /log streams
pane new/del/rename/save events. The tree code lives in src/ninep/
(tree, pane, ctl, addr, pty, events, screen, sources); fs.zig keeps host
access, mounts, resolution and find/grep. Same engine and transports.
README (fs-help.txt) and docs rewritten; tests updated and extended.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| | |
|
| |
|
|
|
|
|
|
|
|
| |
The tag's first character moved to TAG_TEXT_INSET when the grip's hit area was
separated from the text by a gap; the assertion kept reading GUTTER, which is
now the gap itself and carries the plain tag background. The test's own point —
that a preview on the tag's first character is paintable at tag column zero,
PREFIX_W being a body concern — is unchanged; only where column zero is moved.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A swipe that was still moving when the fingers lifted stopped dead. The dial
next to it has thrown properly since pardes_rotate_end, and this is that same
curve on the scroll accumulator: velocity sampled between events off the
monotonic clock, weighted toward the newest sample because a flick is decided
by how the hand was moving when it left, and a fling that ramps up from zero
at the floor rather than switching on at it.
A coast stops at the end of a document rather than spinning its remaining
velocity against the edge, which is why spendScroll now reports whether the
pane moved and why paneAt is public. Only a step that delivered a press can
report an edge — the many steps between two rows cross nothing.
Nothing suppresses AppKit's own momentum, and nothing needs to: its momentum
events are ordinary pardes_scroll calls, every one of which cancels the coast
before spending its travel, so on a real trackpad the system takes the gesture
over about a frame after the lift and the tail it ends on is below the floor.
This is the path for devices AppKit does not fling for — and the only one a
script can reach, since NSEvent phases have no public constructor.
momentum.snap asserts both halves, which is why it needs the long scrollback:
a flick with no document left proves nothing. A slow swipe is byte-identical
after the release; a hard one coasts twenty-four rows further on its own, and
a finger back on the pad stops it there.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
| |
Empty marker on the last pre-Reload change. Keep the Reload experiment on reload (3801914), its first change on reload-start (200a1fc), and the unfinished performance investigation on reload-perf-wip.
|
| |
|
|
|
|
| |
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples.
Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
+Grep, +Search and every language answer render rows like
src/look.zig:718:12-16 fn grepText(path: []const u8, text: []const u8...
— a location, a space, and a piece of some file. The location names the file,
the file names the grammar, and the rest of the row is a fragment of that
language, so a grep over Zig reads as Zig and one Markdown row in the same
buffer does not pretend otherwise. The location itself is left uncoloured: it
is not code, and painting it as code is how a path starts looking like a
keyword.
`look.parsePathLine` decides what counts as a location — the same primitive
n/N already walks these buffers with, so the two agree by construction about
which rows are locations. NOT `lookableLineSpan`, which is n/N's whole
heuristic: it calls `resolve`, and a `realpath` per row per scroll is not
something a render path can afford. A bare filename is refused too; only
`path:line` counts, or a prose line whose first word ended in `.md` would
colour the rest of a sentence.
THE BUFFER IS COLOURED WHOLE, ONCE, WHEN IT IS FILLED. An adversarial pass
measured the obvious per-window implementation and it was untenable: the rows
are independent, so a window pass buys no fidelity, only amortisation, and
pays a burst on every scroll that outran the covered range. Grammars compile
their highlights query on first use — zig 26.9ms, cpp 18.9ms, rust 14.3ms —
so a polyglot grep showing six languages stalled a frame by 66ms, moving a
cost the syntax module had deliberately put on "opening a file" onto a scroll.
It also raised tree-sitter's allocation rate 3.5x (8,785 per refresh against
2,454) into a 16 MiB bump arena that only reclaims LIFO, so ~16 scroll
re-highlights exhausted it — and that arena is shared with real file panes, so
a results pane could evict editing. A grep is capped at 512 rows; colouring it
once makes the covered-range check true forever after and scrolling free.
The rest of that pass, in the same spirit: injections off for a single row
(both build a SECOND parser, per fenced block and per inline node, which is
absurd for one truncated row that almost never contains a fence), one query
cursor for the buffer instead of one per row, a one-entry extension memo so
non-matching rows stop paying a 29-spec scan, and NO highlights at all when
nothing painted — an all-zero run is not the same as none, and it defeated
`recolorSyntax`'s fast path, making every +Help and +Config walk its graphemes
every frame to paint nothing.
One correctness bug from the same pass: a failed `setLanguage` has already
nulled the parser's language, so leaving `held` on the previous grammar made
every later row of it skip the call and silently lose colour.
Documents keep `.source`: a New scratch and a real file are output-shaped but
have one language and an edit per keystroke, and `saves` is the line between
the two.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A message row is cleared by the next keystroke, so anything reported while you
were looking at another pane was gone before you could read it — a save that
failed, a watcher's reload, a builtin's complaint. `setMessage` now records
into a fixed ring first: no allocation and no failure path, because it sits
underneath `reportError`, which is reached from sites that are reporting an
allocation failure. `Messages` (`SPC h m`) reads it back oldest-first.
Three things an adversarial pass found, each of which defeated the feature:
PROGRESS IS NOT A MESSAGE. A language server emits `Indexing 47%` several
times a second, and every tick is a distinct string BY CONSTRUCTION, so no
de-duplication can collapse it: at the client's one-per-150ms throttle it
takes about nineteen seconds to push every real message out of the ring. A log
that one indexing run empties is not a log. That path is `setStatus` now —
the row, and nothing else.
THE CLOCK MADE EVERY HOST MESSAGE UNIQUE. `message.stamp` prefixes `HH:MM:SS`,
so `saved /x.zig` at 14:32:07 and at :09 compared unequal and the ring filled
with rows that look identical and each say (x1) — exactly the case the
de-duplication exists for. It compares `message.body` now, the row without its
clock, and the newest wording wins so the row carries the last time it
happened rather than the first. It also keys on the PANE (one pane's failure
must not be recorded as another's) and compares the truncated form, so two
identical messages over 256 bytes stop being two rows.
AND THE CAPACITY BELONGS IN limits.zig. 128 entries is 32.75 KiB that is
allocated whether or not anybody reads it — 8.5% of the ESP32-P4's whole
384 KiB heap, about the size of its effect ring. The board takes sixteen.
The builtins/leader goldens move because the listing gains a row, and
builtins.snap middle-clicks a SCREEN COORDINATE that Tutor moved out of; both
updated selectively and verified against a fresh run.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`|` 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.
|