1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
|
# Changelog
## 0.0.2
- The macOS trackpad's two verbs trade places: a two-finger click is Exec and
a deep press is Look. Exec is what a hand spends a session doing, so it takes
the gesture that costs nothing, and the verb that goes somewhere is worth one
deliberate push past the click. Three fingers stay Exec, and that is the
hardware rather than a gesture left unused: macOS's secondary click is "click
or tap with two or more fingers", so a three-finger click arrives as
`rightMouseDown` exactly like a two-finger one, and the resting count is the
only thing that keeps either of them off Look. `pressureChange` still
releases the click in flight before the new button goes out, which now steps
around acme's 1-3 chord (Paste) where it used to step around 1-2 (Cut). Look
by finger count is gone, so a Mac with "Force Click and haptic feedback" off
— or a trackpad with no force sensor — reaches it through a real mouse's
right button, the `Look` builtin, or Enter on the word. `trackpad.snap`
asserts all of it without a trackpad, because `NSTouch` and the pressure
stages have no public constructors and `Trackpad` is therefore pure policy a
script can name finger counts to; the swap left every golden screen
byte-identical under the other gesture, which is what shows the two paths
were exchanged and nothing else moved.
- A filtered terminal costs what an unfiltered one does. `Filter`'s second
stage asked `RGB.contrast` for every cell it painted, and that call ends in
`std.math.pow` six times over — a libm round trip per cell, per frame, to
re-derive a ratio against a background that had not moved. The indexed path
takes a `u8`, so all 256 of its answers are now enumerated once per pass,
after the two default roles are fixed and before the first cell is read;
what a cell names is an array index into them. Only truecolour, whose
16.7M inputs cannot be tabulated, still reduces, and the direct-mapped RGB
cache is what keeps that cheap. Tracy over the AppKit shell, ReleaseFast,
190x56: the recolour pass falls 3.09 ms to 0.130 ms and the whole frame
3.37 ms to 0.299 ms. The luminance table is comptime-evaluated from
ghostty's own expression and a test pins it to `RGB.luminance` and
`RGB.contrast` exactly, across every channel value and all 65 536 palette
pairs, because the filter's decision is a threshold comparison where one
ULP is a different colour on screen.
- A pardes launched from the Dock finds the same programs a pardes launched
from a terminal does. LaunchServices hands a bundle launchd's own
environment, whose `PATH` is `/usr/bin:/bin:/usr/sbin:/sbin`, and every pty
shell, `|` filter and language server inherited it — so `yazi` in
`/opt/homebrew/bin` was missing in the app and present in the same build run
from a shell, which reads as "the Dock build is broken". `shell_bin`
composes `/etc/paths` then `/etc/paths.d/*` in the order `path_helper`
reads them and adopts the result before the first fork in all four native
hosts. It appends, so an inherited entry is never demoted and a configured
`PATH` is left byte-for-byte alone: the rule is that only a `PATH` nobody
configured gets repaired.
- Language servers other than ZLS start on macOS. The protocol client opened
its control socket with `SOCK.CLOEXEC`, which Zig defines for Linux and
which Darwin answers with `EPROTONOSUPPORT` — so `socketpair` failed before
any fork, every spec in the table reported "no server", and rust-analyzer,
clangd and gopls were unreachable in every macOS build. It is a plain
socket plus `fcntl(FD_CLOEXEC)` now, the route `fuse.zig` and `nested.zig`
already used. Verified end to end against a 200-crate Rust workspace:
rust-analyzer indexes, and `gd`, `document_symbols` and `hover` answer.
- `SPC l i` reports what the language backends are doing in the macOS app,
and `|` filters a selection there. Both effects were unimplemented host
methods — `lsp` and `pipe` were absent from the AppKit vtable, so the core
answered its own empty answer and the panel rendered blank while a sort
filter silently did nothing. The snapshot-plus-worker body each shell had
its own copy of is one module (`src/lsp_host.zig`), and the in-flight pipe
table is `selection_pipe.Tasks`; all three native shells share both.
Unsolicited server news ("rust-analyzer indexing 45%") reaches the message
row because the status sink is registered rather than merely defined.
- Animation runs at the speed it claims. `pardes_animation_tick` advanced one
scene frame per callback and reported time as `frame_count / 60`, so
animation time was a count of how often the callback happened rather than
how much time had passed — and the AppKit chain re-armed `asyncAfter(.now()
+ 0.016)` only AFTER the previous frame's work, making the real period 16 ms
plus all of it. Motion ran at roughly three quarters of wall clock,
unevenly. The tick measures elapsed monotonic time and spends it in whole
16 ms steps, banking the remainder, so a late callback advances two frames
instead of stretching one; the arithmetic is a pure function with its own
tests, no display attached. On macOS 14 and later the run is driven by one
`CADisplayLink` phase-locked to vsync instead of a chain rebuilt per frame.
- A filtered terminal's colours are mapped in two stages, and the second one
keeps text off the page. `Filter` reduces every colour to its nearest
canonical xterm key and reads that key out of ghostty-vt's theme-derived
256-colour projection — a comparison of RGB triples, which knows about hue
and nothing about the background. The projection is generated FROM the
default foreground and background, so its cube corners are those two
anchors, and the nearest key to a truecolour extreme was therefore the
background itself: `\x1b[38;2;255;255;255m` on acme's `#ffffea` paper
resolved to `#ffffea`, a WCAG ratio of 1.000, text painted the colour of the
page under it. Every curated theme owned such a key — 231 on the light one,
ANSI black on both dark ones, which is a bare `\x1b[30m`. The two default
roles are now mapped first and named as the anchors they are, and every
other FOREGROUND has to clear `config.tty_filter_min_contrast` (1.5) against
the mapped background; one that cannot is not mapped at all, and takes
whichever anchor is still visible there. Backgrounds are exempt, because a
background is the page the floor is measured against. Measured across the
curated three the floor refuses 12, 16 and 7 of 256 keys, where a WCAG
body-text 4.5 would refuse 61, 154 and 91 and flatten the palette.
## 0.0.1
- Pane taglines sit where they do in the SDL window. The macOS shell had its own
copy of the band geometry and it only ever centred the shorter band in its
body-sized row, which is the exact case `gui_topbar_pane_border_px` exists to
prevent: the topbar's unused half-band and the first pane tag's unused
half-band meet, and the window background shows through the seam — 4 physical
pixels at the default 82%, 10 at 50%, 14 at 30% on a 20-pixel cell, so it grew
as the tagline face shrank. The rule moved into the core and both pixel hosts
now call it: row zero bottom-aligned, the first pane-tag row top-aligned, the
two joined by the themed rule, rows between centred, and a `Tagbottom` band on
the last row flush with the window edge with the sub-cell strip beneath it
painted in the band's own colour instead of the page's.
- Files reload themselves on macOS. The tty shell, the SDL window and the
detached daemon all watched with `inotify` and therefore watched nothing off
Linux: an edit made outside pardes never reached the pane, and a PDF replaced
on disk kept rendering the old inode. They now share one mark/reconcile
transaction over two kernels — `inotify`, or a `kqueue` whose queue the hosts
wait on and poll 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 follows rename-over
saves, the file catches in-place writes, and the file filter is re-armed once
a rename-over has moved the inode. That is the same pair the AppKit host's
DispatchSources already used for the same reason.
- Snapshot captures are deltas against the previous capture in the same script,
and a scripted click may name a word (`press middle @Del 2`, `@Del#2` for the
second pane on a row, `@Save-2` for a column beside one) instead of a screen
column. Adding one builtin word to a tagline used to rewrite 78 goldens
(3,758 lines) and silently move seven scripts onto the wrong word: `tutor.snap`
had been clicking `Grep` and asserting a tutor pane, `tagbottomimage.snap`
clicked 176 columns from the `Del` it claimed to execute. Seven tests regained
their subject, the corpus dropped from 16,700 lines to 5,722, and the same
edit now moves 337 lines.
- `zig build snap -- --update` regenerates in parallel at the widest probe
settings and then verifies every golden it just wrote, instead of capturing
once, serially, and blessing whatever it saw: 77s to 41s, and a capture that
does not reproduce is reported rather than committed.
- `zig build unit-test -Dtest-filter=<substring>` runs one test. The whole
binary is 14s, so a one-line change used to cost the full run; the test
runner's own `--test-filter` was silently dropped by the build.
- A snapshot script covers the `Save` prompt for the first time: armed on a
`+New` scratch it draws on the message row, prefilled with the pane's own
directory, and submitting it turns the scratch into the file it names.
- `pardes --fs` serves acme's control filesystem over Linux FUSE: a directory
per pane holding `addr`, `body`, `ctl`, `data`, `errors`, `event`, `rdsel`,
`tag`, `wrsel`, `xdata`, plus `index`, `cons` and `new/` at the root. A
program that opens those files is an editor extension — no plugin API, no
interpreter. `--fs=<dir>` names the mount point instead of deriving one under
`$XDG_RUNTIME_DIR`; every pane shell gets `PARDES_FS` and `PARDES_PANE`.
See docs/acme-fs.md and examples/acmefs/.
- While a script holds a pane's `event` file open, buttons 2 and 3 in that pane
are REPORTED to it instead of performed, so the words in that pane's tag are
the script's commands (acme's extension model). Keyboard Enter/Tab still
execute, so a scripted pane stays editable.
- Writing an event record back (`origin type q0 q1`) makes pardes perform the
Look or Exec it names — the same door acme has always had, and the reason
the mount is 0700.
- Version tracking: the compiled version is embedded from build.zig.zon and shown by Changelog.
- New builtins: Changelog, Newtty, and Joincol.
- Markdown tree-sitter highlighting, including nested fenced code blocks.
- `.patch` and `.diff` files are highlighted.
- Coloring dispatch keys off the buffer title.
- Opening a new file only splits into a new column when both panes would be at least 100 columns wide.
- Terminal ANSI colors render only in tty mode; normal mode is a plain editing
view, so an edit can never shift a shell row's colour.
- `New` and `Newcol` open an empty scratch buffer instead of a shell or a
temporary file. Its directory is inherited from the pane it was opened from.
- `Save` on a scratch asks for a path, prefilled with that inherited
directory, and the buffer becomes an ordinary file once written.
- `Save` reaches every pane that holds text of its own and leads its tagline:
a terminal ("Save New Newtty Del Filter") and an output buffer like
`+Search` ("Save New Newtty Del") as well as a file. Images and PDFs keep
the plain tail — their bytes on disk already are what they are.
- `Save` takes the path as an ARGUMENT (`Save notes.txt`, or a selection
chorded onto the word) and asks for one only when it was not given, the way
Find and Grep ask for a pattern. A relative path resolves against the
pane's own directory, with `.` and `..` normalized; a path that cannot be
made absolute is refused rather than written next to the process.
- Writing to a path never rewrites the pane: a terminal stays a terminal, an
output buffer keeps its rows and its place in the n/N ring, and an open file
keeps the file it has, so `Save <elsewhere>` is a copy and not a rename.
The scratch `+New` is still the one exception, becoming the file it names.
- A dropped pane's memory outlives its frame, so a pane pointer taken during
that frame stays valid until the next one repairs it.
- The core owns the event loop. Each platform is now a `Host` of optional
function pointers; an unimplemented method falls back to an in-process
default, so a host with no methods at all is still a complete pardes.
- Ctrl-V and Ctrl-Shift-V paste in a tty pane: the default register and the
system clipboard, typed at the program the way a terminal emulator pastes
(bracketed under mode 2004). A paste arriving from the desktop reaches the
shell instead of an edit buffer the pty cannot see.
- The default filesystem is pardes's own source, embedded from an allowlist
(src/source_manifest.zig): what a host with no file method reads, and what
the browser has always read. It replaced a build-time generator that embedded
every tracked .zig file.
- `zig build run-isolated` builds and runs `pardes-isolate`: the same source
with one comptime option, whose host supplies only the terminal. The
filesystem is not disabled in it, it is absent — the libc paths compile away.
- Typst highlighting: headings, `*bold*` spans, raw/code blocks and inline raw
spans, and `#`-prefixed function calls (including the sigil itself).
- Fenced Typst raw blocks with a language tag (e.g. ```` ```zig ````) get that
language's grammar injected, the same treatment markdown fenced blocks
already had.
- Markdown bold, italics and inline code spans are highlighted: pardes now
builds the markdown inline grammar, a second parser off the same dependency,
and re-parses the block grammar's `inline` nodes with it. Only the block
grammar was built before, so emphasis had no nodes to match.
- Escape in a PDF pane is the document's own cancel: it drops the mouse
selection and the search highlight overlay and stays where you were reading.
It used to run `Last`, which moved focus out of the pane and left every
following key going somewhere else. `Shift-Esc` is now the hop out — the same
chord that leaves a raw tty.
- `gj`/`gk` move one VISUAL line, following the automatic breaks of a
soft-wrapped body and keeping the goal column inside the row. `j`/`k` are
unchanged: whole file lines. With `Wrap` off a line is one visual row and the
two pairs are the same motion.
|