| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
| |
With the line run under job control (the change before), the job running
has a group of its own, and Kill's SIGTERM to the tty's foreground group
stopped only that job: of `sleep 30; touch x` the touch still ran. For a
command pane the hosts now signal the shell's group as well, so the whole
line stops, as it did; a line typed at a prompt still loses only its
foreground job. The test forks a real pty for both halves: a background job
outliving its command and the pty's hangup, and a killed line not running on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
A dogfood agent's background jobs died when the command that started them
finished, even under nohup. The command's shell leads its terminal's
session, and on its exit the kernel hangs up the terminal's foreground
group, which with job control off is the shell's and every job's. The line
now runs with job control on (bash, sh, dash, zsh, ksh -m; fish status
job-control full), so a job has a group of its own and lives on, printing
below exit N, and survives the pane closing. While a job holds the pty the
pane is not reused, so the directory's next command gets a second pane
(cmdexit's golden, re-recorded by name).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
| |
pardes leaves none behind; tests remove their /tmp dirs
Every pardes host wrote its own pardes-osc133-bash-* and -fish-* files to /tmp and removed them only at a clean teardown, so each killed session, test and crash left two: 72K of them had piled up. They are now written once per content, named by its hash, in the user's private runtime directory, renamed into place whole and shared by every pardes; without that directory the old private /tmp files remain. fish's -C source is quoted. The 9p_io tests remove their runtime dirs with what the listener left in them, and the snapshot runner removes its retry captures when every retry passed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |\ \ |
|
| | | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | | |
A shell now answers Host.now, its monotonic clock in ns, and the pump
advances core animation to it: one .tick per whole 16 ms frame since core
time last stood still. The same animation therefore takes the same time at
60, 120 and 144 Hz, over ssh and after a slow frame (the GUI's re-armed
clock ran them ~25% slow at 144 Hz; the tty's post-frame sleep drifted).
nextWake() lists every core animation in one place: a frame from now while
anything moves, the end of the wait while something only waits (a message
lingering, the look-hover delay), null when idle. Shells sleep exactly that
long, and a wait is jumped to its end in one step, so an 800 ms linger
costs one frame instead of fifty. An overshoot under 1.5 ms after a step is
let go: at 60 and 120 Hz every display frame takes exactly one step (a 16 ms
frame against a 16.67 ms vsync would otherwise double-step every 24th).
The six tick drivers are gone: tty's timer thread only times the wait,
interruptibly (a newer, shorter request cuts short a sleep still timing a
longer one); the GUI's
AnimationClock, web's JS tick bank (pardes_tick now takes rAF's timestamp),
the detached server's and the board's ticks; grid mode steps a virtual
clock straight to each wake. PARDES_TEST_CLOCK, set by the snapshot
harness, gives tty and the detached server the same virtual clock.
Every stepped frame is drawn. The old pump never drew the last frame of a
fade (a .tick asks for no frame, and the fade was over by the check), so a
theme switch stopped at 9/10 of the way until the next input: theme.golden,
themesel.golden and the GUI's acme-light scene move to the theme's true
colours, and nothing else changes. The frozen previous grid is captured only
while a panel transition is chosen, not on every idle frame.
|
| | | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | | |
The watcher woke the host a second time 60 ms after the exit by its own clock
while the grace was counted from the reap: a host busy for 10 ms missed it,
and the tag said running for ever and Kill did nothing; and under load exit 0
could land before the last output. As decided, no timer: the exit is told
once it is reaped and the pty says nothing is left (poll: no POLLIN, and no
POLLHUP, which means the end of file is on its way behind the output), else
at that end of file, checked after each chunk of output. The tty host checks
inside its step so the frame shows it. The four hosts' copies are one
host_io.takeExits/commandEof, which close a told command's pty at its end of
file (the fd and the GUI's reader leaked when the exit came first). A
finished command pane whose pty a job it left still holds is not reused, so
that job is not hung up; and the reset before a reuse is SGR 0, not DECSTR,
which ghostty's stream does not implement.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |/ /
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Tty /nonexistent or Tty fsh started the host's fallback shell without a word,
and when spawnTty made no pane, the argument became the calling pane's shell.
Tty now looks the shell up first and fails 'Tty: no shell "fsh"' when it is
neither a name on the usual paths nor a path to one; spawnTty answers the pane
it made, the one given the shell, and says why when it made none. The lookup
(host_io Shell.find) gives the turn up around its access() calls, since a
typed path may be under a mount this editor serves, and forkShell copies the
pane's shell before looking it up and checks the pane is still its own after.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
A job left in the background (sleep 100 &) held the pty open, so the pane
stayed running and the child a zombie until the job ended; a command that
closed its terminal and ran on got its end of file at once, the host waited
100 ms for an exit, reported exit ? and hung it up. Now each command's child
is watched on a thread (waitid with WNOWAIT, so its pid stays its own until
the host reaps it), and the host tells the core the exit from that: after the
pty's end of file, so the output before the exit is in, or 50 ms after the
exit without one, a job holding the pty. The pty stays open until both, so a
command that let go of its terminal is never hung up by it. All four front
ends; a host that cannot start the watcher reads the exit at end of file as
before. Tests: host_io's for both cases, and cmdexit.snap end to end.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
retaken spawns nothing
The spawn effect carried only a slot, and the host read the command and shell
off whatever pane held the slot as it forked: a command pane closed and its
slot taken by the next one before the effects ran forked the new pane's
command twice. The effect now carries the pane's serial and is dropped when
the slot holds another; forkShell checks it again after giving up the turn
for its directory's stat, the one moment a 9P client can change the panes
under it. Every front end performs spawns with the turn, from its step.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
A terminal's tag said Tty whatever shell it ran. The host's acknowledgement
of the shell it forked now sets the word to Tty+<shell>, tinted like the tag's
name. Word+arg is taken as Word arg, one word a tag can hold and a click can
take whole (+ is a word character), so Tty+fish opens a terminal on fish, as
Tty fish does; Tty takes the shell as its argument, which the host forks for
that pane instead of the configured one. Every snapshot whose screen shows a
terminal's tag changes: 69 goldens, re-recorded by name, whose 297 changed
lines are all Tty becoming Tty+bash (the harness runs bash), the cursor
columns and style spans that moved with it, or that text cut at a pane's
width.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
shell's prompt
A middle click, an exec write or a tag word that no builtin knows was typed
into some terminal for the pane's directory, sharing whatever state that
shell was in and answering nothing, so a misspelling vanished into a shell.
Now only a line clicked at an interactive terminal's prompt is typed there.
From anywhere else it runs as a command pane: a terminal whose child is
$SHELL -c the line in the pane's directory, full emulation, which shows its
output and then exit N from the host's reaping of the child, and stays. A
finished command pane is the next command's for its directory, which runs
below what it showed after a '% line' line (acme appends to +Errors and never
clears it, util.c:213); a running one gets a second pane. Kill ends a command
pane's whole process group, the log records run and exit, exec reads back the
command pane's serial, and a line is at most 1 KB, read off the pane as the
host forks rather than carried in every spawn effect. ttyForDir's search for
a free shell is gone. The goldens of chordcut, cmdword and layout-open change
where a file's exec now opens a command pane, and ttytaken is rewritten to
exec from the terminal itself; docs/open-questions.md records the decision.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
Kill queued a signal effect for later, so the job it saw running by its marks
could have ended, and another started, before the signal went; and with job
control off the job shares the shell's process group, so the host skipped it
and Kill reported nothing. Kill now calls the host's kill_job while it holds the
turn, and when the only job is the shell's own group says 'Kill: no job to
signal', which also fails a write of Kill to ctl.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
the hangup
tty and macOS hung the pty up and called waitpid once without waiting, so a shell
still exiting, or one that ignores SIGHUP, stayed a zombie or ran on with nobody
reading it; tty also never reaped a shell that exited by itself, and its spawn
into an occupied slot closed the old pty without ending the shell. host_io's
retireShell says hangup, waits 100 ms on a thread (macOS has no host timer to
poll from), then kills and reaps. The gui's own retired list, when full, left the
slot holding the old shell and refused the next spawn into that pane; it now
hands that shell to retireShell instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Kill quit the editor, which in acme is Exit; acme's Kill stops the
commands it started. Exit now quits as acme's does (exec.c, rowclean):
it refuses once, naming each pane with unsaved text, and quits when
asked again with nothing edited since (a small scratch is not asked
about). Kill, bare or with names, stops the commands pardes typed into a
terminal (an exec, a middle click, a pty/run) while their shell's marks
say they run, by SIGTERM to the terminal's foreground job, never to the
shell (acme posts the kill note, which terminates). Both are session
builtins; the topbar's Kill becomes Exit, same width, and every golden's
topbar row changed by exactly that word (checked line by line); the
builtins script scrolls one more row for the index's new line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The detached server closed a pane's pty only when the shell was respawned,
hit EOF or the session shut down, so rmdir, Del or Delcol on a terminal
left its shell running with nobody to read it; the tty front end and
macOS did the same. Retiring a terminal pane now emits a close_pty
effect, which each host that runs shells answers by hanging the pty up
and ending the shell (the detached server's and the GUI's existing
retire-and-reap path, and a close plus SIGHUP in the tty and macOS
shells).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
acme keeps what edits a text in its Text (dat.h:171-190) and the window
holds a body and a tag of that type. The cursor, the selections, the modal
state and the edit-buffer undo move off Pane into Text.zig, Pane holds
them as its body, and the edit and normal-mode operations take the Text
they edit. Nothing changes in behaviour; this is the step that lets the
tag become a second Text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Documentation only, no code change: README's reading order and layout,
docs/design.typ's paragraph on where pane kinds and editor parts live,
docs/helix-keys.md's code map (normalInput and the executors now in
normal.zig and edit.zig, insertTab in edit.zig), docs/open-questions.md
(execute and ttyForDir in exec.zig), and the comments that named pardes.zig
for fold, Cell, takesCommandLine and runBuiltin (tools/gen_themes.zig,
themes/helix.zig, detached/wire.zig, host_io.zig, lsp_zls.zig).
docs/design.pdf is a retained fixture and is not regenerated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review fixes to pty/run and /log:
- Both are streams with a per-open cursor. A shell's exec 3<>file shares
one offset between write and read, so cat <&3 after echo make >&3 asked
for offset 5 and got "0" instead of "exit 0"; log after follow lost its
first bytes the same way.
- A run is accepted only in pardes's own tagged input phase, so a nested
shell's prompt (ssh, a shell with its own integration) is never taken for
this shell's.
- A line the shell refused is taken back so the next finds the prompt
clear: bash's continuation prompt (now tagged k=c) gets Ctrl-C and the
answer waits for the fresh prompt; a bash syntax error (no C, but a D) is
answered at once; fish's kept line gets Ctrl-U (a Ctrl-C sent while fish
redraws is lost). fish's right prompt no longer reads as typed input.
- bash marks a command's start from PS0 on bash 4.4+, not a DEBUG trap, so
a user's own DEBUG trap (bash-preexec, atuin) survives. bash's own job
notices now show as in plain bash (ttyfollow golden).
- 32 run slots; a new line on an open starts a fresh answer.
Checked live in bash, fish, and fish with a right prompt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
/pane/<n>/pty/run takes factotum's rpc shape: write one line on an open,
read the answer on that same open. The answer is exit N once the command
ended and the shell is back at a prompt; busy at once when a command runs,
text is typed at the prompt, or the shell has not drawn its first prompt;
error not run when the shell refused the line without running it (fish on
a syntax error keeps it on the prompt, so it is taken back with Ctrl-U);
error shell gone when the pane closes or its shell is replaced; error no
prompt marks for a shell pardes could not instrument.
The end comes from the shell's OSC 133 marks. ghostty parses D's exit
status and drops it, so the stream now runs through a handler that wraps
ghostty's and follows prompt -> input -> running -> done. The marks pardes
injects into bash and fish carry aid=pardes and only those count, so fish
4's own marks (which doubled ours), a nested shell's, and a stray 133;D in
printed output are ignored.
Checked end to end against real bash and fish: false, exit 7, a syntax
error, sleep, busy while running, and the pane closing mid-command.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
| |
bash and fish now print OSC 133;D;<status>, which ghostty already parses;
the planned /pane/<n>/run file reads it to answer when a command finished
and how.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The editor's loop was the only thing that could answer a 9P request, which
made the editor's own syscalls through a mount of its own tree -- a Look at
/mnt/9p/pardes/<me>/anything under a `9ns --mntgen` view, a Save into it --
requests only the blocked loop could serve. The name-based refusal that
followed (ownMountSuffix) and the in-process routing of a mount of oneself
(Client.sameSession) were patches over that, and both are gone, with the
mailbox that shipped every request to the editor's thread.
One rule replaces them, `pardes.turn`: the core is single-threaded, the
editor's thread has the turn by default and gives it up in two kinds of gap
-- while it waits for input and while a step of it is out in a host syscall
-- and a cloud9 connection task takes it in those gaps to answer. `out`
counts the steps that are out, from any thread: while one is, the core reads
consistently but that step still holds pointers into it, so a request that
would change a pane (a write, a truncation, an rmdir) is parked in the
engine and retried when the turn is next given up with nothing out, and the
editor's own wake waits for the count to reach zero. It is never a write of
its own that a step waits on out there -- writes come from a shell
performing a save between steps -- so a parked request is never the
syscall's own, and making a pane or rendering a screen need not park:
every yield sits before its step's mutation, so the layout and the surface
are whole under it. A changing request that queued effects is answered
once the editor has performed them (`echo Save > exec` returns with the
file written, as acme's `put` does), and it settles the way a step does,
because without that a /log reader waited for the user's next keystroke.
Every host syscall on a user path has to give the turn up, not fs.zig's
alone: the first end-to-end run hung in `inotify_add_watch` performing the
new pane's watch effect. PDFs and images are read whole at open, so no
draw goes out into the host. The core's allocator takes its fixed buffer
through the lock-free interface, since a connection task allocates while
the editor's thread is out in a syscall that allocates too. A Restore puts
the replacement in first and releases every task waiting on the old core.
cloud9 (pinned at eb1a104) parks an open, a truncating wstat, a clunk and a
remove on `again`, not only reads and writes, and answers a parked job
whose fid was clunked without asking the backend.
Verified: test/selfmount.py runs the editor under `9ns --mntgen` and
Looks at, reads and Saves its own tree through the mount; a unit test pins
that a change parks while the editor is out mid-step and lands when it
rests, while a read is answered in the window. 9P over the Unix socket
against a tty session, same machine, Debug builds: a read of /index 278us
-> 61us, a truncating body write 1184us -> 609us, exec Save 718us -> 583us;
the gesture benchmark is unchanged (geometric mean 0.997 over 53 cells).
Also from the reviews: a notice chip over an image or PDF pane was painted
out by the picture drawn after the cells, so pictures give up the rows; in
the GUI a tree-sitter context band painted over the chip, so body layers
are emitted first; a message is one row of printable text, its 256-byte
cut never leaves half a glyph, and one wider than its pane keeps its tail
(the file name, the reason) rather than its head.
Co-Authored-By: Claude Fable 5.1 <[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]>
|
| | |
|
| |
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
|
|
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.
|