| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Wearing a theme meant pasting its `theme { }` block into config.kdl, or
including a path to a file you had copied there yourself. Both make the
palette a literal in your config, and a literal is the one thing you cannot
change cheaply: not from a script, not from a running session, not by a list
of what is available, and never again from upstream -- the colours you pasted
are the colours that theme had the day you pasted them.
So a theme is a file in a directory now, and a config names it:
theme_name "phosphor"
theme { frame_focus "#00ff88" } // ...but that one colour is mine
theme_name is applied after the includes and before everything the file says
itself, which is the rule includes already follow and for the same reason: the
loader reads a document by asking it for names rather than walking it in order,
so "here" is not a position it could honour, and "what you asked for is the
base, what you wrote is yours" is the rule worth having.
Two directories, yours first: `themes/` beside the root config -- beside the
file that was loaded, so a config somewhere else (SLOSH_CONFIG, or one handed
to --check) takes its themes with it -- and then <prefix>/share/slosh/themes,
compiled in from PREFIX. install(1) has been putting themes there all along
and nothing could read them; now a shipped theme is nameable without being
copied, and a file of yours with the same name shadows it, which is how a
root-owned theme gets edited.
A name, not a path. The directory is what makes a theme switchable, listable
and shadowable, and a path silently does none of those, so `theme_name
"../x.kdl"` is refused with a line pointing at `include`, which still works
and is still the answer for a file somewhere a directory of names cannot
reach. A theme that is not there is one complaint and no more (D9): the rest
of the config applies, because losing your keybindings over a mistyped colour
scheme would be the wrong trade. A theme file is read by the same loader, so
it is watched like any other: saving one reloads the session wearing it, and
the derivations (tab_status_fg, tab_idle_bg) run over the theme's own colours
exactly as before.
Switching without editing anything, over the socket a session already has:
slosh -s main cmd theme # amber default* ...
slosh -s main cmd '{"cmd":"theme","name":"amber"}' # now
slosh -s main cmd '{"cmd":"theme","name":"amber","save":true}'
The session holds the name over later reloads, so saving your config for an
unrelated reason does not put the old colours back; `save` writes the
theme_name line into the file, replacing the one already there and leaving
every other line and comment alone -- dumping the whole config back over
itself would reformat a hand-written file and drop its comments, which is not
a fair price for changing a colour scheme. A name is tried on a throwaway
config first, so a typo costs nothing. An empty name gives the answer back to
the file.
--dump-config stops writing colours: it writes the theme's name and the
colours a config overrode by hand, so a dumped config keeps following its
theme instead of freezing it. --dump-theme is the other half and is new: the
resolved palette as a theme file, which is how you start one from the session
in front of you rather than from an empty file. It is also what keeps
contrib/themes/default.kdl honest -- that file is now the annotated reference
(the commented palette moved there out of config.kdl, where it was 119 lines
of colour in the middle of the prose), and a test compares it against the
compiled-in one.
While in there: tests/test_themes.py's "every theme sets every colour" guard
had been grepping for `&c->`, the spelling before the colour table moved to
offsetof, so it had been finding zero colours and passing for months. Fixed,
with the derived and opt-in colours exempted for the reasons they are derived
and opt-in, and with a check that the guard found a table at all.
|
| |
|
|
|
|
|
|
|
| |
The lookup miss now writes the embedded entry to ~/.terminfo and
uses it, so a bare box gets the full TERM with no step at all. An
entry that exists anywhere is respected -- possibly newer, certainly
somebody's -- and a HOME that cannot be written leaves the
xterm-256color fallback standing. --install-terminfo stays as the
by-hand spelling; the writer moved to pty.c where the panes are.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The xterm-256color fallback works everywhere; the real entry is
better, and a tarball user on a bare box had no easy way to get it.
The entry is generated from the vendored tree (contrib/terminfo, so
it describes the terminal core we actually ship), compiled with tic,
embedded as bytes, and written to ~/.terminfo under every name it
answers to, letter and Darwin-hashed layouts both. No root, no
package, no conflict with a ghostty installed later. Proven end to
end in a mount namespace: bare box falls back, installs, and the
next pane says xterm-ghostty.
|
| |
|
|
| |
Last input wins, canvas size is adjusted to active connection.
|
| |
|
|
|
|
|
|
|
| |
A new client displaces the current display, so `slosh` typed into a pane
of "main" displaced the very client showing that pane -- you watched your
own session detach, from inside it. SLOSH_SESSION names the pane's own
session; that one name is refused with the way out spelled in the message.
Any other name is deliberate nesting, and cmd/ls are untouched: they are
how a program in a pane is supposed to reach its own session.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The old name argued that the extension should say how to read the
file. The argument lost to a collision: zellij speaks the same KDL
syntax under that same extension with a different schema, and to
anyone arriving from there .kdl said *whose* file it is -- naming the
wrong owner is worse than not naming the parser. So a layout is
x.layout, a project's is slosh.layout, the name says what the file is
and --check says how it was read. (It is still KDL underneath; point
your editor's highlighting at *.layout.)
Nobody's checked-in workspace vanishes on upgrade: the project scan
still finds slosh.layout.kdl, never writes it, and where both
spellings exist the new one wins, because two files disagreeing needs
one answer. The unparseable-file heuristic in --check learned the new
suffix -- it matched '.layout.' with a trailing dot, which the new
name no longer has.
The webdemo overlay moves with it (demo.layout), rebuilt and
boot-checked.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The bumpy one. `make fmt` over every `.c` and `.h` outside `vendor/`: 32 files,
1,843 lines rewritten, no behaviour changed. Nothing here is worth reading -- the
next commit adds it to `.git-blame-ignore-revs` so `git blame` walks past it.
Two thirds of it is alignment the formatter now owns: trailing comments in a
struct, continuation lines under a signature. The rest is the hand-packed tables
opening up to one entry per line -- `ACTIONS`, `NAMED_KEYS`, `ACTION_HELP`. That
is a real loss of density and a real gain in diffs: adding an action was a reflow
of the block and is now one line. Anything worth keeping packed can be fenced with
`// clang-format off`; nothing was, on the grounds that a table is the thing a
formatter is for.
**One change is not formatting.** `src/config.h` had three trailing comments glued
onto one field:
uint16_t hover_delay_ms; /* how long the pointer must rest to arm a guide */
/* how long an announcement stays up */ /* rows per wheel notch */
-- `toast_ms`'s and `scroll_lines`' comments, landed on their neighbour by an
earlier edit, documenting the wrong things ever since. It is in this commit
because it is what caught it: clang-format could not settle on that line, wrapping
it one way and then re-indenting the wrap on a second pass, so `make fmt` was not
idempotent. The comments are back where they belong and it is.
Verified after the reformat, not before: `make clean && make all` is
warning-free, `make retest` is 1801 checks green, and `make fmt` twice in a row
changes nothing the second time. A formatter that alters behaviour would be worse
than none, so the gate was the whole suite rather than a reading of the diff.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`pane_select_word()` was written and never wired to anything: no declaration, no
caller, no way to reach it. This is the rest of it -- the gesture, the setting, and
the tests.
**The word is defined by what it stops at, not by what it is made of.** Anything
that is not blank and not a listed separator belongs to a word, so CJK, accented
letters and emoji need no table and come out whole; whitespace always separates,
listed or not, because a double-click that selected the line would be a different
gesture arrived at by surprise. Wide glyphs continue the run they are in rather than
each being their own word -- otherwise every CJK character is a word.
`word_separators` defaults to `"'`()[]{}<>;,|`, and what it leaves *out* is the
decision: `/ . - _ : = @ # % ? & + ~ $` stay inside words, so a path, a dotted
name, a snake_case identifier, a key=value pair, a URL and `src/app.c:1234` are
each one thing you double-clicked to copy whole. `:` is the difference between
copying an error location and copying half of one, which is why kitty keeps it too.
The gesture reuses what selecting already has: the press starts a selection at the
cell -- which anchors a drag out of the double-click -- then widens it to the word,
and the release path that already copies a drag copies this. So the word *is* the
copy, in the one place that rule lives, and the highlight is the copied span rather
than an approximation of it. Tracked per cell as well as per pane, so clicking two
different words quickly selects the second rather than everything between them.
A program that asked for the mouse still keeps its clicks, with shift as the escape
hatch, exactly as the drag has it.
**One thing this needed on the way: quoted values were dumped unescaped.** The
default separator set opens with a quote, and `cb_add(&b, "key \"%s\"\n", ...)`
wrote it raw -- so `--dump-config` produced a file that no longer parsed, breaking
the property the dump exists for. The escaping loop already existed inside
`cb_chord`, for the same reason and with the reasoning written down; it is now
`cb_qval`/`cb_qstr` and every string setting goes through it: the marks, `shell`,
`editor`, `shader_dir`, `project_roots`, `project_layout`. Latent for all of those,
load-bearing for this one.
**And `--dump-config` does not do what four comments and the docs said it did.** It
calls `config_dump_defaults()`: every setting with its *default*, never the value
your config gives it. The function name was the honest one. Corrected in
`docs/config.md`, `config/config.kdl`, `main.c`, `config.c`, `app.c` and the test's
own docstring, rather than changing what the flag does -- starting a config from it
is the documented use, and `--check` is what reads yours. There is no way to ask for
the effective config; that is a gap, not a lie, and it can be a flag when somebody
wants it.
Verified: 22 checks in `tests/test_clipboard.py`. An error location, an identifier
stopping at a bracket, a URL, a word inside quotes and a run of CJK; the highlight
being exactly as wide as the copy; a separator, a blank cell and a single click each
leaving the clipboard alone; two clicks on different cells not being a double; two
slow clicks not being one; the separator set honoured from a config, including a
quote and a backslash carried through KDL escaping; a word in a scrolled pane coming
from the row on screen; and a mouse-tracking program keeping its double-clicks until
shift takes them back. Three of those failed first because the test asked for the
answer twice -- a second `worded()` call is a second pair of clicks -- and one
because it mapped string indices to columns without counting wide glyphs as two.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A config linter, on the CLI:
$ sl0ppty --check # what a session would read
$ sl0ppty --check themes/mine.kdl # or a file you have not installed
config.kdl:101: bad prefix: ctrl+nosuchkey
themes/mine.kdl:7: unknown shader: bloom
~/.config/sl0ppty/config.kdl: 2 problems
$ echo $?
1
A mode rather than a script, because the only checker that cannot drift
from what a session does is the loader a session uses. The single
difference is how many problems each reports: a session has one status
line and shows the first, and a linter that stopped at the first mistake
would make you run it once per mistake.
Exit 1 when it has anything to say, so it drops into an editor's compile
step or a pre-commit hook with no glue. A clean run prints what it read --
the files, including everything `include`d, the prefix it ended up with,
how many bindings -- because a config that was never opened and a config
with nothing wrong otherwise look identical, and "it printed nothing" is
not evidence.
An optional path so it can lint a theme before you install it, and
$SL0PPTY_CONFIG is honoured when there is none, so it checks the same
file the session will.
tests/test_check.py covers the contract: exit status both ways, every
problem reported rather than the first, file and line on each, an
included file naming itself, a file that will not parse saying nothing
applied, a missing file being a problem rather than a crash, and the one
that would have caught the bug in the commit before this -- a config
written by `--dump-config` lints clean.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Kitty graphics were green in the suite and a real program still drew
nothing. libghostty-vt ships no image codec: built as a library it sets
its `sys.decode_png` hook to null, and a null hook rejects every `f=100`
transmission outright -- no image stored, so no placement exists, so the
re-emission layer this project is mostly *about* is never reached. The
program gets no error and no picture.
Raw RGB worked throughout, which is why it looked done: every test here
sent raw pixels, because that is what is easy to write in a test. It is
the same lesson the last kitty graphics bug taught -- write the test the
way a program actually behaves -- and a program with a picture on disk
has a PNG. Found because ida-tui's startup logo never appeared, and its
detection probe answers `OK` first, so it fails silently rather than
falling back to its block art.
PNG is the whole of the gap and cannot grow: the protocol has exactly
three formats (`f=24`, `f=32`, `f=100`), the first two are raw pixels
lib-vt already handles, and `decode_png` is the only codec hook it has.
Decoding only, never encoding.
The decoder is the stb_image lib-vt already vendors and we already
commit -- upstream compiles that same header with the same
`STBI_ONLY_PNG` for this same purpose, it is just not in the
`-Demit-lib-vt` build. So nothing new is pulled in; a file we already
ship is finally compiled. src/png.c is the hook, a header pre-check and
a copy: dimensions are read before decoding because the buffer we return
is allocated through lib-vt's limited allocator but stb's own working
allocation is plain malloc and answers to nothing.
Tests cover a PNG placed at the size it asked for (RGB and RGBA sources,
both normalised to RGBA), the client being handed `f=32` rather than the
png it was given, and a malformed png placing nothing without taking the
pane with it. They fail on the previous binary.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
You were right. The watch is on the *directory* -- editors write a new file
and rename it over the old one, so watching the file stops working the first
time you save -- but a directory that does not exist cannot be watched at
all. inotify_add_watch failed, the session gave up on watching for good, and
writing a config later did nothing until a restart. Which is exactly the
state a fresh machine is in, and exactly the state C-a e leaves you in.
sl0ppty creates its own config directory at startup now, so there is always
something to watch and a file appearing later is picked up. Tested from a home
directory with no .config at all.
Answering the other half of the question -- which settings actually reload --
turned up one that never worked at all: `shell`. main.c read $SHELL directly
and nobody read the config's value, so the setting parsed, rendered in
--dump-config, and did nothing. It is resolved per pane now, which fixes it and
makes it live: edit it, save, and the next pane you open uses it.
Everything else measured rather than assumed, and it is nearly everything:
colours, geometry, the status bar and line, frame marks, shader chains and
their states, and the keys including the leader itself. The two exceptions are
about *when* a value is read, not reluctance -- `shell` at pane creation, and
shader plugins which load on reload but never unload. Both are now written
down where someone would look.
One test needed rescuing from itself: it asserted the new leader worked by
splitting, in panes that were by then too narrow to split. A test that fails on
geometry while claiming to be about the prefix is worse than no test; it opens
a tab instead.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`C-a e` on a machine with no config opened an empty buffer, which tells you
nothing about what you can set. The obvious fix -- embed config/config.kdl and
write it out -- trades one problem for a worse one: a checked-in copy of the
defaults is a second source of truth, and it drifts. Ours already had. Four
theme colours existed in the code and not in that file, and nothing said so.
So the file is generated. `sl0ppty --dump-config` renders every setting with
the value it currently holds -- geometry, behaviour, all 48 theme colours, the
state shader chains, and every binding including the leader-less ones -- and
`C-a e` seeds a missing config with exactly that, creating the directories on
the way (a fresh container has no ~/.config either).
config/config.kdl stays, and is now explicitly the *prose*: what a setting is
for, what it cost to get right, which ones are opinions. Values are generated,
essays are written, and the file says so at the top.
Three things keep the two honest:
- the theme colours became one file-scope table that the parser and the
renderer both walk. 48 names that used to exist in one place and be typed
again in another now exist once.
- the dump must parse back to itself byte-for-byte. That is what caught
`bind \ split-cols` rendering unquoted -- valid-looking output that would
not have survived being read back.
- a test scans config.c for `kdl_child(root, "...")`, which is the parser's
own vocabulary, and fails if a key is missing from the dump. Add a knob,
forget to render it, and the suite tells you rather than a user.
It never overwrites a config that exists: fopen(..., "wx") loses the race
rather than an edit, and there is a test for that too, because it is the one
thing here that must never go wrong.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The middle of the status line is empty unless the pointer is on something,
which is most of the time. It says which sl0ppty this is now.
That reads as decoration and is not. A session keeps the binary it started
with, so after a rebuild there are two versions in play: the one on disk and
the one you are attached to. This session has been dogfooding sl0ppty for a
while, and "which build is this?" has already been the answer to two bugs
that looked like something else -- once when a config reload appeared to do
nothing, and once when kitty graphics were fixed and still broken on screen.
The banner answers it without asking, and `sl0ppty --version` answers for the
binary, from the same string.
The version is baked in by the Makefile from `git describe --always --dirty`,
into a generated header that is only rewritten when the value changes -- so a
commit rebuilds and a second `make` does nothing.
It takes the hint's slot and yields to it: a hint is about what you are doing
right now, the banner is about always, and it comes back the moment the
pointer moves off. Both give way to the ends of the line, which was already
the rule for hints and is why a narrow terminal loses the middle rather than
its session name. `version_banner false` for anyone who finds it noisy; it is
a separate knob from `hints`, and the tests hold both directions of that.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The reload existed but you had to ask for it, which is the wrong shape for the
thing it is mostly used for: changing a colour and looking at the result. The
server now watches the config and re-reads it when it changes.
It watches the config's *directory* and filters by name, rather than watching
the file. Editors overwhelmingly save by writing a temporary file and renaming
it over the target, which swaps the inode out from under a watch on the file
itself — it fires once and then never again, which is worse than not watching
at all because it looks like it works. The test writes in place *and* renames
over, because only the second one would have caught that.
Reloading goes through the same call the command does, so it keeps the
fail-open behaviour: a file that does not parse leaves the running config alone
and says so in a toast. That matters more here than it did for the command,
because a file caught halfway through a save is a normal thing for a watcher to
see rather than an operator mistake — and there is a test for it, and for the
recovery once the file is valid again.
Turning it off is a command-line flag rather than a setting, because a setting
would be circular: you would have to reload the config to stop it reloading the
config. --no-reload only stops the *watching*; asking explicitly still works,
which is the distinction the flag name is trying to make.
The test is a live one because inotify lives in the server's poll loop and the
headless driver has no server. It polls for the colour it expects rather than
sleeping a guess, so the passing cases cost milliseconds; the two cases that
assert nothing happened are the only ones that wait, because proving a negative
has to.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A pty is a pseudo-terminal, and that is what this multiplexes: the extra t
was never standing for anything. Renaming now, while the only users are the
people who typed the old name on purpose, is cheaper than renaming later.
Mechanical, but not only cosmetic. The name is also the runtime contract, so
this moves $SL0PTTY and $SL0PTTY_CONFIG, ~/.config/sl0ptty/config.kdl, the
socket directory ($XDG_RUNTIME_DIR/sl0ptty, /tmp/sl0ptty-<uid>), and the
identity string a program gets back from an OSC 5577 hello. A session left
running under the old binary keeps its old socket directory and is invisible
to the new one; there is no migration and it does not seem worth writing one.
The sl0ppi fork keeps its name: it is a different thing, and D1 still points
at it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A session can be a file:
layout {
tab name="api" purpose="project:api.a1b2c3" cwd="~/dev/api" {
pane purpose="agent:main" command="pi"
pane split="rows" {
pane purpose="service:web" command="npm run dev" suspended=true
pane purpose="shell:scratch"
}
}
}
built by --layout at startup or applied to a running session with
{"cmd":"apply-layout", path|kdl, replace?}. Idempotency stays with the caller,
which is where sl0ppi's CLI design already puts it: diff list-tabs against the
projects on disk, then apply what is missing. close-tab completes that loop.
Purposes a layout declares are locked, so identity comes from the file rather
than from whatever the program inside prints — tested by having a pane try to
take one in-band and be refused.
Suspended panes: a pane that exists, is laid out, and has run nothing, showing
what it would run and starting on its first keystroke (which is consumed, not
forwarded). This is sl0ppi's D6 — a root with twelve projects must not become
twelve running servers — and it is the reason can be cheap.
Also fixes a test-isolation bug latent since M2: the live tests attached to the
session called "main", which is whatever happens to exist on the box rather
than one they created. They now use a unique session per run, which is how a
run that had somebody else's htop in it became reproducible.
284 checks green (257 headless + 27 live).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The session now outlives its client, which is the point: agents keep running
while you are gone. The server owns the panes, the layout and the renderer;
the client is a terminal in raw mode and a socket, forwarding bytes and
painting what it is sent. It does not even decode input, so the decoder can
change without redeploying clients.
sl0ptty attach to 'main', creating it if nobody is home
sl0ptty -s NAME a named session
sl0ptty ls live sessions
sl0ptty cmd LINE one control command
C-a d detach
Connections are not clients. A connection becomes the display client only by
sending HELLO; control commands and liveness probes are just connections and
never displace anybody. This was found the hard way, one hour after the server
first ran: 'sl0ptty ls' kicked the attached client off, because ls connects to
see whether a socket answers. That is precisely the fault that gave the zellij
fork a 20-25% CLI failure rate, rebuilt and then fixed structurally. The
regression runs 20 x (ls + control) against a live client and demands 0
failures and a client that can still type.
cmd.c now holds the command vocabulary once, shared by the headless driver and
the control socket, so a script written against one works against the other —
'sl0ptty cmd "snapshot text"' against a detached session prints its screen.
tests/test_session.py: 18 checks over create, split, probe-resistance, detach,
survival, drive-while-detached, reattach, displacement and cleanup.
123 checks green overall.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
app_t owns the tree, focus, the prefix and what a key means; the interactive
client and the headless driver are now both thin front ends over it, so there
is exactly one implementation of every command.
Layout is the pure function D6 promised: layout(tree, rect) recomputed every
frame, remainder spread across children so nothing drifts, and splitting in a
direction that already exists grows that split rather than nesting — three
splits give three equal columns, not 1/2 + 1/4 + 1/4.
Chrome: rounded frames, aspect-corrected gap, centred titles. The split button
is budgeted BEFORE the title and hit-tested from the rect it was drawn in,
which is the fix the fork needed four attempts to land.
Every painted element registers (rect, action) as it is painted; a click is a
lookup. Mouse into a pane is translated to pane-local coordinates.
Three bugs the harness caught, none of which unit tests would have:
- one click on the split button split twice: press *and* release both fired.
Chrome now acts on press; forwarding to a pane still gets every event.
- callbacks were registered as &fn, but the option is pointer-typed, so the
stored 'callback' was a stack address. Any OSC 2 or DSR jumped into a dead
frame. Now a hard segfault turned into two regression tests.
- a keystroke that changed nothing still emitted a cursor hide/show pair.
The cursor is hidden lazily now, so an idle frame costs zero bytes.
96 checks green in 6s (40 unit, 23 screen, 33 layout), plus 9 live.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
--script runs the whole compositor with no tty: commands in (send, raw,
settle, resize, snapshot, alive), composited screen out as JSON — rows of
text, style runs, cursor, and the hit list.
Styles are emitted as runs rather than per-cell objects, because a test wants
'columns 0..5 of row 0 are bold red', not 1920 objects.
The hit list is empty until M1 paints something clickable, but the type, the
JSON, and hit_test() exist now: one geometry, established before there is
anything to get wrong.
Also fixes two host-policy defaults found by the cursor never appearing.
lib-vt is a VT library, not a terminal, so it starts with DECTCEM (mode 25)
off and grapheme clustering (2027) off. Both are now set via MODE_DEFAULT, so
they survive RIS.
tests/harness.py + tests/test_screen.py: 19 checks over composition, styling,
wide chars/clusters, scrollback, resize and the full input round trip —
including the kitty/legacy re-encoding matrix, which no longer needs a pty.
make test is now 2.4s for 59 checks; make test-live keeps the four that
genuinely need a tty.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The half libghostty-vt does not do: decode what the outer terminal sends us
into semantic events, then re-encode each event for the target pane against
that pane's own negotiated modes.
Decoder (src/input.c): legacy keys and modifiers, SS3, tilde keys, the kitty
keyboard protocol (CSI u, shifted/base layout sub-params, press/repeat/release,
associated text), SGR mouse incl. drag and wheel, bracketed paste, focus
events, alt-prefix, UTF-8, and partial sequences retained across reads.
Re-encoding (pane_send_key/mouse/paste) drives lib-vt's key and mouse encoders
via setopt_from_terminal, so a pane gets exactly what it negotiated. The mouse
encoder wants surface pixels; we declare a 1x1 pixel cell so cell coordinates
are exact rather than approximated.
The prefix key is handled on decoded events, not bytes, so it cannot be
confused by a byte that merely looks like C-a inside a UTF-8 or escape
sequence.
Tests: 40 table-driven decoder cases (tests/input_test.c) + a live check that
drives the same client into a legacy pane and a kitty/mouse pane and asserts
each receives its own encoding (tests/live_input.py). 59 checks, all green.
|
|
|
Design contract in DESIGN.md. libghostty-vt vendored fresh from ghostty
main@bedb6f2 (zig 0.16), built to a 14MB static lib.
M0: pty spawn (no libutil), a lib-vt terminal per pane, a screen cell buffer,
a diff emitter that repaints only what changed, raw mode, SIGWINCH, and a
~120Hz frame cap that coalesces pty reads.
Also the seed of the headless harness: --headless dumps the composited screen
without a tty, so composition is testable in milliseconds.
tests/live_m0.py covers what headless cannot: raw mode, the diff emitter,
resize propagation, and the quit path. 6/6 green.
|