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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
|
# Roadmap
## Phase 0 — scaffolding ✅
- [x] monorepo with real history: 62 fork commits replayed into `sl0ppij/`, 33
pi-ext commits replayed into `pi-ext/`, both verified byte-identical to
their source trees
- [x] `config/` — the live dotfiles imported as source of truth
- [x] `FORKS.md` + `VERSION` — one sl0ppi version, explicit upstream bases
- [x] `Makefile` + `packaging/` — build → vendor → stage → tarball
- [x] proven: vendored bun runs vendored pi, so the tarball needs no system node
## Phase 1 — the rename ✅
[rename.md](rename.md) is implemented: own binary name, config dir, cache dir,
socket dir, log file and plugin URL scheme, all behind a single `branding.rs`
seam. `$ZELLIJ*` kept and `$SL0PPIJ*` added on top, ours winning on read.
Verified live beside a running stock zellij; both suites green.
Left for later (both need work disproportionate to their value right now):
- [ ] strings *inside* the default plugins (About screen, status-bar tips) still
say "zellij" — they are baked into prebuilt `.wasm` assets
- [ ] `assets/zellij.desktop` / `zellij.rc`, used only by distro/Windows packaging
## Phase 2 — config becomes portable
- [x] `config/config.kdl` repointed at `~/.config/sl0ppij/` and the `sl0ppij:`
plugin scheme; verified to load under the renamed binary
- [ ] decide whether the opinionated defaults are also *compiled in* as
fallbacks, so a bare `sl0ppij` with no config dir still looks right
## Phase 3 — prove the artifact ✅
- [x] `make package` end to end → `sl0ppi-0.1.0-x86_64-linux.tar.gz` (83M,
328M staged) + sha256
- [x] installed into a throwaway `HOME` on a minimal PATH **with `node`, `npm`,
`bun`, `cargo` replaced by poison stubs that fail loudly**: `sl0ppij`,
`pi` (0.84.1, on the bundled bun) and `pir` all run. Nothing reached for a
system runtime.
- [x] real session started under a pty from the installed tree: config loads
("Well defined"), layout + fork pane titles render, both wasm plugins
load. Only log line on a fresh profile is the benign "permission cache
file not found".
- [x] **found and fixed two real packaging bugs** (see below)
- [x] **`musl` is now the release target.** The `gnu` build demanded
`GLIBC_2.39` (Jan 2024), so the tarball would have failed on Debian 12,
Ubuntu 22.04 and RHEL 9 -- the very machines this repo exists to install
on. The shipped binary is now `statically linked`, with zero `GLIBC_`
symbols. `stage.sh` fails loudly if a dynamically linked binary ever
sneaks back in.
Cross-compiling needs a musl C toolchain, because `isahc`/curl drags in
`libz-sys` and `libnghttp2-sys` unconditionally (not behind
`vendored_curl`, so it cannot be feature-flagged away). We use
`cargo-zigbuild` + the `ziglang` python module, both already present on
this box: no sudo, no docker daemon, no system packages.
The tarball's remaining libc floor is **bun's glibc 2.17** (2012), which
is effectively universal. If Alpine support is ever wanted, bun also
publishes `bun-linux-x64-musl.zip` -- a one-line change in
`fetch-vendor.sh`.
- [x] the tarball now ships the `sl0ppi` CLI too, bundled to a single 31K file
by `bun build` and run on the bundled bun — no source tree, no extra
runtime. Re-cut after the protobuf fix, so the artifact is coherent again.
- [ ] optional: `bun build --compile` pi into a single executable instead of
shipping 183M of `node_modules` — but only if runtime-loaded `.ts`
extensions still work in a compiled bundle. Test before committing to it.
- [ ] optional: pre-seed the plugin permission cache so a fresh install does not
open on a "Allow? (y/n)" prompt.
### What the smoke test caught
Both were invisible to `make package` and to any amount of reading:
1. **`zjstatus` was missing entirely.** The config snapshot commit dropped it as
"a third-party blob no longer referenced by config.kdl" — true of `config.kdl`,
false of `layouts/default.kdl`, which builds the whole status bar out of it.
It turned out to be a *third fork* (`sl0pstatus/`, now vendored) whose live
`.wasm` was a custom build. Sessions started with a plugin load error.
2. **`install.sh` installed a hardcoded plugin list**, so even once staged,
`zjstatus.wasm` never reached the config dir. Now it installs whatever the
package contains.
Both failure modes only appear *after* a session starts, which is the argument
for keeping the clean-room session test in the release routine.
## Phase 4 — the `sl0ppi` CLI
Design agreed: [cli-design.md](cli-design.md). One root dir = one session, one
project = one tab keyed by its `project:` tab purpose, panes from a
`.sl0ppi/layout.kdl` fragment. `up` is idempotent and everything rests on that.
Entry point fails open to plain `sl0ppij` so an orchestrator bug can never cost
you a terminal.
MVP is complete: discovery + idempotent tab diff, `up`/`ls`/`open`/`close`,
`init`, `doctor`, and the fail-open entry point. 88 tests.
Deferred: `agent --resume` (needs the pir port off python3), fuzzy picker for
tab overflow, root order file, per-project eager.
## Phase 5 — distribution ✅ (scripts, not CI)
Live at **https://sl0ppi.sl0p.foo**.
- `make release` — tests, then a fresh tarball
- `make publish` — renders the site, uploads artifact + pointers, then asks the
live site whether it agrees (status codes plus a re-downloaded checksum)
- `curl -fsSL https://sl0ppi.sl0p.foo/install.sh | sh` — verifies sha256 before
unpacking, and refuses to install anything on mismatch (tested by pointing it
at a doctored latest.json)
The vhost lives in the sl0pday repo (`nginx/sl0p.conf`), because that is where
that box's nginx config is deployed from. It is static-only: no app, no build on
the box.
Deliberately still not CI. A build host and a pipeline are worth it once there
is more than one target to build, and the publish path is scripted either way.
Guard rails worth knowing:
- `publish.sh` refuses to overwrite an already-published version whose bytes
differ. `/releases/` is cached for a year, so republishing changed bytes under
one name would serve two different things to two people. Bump `VERSION`.
- artifacts upload before pointers, so `latest.json` never names a file that is
not downloadable yet.
---
## Fixed: sl0ppij CLI actions were racy
`zellij action` used to fail ~20-25% of the time against a live session on a
2-vCPU box, and attached clients were dropped with "lost connection to the
zellij server". Two independent causes, both now fixed and measured:
1. **Client ids were recycled instantly** (`sl0p: stop recycling client ids`).
Every action probes liveness first (`assert_socket` connects, sends
`ConnStatus`, disconnects), and that probe is a full client. `new_client()`
returned the lowest free id, so the action connection got the probe's id
back; the queued `ConnStatus` removal then deleted the *action* client, and
its reply was dropped by `send_to_client`, which returns `Ok(())` for an
unknown id. Result: exit 0, no output, nothing logged.
2. **Suspended panes spammed resize errors** (`sl0p: stop suspended panes
spamming resize errors`). A reserved-but-unstarted terminal has no fd, which
was treated as an error and logged per pane per resize, on the server's hot
path. sl0ppi puts a suspended agent in every project, so it was the heaviest
possible user of that path.
| | before | after |
|---|---|---|
| `list-tabs` | ~20-25% fail | **0/50** |
| `list-panes` | ~20-25% fail | **0/50** |
| `list-tabs`, client attached | ~25% fail | **0/30** |
| `sl0ppi ls` | failed most runs | **0/15** |
| 5 attach cycles | dropped | **5/5 stable** |
| resize errors logged | 126+ | **0** |
Worth revisiting: `send_to_client` silently returns `Ok(())` for an unknown
client id. An unknown id is a caller bug, and swallowing it is what made this
take a day to find instead of a minute.
## Known issue: `attach --create-background` leaves a phantom client
A session created with `attach --create-background` keeps one client in its
connected set for the rest of its life. Nothing renders that client directly,
but it makes `connected_clients.len() > 1`, so `multiple_users_exist_in_session`
is true and **every pane frame shows its multi-user form** — `MY FOCUS AND:`
followed by one anonymous colour block. It looks like a stranger is in your
session.
Isolated by comparison, on the test box:
| session created by | marker on first attach |
|---|---|
| `attach --create-background` | **yes**, and on every later attach |
| ordinary `attach --create` whose client later dies | no |
Exactly one phantom, always the session's first client; it does not accumulate.
Not caused by the client-id change — that only made it *visible*, since recycled
ids used to hand the phantom's id straight to the next attaching client.
Things ruled out by measurement: it is not the CLI-action clients (a session with
15 actions and no attach shows nothing extra), and it is not a race with the
creating client exiting too early (making that client wait for a server ack
before exiting — verified 94ms, server answered — changes nothing, so that
experiment was reverted rather than shipped).
**Worked around, not fixed.** `sl0ppi up` now creates the session by *attaching*
to it whenever it has a terminal, which is the path that does not leak; the
background path remains for headless use, where no one is looking at a frame.
The fork bug is still there for anyone calling `zellij attach -b`, and the next
step is to instrument `ScreenInstruction::RemoveClient` on a debug build to find
why the removal never reaches `screen.connected_clients`.
## Fixed: panes stacked after a responsive collapse/expand cycle
A pane added after a narrow->wide cycle was absorbed into a stack at full width.
The expand had never actually happened: `SwapLayouts::tiled_layout_at` checked
whether a layout fits by positioning it against `visible_panes_count()`, and a
stack hides all its members but one, so a layout with no room for the hidden
panes passed as feasible. Fixed by checking against every tiled pane.
**How to re-run the check** (the tab test harness cannot express this -- it does
no rendering, so `panes_to_hide` is always empty and both counts are equal):
```bash
# a pty that starts narrow and widens mid-session
python3 resz.py 80 24 160 45 30 sl0ppi # see the commit for the script
sl0ppij -s dev action new-pane
sl0ppij -s dev action list-panes --json # compare (x, y, cols, rows) per pane
```
Two panes sharing one rect means a stack; distinct rects mean it really
expanded. Check both directions (create narrow then widen, create wide then
narrow) and add panes afterwards.
## Fixed: the `+` split button was drawn nowhere and clicked in the gap
Shipped in 0.1.7 and 0.1.8 without ever being visible on a live session, while
rendering in 265 unit snapshots. Four separate faults, each individually enough
to hide it:
- `title_line_without_middle` computed the right side only if the pane had a
title -- and `hide_default_pane_names true` means unnamed panes have none;
- `title_line_with_middle` dropped the right side whenever the left was absent,
which is every frame in a session with a second client (`MY FOCUS AND:`);
- the title was budgeted before the button, so a title longer than the frame
(i.e. any shell showing a path) consumed its columns;
- the click hit-tested the pane's allocation while the frame is drawn inset by
the pane gap, putting the clickable columns two to the right of the glyph;
- and, found after that shipped, stacks move the frame twice more: a stack list
pushes the visible member's frame down by its reserved rows, and the collapsed
members drew a button on a one-row title that nothing could ever honour.
The first three are fixed in `ui/pane_boundaries_frame.rs` by reserving the
anchored button before anything else and never discarding the right side; the
rest by `Pane::frame_rect_with_top_reserved()`, the rect the frame is actually
drawn in, and by not drawing a button on a stack's collapsed members.
**How to re-run the check** -- unit tests were necessary and insufficient here
for the fourth time, so this one reads the screen:
```bash
make check-live # packaging/checks/split-button-live.py
```
It starts a session at a pty, finds the `+` on the frame, attaches a second
client (the middle builder), splits and stacks, then clicks every button it can
see and expects one new pane per click. It runs twice, with a minimal config and
with `config/config.kdl` -- the real one, whose stacks are where the second
round of bugs was. Every fix was confirmed by reintroducing it and watching the
check fail.
If it ever disagrees with the code, do not theorise about the screen: put a
`log::info!` in `intercept_split_button_click` and read
`/tmp/sl0ppij-$UID/sl0ppij-log/`. The server's own geometry settles in one run
what screen-scraping cannot.
## Fixed: the shipped plugins asked for permission on first launch
pane-finder requests three permissions as it loads, so the first press of its
keybinding on any new machine opened a consent screen instead of a fuzzy finder
-- a nag about our own software, on a binding we ship.
Plugins we ship are now granted implicitly: builtins (via the existing
`PluginConfig::is_builtin`, the same predicate `check_command_permission`
already uses) and any wasm inside a plugin directory we install into -- the
config dir's `plugins/` and the data dir's `plugins/`. The rule is
`plugin_is_shipped_with_sl0ppi` in `zellij-utils/src/input/permission.rs`, and
it is deliberately narrow:
- the same wasm from any other path still prompts;
- remote plugins are never trusted;
- paths are normalised lexically first, so `plugins/../../evil.wasm` is not
"inside" the plugin dir;
- the trusted directories are derived from the environment and the default
locations, so getting them wrong costs a prompt and can never grant one
somewhere else.
The trade is that `<config dir>/plugins/` is now a trusted directory, documented
in `config/README.md`: anything that can write there can already put arbitrary
commands on a keybinding in `config.kdl`.
**How to re-run the check**:
```bash
make check-live # includes packaging/checks/plugin-permissions-live.py
```
It launches the real pane-finder wasm from a temp config dir and asserts it
runs without the prompt, then launches the *same file* from outside that dir and
asserts it still asks. Confirmed by disabling the rule and watching the first
half fail with the exact "asks permission to ... Allow? (y/n)" screen.
## Status bar: first pass of the overhaul
Two things, both in `sl0pstatus/` (our zjstatus fork) plus the layout that
configures it:
- **`margin_left` / `margin_right`** — the bar is a full-width pane while every
pane above it is inset by the pane gap, so its ends stuck out past every frame
on screen. The margins pull its content in. They are applied by rendering the
bar into an inset viewport (`ModuleConfig::inset_state`), which is also what
click positions are translated by, so the drawing and the hit-testing cannot
disagree — the lesson from the split button, applied up front this time.
- **`{new_tab}`** — a clickable button that opens a tab and focuses it, the
mouse spelling of the `n` binding in TAB mode. Clicking a tab already switches
to it and double-clicking renames it; making one was the gap.
Keep `margin_*` in step with `config.kdl`'s `gap` x `gap_aspect` (1 x 2 = 2).
The plugin cannot see that config, and inferring it from pane geometry would
break on the first borderless pane.
**How to re-run the check**:
```bash
make check-live # includes packaging/checks/status-bar-live.py
```
It reads the bar off a real session and asserts it spans *exactly* the columns
the pane frames span (2..97 at 100 columns), then clicks the `+` and expects one
more tab, and clicks beside it and expects none. Confirmed by setting the
margins to 0 and watching the span assertion fail with `(0, 99)` against the
frames' `(2, 97)`.
That check tracks painted cells, not ink: a bar segment starts with a padding
space inside its coloured pill, so stripping colour puts the left edge one
column off — the exact size of the thing being measured.
### The bar as a framed box
Second pass: given three rows the bar draws the same rounded box the panes do,
and given one it stays the single line it always was. Verified live — its
borders span exactly the columns the pane frames span, and the corners are the
same glyphs.
The height is the **layout's** decision, because a plugin cannot resize its own
pane:
- `config/layouts/default.kdl` defines the bar as a `pane_template` named
`sl0pbar` and places it at `size=3`;
- `config/layouts/default.swap.kdl` defines a `tab_template name="mobile"` that
places the same template at `size=1`, and the `stacked` swap layout — the one
`responsive_layout` collapses to below `responsive_min_cols` — uses it.
That reuse is only possible because zellij parses a layout and its `.swap.kdl`
with the **same parser**, deliberately, "so that we can reuse assets (eg. pane
and tab templates)". Without it the collapsed template would have to duplicate
the entire 100-line zjstatus configuration.
The plugin adapts to the rows it is handed (`ModuleConfig::viewport`): three or
more and it frames itself, fewer and it does not, so a `size=1` bar can never
render half a box. That one function also returns where the content line starts,
and click routing reads it — which is why a click on the new box's border hits
nothing instead of the widget underneath.
### A tab made at runtime had no status bar
Reported as "creating a new tab with the icon from the status bar created a tab
without statusbar" -- but the button was innocent: the `n` keybinding did it
too. A generated session had no `default_tab_template`, so there was nothing for
a runtime tab to take a bar from.
The reason it had none was the trap recorded below: templating a tab dropped its
`purpose`, and purpose is identity, so the CLI inlined the template into every
tab instead. That trap turned out to be one missing line in the fork --
`parse_tab_node_with_template` read `name`, `cwd`, `focus` and
`split_direction` off the tab node but never `purpose`, so the template's (None)
won. It now keeps the tab's own, and both properties hold at once:
| | inlined (before) | templated (before) | templated (now) |
|---|---|---|---|
| tab keeps `purpose` | yes | **no** | yes |
| runtime tab gets a bar | **no** | yes | yes |
So `sessionLayoutKdl` emits `default_tab_template` again, and the generated
layout shrank further: one copy of the bar's configuration for the session
rather than one per tab. `singleTabLayoutKdl` still inlines -- that layout is
handed to a session that already has a template, and emitting another would give
the new tab two bars.
`sl0ppi-up-live.py` checks both halves, because fixing either one alone
regresses the other: every tab (including one made at runtime) has exactly one
bar, and `sl0ppi ls` still finds a tab per project. Confirmed by reverting each
half in turn and watching the matching assertion fail.
Counting bars needs care: `dump-layout` includes the template itself and every
swap layout, so grepping it reports definitions as if they were panes -- that
cost an hour of chasing a doubled bar that did not exist. Count panes per tab
from `list-panes`, and do it BEFORE any client attaches (see the trap about what
that hides).
### `sl0ppi up` and the generated layout
Making the bar a `pane_template` broke `sl0ppi up` outright:
```
× Failed to parse Zellij configuration
43 │ sl0pbar size=3
· ╰── Invalid tab property: sl0pbar
```
The CLI writes its own layout file and inlines the tab template into each tab
(a `default_tab_template` would destroy the tab purposes that are its identity),
so a name the template body refers to has to travel with it. `extractPaneTemplates`
does that now, and the generated layout is smaller than before as a bonus: one
copy of the bar's configuration instead of one per tab.
**Do not "fix" the collapsed bar in generated sessions by copying the user's
`default.swap.kdl` next to the generated layout.** It looks right — at 70
columns the bar becomes one line — and then `auto_layout` swaps the tab to one
of the other swap layouts, whose bare `tab` nodes expect a
`default_tab_template` that a generated layout does not have, and the bar
disappears completely. Measured on screen, both ways. So in a generated session
the bar stays framed on a narrow terminal; the panes still collapse, via the
synthesized stack layout.
That measurement is also a warning about `action list-panes`: once a client is
attached it stops reporting the borderless plugin pane, which had me believing
the bar was gone in cases where it was on screen the whole time. `dump-layout`
and the screen agree; `list-panes` does not.
### A tab made at runtime now gets a status bar
Clicking the bar's `+` (and pressing `n` in TAB mode) produced a tab with no
status bar in `sl0ppi up` sessions. The button only made it visible: a generated
layout had no `default_tab_template`, so there was nothing for a new tab to take
a bar from.
The reason it had none was `parse_tab_node_with_template`, which read a tab
node's `name`, `cwd` and `focus` but not its `purpose` -- so templating a tab
erased the purpose that is the CLI's notion of identity, and the CLI worked
around it by inlining the template into every tab. That is now fixed at the
root (`sl0p_a_templated_tab_keeps_its_own_purpose`), and the CLI emits the
template again:
| | inlined (before) | template (now) |
|---|---|---|
| tab keeps `purpose=` | yes | yes |
| runtime `new-tab` has a bar | **no** | yes |
| copies of the bar's config in the layout | one per tab | one |
Both halves are checked by `sl0ppi-up-live.py`, because they trade off against
each other: it asserts `sl0ppi ls` still sees a tab per project AND that every
tab, including one created at runtime, has exactly one status bar.
Count bars per tab from `list-panes`, not by grepping `dump-layout`: that dump
also contains the template itself and every swap layout, so substring counting
there reports "4 bars for 3 tabs" and sends you looking for a doubling bug that
does not exist. And do it before any client attaches, because `list-panes` stops
reporting borderless plugin panes for a tab a client has rendered.
### Still open on the bar
Content and further click targets: a close-`x` per tab, session name → session
manager, mode indicator → unlock, drag-to-reorder tabs. Also: the pane-split `+`
and the new-tab `+` are currently the same glyph for different verbs.
## Fixed: the vendored bun required AVX2
The tarball shipped bun's default x64 build, which needs AVX2 — so on a plain
KVM guest sl0ppi installed cleanly and then died with `Illegal instruction` the
first time anything ran pi. `packaging/fetch-vendor.sh` now fetches
`bun-linux-x64-baseline.zip`.
Verified without needing such a machine, under an emulated CPU that has no AVX2:
```bash
qemu-x86_64 -cpu Nehalem vendor/cache/bun-*/bun --version # baseline: 1.3.14
qemu-x86_64 -cpu Nehalem /tmp/bun-linux-x64/bun --version # default: Illegal instruction
```
(That qemu-user binary is the one the tenet tracer builds, at
`~/dev/tenet/tenet-original/tracers/qemu-user/qemu-build/build/qemu-x86_64`.)
The baseline binary still contains 641 AVX2-only instructions against the
default's 10980 — those are runtime-dispatched paths in bundled libraries,
which is why running it is the check that matters rather than counting opcodes.
## Standing risks
**`sl0ppij/target` is a symlink to `~/lol/zellij/target`.** Introduced when the
box had 12G free, to reuse the old checkout's compiled dependencies instead of
building a second multi-GB tree. `CARGO_TARGET_DIR` alone was not enough,
because the debug-plugin `include!` path resolves relative to the source tree.
This is the one thing tying the monorepo to the old checkout: **deleting
`~/lol/zellij` leaves a dangling symlink and confusing build errors.** To cut the
cord (costs one ~5min cold release build):
```bash
rm sl0ppij/target && make build plugins
```
`sl0pstatus/target` is already a real directory (572M).
**Release builds are slow by design.** `make build` is 5m16s here (LTO,
`codegen-units = 1`); `make dev` is the ~5s iteration loop. Do not put `make
build` in an edit cycle.
That loop was 52s until the `fastdev` profile started splitting the crates we
edit into 256 codegen units. zellij-server is one enormous crate, so with 16
units a one-line change re-optimised a sixteenth of it; the incremental cache
can only skip work at codegen-unit granularity. Measured on the same one-line
change, checkout to linked binary: 52s at `codegen-units = 16`, 6s at 256, 5s
at 256 plus `opt-level = 1` (not worth a slower binary). Linking the 69MB
binary is only ~2s, and lld did not improve on it -- the linker is not the
problem here, and neither is opt-level.
**pi version coupling.** `pi-ext/` targets pi's extension API, pinned at 0.84.1.
Bumping the pin is an integration test, not a version bump.
**The OSC 5577 protocol is unversioned.** Fork and extensions are currently
kept in sync by being released together. If sl0ppij and pi-ext ever get
installed independently, this needs a handshake.
|