| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`tab_bar_width` was a reservation: the configured columns came off the
screen whatever the screen was, and the only fallback was all the way back
to the top row. On a narrow terminal that is chrome displacing the thing it
is chrome for, and the jump to the top moves every target you were aiming
at.
It is a ceiling now. app_tab_bar_cols takes the app, because the answer
depends on the terminal and not only on the config: held to the columns
left after a pane's collapse width, and to a third of the screen, and
narrowed rather than dropped. TAB_BAR_FIT (14) is the narrowest width this
code will invent on its own -- a config that asks for less is taken at its
word -- and 0 means there is no room for a sidebar at all, which is the one
case that still falls back on top.
Height counts too, which it did not before: a sidebar spends a row per tab,
so a wide but short terminal runs out of list before it runs out of screen,
and a tab with no row is not clipped, it is unreachable. tab_bar_min_rows
is that floor. Not when the top strip could not seat them either -- moving
them would gain nothing, so the side you configured stands.
All of it is re-derived per frame, like the layout, so a resize moves the
bar and nothing remembers that it did.
test_tab_bar also stops depending on where the suite is run from. An
unnamed tab borrows the basename of its focused pane's cwd, so eight checks
read `tests` under `make test` (which cds there) and `slosh` from the
repository root, where they failed. They are about padding, alignment and
the index, not about that derivation, so they name the tab first -- by id,
since `set-name` takes 0 for the focused *pane* only. 147 checks, green
from either directory.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
It was drawn in tab_status_busy, the colour of the words it sits beside, which
made one decision out of two. The words have to stay readable at a glance and
a mark does not: a theme may well want the glyph loud and the text calm -- or
the text in the accent and the mark a quiet dot -- and with one entry neither
is available without moving both.
Unset it follows tab_status_busy, derived after it and from whatever that
resolved to, named or derived, so nothing changes for anyone who does not ask
and no theme in contrib needs a line. `--dump-theme` writes both, like every
other colour the compositor draws.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`│⠋bash: cd` -- the mark landed in the one column of left padding there was,
so it ended up welded to the first letter of the status. The rule was "the
mark takes a column the padding was already holding blank", which is right
about not reflowing and wrong about there being enough of it.
So the mark takes the *first* column of the indent and whatever is left over
is the air after it, and the default indent is two columns instead of one. One
column is still available -- `tab_bar_padding 0 1` -- and now means the
squashed look on purpose rather than by default; three gives two spaces; zero
still puts everything against the frame. Nothing reflows when a program starts
or stops working, which was the point of taking a column that was already
blank.
The labels and the tab numbers move with it, because the indent is the strip's
and not the status's -- a status still starts in the column its tab's name
starts in, which is the rule the leading space existed for in the first place.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The active tab has been filled with the accent since the strip existed; every
other tab was words on the background. That leaves the strip with one shape on
it and nothing to compare it against -- where a tab ends, how far the row you
are about to click reaches, and in the sidebar which rows are tabs rather than
statuses, all had to be read off the text.
tab_idle_bg fills them. Barely off the background and in the label's own grey,
because this is not a second highlight: it gives a tab edges, and it gives the
accent something to be brighter than, which on a strip of short labels is most
of how the eye finds the tab it is in. Hover keeps it for the same reason the
active tab keeps its fill -- losing the plate under the pointer would read as
the tab going away -- so hovering still lands on the text, and the bell mark
sits on the plate rather than punching a hole in it.
Derived a third of the way from default_bg to tab_idle when nobody names it,
the same bargain tab_status_fg and tab_status_stripe already make: both ends
are colours every theme in contrib defines, so each gets a plate in its own
family -- paper lightens, phosphor greens, mono stays grey -- without a line
added to any of them. Naming it wins, and naming your own background is how
you ask for the bare tabs back.
The `+ new tab` button and the gaps between tabs stay unpainted: the button is
the one row in the list that does not mean "go here", and tab_gap is air
between plates, which is what makes them read as separate tabs 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Rows were spent in tab order until they ran out, so statuses from early tabs
simply consumed the rows later tabs needed for their labels. Four tabs of
three announcing panes in a sixteen-row terminal left the fourth tab with no
row and no hit anywhere on the sidebar -- not clipped, unreachable -- and the
new-tab button went with it.
The list is the navigation and a status is an annotation on it, so the
annotation is what gives way. Before drawing a tab's statuses, reserve a row
for each tab still to come and one for the button, and allow the statuses
only what is left over. The "and more" ellipsis is measured against that
allowance rather than the cap, so a list cut short by the height still says
it was cut, the same as one cut short by the setting.
The pad is deliberately not reserved: it already gives itself up a cell at a
time, which is the same order of surrender -- spacing, then extra statuses,
then statuses, never the thing they are about.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three columns of indent is a fifth of a sixteen-column sidebar, spent
saying 'this belongs to the row above'. The slant says it in none: a
status now starts in the same column its tab's name starts in, and is
set in italic. That also survives the monochrome themes in contrib,
where a colour-only distinction would have had nothing to spend.
tab_status_fg / tab_status_bg are there for themes that do have colour
to spend. The fg follows tab_count when a theme does not name it -- the
same declared/derived split apply_scrolled makes -- so the six themes
in contrib get coherent status rows without a line added to any of
them. The bg is unset by default, because a band is opt-in and 'none'
is what a translucent terminal wants; the row is padded so a band that
*is* set covers the row rather than the words on it.
Two things fell out. The padding loop existed three times and is now
pad_cells. And cb_color wrote #000000 for an unset colour, so
quietly pinned attach_bg -- 'gray
ink on the terminal's own background', says its comment -- to a black
band. Unset colours are now written commented out, the same trick the
states block uses for what it wants re-derived rather than pinned.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The mark went bare because in the strip it sits at the end of a row of
tabs, where the company it keeps says what it does and columns are
scarce. A sidebar row is neither: it is a row of its own with columns
to spare, and a lone glyph on it reads as decoration -- the hint under
the pointer cannot help, since you have to suspect a thing is a button
before you hover it.
So with the room it reads '+ new tab', in the word hint_for already
gives this action rather than a second name for the same thing, and
falls back to the bare mark when the sidebar is too narrow. The strip
is untouched. The row fills and hits like a tab's, so hover and the
drop-target fill cover the row rather than the word on it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
name, then purpose, then the basename of the focused pane's cwd, then
the bare number. The kernel's answer (live_cwd, the same question
save-workspace already asks), so a cd moves the label with you; home
shows as ~. The focused pane rather than any consensus of the tab's
panes: when they sit in different directories, the one you are typing
into is what the tab is about, and a rule anyone can predict beats a
cleverer one nobody can. Derived at draw time and never stored, so a
real name wins the moment one exists, dumps and the tabs JSON stay
unpolluted, and renaming starts from a genuinely empty name.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Panes already announce a one-line status over OSC 5577, and it rendered
in exactly two places -- both inside the pane's own frame, which is the
one place you are not looking when you most want it. The sidebar has
the vertical room the top strip never had: under each tab, up to
tab_bar_status rows of what the panes inside said, in tree order, dim
like the pane count. A dead pane's row says how it died instead
(exit_words), the same substitution its own frame makes.
The cap's last row is spent on an ellipsis rather than one more status
pretending the list is complete, and a status cut to the sidebar's
width ends in one too -- 'building 3' is a different fact than
'building 3/7'. Each row is a find: hit, the verb that already means
'go to this pane', so clicking a status jumps to the pane that said it,
across tabs. Freshness costs nothing new: an OSC status marks the pane
dirty, the same path the bell indicator rides out of background tabs.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
One blank row above the first tab by default: a list starting in the
very corner reads as cramped, and in compact mode the row lines the
first tab up with the pane content inside the ring rather than with the
ring's top line. The pad is where the drawing starts, not a layout fact
-- the panes' rect is identical at pad 0 and pad 5, so tuning it
repaints without reflowing anyone.
The bottom indicators (pane count, prefix badge) move in one cell to
sit under the labels' own leading space; flush at the edge column they
read as misaligned on the left and collide with the compact ring's
corner on the right.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A sidebar lists one tab per row down the chosen edge, tab_bar_width
columns wide; the panes trade the strip's row for the sidebar's columns
in tab_area(), the one place that arithmetic lives. The per-tab cell
drawing is extracted from draw_tab_strip (label, rename editor, bell,
hover, drop states, hit) and shared, so every mouse verb -- click,
double-click rename, drag to a tab, drop on + -- works in both shapes
because the hits move with the cells.
The strip's indicators (pane count, prefix badge) take the sidebar's
bottom rows, reserved first under the same budgeting rule the strip
applies from the right. The status line below keeps the full width. A
terminal too narrow to give up the columns falls back to top, re-derived
per frame, so a resize moves the bar without anyone storing state.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
A compact pane owns no border cells -- its bottom border is the shared
line one cell outside its rect. With panes minimised, that cell was the
bar's top row: the chips sat directly on the bottom panes' content,
reading as frames cut into the pane. Classic panes carry their own
border row and never collided.
The layout now reserves one extra row above the bar in compact, and
draw_compact_lines strokes the boundary there: it joins the outer ring
with real junctions, a divider from above ends into it, and a focused
bottom pane re-strokes its stretch like any shared line.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
C-a / opens an inline search bar on the focused pane's status row, on
lib-vt's GhosttySearch (D4's pattern: the scrollback walk, reflow and
alt-screen survival are the library's, the UI and the wiring are ours).
Type to search live; the viewport follows the current match; up/down
(or C-n/C-p) step through them with wraparound; Enter keeps your place,
Escape snaps back. A k-of-n counter reads on the right, or "no matches".
Every match on screen is tinted, and the one the bar is on is tinted
apart -- warm gold by default, painted after the shader pass so the
scrolled wash does not dilute a match you are hunting for. Four themeable
colours (search_fg/bg, search_cur_fg/bg).
Bare / took the key from help, which keeps ?; that needed the legacy
decoder to read the shift out of a punctuation byte the way it already
did for capitals -- without it ? and / decode identically (SLASH, no
mods) and cannot be two keys on a client without the kitty protocol.
tests/test_search.py drives it end to end (39 checks): the bar, the
counter, stepping and wrap, all-match vs current highlighting, Enter/
Escape, new output updating the count, case-insensitivity, the keyboard
belonging to the bar, click-away, and that / and ? no longer collide.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A smooth bounce shimmered: anything placed with sub-cell X=/Y= offsets
had its source rectangle shaved by a different sliver every frame, so
the picture's edges breathed as it moved. dvd.py made it obvious --
the logo's crop wobbled between w=124 and w=130 around a true 142.
The pixels-per-cell a placement reports are its own pixels over its
own grid, floored, and the grid includes the cell the sub-cell offset
spills into, so the figure runs a shade small -- 142px over 19 cells
says 7 when the terminal's cell is 8. That was harmless while the
number was only used proportionally at real cut edges. The pieces
rewrite ran it over every placement: axis_crop() clamped even an
uncut image's source to cols * cell_px - offset, and as the offset
moved the clamp moved with it.
The far edge of a piece that reaches the placement's last cell is
where the source runs out; only an interior cut needs the cell
arithmetic. With that, an uncut placement's source rect passes
through untouched, which is what the old emitter always sent.
Two companions in the same sum. A scaled piece so narrow its share
of the source rounds to nothing now keeps the pixel it is inside,
because w=0 on the wire means "the whole image", which is worse than
wrong. And a cut piece whose source is genuinely empty is cells the
image never reaches, so it is not placed at all -- the uncut
placement stays exempt, since the pane-edge crop has always used 0
for "the rest of it".
The headless check places with X=5: the whole source rect must
survive the offset, where the clamped code sent w=0.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Overlays are painted over the panes and the compositor needs no more
than that: painted later is on top. A placement is not painted, though
-- the client's terminal draws the image over the cells, in whatever
order they arrived -- so an overlay that is only drawn is an overlay an
image sits on top of. The cheatsheet came up under a picture and was
unreadable through it, in the terminal and in the mirror both.
So a frame now records where its overlays landed, and the graphics pass
takes them out of a placement exactly as it takes out a float: the
modal's box (the cheatsheet and the picker wear the same frame), each
toast, and the splash. The list is rebuilt every compose, so a rect that
is not registered again is one that is no longer on screen.
Only the opaque things are registered. The scrim behind a modal tints
what is behind it rather than hiding it, and an image cannot be tinted
by us at all -- so a picture beside the cheatsheet stays bright while
the text around it is pushed back, which is wrong-looking but a great
deal better than the image disappearing while a modal is up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRz3zwZJvBvzsR9k4cj6dd
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Occlusion for placements is clipping: the cell compositor gets it free
from paint order, but images are sent after the diff and drawn over it,
so what a float covers has to be taken out of the placement itself. A
clean edge came out as a smaller rectangle; anything else -- a float
across the middle, a float on a corner, a float wholly inside -- was
refused, on the grounds that the remainder is an L and a placement is a
rectangle. The image vanished, all of it, the moment a floating pane
crossed it, on the terminal and in the mirror alike, and came back when
the float moved off.
The remainder is not one rectangle, but it is always a few: the strips
above and below what is covered, and what is left beside it between
them. So rect_subtract() cuts the visible remainder into rectangles and
each is placed in its own right, with the source rectangle the same
arithmetic the pane edge already used -- generalised to a cut on any
side, and to a first piece that keeps the sub-cell offset while the
others start on a cell boundary. Eight pieces is the ceiling (two
floats' worth); past it the old refusal stands.
The pieces of one placement need an id each, and the program's own
placement id is one number that cannot be several, so the model now
hands out the placement ids the client is told and keeps the program's
as the identity it was. `graphics` reports the piece alongside it.
Checked against a float parked over an image: the picture is cut around
the float, the strips tile exactly the image minus what the float
covers, and their source rectangles land where their cells do (a float
inside a 40x10-cell natural-size image gives 320x32 above, 320x32
below, 56x96 left and 72x96 right).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRz3zwZJvBvzsR9k4cj6dd
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every frame re-emitted `a=p` for every visible placement. On a terminal
that is invisible: a kitty placement is a standing instruction, not a
per-frame one ("the other commands to erase text must have no effect on
graphics"), so a repaint of the cells under a picture does not disturb
it and saying it again says nothing new.
It is not free anywhere else. A mirror cannot know that the identical
placement is identical: shellglass re-decodes and re-encodes the image
on every one, holds the placement back while that runs, and -- because
an image change rides only in a full frame -- republishes the whole
screen each time. Measured on one image and a pane printing text for
twenty seconds, the mirror published 430 full frames and re-sent the
image list 215 times; 215 of those fulls carried no image at all,
because the placement was still mid-decode. That alternation is what
was visible in the browser: a picture that flickered while anything on
screen moved and vanished when it stopped. The same twenty seconds now
costs one full frame, 412 diffs and one image list.
So a placement is emitted only when it is new or has actually changed,
and the client's state records what it is holding. Three things have to
be right for that to be safe: a dropped frame un-marks the placements it
carried (they are owed again, exactly as transmissions and deletions
already were); a retransmit of the pixels invalidates the placement,
because kitty deletes an image's placements when its data is replaced;
and a full repaint clears the screen, which takes the images with it, so
every placement is owed again -- gfx_repaint(), not gfx_reset(), because
the pixels the terminal was sent survive a clear and a deletion still
owed from a dropped frame is still owed.
The live check drives a picture that does not move while the screen
around it repaints: one placement across 121 frames, where the old
behaviour emitted 120.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRz3zwZJvBvzsR9k4cj6dd
|
| |
|
|
| |
Last input wins, canvas size is adjusted to active connection.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A pane image is re-emitted to the client as decoded RGBA, which for one
screenshot is ~4MB of base64 in a single indivisible message -- and
conn_send silently dropped anything that would overflow the 4MB outbox,
while push_graphics ignored the refusal and the model advanced anyway.
Everything stateful in that dropped frame was lost for good: the image
was marked sent (so its placements drew nothing, forever), and the
deletions rode out with it (so a placement the client had was never
revoked). Reported as screenshots in a recovered pi transcript parked
at a fixed spot on the screen, immune to scrolling and pane swaps --
those were placements whose deletion left with a dropped frame. The
1000x800 one could never be delivered at all: its transmission alone
was bigger than the cap.
So the frame is now transactional. Deletions queue and are repeated
every flush until gfx_commit() confirms the client heard them;
transmissions recorded in a frame are unwound by an undelivered commit
so they are sent again. A flush nobody commits counts as undelivered at
the next gfx_begin(), which is what the `graphics` control command is:
bytes rendered for a CLI, not the client. The server reports the
verdict and keeps the paint pending until it lands.
The outbox cap now bounds the backlog rather than backlog-plus-message,
so one big transmission on a drained queue always fits -- and that
surfaced a second way to lose: a queued image legitimately holds the
backlog over the cap until the client swallows it, and push_frame read
that as a dead client and closed the connection. The session
auto-detached every time a screenshot crossed the wire. A full outbox
is a client that is *behind*, not gone: push_frame now marks the screen
for a full repaint (the dropped diff means the client's cells are
unknown) and retries as POLLOUT drains the queue; a client that truly
left still shows up as a write error there.
The headless suite pins the owed-until-committed contract; the new
tests/live_graphics.py drives the real socket with a
bigger-than-the-cap image and a reader that stalls, and would have
caught both bugs: the flood must not detach, the transmission must
arrive whole, and a placement scrolled away must be deleted -- then
come back on scroll without a second copy of the pixels.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
A tiled pane sits on a cleared screen, so unpainted padding looked fine by
accident; a float sits on whatever was composited under it, and every
unpainted cell let that show through. draw_frame now fills the ring between
border and contents with exactly what screen_clear leaves, and a suspended
pane's never-composed content rect gets the same treatment.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A dumped session quietly re-tiled its floats, which was the one data-loss
gap left in D22. dump-layout now records `floating=true x= y= w= h=` -- the
*wanted* rect, because intent is what survives a session and the next
screen's layout clamps it like any frame -- layout files declare the same
(linted: floating is a bool, x/y/w/h mean nothing without it), and a file
whose every pane floats self-heals through ensure_tiled: the top float
lands, the same rule closing the last tiled pane follows, no lint required.
save-workspace inherits all of it from the shared dumper.
`float` over the API takes x/y/w/h and then *places* rather than toggles --
a placement is a statement about where, not about whether -- floating the
pane first when it is tiled, re-placing when it already floats, with
omitted fields meaning "keep". The intent is stored exactly as asked: the
write-back rule is for hands, whose drags need the clamp to agree with the
eye; a script means what it typed.
And kitty graphics respect the stack. The cell compositor gets occlusion
free from paint order, but placements are sent after the diff and would
paint an image straight over a float covering it. Each pane's placements
are now clipped by the float rects above it -- every float for a tiled
pane, the higher-raised for a float -- cropping what one clean edge can
express (the pane-edge source-rectangle arithmetic aimed at another
clipper, including from the top and left, where the source origin moves and
the sub-cell offset is consumed) and suppressing what it cannot: a strip
across the middle or a corner overlap leaves a shape one placement cannot
say, and the image returns whole when the float moves. No z= needed --
clipping is the z-order, since nothing is ever allowed to overlap.
D22's "still owed" paragraph is paid and rewritten; M10 is fully checked.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every non-space glyph of the logo is a particle that flies to its cell
over the first two fifths of splash_ms, then *is* the logo for the
rest. There is deliberately no particle state anywhere: a position is
a pure function of (target, elapsed, seed), recomputed every frame
like the layout and the pane states and for the same reason -- physics
that is stored can drift, physics that is derived cannot. The seed is
the splash timestamp, so every attach flies differently and one splash
is coherent frame to frame; a per-particle hash staggers departures,
and the delay rescaling lands every straggler exactly at the end,
because an almost-assembled logo reads as a bug.
Five motions, picked by the clock and pinned by {"cmd":"splash",
"motion":N}: rain (fall in, column by column), scatter (converge from
everywhere), slide (rows enter from alternating sides), vortex (spiral
in around the centre, orbit shrinking and unwinding), shuffle (the
glyphs trade places, then sort themselves). The colour chains keep
playing over the box throughout, so particles catch their hue as they
cross into it.
expr_sin() is the expression language's own sine exported for C-side
animation: a motion sketched in the repl and one compiled into the
binary cannot disagree about what sin(90) is.
The tests learned two things the flights taught: the logo's opening
run appears three times per line and assembly is staggered, so a
middle occurrence can land first -- "assembled" is the row whose first
block glyph is where the match begins -- and windows need margins that
survive a parallel suite on a busy box.
|
| |
|
|
|
|
|
|
|
| |
The logo is half-block art: its visible pixels *are* the foreground
and the notches are the terminal's own background. A pass that painted
backgrounds therefore turned the art into a slab with a logo-shaped
hole -- so every splash pass is channel fg now, the effect lands on
exactly the art's pixels, and the backdrop stays dark for free (a
blank cell has no glyph for a colour to land on).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One tint is one colour, and every effect wore the theme accent -- six
effects that were all the same dull blue. An effect is a *chain* now:
phase-shifted tints in loud literal colours, mixing per cell, which is
how a fixed-colour pass machinery does rainbows. Passes may light the
backdrop's background too, so the box is a scene rather than tinted
text, and the logo is drawn white -- the canvas that takes every hue at
full saturation, where tinting blue towards pink lands on mud.
The six: spectrum (rainbow bands), plasma (two interference patterns in
clashing hues), fire (heat up the glyphs, embers in the box), rings
(rainbow ripples), glitch (a reveal under two colours of static),
aurora (green and violet curtains over cyan).
The splash is deliberately the one place that does not defer to the
theme: it is branding wearing a demo, not chrome.
{"cmd":"splash","fx":N} picks an effect by index -- a test needs a
deterministic one, and "show me the aurora again" deserved an answer.
The new test counts dominant-channel hue families on the logo rows,
because two hundred shades of the same blue is exactly the failure it
guards against.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
logo.txt is the source of truth; the Makefile bakes it into
build/logo.h the way version.h is baked. When a client attaches
(MSG_HELLO turning a connection into the display), the logo appears
centered over everything for splash_ms (900, 0 for never), on a
cleared backdrop, in the accent -- and one of six effects runs over
its rect through the ordinary shader pass: a diagonal sheen, plasma,
rings, sparkle, a left-to-right reveal, a breathe. Which one is picked
by the splash's own timestamp, so every attach looks a little
different without anybody keeping a counter.
Every effect reads the clock, so the frame clock runs only while the
splash is up (note_animation, same as any animated chain), and `since`
in a splash context is milliseconds since it appeared, so one-shot
effects sweep exactly once. Any key or click ends it and still lands
where it was aimed: a greeting is not a modal. A screen the box does
not fit on gets no greeting rather than a cropped one.
Headless and scripted sessions never attach, so the whole fast suite
never sees it; {"cmd":"splash"} triggers it on demand, which is what
makes it testable from a scripted session -- and lets you replay the
greeting whenever you like.
|
|
|
app.c was 7k lines wearing section markers; now the markers are files,
cut exactly along them, no behaviour change:
app.c the session core: config in force, pane and tab
lifecycle, walking, input routing, introspection
app_layout.c layout, tree edits, resizing, equalising, rotating,
focus
app_session.c layout files, checking, workspaces, dumping a session
app_draw.c drawing, the shader passes, kitty graphics
app_ui.c the finder, modals, the palette, the project picker,
renaming
include/app_internal.h carries what the five share: the tree/tab/app
structs, CFG and the palette macros, and the 50-odd formerly static
functions the cut made cross-unit, declared where they are defined.
They keep their names; only the five units see the header.
One test moved with the code: test_portable_layouts' staleness check
greps the source for the layouts section, which now lives in
app_session.c.
Mechanical, so blame for anything in the four new files continues in
app.c before this commit.
|