| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A status is a line of text, and a line of text cannot say whether it is still
true. "building 3/7" reads exactly the same while it builds and ten minutes
after it stopped, which is the one thing somebody reading it from another tab
wants to know -- and the only workaround available to a program was to write
the answer into the words and keep rewriting them.
So `busy`:
printf '\033]5577;1;status;make test\033\\'
printf '\033]5577;1;busy;1\033\\'
...
printf '\033]5577;1;busy;0\033\\'
A flag of its own rather than a field of `status`, because a status is the
*whole* payload after the verb -- `status;a;b;c` has always been the text
"a;b;c" -- so anything added to that line would change what every existing
sender means, including the five pi extensions D1 promises byte compatibility
to. Orthogonal, so a program can leave one line up and toggle the other;
anything but 0/false/no/off is on; `clear` takes both; and a pane whose program
has gone is never busy, whatever it last claimed, because a spinner turning
under a dead tab is a lie you keep looking at.
In a sidebar the row turns `tab_status_busy` -- the theme's accent, derived
like the other status colours, so no theme needs a line -- and takes a spinner
in the column `tab_bar_padding` was already holding blank, so nothing reflows
when a program starts or stops working. `busy_mark` is the frames, separated by
spaces: one frame is a static mark and costs nothing, several make it turn, and
only then does the session keep a frame clock -- via the same `animating` flag
an animated shader uses, so the cost lands only while such a row is actually
composed, and `""` drops the mark entirely and leaves the colour to say it.
The frame is a pure function of the clock, like every other pulse here, so two
clients attached to one session show the same one.
`panes` now reports `status` and `busy`, which is the scriptable half of the
same fact: what a pane says about itself, without reading its screen.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`padding` is a pane's: the ring between a frame and the text it holds, in rows
times gap_aspect so a `1` looks square around a block of prose. Handing the
same knob to the sidebar looked tempting and is wrong twice over. The sidebar
is chrome, not a pane, and its width is fixed by tab_bar_width -- so `padding
1` would quietly eat four of a sixteen-column strip (two cells a side, aspect
included) and shorten every tab name, because somebody wanted their terminal
text to breathe. And its two axes are not one idea: the columns are an indent
for labels, the rows are air above a list.
So tab_bar_padding, its own knob in the family the rest of the sidebar's air
already lives in (tab_bar_pad, tab_gap, newtab_pad), taking the same 1/2/4
CSS-order values as `padding` -- one reader for both, so they cannot drift
into meaning different things -- but counted in cells.
The default `0 1` is what the sidebar already drew: that one column is where
every label's leading space has been hard-coded since the sidebar existed, and
moving it into a setting is what makes `tab_bar_padding 0` possible at all --
names flush against the frame, which was not previously a thing anyone could
ask for. The statuses, the `+ new tab` row and the pane count follow the same
indent, since they were each carrying their own copy of that space.
It is air *inside* the plate: a tab's fill still spans the whole row and the
whole row is still the hit, because the row is what you click and a highlight
that stopped short of the frame would read as a highlighted word rather than a
chosen row. The bottom rows are given up when the strip is short of them, like
every other piece of air in here.
tab_bar_pad is the older name for the top component alone. It still works,
still means exactly that, and wins where a config writes both -- it is the
more specific of the two. The dump writes tab_bar_padding and mentions it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`1:api` puts a number at the start of every row, and in a sidebar that is a
column of digits in front of the thing you are actually reading -- the names
start one, two or three cells in depending on how many tabs you have, and the
numbers are only wanted at the moment you reach for C-a 2.
tab_bar_index "right" moves them:
│ tests 1 │ │ 1:tests │
│ verylongproj 2 │ vs │ 2:verylongproje│
│ api 3 │ │ 3:api │
Every name starts in the same column and the numbers line up in one of their
own, which costs a column of sidebar and buys the list a shape. "prefix" stays
the default, because it is what the strip has always drawn and the strip is
where most people meet it.
Sidebars only, and not by omission: a tab in the top strip is exactly as wide
as its label, so there is no far end to align against and a number floating
after a name it does not belong to would be worse than the prefix it replaced.
The setting is ignored there rather than half-honoured. A row too narrow to
spare the columns (under the number plus five) also keeps the prefix, since
the name is the part you came for, and the rename editor keeps the whole cell
as it always did.
A name long enough to reach the number stops one cell short of it, because
`verylongproje2` is a tab called verylongproje2 as far as any reader can tell.
The cell comes back as padding, so the plate is unbroken and the hit is still
the whole row -- clicking the number is clicking the tab, and the number
brightens with the label under the pointer rather than leaving half a row lit.
The bell mark keeps its place between them, budgeted first as before, so a
long name cannot eat either of the two things that are not the name.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`theme_name "sl0p"` with all seven themes sitting in /usr/share/slosh/themes:
"no theme named sl0p". The lookup knew two places -- your own themes directory
and the PREFIX this build was compiled with -- and on that machine the first
did not exist and the second was /usr/local, because that is the Makefile's
default and the package was built with PREFIX=/usr. Neither was where the
themes actually were, and nothing in the search could ever have found them.
A compiled-in prefix is a guess about where the binary will end up, and the
binary is the one thing that knows where it really is. So the search now asks
it: path_self() (/proc/self/exe, _NSGetExecutablePath, GetModuleFileName --
lifted out of server.c, which was already doing this for SLOSH_BIN) gives
<bin>/../share/slosh/themes for an installed slosh wherever it was installed
to, and <repo>/contrib/themes for one being run out of a build tree, which
makes a checkout able to name a theme without installing anything first. The
compiled-in prefix stays as the last candidate, for a binary sitting somewhere
that says nothing about its data, and duplicates collapse so the usual install
does not report the same directory twice.
Your own directory is still first and still shadows everything, which is the
property that matters; the rest is about the shipped set being findable at all.
The tests that assumed a closed world of two temp themes now narrow to their
own names by typing, since the shipped ones are (correctly) always in the list
-- and the "no themes anywhere" toast lost its test, because with the shipped
set reachable that state takes a binary with nothing beside it. The case that
started this is a test of its own.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A tab carrying three rows of wrapped status and the tab under it carrying
three more read as one column of text: the labels are in there, but the list
has stopped looking like a list. tab_gap is the blank rows between them, 0 by
default, which is exactly what the sidebar has always drawn.
It is the space *between* tabs and nothing else. Above the first tab is
tab_bar_pad's and before the button is newtab_pad's, so the gap is applied
before each label except the first and none of the three is ever paid twice.
Given up a row at a time when the list is short of room, ahead of every label
and the button, which is the same order the pad and the statuses already
follow: a gap that pushed a tab off the bottom would be spacing costing you
the thing it spaces. Paint rather than layout, so tuning it repaints without
reflowing any pane. Sidebars only -- the top strip is one row, and the space
between tabs there is the label's own padding.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Sixteen cells is not enough for a sentence, and a sentence is what a pane
has to say: "add a summary…" is most of a status and none of its meaning.
tab_bar_status_lines 3 word-wraps instead of cutting, and the same column
carries forty-eight cells rather than sixteen.
Wrapped, the rows under a tab stop being a list of panes and become a
paragraph each, so the shape no longer says where one pane's status ends and
the next begins -- both are italic text down one column. Alternate panes are
drawn on alternate bands to say it instead: tab_status_stripe, derived from
the theme like tab_status_fg, so a theme that has never heard of any of this
still gets a band in its own family. Nothing is painted at one line, where
there is no ambiguity to resolve and a band would take a translucent
terminal's background away for nothing.
1 is the default and is the old behaviour exactly, down to cutting at the
column rather than the last word before it: with no next row to move the word
to, breaking early would spend cells on nothing. The reason to prefer a word
boundary only exists once there is somewhere for the word to go.
Rows count against tab_bar_status as before, so wrapping trades panes for
words within the same allowance, and the allowance itself still gives way to
the tabs. A status that will not fit whole is dropped whole and reported with
the ellipsis, rather than shown as a fragment that reads like the whole
thing. Every row of a status is the same `find:` door.
tabstatus's buffer goes to 256 to match pane_status's own slot: at one line
the drawing cut it anyway, but wrapped it is the whole string that has to
survive the trip.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The `+` is the one entry in the strip that does not mean "go here", and it
sits at the end of a list where every other entry does. Flush against them it
is what you hit when you meant the last tab -- most easily in a sidebar, where
the target is the full width of the row and the pointer only has to be one row
low. newtab_pad is the nothing in between: blank rows in a sidebar, blank
columns on top, one by default, which is enough to make the slip a miss.
It is paint rather than layout, like tab_bar_pad: the panes' rectangle is the
same at 0 as at 5, so tuning it repaints without reflowing anyone. And it is
given up before the button is -- a strip short of room drops the spacing one
cell at a time and still draws what the spacing was protecting, because
spacing must never cost you the thing it spaces.
newtab_button false removes the button outright, for people who make tabs from
the keyboard and would rather have the row (or the columns) back. Only the
place to click it goes: the chord and the palette entry still work. Setting
newtab_mark "" already did this by omission; this says it on purpose, and says
it in --dump-config.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(tab_bar_chrome)
A list of tabs sitting in bare columns beside a framed layout reads as
text that wandered in from outside. So the sidebar gets a frame of its
own -- and in compact mode not its own: its inner vertical lands on the
tab area's ring column, and because the lines are strokes that union
with whatever is already in the cell, the corners become tees without
anyone computing a junction. The sidebar and the layout come out as one
figure, which is the whole point.
Classic mode gets a free-standing frame in the idle frame colour,
rounded or square with everything else. The frame costs a column each
side and a row top and bottom, taken from the sidebar's own budget --
and dropped entirely, before the list is, when the sidebar is too small
to afford it: chrome must not displace what it is chrome for.
The content box (cx/cw) now feeds the labels, the status rows, their
hits and the truncation budget alike, so the frame cannot disagree with
where the clicks land.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
A 'The tab bar' section on the panes page: the sidebar, its four knobs,
the status rows under each tab, and the borrowed directory name for
unnamed tabs. The config page's knob table learns the tab_bar_* keys.
scripting.md and the driving-slosh skill stop underselling the status
verb: it was documented back when the pane's own frame was the only
place a status could appear, which made it decoration. With the
sidebar it is how a pane reports progress to somebody working in
another tab -- so say that, and say to keep it current and clear it
when the work is done.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A small tag in each client's own top-right corner when several
terminals share the session: the viewer count, and on a view that is
a crop being panned by somebody else's cursor, the crop's offset into
the shared screen ("2 clients +12+4") -- which is exactly the moment
a screen moving on its own needs explaining. Stamped per client after
projection, because every terminal has its own count of others and
its own offset; the shared status bar is composed once and can be
cropped out of a panned view anyway. attach_indicator false hides
it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The patch made shared attach unconditional; these make it a policy.
multi_attach false restores the classic displacing attach
(EXIT_REPLACED, which old clients already understand). size_follows
picks whose size the shared screen takes: active (the patch's rule),
smallest (the tmux rule: filler, never a crop), or largest. The size
fold lives in apply_canvas, so any attach, resize or departure can
move the canvas under the folding policies -- including a non-active
client leaving, which under smallest may have been the very client
holding the floor down. A config reload re-applies both knobs to the
running session.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The static musl release builds cannot dlopen -- musl compiles it out
-- so loadable shader plugins were a feature most shipped binaries
could not use. Nothing we ship used it either: every shader in
contrib/shaders is an expression. Gone: the dlopen/glob loader in
shader.c, shader_dir in the config, the plugin ABI table and entry
macro, the Windows dl*/glob shims that existed only for it, the
contrib/shader-plugin skeleton, its tests, and every doc mention.
shader_abi.h stays as the shader model (cell_t, shade_ctx_t,
shader_t) with a note about what it used to be.
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Windows has no VEOF. The console driver does not know the character, so
^D arrives at cmd.exe and PowerShell as an ordinary keystroke and they
ignore it -- it even echoes, which is how you find out: the line reads
`echo hello^D`. The one key that closes a pane on every other platform
does nothing at all, and closing a pane becomes a word you have to type.
That you can type `exit` instead is the tell: the intent was never
ambiguous, only unspoken. So slosh speaks it. What goes to the pane is
the word, not a signal and not a close from outside -- the shell runs
its own cleanup, returns its own status, and keep_dead decides what is
left behind, all three of which reaching past it would skip. It is also
what the user would have typed, so there is nothing new to explain.
The knob is what makes it honest, because a wrong guess costs a pane.
It fires only with the shell itself in the foreground (a program that
is running may want the EOF for itself -- `cat` reading to end is the
whole job), only on the primary screen (in an editor or a pager ^D is
half a page down, and the alternate screen is the pane stating that
rather than us matching process names), and only on an empty line
(POSIX sends EOF there; what it does not do is discard what you typed).
The line is an estimate. The shell's editor is on the far side of a
pty, so pane_send_key counts what it forwarded since the last Enter.
Editing it does not model -- a recalled history entry, ^U, completion
-- leaves the count too high, which forgets to exit. That is the
direction to be wrong in; the other one throws away a half-typed
command.
Default true on Windows, false on POSIX, where the line discipline
already does this and does it properly: ^D there is also EOF for `cat`
and every REPL, none of which is ours to intercept. A knob rather than
an #ifdef so the guards are the same code everywhere and can be tested
where the test suite runs -- test_ctrl_d.py turns it on and drives all
four decisions, plus the case that does exit.
Telling trapped from forwarded needs a program that reports what it
received: on POSIX a forwarded ^D also ends an idle shell, and looking
for the word `exit` cannot separate them either, because bash prints
`exit` itself on EOF.
Verified on the Windows box as well, where it is the default: ^D at an
idle cmd.exe prompt takes the pane (and with it the session, having no
panes left), `more` in the foreground keeps its own EOF, and a
half-typed line survives with the line still on screen.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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'.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
An audit of every claim in docs/config.md against the parser and the
dump found the page complete -- all 47 top-level settings named, no
ghosts, defaults and error strings as printed, 48 theme colours exactly
-- and three statements wrong or imprecise, all fixed here and in the
two comments that repeated one of them:
- compact does not switch off gap_aspect. The divider ignores it, but
padding is still aspect-corrected through it and the float nudge step
still uses it; saying it 'stops applying' was wrong in config.md,
config.h and config/config.kdl alike.
- 'Two of those take more than one value' miscounted: padding,
min_pane/min_split (cols=/rows= properties, whose syntax the page
never showed) and project_roots all do.
- a compact boundary saves four *columns* or three *rows*; 'four' with
no axis was the columns number wearing a general claim.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One knob, `compact true`: panes pack flush against 1-cell divider lines
that meet in real junctions, one frame rings the tab, and a pane's title
rides the line above it -- a boundary costs one cell where the default
look spends four. The lines are strokes: each write says which edges of
its cell it needs, unions with what is there, and never clobbers text,
so ├ ┬ ┼ fall out of paint order and the focus ring recolours without
inventing arms. The divider *is* the gap (eff_gap, one answer for the
layout, split_fits and the resize clamp), so gap hits, hover hints and
corner drags work unchanged. Interior edges give up click-to-split --
one cell cannot hold two verbs -- and a pane's status row overlays its
last content row. Floats, zoom and the D6 flatten keep classic frames
through place_hidden: zero new tree cases.
Underneath it, mode or no mode: a border drag now moves the boundary on
that side, converted on the first motion so the press that never moves
keeps its old meaning -- the handle's click still splits, the rim's is
still deliberately nothing (drag.rim). Which is also the fix for gap 0,
where flush panes leave no gap cell to grab and the resize gesture had
silently vanished with it.
|
| |
|
|
|
|
|
|
| |
splash_ms and the project settings join the knobs table, the pane-state
ranking gains the floating it was missing, contrib/themes' seventh file is
named for what it is, the shader table's numbers are called examples with
the real omitted-value defaults beside them, the index links workspaces,
and the verb table learns splash.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The out-of-the-box look, rethought as one piece:
- the compiled-in palette calms down: muted blue #7aa2f7 on the same
neutral darks, with the colours that mean something kept semantic --
amber bell, red-orange dead. sl0p stays one include away as the house
style, and contrib/themes/default.kdl is the starting point written
down (theme-tour shows it).
- derived state defaults now actually follow the theme: the scrolled
wash is rebuilt from scroll_bg after the theme block (it used to bake
the compiled accent), and declaring a state -- in any file of an
include chain -- still wins. The dump writes derived states as
comments so C-a e cannot pin what is meant to keep following.
- a bell you can see: the frame blinks three times in theme's bell
colour then breathes until answered, and a soft diagonal shimmer --
the bell colour halved towards default_fg, width breathing on a sine
-- crosses the body for the same 900ms. bell_indicator false silences
all of it; states { bell { } } replaces it like any state. since
carries the clock dependency, so an idle session pays nothing.
- scrollback says where it continues: while scrolled, the viewport's
top and bottom rows melt towards an edge with content past it and
turn solid at the buffer's end. New expression variables above/below
(lines hidden past each edge, own dependency bit, cached per scroll
position), carried in shade_ctx_t -- SLOSH_SHADER_ABI 3 -> 4.
- expressions survive the dump: a program keeps its source and cb_chain
writes it back, so amount="(y % 2) * 40" no longer dumps as a number.
- headers move to include/, -Iinclude everywhere; the plugin Makefile's
SLOSH var becomes SLOSH_DIR because slosh exports SLOSH=1 into every
pane it runs, and building from inside a session read that as a path.
- comment drift culled where features outran the prose, and the
derivation boilerplate folded into derived_pass()/ctx_scroll().
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`config/layout.example.kdl` becomes `config/example.layout.kdl`, and the
convention for the files people write is `project.layout.kdl`.
Not a new extension, and the reason matters: the syntax genuinely *is* the same
hand-rolled KDL subset for both documents -- one parser reads them -- so calling
layouts something other than KDL would trade one wrong signal for another and
lose every editor that highlights it. The confusion is about *schema*, so the
schema goes in the stem: the extension says how to read the file, `.layout` says
what is in it. `docker-compose.yml` and `tsconfig.json` settled this convention
long ago.
**But a filename cannot enforce anything, and that turned out to be the real
bug.** `sl0ppty --check` on a layout file printed `ok` and reported "bindings 53"
-- it had read a document containing nothing it understood and said so was fine.
A config that `include`d a layout linted clean for the same reason. So did
`rouded true`. The loader reads a document by *asking it for the keys it knows*,
which is fast and clean and never notices what it was given.
So it notices now. `KNOWN_TOP` is every node the loader reads, and anything else
at the top level is a complaint with a file and line:
s.kdl:2: unknown setting: rouded
example.layout.kdl:14: this is a layout, not a config: `sl0ppty --layout` reads those
sl0ppty: config.kdl: this is a config, not a layout: `gap` is a setting
The `layout` node gets its own message because it is the mistake people actually
make, and the reverse direction is the same list read backwards -- the layout
loader asks `config_is_setting()` rather than keeping a guess of its own, so the
two answers cannot disagree about which file is which. A layout whose own tabs
are misspelled still gets "layout declares no tabs", which is a different
mistake.
D2 records it, since it is the decision that two schemas share one syntax.
Verified: `tests/test_config.py` gained thirteen checks -- the unknown setting
with its line, both wrong-schema messages, the misspelled-tabs case keeping its
own, the shipped config linting clean and the shipped layout loading as one, and
a drift guard that greps `kdl_child(root, ...)` out of the loader and fails if a
setting is missing from `KNOWN_TOP` (verified by removing `gap_aspect` and
watching it fail). Full suite green; my own config and the reference config both
still lint clean, which is the thing this change could most easily have broken.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
printf '\033]5577;1;shader;chrome;tint color="#ff5fd7" amount="abs(t / 8 % 510 - 255)"\033\\'
The field after `shader` is which rect -- `content` or `chrome` -- and the
rest of the payload is a chain in exactly the syntax a `shaders { }` block
uses, taken verbatim so that `;` can go on separating entries. Empty text
clears that chain. The session answers on the program's stdin, `shader-reply;ok`
or `shader-reply;error;bad amount for tint: ...`, because a prototyping loop
that gets no answer cannot tell a typo from a build without the feature.
`contrib/shader-repl` is the prompt this exists for: type a chain, watch the
pane, and `:paste` prints what you have as a config block. Both ends are the
same parser, so what you arrive at is what you can keep -- verified by
pasting `:paste`'s output into a file, linting it, and checking the frame
comes out the same colour.
**D13 said this was impossible on purpose, and the reasoning stands**: a
program that can restyle the session it happens to be running in is a hazard,
and `cat hostile.txt` should not dim your panes. But *impossible* was the
wrong shape for it -- editing a file, saving, looking and guessing again is
slow enough that shaders go unwritten. So it takes `in_band_shaders true`,
one line of consent, and it is scoped to the pane that asked: its own two
chains, never its neighbour's, never what the config said about anyone else.
There is a test for the neighbour.
Each chain owns the programs its expressions compiled to, and is swapped in
whole, so a chain that turns out not to read leaves the pane looking like it
did rather than half-restyled. Measured: 1,800 replacements of an animated
chain, resident size flat after the first pass.
Also here, because it is the same paint order: the chrome chain now sits
between the config's and policy's, exactly where the content chain does.
Dead code out -- `app_shade_add`/`app_shade_clear`/`app_shade_count` had no
callers and their comment claimed the opposite of what this commit does.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
padding 1 // every side
padding 0 2 // vertical, horizontal
padding 2 0 0 1 // top, right, bottom, left
One value, two, or four in CSS order, because that is the order anybody
who has written a stylesheet already knows. `pad` becomes four fields and
the content rect is deflated per side rather than symmetrically, which is
the only real change: everything downstream already worked off
`n->content`.
The units do not change with how many numbers you write. They are ROWS,
like `gap`, and a horizontal one is multiplied by `gap_aspect` on the way
out -- so `padding 1`, `padding 1 1` and `padding 1 1 1 1` are one padding
rather than three, and a ring that looks square is still one number rather
than an arithmetic problem. Anyone who wants plain cells sets
`gap_aspect 1` and gets them everywhere, which is what that knob is for.
Three values is refused with a line saying what it wanted. CSS reads it as
top/horizontal/bottom, and a line whose meaning you have to look up is a
line nobody can read -- and `sl0ppty --check` now has something to say
about it, which is the difference between refusing and ignoring.
The dump writes the shortest form that means what is in force, so `padding
0` stays `padding 0` and a dump of a dump is still the same dump.
Checked by measuring both rects: a pane's insets from its own frame, four
numbers, for every form -- including that `gap_aspect 1` turns the
horizontal ones into cells, and that a refused padding leaves the rest of
the file applied.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
|
|
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.
|