aboutsummaryrefslogtreecommitdiffstats
path: root/docs/keys.md (follow)
Commit message (Collapse)AuthorAge
* themes: a picker that wears them as you browse (C-a t)user2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | `theme_name` is a line in a file, and a name in a list tells you nothing about a colour scheme. So the picker previews by *applying*: moving the selection puts that theme on, which is the only honest preview of a palette meant to dress a whole session -- a swatch in a list would be a small lie about it, and the session is already on screen behind the box. It opens on the theme being worn rather than at the top, so the first arrow press moves off it instead of silently changing the session, and the row your config names says so, because "which one is this" and "which one comes back on restart" are two different facts the moment you switch. Escape puts back what was worn, since a picker that changed your session by being looked at is one you stop opening. Enter keeps it for the session and says as much. Only C-s writes `theme_name` into your config, which is the one place in the UI where a keystroke edits it, so it is a deliberate chord and the footer says so: "C-s keeps it, esc undoes". Reuses the picker the finder, the palette and the projects list already wear -- one machine with a subject, so the query, the selection, the mouse, the frame and the scroll behaviour are the ones already tested. The list is scanned on the keystroke, like the projects are: the moment it opens is the moment it has to be right, and a theme written since the session started is exactly the one being looked for. With nothing to list it toasts the directory it looked in rather than opening an empty box. `t` was free: tabs cycle on tab, and new-tab has had `c` since the first day.
* search: a scrollback search bar on /, matches highlighteduser2026-09-08
| | | | | | | | | | | | | | | | | | | | | | | | C-a / opens an inline search bar on the focused pane's status row, on lib-vt's GhosttySearch (D4's pattern: the scrollback walk, reflow and alt-screen survival are the library's, the UI and the wiring are ours). Type to search live; the viewport follows the current match; up/down (or C-n/C-p) step through them with wraparound; Enter keeps your place, Escape snaps back. A k-of-n counter reads on the right, or "no matches". Every match on screen is tinted, and the one the bar is on is tinted apart -- warm gold by default, painted after the shader pass so the scrolled wash does not dilute a match you are hunting for. Four themeable colours (search_fg/bg, search_cur_fg/bg). Bare / took the key from help, which keeps ?; that needed the legacy decoder to read the shift out of a punctuation byte the way it already did for capitals -- without it ? and / decode identically (SLASH, no mods) and cannot be two keys on a client without the kitty protocol. tests/test_search.py drives it end to end (39 checks): the bar, the counter, stepping and wrap, all-match vs current highlighting, Enter/ Escape, new output updating the count, case-insensitivity, the keyboard belonging to the bar, click-away, and that / and ? no longer collide.
* docs: big overhaulPeter Geissler2026-09-02
|
* docs: less wordyuser2026-08-29
|
* README/docs: stop leaning on the aside dashuser2026-08-27
| | | | | | | | | | | | | Some 230 sentences hung their qualifications between dashes, and at that density the tic reads as the writing's signature rather than a tool it occasionally picks up. Each one is now the punctuation the sentence actually wanted: a colon where the aside defines, parentheses where it whispers, a comma where it barely pauses, a full stop where two thoughts were pretending to be one. Definition lists and reference tables take colons throughout. Two double hyphens remain on purpose, both literal: the argv separator in a shell example, and a table cell that means 'takes nothing'.
* README/docs: spell the dash --, as the rest of the tree doesuser2026-08-27
| | | | | | | | | | | Every C comment, commit message and generated config in this repo writes the aside dash as a spaced double hyphen; the Markdown alone used U+2014, some 220 of them. One spelling, and the ASCII one -- the prose now matches the code it documents. Mechanical (every occurrence was the spaced form, checked including the one inside a code fence, where it was comment text), so the words themselves are untouched.
* slosh: floating panes and the drop grammaruser2026-08-20
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A pane can float: lifted out of the layout, drawn over the tiled panes and under the modals, freely moved and shaped. The model is a flag and a rect on the leaf, exactly like `minimized` -- the seat in the tree is kept, layout skips it, the siblings absorb its share, and un-floating returns it home. The drawn rect is the wanted one clamped to the tab every frame, never written back, so a terminal squeeze round-trips exactly; an explicit move or resize *does* write back, because the hand is the author of the intent. Floating pops the pane to the centre: the jump is what says it happened. Painted after everything tiled, so the hit list -- searched backwards -- resolves an overlap to the float you can see: the z-order and the click order are the same list. It casts a shadow (float_shadow, one number), and `floating` joins the states table above `unfocused`, so a float is never dimmed as merely-unfocused. The hands: the title moves it, any other border resizes it (a bottom corner on both axes; the grabbed edge pins at walls and floors, never sliding the window), H J K L move a focused float, the =/+ key grows it about its centre, `-` shrinks it (its split was a refusal anyway), and hovering shows which edges a grab would take, from the same derivation the press uses. Equalize moved to `0` to free the key; `C-a f` floats (finder went to `s`, where it reads as search); `C-a F` opens a new floating shell -- the throwaway terminal; the function row f1..f12 became nameable in configs along the way. Guards throughout: the last tiled pane cannot float or minimize away, closing it lands the top float, splits are refused with their own words, floats are not swap targets. Dropping got a grammar: while a pane is carried, the pane under the pointer subdivides into a swap centre and four edge bands meaning "insert me beside this pane, on this side" -- detach_leaf + place_beside, the surgery cross-tab moves already had. Bands are thirds (the first map of quarters showed the swap owning half of each axis), the hovered band fills the half the drop would hand over, the release resolves against the painted frame's hits so it cannot disagree with the fill, and a drop between siblings along the axis is a reorder that always fits -- refusing it was refusing the most ordinary drag there is. Also: rename-pane/rename-tab as palette actions (renaming was a double-click and nothing else, the one verb the palette could not reach), and C-u clears the rename editor like it clears the picker's query. Design log in PLAN-FLOATING-PANES.md; to fold into DESIGN.md as D22/M10-11.
* slosh: rename the project from sl0pptyuser2026-08-18
| | | | | | | | | | | | | | | | | | | | | sl0p + pty was error-prone to type and the zero made it worse; slosh (slop + sh) is one syllable, impossible to typo, and slosh.foo is ours. Mechanical rename everywhere it appears: binary, env vars (SLOSH, SLOSH_SESSION, SLOSH_BIN, SLOSH_CONFIG), socket dir (/tmp/slosh-<uid>, $XDG_RUNTIME_DIR/slosh), config path (~/.config/slosh), header guards, src/sl0ppty.h -> src/slosh.h, contrib/sl0ppty-dev -> contrib/slosh-dev, the driving-slosh skill, docs and tests. The sl0ppi fork references and the sl0p theme stay: one is history, the other is the brand. The rename surfaced a latent bug in test_osc5577: the hello-reply answer always appeared on screen twice (ECHOCTL echo plus cat -v), but a seven-character implementation name happened to wrap the second copy across the 60-column boundary, so counting substrings in wrapped text found one and passed for the wrong reason. A five-character name moved the wrap and exposed it. The loop guard itself is correct -- with echo off before the request there is exactly one reply -- so the test now counts on de-wrapped text and tolerates the pane's own echo.
* sl0ppty: workspaces, and layouts that write themselvesuser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A **project** is a directory on disk. A **workspace** is the tab it occupies in a session. Two words because they are two things, and conflating them is how a session grows a second source of truth about what it is looking at. `project_roots "~/dev" "~/work" depth=2` is scanned for subdirectories holding a `sl0ppty.layout.kdl` -- or a `.git`, which is the project that has not said what it needs yet. Both markers, because a `~/dev` of forty checkouts and three layout files is a picker with three rows, and `C-a W` is how a project stops being the second kind. A directory that *is* a project is never descended into, so the walk stays off `node_modules` without anyone writing a rule about `node_modules`. `C-a w` is the third subject of the one picker (D19): the finder searches what this session has, this lists what is on disk, including the projects with no workspace open yet, which is most of them and the whole point. **Identity is the tab's `purpose`** -- `project:<name>.<hash8>` of the resolved path, the shape D3 already published -- so there is no second field to disagree with it, membership is a string comparison, and a dumped session restores it for free because a dump already writes tab purposes and applying one already locks them (D8). Hashed on the *path* rather than the name, so two worktrees of one repo are two workspaces, which is what they are. A tab whose layout gave it a purpose of its own keeps it and is simply not a member: overwriting a declared purpose is the one thing D8 forbids, and the reply counts the ones it left alone. **Nothing watches the filesystem.** The list is derived when it is asked for -- one `readdir` per root and two `stat`s per entry -- and an answer nobody remembered cannot be stale, which is a promise no watcher can make on a bind mount. The picker scans once as it opens, not per frame, because `draw_picker` asks for its rows every frame and a `readdir` per repaint is a filesystem walk at 120Hz. Opening is idempotent (`sl0ppi up`'s property): the same request twice is one workspace, focused, and the answer says which branch it took. Four things had to be true before `save-workspace` could exist, and none of them were. **A relative `cwd=` in a layout file now means that file's own directory** -- the rule `include` already followed for configs, applied to the other half of the same syntax. A project's layout could previously only name absolute directories, which are right on exactly one machine and wrong in a worktree of the same repo. **A dump can be asked for one tab, against a base.** `dump-layout` takes `tab`, `relative_to` and `suspend`. Writing it also turned up a bug: `dump_node` asked `cur(a)->focus` whichever tab it was writing, so a dump of three tabs restored the focus of one. **A dump records what a pane is *running*.** `label` only ever knew what a layout said, so a pane you split and typed `npm run dev` into had nothing to say for itself and came back as a bare shell -- which made arranging a project and writing it down two jobs instead of one. The kernel knows what the label does not: a pty has a foreground process group, and that group is by definition the job that owns the terminal, so the answer is read the way `live_cwd` already reads `/proc/PID/cwd`. Three things are deliberately not commands: a shell at a prompt (its group is the pane's own, and `command="zsh"` on every idle pane restores a session where each shell runs inside a shell), a background job (it does not own the terminal), and an ephemeral pane. A declared label still outranks a live process, because it survives the program exiting (D14) and re-saving must not degrade `npm run dev` into whatever the process tree looks like this minute. argv is joined with shell quoting, since `command=` goes through `/bin/sh -c`. **A pane from a split starts where the pane it came out of stands**, not where the server was launched. Wrong on its own, and it had been quietly writing an absolute path to somebody else's directory into every saved project. A saved project defaults to `suspend="commands"`: the pane running this morning's dev server is written asleep, because a *project's* layout is a statement of shape and a *session's* dump is a statement of fact. Over the API, replacing a layout that is already there is refused without `force`; from the keyboard `C-a W` replaces and says `replaced`, because a key cannot pass a flag and the file is committed text whose diff is the review. `--check` now holds a layout to the layout schema instead of answering with the name of the flag that reads one. `unknown pane property: cmd`, `split is cols or rows`, `purpose is ignored on a pane with panes in it` -- the mistakes a layout can make that still parses. The lists live beside `build_pane` and a test greps for every prop the loader reads, the arrangement that keeps `KNOWN_TOP` honest. **`C-a P` sets a pane's purpose**, a third subject on the editor the pane title and the tab label already share. A purpose is what tooling finds a pane by, and it could only be declared by a layout or set over the socket -- so every pane anybody arranged by hand had none, and a layout dumped from one was a shape with no tags in it. Typed by an operator it counts as declared (D8: "a layout or an operator"). Clearing one now *unlocks* it: a lock held over an empty string was the one state nothing could escape. `set-purpose` also reads `id: 0` as the focused pane now, which every sibling verb already did. Two gaps found on the way, both reachable only over the socket before: **`C-a X` closes a tab.** It used to be a side effect -- press `x` once per pane and the tab evaporates when the last one goes -- so four panes meant four presses and counting. It says how many panes went, since most of them were not the one you were looking at, and on the last tab it refuses and points at `q`: a key that closes a tab four times and ends your session the fifth is one you cannot press without counting first. No `✕` on the tabs, for the arithmetic that took the per-pane `+` away. **`scrollback` is a number we choose.** D4's "free from lib-vt" was true of the machinery and quietly false of the size: handed no limit, lib-vt picks its own -- 10,000 *bytes*, which measured at 622 lines of an 80-column pane, less than one `make` run. `scrollback` is lines, defaulting to the 10,000 every other multiplexer settled on, and `scrollback_bytes` is the ceiling it runs into, because either limit can be reached first and neither can see what the other depends on. Measured at 10,000 lines of styled output: ~6 MB at 80 columns (the ceiling never bites), ~15 MB at 200 (2% of the lines), ~32 MB at 400 (half of them). No `unlimited`, deliberately: the ceiling is per pane, and a negative is refused rather than wrapped into eighteen quintillion lines. Read when a pane is created, so it applies to the next pane you open -- same rule as `shell`, because shrinking a pane's history retroactively throws away output somebody is reading. `--project-root` was considered and dropped: the config is the one place projects are declared, and tests pin a config like every other config-driven feature. Verified: 1752 checks, all green. 90 in `tests/test_workspaces.py` (the two markers, depth without descending into a project, two worktrees as two workspaces, idempotent open, the honoured-purpose count, the save/close/open round trip byte-for-byte, the set-up-by-hand-and-write-it-down flow, the picker, and every verb's bare form); 53 in `tests/test_portable_layouts.py` (relative and absolute `cwd=`, text having no base, per-tab focus, the four `suspend` words, captured commands, an idle shell and a background job captured as nothing, spaces surviving the round trip, label precedence, and the checker's lists not going stale); 21 in `tests/test_purpose_editor.py`; 14 in `tests/test_scrollback.py`; 5 more in `tests/test_tabs.py`. `tests/test_osc5577.py` had a vacuous check: `\033` through the driver's unescaper is NUL + "33", so "a pane cannot relabel a declared purpose" passed without the sequence ever arriving. It uses `emit_cmd` now and has a positive control that the same sequence lands once the purpose is cleared. Three cheatsheet tests were pinned to terminal sizes that exactly fitted the old sheet, which four new bindings pushed past. They now ask for room; truncation has its own test.
* sl0ppty: `C-a Enter` splits the longer wayuser2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | One key for "I want a pane and I have no opinion about where". It cuts across the pane's longer side, so the two halves come out as square as the pane allows, and pressing it four times on a 100x30 terminal tiles the tab the way you would have done it by hand: a column, then rows inside the new column, then rows again, then a word saying there is no room left. "Longer" is a question about what you can *see*. A cell is about twice as tall as it is wide, so 80x24 is a wide pane (80 against 48) while 40x24 is not (40 against 48). `gap_aspect` is already this program's answer to how many columns a row is worth, so that is the number used rather than a second constant meaning the same thing -- and a config that says cells are square gets a square answer. There is a test at 74x44, the size where the two readings disagree: square cells split it into columns, the default splits it into rows. The fallback is the other half of "sensible". A pane that is wide but ten rows tall wants cutting across; if `split_fits` says that will not fit, the other axis is tried before giving up. And it says which way it went -- `split right` or `split down`, the same two words the border click already uses -- because one key that can do two things has to tell you which it did. **`split_fits` had a hole this action made obvious.** It only ever asked about the dimension being divided, so a pane four rows tall on a short terminal "had room" for two columns: the width was fine, and the two panes it produced were immediately collapsed into a list by the layout. The border was offering that, and both split keys were doing it. It now also requires the axis it does *not* divide to clear the floor, because splitting cannot improve that one -- the same hole `layout_node` closed at the leaf, one level up. On a 100x8 terminal all three now say "no room to split" instead of undoing the arrangement. Verified: nine checks in `tests/test_split_ux.py` -- the axis chosen for wide and for tall panes, the aspect deciding at 74x44, both fallback directions, the refusal and that the explicit keys refuse the same way, and that it appears on the cheatsheet under `enter` and in the palette by name. The existing 65 checks about what the border offers still pass, which is what I was most worried about.
* sl0ppty: docs that match the programuser2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Audited every page against the source and the binary rather than against my memory of it. Eight pages changed; the lists were all fine and the *numbers* were all wrong. Checked mechanically, and each of these came out complete: all 37 action names, all 28 control verbs, all 9 shader names, the expression language's 10 variables, 2 constants and 7 functions, the 8 pane states, every knob the dump prints, every OSC verb, every internal link and anchor, and every page's place in `docs/nav`. Every fenced `kdl` block was run through `--check`; every `{"cmd":...}` in an example is a verb that exists. What was stale: - **`make` does not build.** The default goal is `help`, so the install block -- `make vendor`, `make`, `make test` -- never produced a binary. It says `make all` now, timed at 5.5s from clean rather than "about a second". - **700-odd checks in about eight seconds** is now 1,500-odd in about thirteen, plus 260 from the C unit tests, which the sentence did not count at all. - **About 10k lines of C** is 15k. (10,799 of those are neither blank nor a comment, which is presumably where the old figure came from.) - **Seven chrome presets** are thirteen, and the six new ones use `sin` and `TAU` -- so the perimeter section gained the angle idiom it now depends on, and its "two things fall out of it" became three. - **`--check`'s example transcript showed two errors nobody can produce any more**: `bad prefix: C-a` and `bad key: ←`. Both inputs became *valid* -- `C-a` when the chord parser learned the notation the cheatsheet prints, and `←` when it learned the arrow glyphs, precisely so a chord copied off the screen parses. Replaced with a transcript I generated, and a note that the problems go to stderr, which matters for the pre-commit hook the page suggests. - **`bell_indicator`** was a real knob documented nowhere. - **"There is no key for this yet"** about moving a pane between tabs, two commits after the keys and the drag landed. - The cheatsheet's `+N more` line, the finder's `C-u`, and `tab_id` in a pane report were all real and unmentioned. Verified: the whole suite green, `make docs` builds nine pages, and the changed pages read correctly in a browser -- including the new transcript and the `>` `<` row of the key table.
* sl0ppty: `C-a >` sends this pane to the next tabuser2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | C-a > push it to the tab after C-a b into a tab of its own C-a < push it to the tab before Three actions rather than three special cases in the key handler, which is what puts them in the cheatsheet and the palette without another line of code -- and the palette is where you find a thing you did not know to look for. You do not follow the pane, and a toast says where it went: "moved to tab 2 (api)". A key you might press twice needs to say what it did, and following would take you away from what you were doing. The tab's *name* is read before the move and its *number* after: emptying this tab removes it and shifts everything along, so the number read first is not the number anybody will see. When the tab you were in is the one that goes, you arrive in the survivor because there is nowhere to stay. `>` and `<` are the shifted period and comma, and the defaults bind both halves -- a terminal does not report shift for punctuation, so `>` arrives as a bare period the way `?` arrives as a bare slash. The config loader already did this for a chord you write by hand; the defaults had to do it themselves. The cost is that `.` and `,` are spoken for, which is the cost `/` already pays for `?`. `b` is for break out, the word tmux taught everybody for this. **Two things the tests caught, both mine.** `panes` reported a pane's tab as an index while every command that takes a tab wants an id -- and my own test conflated them, passing only because the two agree until a tab is removed, which is half of what these keys do. There is now a `tab_id` beside the `tab`: one is where it sits in the strip, the other is what you pass back. And three more rows overflowed the cheatsheet: `modal_frame` clamps the box to the screen, so at 56x44 the `session` group was simply not drawn. The sheet now says `+7 more, in docs/keys.md` when it cannot show everything -- counted by walking the rows it skipped, because arithmetic on two uneven columns claimed six were hidden on a screen showing all of them. A list that quietly stops is worse than a short one: the reader cannot tell that the key they wanted was on it. Reflowing to two columns when the height forces it would be better than a footnote, and is not this commit. Verified: `tests/test_move_pane.py` at 38 checks -- the keys in both directions, breaking out, the refusal when there is only one tab, pushing the last pane out and landing in the survivor, and that the sheet prints `>` rather than the bare period it also answers to while the palette finds it by label. `tests/test_help.py` gained the short-terminal case and lost an assumption about its own fixture height.
* sl0ppty: three ways back from a painted paneuser2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | printf '\033]5577;1;shader;\033\\' # from the program sl0ppty cmd '{"cmd":"clear-shaders","id":0}' # from outside it bind "z" "clear-shaders" # from the keyboard `app_clear_pane_shaders` empties both of a pane's chains and says whether there was anything there, which is what lets a key answer "shaders cleared" or "no shaders on this pane" instead of maybe having done something. In-band it is not a new verb: naming no rect means all of them. `shader;` sits under `shader;chrome;` and `shader;content;` as the degenerate case of "which rect", so a program puts a pane back in one exchange and cannot leave it half done. `contrib/shader-repl`'s `:both` now uses that rather than two. **None of the three is gated on `in_band_shaders`.** A chain outlives the setting that allowed it -- paint a pane, then turn the setting off, and the paint is still there, because a chain belongs to the pane and not to the file (there is a test that reloads with it off and clears anyway). A way out that the setting can take away is not a way out. The operator path matters for the same reason: the program that made a pane unreadable is not always in a state to put it back. They clear what the *pane* set. The config's chains and the session's own dimming are not this pane's doing and are derived every frame anyway. The action is bound to nothing, like `scroll-up`: it is in the palette, which is what the palette is for, and the docs say to bind it if you leave the setting on. Which is where a second bug turned up -- the palette truncates a label at 26 characters, so "undo this pane's own shaders" read as "undo this pane's own shade" on screen. Renamed to fit, and `tests/test_palette.py` now checks every label against the column, since nothing failed for that: it just looked like a typo in front of someone.
* sl0ppty: every complaint, and the chords that produced eleven of themuser2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Reported as "C-a is not a valid prefix in my config.kdl", which it was not, and neither were ten other lines of the same file -- a file written by `sl0ppty --dump-config`. The loader now collects every complaint instead of keeping the first. Each one carries the file and the line it happened at, so "line 14: bad binding" became "config.kdl:116: bad key: ←", and a complaint from an included file names that file. `err` still means the first message, so a session's one status line behaves as before; the list is what a linter needs, and `--check` (next commit) is that linter. Both changes live in the same regions of config.c, which is why they are one commit. Then what the list found. `--dump-config` is documented as "a file you could have written", and `--dump-config > ~/.config/sl0ppty/config.kdl` as a supported way to start one. It wrote every chord in the *cheatsheet's* notation: prefix "C-a" bind "?" "help" bind "←" "focus-left" bind "S-tab" "prev-tab" and the parser only spoke the config's, so eleven of the file's own lines came back as complaints and the session ran on the defaults those lines were describing. Two sessions looked identical for the worst possible reason, which is why the test asserting exactly that never noticed. Both halves were wrong, so both are fixed. The parser accepts what the sheet prints: `C-` `M-` `S-` as well as `ctrl+ alt+ shift+`, the arrow glyphs as well as `left`/`right`, and a character that carries its own shift -- `?` is shift+slash, `H` is shift+h, `|` is shift+backslash. config.kdl has claimed for months that `?` and `S-slash` were both writable; only one of them was. A punctuation binding registers *both* encodings, because whether a terminal reports `?` as shift+slash or as a bare byte is up to the terminal and not to what the config meant. The shipped default has bound `?` twice by hand for exactly this reason since it was written; this is that, done once, in the one place that knows the character implied the shift. A capital letter needs no such thing -- the decoder reports `H` as shift+h everywhere. And the dumper writes the canonical config spelling: `ctrl+a`, `shift+slash`, `left`, `shift+tab`. `shift+` spelled out rather than folded back into `?` or `H`, because both are accepted on the way in and one spelling on the way out keeps a dump of a dump the same dump. A key with no spelling the parser would accept is written as an empty chord rather than as its display form: writing the display form is what caused this, and a dump that does not load is worse than a dump missing a line. The test that should have caught it now does: a session started from the dump is asked to reload and must have nothing to say. Looking at the screen was never going to find this, because the screen was right.
* sl0ppty: a README you can read, and docs/ for the restuser2026-08-15
The README was 331 lines and had become a manual with a title. Everything in it was true and most of it was the wrong thing to hand somebody who has just arrived: five pages of shader expression semantics before they have split a pane. So the front page is 60 lines -- what it is, install, run, where the docs are, and six bullets on what is actually different about it -- and the detail moved into docs/, nine pages that each answer one question: index.md what it is, install, run, where to go keys.md the leader, every default, rebinding, direct binds panes.md splitting, resizing, the mouse, dead panes, small terminals config.md the file, live reload, include, themes, the knobs shaders.md the passes, the built-ins, the expression language chrome.md where="chrome", channel, since, the perimeter trick layouts.md a session as a file, and writing one back out scripting.md the control socket, OSC 5577, headless design.md the two decisions, and a map of the source docs/nav holds the order and the grouping and nothing else -- titles come from each page's own heading, so a renamed page cannot end up with two names. It is what the site generator will read. Reference material that only existed in code comments is written down for the first time: every action that can be bound (including the two with no default key, which is what the palette is for), every control verb, the built-in shaders with what each takes, the pane-state ranking, and the layout file's shape. Two things I got wrong on the first pass and fixed before committing: the docs linked repo files through github.com URLs for a project whose remote is a private git server, so those are inline paths now; and the README advertised `make docs`, which does not exist yet. It will in the next commit, and it can be advertised then.