| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Clicking the bar's `+` made a tab with no status bar. The button only exposed
it: pressing `n` in TAB mode did the same, in any `sl0ppi up` session.
The chain: a generated layout had no default_tab_template, so a tab created at
runtime had nothing to take a bar from. It had none because
parse_tab_node_with_template read a tab node's name, cwd and focus but never
its `purpose` -- so templating erased the purpose that IS identity here
(`sl0ppi ls` finds project tabs by it), and the CLI worked around that by
inlining the template into every tab instead of emitting it.
Fixed at the root: a templated tab keeps its own purpose, and the CLI emits
`default_tab_template` again. The generated layout also shrinks -- one copy of
the bar's configuration instead of one per tab.
The two properties trade off against each other, so sl0ppi-up-live.py now
asserts both at once: `sl0ppi ls` still sees a tab per project, and every tab
including one created at runtime has exactly one status bar. Confirmed by
reverting each half separately: without the parser fix the tabs lose their
purpose and `ls` shows "alpha —"; without the CLI change the runtime tab has
zero bars, which is the reported bug.
Two measurement traps cost an hour here and are now written down: counting bars
by grepping dump-layout also counts the template and every swap layout ("4 bars
for 3 tabs"), and list-panes stops reporting borderless plugin panes for a tab
once a client has rendered it.
Also in here, from the same session:
- the split button check found buttons by searching for " + " on any row with a
frame glyph, which matches the stack list's "+N more" once two panes sit side
by side, and then reports the product broken. It now requires the frame
corner the button is drawn against.
- the checks killed their sessions but did not delete them, so each run left an
"EXITED - attach to resurrect" entry: 35 had quietly filled the session
picker. They now delete-session by name.
- and the trap that cost the user their running client: I killed a bare
`sl0ppij` because "there are no user sessions on this box". That was a guess,
not evidence, and wrong. A check's processes always carry that run's own
socket dir or `attach sl0ptest-...`; a bare `sl0ppij` with a cwd in the repo
is a human's. Written into the handover next to the `pkill -f` trap, which is
the same mistake in a different costume.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pane-finder requests three permissions as it loads, so the first press of a
keybinding we ship opened a consent screen instead of a fuzzy finder. Prompting
is right for a wasm you found on the internet and wrong for the ones that come
in our own tarball.
plugin_is_shipped_with_sl0ppi (zellij-utils/src/input/permission.rs) grants
those implicitly, through the same instruction a human "y" produces, so the
plugin's permissions are set and recorded exactly as before. "Ours" is narrow
on purpose:
- builtins, asked via the existing PluginConfig::is_builtin -- the same
predicate check_command_permission already uses to exempt them, so the two
cannot drift;
- files inside a plugin directory we install into: the config dir's plugins/
and the data dir's plugins/;
- nothing else. Remote plugins are never trusted, paths are normalised
lexically first so plugins/../../evil.wasm is not "inside" the dir, and the
directories are derived from the environment and default locations -- so
getting them wrong costs a prompt and can never grant one elsewhere.
The trade, documented in config/README.md: <config dir>/plugins/ is now a
trusted directory. Anything able to write there can already bind arbitrary
commands to a key in config.kdl, so a consent screen for the wasm beside it was
not buying anything -- but it does mean plugins/ is not a download folder.
packaging/checks/plugin-permissions-live.py drives both halves on a real
session: the shipped plugin must run without the prompt, and the SAME wasm
loaded from outside the plugin dir must still ask. Confirmed by disabling the
rule and watching the first half fail with the exact "asks permission to ...
Allow? (y/n)" screen it was written against.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Slices 3 and 5 of docs/cli-design.md.
`init` writes .sl0ppi/layout.kdl. The template is mostly comments, because the
purpose vocabulary and the start_suspended habit are the two things nobody can
guess, and the point of use is the cheapest place to explain them. A test
asserts the template passes doctor's own fragment check -- shipping people a
starting point that fails our validation would be embarrassing.
`doctor` splits into gathering facts (IO) and judging them (pure), so the rules
are testable. It is honest about its limits: sl0ppij has no parse-only mode, so
fragments get a structural check (empty, unbalanced braces, a whole `layout {}`
pasted in, a `tab` node sl0ppi owns) rather than a claim of validity. It also
reports the known packaging hole -- pir needs python3 and fzf, neither of which
is in the tarball.
Driving them for real turned up a worse bug than either command:
a single broken fragment took down the entire root
On session creation every tab shares one layout file, so one unbalanced brace
made the whole `up` fail and no project opened at all. Fragments are now checked
before use on BOTH paths, broken ones are skipped with the reason, and the rest
open. `up` also registers the root even when some projects failed, since
forgetting it would orphan the tabs that did open.
Two smaller fixes from the same session:
- falling open with no tty used to spawn a bare sl0ppij, which silently
CREATES a session; that is where a handful of randomly-named junk sessions
came from during testing. It now reports and exits.
- a rename leftover: `setup --check` still described "the configuration of
zellij".
Test isolation lesson worth recording: sessions live in the runtime dir
(/run/user/1000/sl0ppij), not TMPDIR, so setting TMPDIR alone does not isolate
them -- an earlier test session called `root` was silently reused by a later
test. SL0PPIJ_SOCKET_DIR is what actually isolates.
88 tests green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`new-tab --layout` / `--layout-string` created the tab and silently dropped
every purpose the layout declared -- the tab's own (which rides the tab's root
layout) and each pane's. Layouts applied at session creation kept theirs, so
this only broke tabs added at runtime, which is exactly what an orchestrator
does. Found by integration-testing the sl0ppi CLI against a real session rather
than trusting that the documented feature worked on this path.
Cause: TiledPaneLayout and FloatingPaneLayout had no protobuf `purpose` field,
so anything travelling as an Action lost it. The original workspace-hints commit
says as much in a comment -- "adding a proto field would force regenerating the
committed assets" -- because generated prost output is checked in and protoc was
not available. It is now (a user-local protoc 25.1, no sudo, no system package),
so the deferral can be paid off properly instead of worked around.
There are two protobuf schemas and the distinction matters: plugin_api/ is what
plugins speak, client_server_contract/ is what the CLI speaks. The first attempt
fixed only plugin_api and changed nothing observable; both now carry the field.
plugin_api/action.proto TiledPaneLayout.purpose = 14
FloatingPaneLayout.purpose = 13
client_server_contract/common_types TiledPaneLayout.purpose = 16
FloatingPaneLayout.purpose = 15
Purposes are sanitised on ingest, so a value arriving over IPC obeys the same
invariant as one parsed from a layout file (bounded token, <=64 chars) whoever
constructed the message. Verified live: a tab declaring `rm -rf /; echo "x"` is
stored as `rm-rf/echox`, and a 200-char pane purpose arrives capped at 64.
Tests: two new wire round-trips (a NewTab layout carrying tab, pane and floating
purposes; and the sanitiser). zellij-utils 473 passed, zellij-server 1499
passed. Live: `new-tab --layout-string` now reports
purpose='project:added/nested' with panes 'agent:main' and 'logs:app'.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Makes the fork installable and runnable beside a stock zellij: separate binary
name, config dir, cache dir, socket dir, log file and plugin URL scheme.
Everything user-visible is defined once in zellij-utils/src/branding.rs and
referenced from the ~15 places that used to hold a literal, rather than sed-ing
the tree -- upstream has 3300 commits of future rebases, and each touched
upstream line is a conflict forever. A conflict in branding.rs itself always
resolves the same way: keep our constant, take their code.
binary zellij -> sl0ppij
config dir ~/.config/zellij -> ~/.config/sl0ppij
system cfg /etc/zellij -> /etc/sl0ppij
cache/data ProjectDirs(Zellij) -> ProjectDirs(foo.sl0p.sl0ppij)
socket/tmp /tmp/zellij-$UID -> /tmp/sl0ppij-$UID
log zellij-log/zellij.log -> sl0ppij-log/sl0ppij.log
plugin urls zellij:tab-bar -> sl0ppij:tab-bar (zellij: still parses)
Two things deliberately NOT renamed:
- The cargo package stays `zellij`; only [[bin]] is renamed. Renaming the
package would ripple through every intra-workspace dep, the plugin API and
the 0.45.0+sl0p1 version trick for a name nobody sees.
- The $ZELLIJ* environment variables stay, because the ecosystem (shell
prompts, "am I in a multiplexer" checks, our own pi-ext) looks for them. We
now export $SL0PPIJ* alongside them, and those take precedence on read.
branding::import_compat_env() folds an inherited ZELLIJ_CONFIG_DIR into its
SL0PPIJ_ spelling at startup, before clap reads the environment -- clap can
only be told one variable name per argument.
Snapshots: 14 updated, all of them the plugin URL scheme or a session message.
The insta churn (assertion_line metadata, trailing newlines) that a bulk
--force-update introduces across 340+ other snapshots was reverted, so the diff
here is only what actually changed.
Tests: zellij-utils 471 passed, zellij-server 1499 passed. branding.rs carries
its own tests, including one asserting no shipped default path contains the
string "zellij".
Verified live: sl0ppij creates /tmp/sl0ppij-1000 next to an existing
/tmp/zellij-1000, and `sl0ppij list-sessions` reports none while a stock zellij
reports three running sessions.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Completes the two pieces deferred from the first workspace-hints pass.
`zellij run --purpose X` / `zellij action new-pane --purpose X` tag a pane at
spawn time, which is what an agent wants for panes it creates itself; the OSC
wrapper still works for programs that declare their own role. Not supported with
--in-place, which replaces another pane rather than being a pane of its own: clap
rejects the combination rather than accepting a flag that would do nothing.
`tab name="work" purpose="agent-work"` gives a whole tab a role, reported by
`list-tabs --json`. It rides the tab's ROOT layout node, reusing the pane field
because a tab's root is the tab container and never itself a pane -- asserted by a
test for the tightest case (a tab holding exactly one pane), since a leak there
would silently mislabel a pane.
The interesting part is why the flag first shipped broken. There are TWO protobuf
boundaries -- the plugin API and the client->server IPC contract -- and a CLI flag
must cross the second one. Defaulting the field in the conversion (what "no
protoc" usually forces) compiles cleanly and then silently drops the value in
transit, so --purpose parsed, printed a pane id, and did nothing. Fixed by adding
the field to both contracts by hand, in the .proto AND the committed generated
prost, kept in lockstep so a future regeneration reproduces it: protoc is only
needed to regenerate, not to build. Both boundaries now have a round-trip test,
and the IPC one was confirmed to fail when the wire read is removed.
Sanitising moved to Grid::set_authoritative_purpose, the single choke point every
out-of-band source passes through, after the CLI path was observed delivering
`bad purpose; rm -rf /` intact into list-panes --json.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Implements the zellij half of sl0p/workspace-hints-proposal.md: a pane can carry
an opaque agent-facing hint for what it is FOR, so a project can ship a layout
that an agent is able to navigate instead of guessing.
zellij never interprets a purpose. The consuming agent owns the entire vocabulary,
which is what keeps this to one string threaded along the existing status_bar path
rather than a policy engine, and lets the vocabulary change without a rebuild.
Two ways in, because declared panes and agent-created panes are different:
layout: pane name="server" purpose="dev-server"
in-band: printf '\033]5577;1;purpose;agent:build\033\\'
The second exists because tagging at spawn time would mean threading a field
through 28 SpawnTerminal call sites, and a new action variant is impossible while
protobuf enums cannot be regenerated. Wrapping the command costs one printf.
A declared purpose outranks an in-band one and cannot be overridden or cleared by
it. OSC arrives from whatever the pane happens to be showing, so otherwise a
`cat hostile.txt` in the human's editor pane could relabel it as the agent's
scratch pane and invite an agent to run commands where the human is working. A
layout is written by a human in a file they can read; in-band data only fills a
blank.
Exposed on PaneListEntry (`list-panes --json`) rather than PaneInfo, which crosses
the plugin protobuf API we cannot regenerate; resolved in the screen thread where
panes live rather than via route.rs's per-pane PTY round-trips. Values are
sanitised to a bounded [A-Za-z0-9_.:/-] token at both entry points, because a
purpose is never rendered -- it is matched against and pasted into consumers'
shell and JSON pipelines.
Verified end to end rather than by inspection: purposes from a layout appear in
list-panes --json against the right pane ids, an agent-spawned pane tags itself,
a declared purpose survives an in-band override attempt, all of it comes back
after kill + resurrect, an unknown pane property is still a parse error, and a
layout with no purpose leaves dump-layout unchanged.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`status_bar_button_style "indexed"` renders `1:Approve 2:Reject` instead of
`[Approve] [Reject]`, so the bar doubles as a key legend for a harness that binds
alt+1..9 to the same actions.
A config option rather than a protocol field: whether the numbers mean anything
depends on what the USER has bound, not on what the program knows. It needs no
wire change and so no version bump, and the click report still carries the id --
never the index -- so a program is unaffected by which style is in force.
The style is threaded into layout_status_bar, the one function the renderer draws
from and the hit-test resolves against. That is what makes this safe: changing a
button's rendered width necessarily changes its clickable span too, verified by a
test asserting span width equals the drawn width and that every drawn column
resolves back, for both styles.
Also fixes the web client's Style construction to read the option from config
rather than defaulting it -- my first pass claimed that code had no pane_frames
config available, which was wrong; it uses it two lines further down.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Second slice of the status/action frame. A program declares buttons, and a click
is reported back to it:
printf '\033]5577;1;buttons;approve:Approve;reject:Reject\033\\'
-> click -> program's stdin receives OSC 5577 ; 1 ; click ; approve ST
The report is a FIXED format carrying only the id, never bytes the payload chose,
and ids are restricted to [A-Za-z0-9_-]. The sequence that creates a button is
in-band and untrusted -- `cat` of a hostile file emits it as readily as a
cooperating program -- so a click that could send arbitrary bytes would let
viewing a file inject into the stdin of whatever is running, a shell included.
Labels are '%'-escaped since the wire format is ';'-separated, and both the
button count and id length are capped because the list lives as long as the pane.
Positions come from one layout_status_bar function that the renderer draws from
and the hit-test resolves against. Computing that geometry twice is how the
stacked-pane picker once selected the wrong pane; a test asserts every drawn
column resolves back to the button drawn there, across 100 widths.
Two things the drive caught that unit tests could not:
- The first delivery path pushed onto grid.pending_messages_to_pty, which is
only drained when the PANE produces output -- so a click on an idle program sat
in the queue forever. The write now happens at the tab level, which owns the
senders.
- The hit-test derived the frame row from the pane's raw geometry, but this fork
draws the frame inset by gap_offset, so with `gap 1` every click missed by one
row. It now derives from the CONTENT rect, which is by definition the inside of
the drawn frame and therefore the same columns the renderer lays out.
Buttons serialise with the session like the status text, so a resurrected pane
comes back with its buttons intact.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The status bar survived detach/reattach (it lives on Grid) but not a
serialise/restore cycle, so a resurrected pane came back blank.
It now travels the whole per-pane serialisation pipeline, mirroring the fork's
existing default_fg/default_bg: pane -> PaneLayoutMetadata ->
PaneLayoutManifest -> TiledPaneLayout -> a status_bar property in the serialised
KDL, then back through the normal layout parser and applied by LayoutApplier
when the pane is recreated.
The sanitiser moved from grid.rs into zellij-utils so both entry points share
one definition. That matters: a layout file on disk is no more trustworthy than
the escape sequence that wrote it -- it may have been hand-edited, or copied from
somewhere else -- so restored text is stripped and length-capped on the way in
just like the OSC payload was.
Verified end to end rather than by unit test alone: set the bar, waited for a
serialisation pass, confirmed status_bar="..." in session-layout.kdl, killed the
session until list-sessions reported EXITED, then attached and watched it come
back from disk. The earlier attempt only proved detach/reattach because Quit had
left the session alive.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Upstream's mouse_hover_effects governs two unrelated things: the frame highlight
on hover, and the "Ctrl <MouseScroll> or <drag borders> to resize" hint. Its own
comment in default.kdl admits as much. So silencing a beginner reminder you have
outgrown also costs you a highlight that stays useful indefinitely.
mouse_hover_help_text (default true) gates only the hint. One line in
execute_send_to_terminal; the highlight is set in execute_update_hover and is
untouched.
Config-file only, deliberately. The option has no protobuf slot, so it does not
cross the client/server IPC contract -- the roundtrip test asserts exactly that,
with the fixture set to None and a comment explaining why. It still works from
the config file because the server parses the file itself and Options::merge
keeps the file value when the client sends None; a CLI flag would be silently
ignored.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Upstream's action focuses an already-running plugin, so a chord bound to it can
only ever open one -- there is no way to dismiss with the same key.
This cannot be fixed in a plugin. Keybindings are resolved into actions in
route.rs before a key reaches a pane, so the launching chord never arrives at
the plugin. intercept_key_presses does not help either: the intercept lives in
screen.rs and only sees keys that already became Action::Write, i.e. keys
destined for a pane. Confirmed empirically -- an intercepting build received
Ctrl-u but never Ctrl-;.
Closes only when the plugin is the focused pane in the client's ACTIVE tab, so
the chord cannot close whatever pane the user happens to be on. A plugin that
exists but is hidden behind a floating-layer toggle, or is focused in a
different tab, is revealed or brought over as before.
Defaults to false, so every existing binding is unaffected. Config-only, with no
protobuf representation: defaulted at both boundaries and dropped when an action
crosses the plugin API, so no prost regeneration is needed.
The 24 snapshot lines are the new field appearing in serialised keybind dumps.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Collapsing and expanding a tab from the default layout stranded the status bar
halfway down the screen, and left the collapsed stack three rows tall.
A synthesized layout describes only where the user's panes go. Applied bare, it
laid out EVERY pane in the tab -- including the status bar, which became an
ordinary grid cell. Real swap layouts do not hit this because their `tab` node
is expanded with the default tab template, which carries the pinned plugin
slots; a layout built at runtime has to take those from BASE instead.
Synthesis now builds into the tab's own structure: the first non-plugin leaf
becomes the stack or grid and inherits its size, further content leaves are
dropped since the replacement accounts for every pane itself, and plugin panes
keep their slots and sizes.
Falls back to the bare replacement when there is no content slot at all -- a
layout of nothing but plugin panes would otherwise be returned unchanged,
silently dropping every user pane. Found by the test, not by the drive.
Only reproducible with a layout that has plugin panes, which is why the
quaddie.kdl drives never showed it and the default layout did immediately.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The boxed entries read as clutter: every entry bracketed in vertical bars, and
the active one marked by a "> " that sits outside its own cell.
`stack_list_style "minimal"` drops both. The active entry is shown by
inverting it instead, which needs no glyph and no column, and the padding is
moved inside the styled run so the inverted block covers the whole cell rather
than leaving unstyled gaps at its edges. Four fewer columns of decoration per
entry, so more fit per row in grid mode.
Uses ribbon_selected rather than text_selected. Themes give ribbon_selected
real contrast because it marks the selected item elsewhere in zellij's UI --
gruber yellow here, white-on-blue in the TempleOS theme -- whereas
text_selected's background is 40,40,40 against a 24,24,24 terminal and read as
essentially no highlight when tried.
Decoration width comes from the style itself, so Tab's column arithmetic and
the renderer cannot disagree about how wide an entry is. Default stays boxed,
so nothing changes for anyone who does not ask.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A tab restores into whatever layout was active before it collapsed, which for a
bare layout is the implicit BASE. BASE is pinned to ExactPanes of the count it
was written with, so adding one pane while collapsed left nothing to expand
into and the tab stayed stacked on every subsequent widening.
Two fixes. When the stored layout no longer applies, synthesize a grid to
spread the panes out rather than leaving them stacked -- a wide terminal is not
a state that retries, so failing here strands the tab for good.
And only consume the restore position once a layout has actually been applied.
It was cleared unconditionally, so after a failed restore the tab believed it
had expanded while visibly still collapsed, and never tried again -- which is
why this looked like "expand stopped working" rather than "expand did nothing
once".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The collapse selects a swap layout by name, and a custom layout file only gets
swap layouts from a hand-written sibling "<layout>.swap.kdl". So for most
layouts there was nothing to select and the feature silently did nothing --
requiring a boilerplate file per layout to use it at all.
Now a missing layout is synthesized: every pane flattened into one stack, in
layout order. Appended rather than inserted, so it cannot shift the BASE layout
at position 0, which is what the collapse restores back to.
Synthesized lazily, at the first collapse, rather than up front: a stack sitting
in the swap cycle would let auto_layout apply it spontaneously when a pane is
added, stacking panes on a perfectly wide terminal.
Also scopes the damaged-layout guard. With no swap layouts, relayout finds
nothing applicable on the first pane added, gives up and marks the tab damaged
-- permanently, and with nothing to do with user intent. Honouring that flag
disabled the collapse for exactly the bare layouts this fallback targets, so it
is now only consulted when the tab has swap layouts the user actually defined.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`stack_list_layout "grid"` packs picker entries several per row, which turns
stack_list_max_rows from a count into a space budget: eight panes in a wide
terminal now fit on one row instead of spending three and hiding five.
The entry renderer already drew each entry as a single chunk at its own x/y,
sized by its geom and centring its own label, so a grid needed no rendering
changes at all -- only the loop assigning every entry the same x and the full
width. Column count is derived from the widest label rather than configured, so
it adapts to the titles; one column is the floor and reproduces the previous
full-width rows byte for byte.
The row -> member mapping now has a column axis, and it had already caused one
wrong-pane bug when two consumers disagreed. So the layout is computed once,
by one function, into explicit cells; the renderer, the reserved-row budget and
the mouse hit-test all read those same cells rather than each doing their own
arithmetic. Windowing is by row, so a grid scrolls a whole row at a time and
degrades to the old per-member window when there is one column.
Cells are separated by a two-column gap: without it the focused entry's "> "
marker sits flush against the previous cell and reads as belonging to it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A stack reserves one row per member plus a padding row, so a deep stack spends
most of the screen listing itself. `stack_list_max_rows` caps that; the list
windows around the focused member and reports the rest as "+N more".
The indicator goes in the padding row the list already reserves, so it costs
no extra row and never displaces a real entry. Entries keep their real rank
rather than renumbering from 1, otherwise the numbering stops meaning anything
once the window has scrolled. The window slides on ordinary focus movement,
which is also why this needs no new keybinding -- Action is a protobuf enum
and the prost assets cannot be regenerated here (no protoc).
A fixed cap rather than hover-to-expand on purpose: reserved_top_rows feeds
pane geometry, so growing the list on hover would resize the pane's PTY and
reflow whatever is running in it on every mouse move. A real expand/collapse
has to be an overlay that reserves nothing, which is a much larger change to
the render path.
reserved_top_rows and the renderer both go through stack_list_window(); if
they disagreed the list would draw into rows the geometry never reserved, i.e.
over the pane beneath it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A `children` placeholder inserts every pane it receives as a flat sibling.
That single hardcoded policy is why a layout can only express "N equal
columns", "N equal rows" or "one stack" without hand-writing a variant per
pane count -- the max_panes=5 / max_panes=8 / max_panes=12 ladders in a swap
layout file exist purely to work around it.
`pane arrangement="grid" { children; }` builds a tree for N panes instead.
Two to start: grid (ceil(sqrt(n)) columns, rows filled left to right) and
master-stack (one 60%-wide master, the rest beside it).
Generates a TiledPaneLayout rather than geometry, so everything downstream
works unchanged: split_space, the minimum-size feasibility check,
LayoutApplier, swap-layout damage tracking, and the responsive collapse. It
composes with that last one for free -- a grid is simply infeasible on a
narrow terminal, so it is skipped and the collapse takes over.
The arrangement builds ONE self-contained subtree rather than splicing a
variable number of nodes in among the placeholder's siblings, so it stays well
defined next to the pre-existing "children node with sibling nodes" TODO
rather than leaning on it.
Like default_fg/default_bg, the field has no protobuf representation, so it is
defaulted at both plugin-API boundaries and does not survive a round trip.
This is why it is a field rather than a LayoutConstraint variant: a new enum
variant would have no representation when converting TO protobuf, and the
committed prost assets cannot be regenerated here (no protoc).
The 48 snapshot updates are the new field appearing in TiledPaneLayout debug
dumps (324 lines, all `children_arrangement: None`). No snapshot content was
removed. The `expression:` metadata churn in those files is pre-existing drift
that regeneration swept up: the stored snapshots recorded the expression
before normalize_layout_debug was introduced.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Splitting a window into thirds is great until the window is 70 columns wide
and each pane gets 23. `responsive_min_cols` collapses the tab into the swap
layout named by `responsive_layout` when it gets narrower than that, and
restores the previously active layout when it grows back.
Selects a swap layout BY NAME rather than adding a size-aware
LayoutConstraint: that type crosses both the plugin protobuf API and the
client/server contract, and the committed prost assets cannot be regenerated
here (no protoc).
Restoring uses a 10-column hysteresis band. Without a dead band, dragging a
window edge that lands exactly on the threshold flips the layout on every
motion event, and every flip resizes every PTY in the tab.
"Did the user rearrange panes by hand" is judged from the damaged flag as
sampled BEFORE each resize, not the live one: resize_whole_tab's own
auto-layout pass sets that flag itself and clears it again only if it found a
layout that fits, so on a terminal too narrow for any of them it latches true
although nothing was rearranged. And once collapsed, restore stays reachable
regardless of the flag, so damage occurring while collapsed cannot strand the
tab in the narrow layout.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
aa51848d replaced "SCROLL: 42/1000" with a compact "^42" glyph and stopped
showing any indicator when the pane is not actually scrolled up, but did not
refresh the insta snapshots that encode the old upstream frame rendering.
That left 30 failing tests in zellij-server from the very first sl0p commit
onward, which in turn masked 5 more in zellij-utils (cargo stops at the first
failing test binary, so zellij-utils never ran).
Bisected to aa51848d; the upstream fork point 0e6e4404 is green.
zellij-server (44 snapshots): frame titles lose the "SCROLL: 0/N" segment when
not scrolled, and render "^N" where they previously rendered "SCROLL: N/M".
Verified every diff is scroll-only, and that the genuinely-scrolled cases
(2/13, 3/33, 6/33) still render as ^2, ^3 and ^6 rather than disappearing.
zellij-utils (5 snapshots): serialized Config gained the sl0p FrameConfig
fields (title_alignment, gap, padding, gap_aspect, inactive_grayscale,
hide_default_pane_names), all at their existing defaults. Additions only.
Several tests assert multiple snapshots and insta aborts at the first
mismatch, so this took four regenerate/verify passes to fully converge.
zellij-server: 1471 passed, 0 failed.
zellij-utils: 441 passed, 0 failed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`ui { pane_frames { hide_default_pane_names true } }`, default false.
A pane now shows no title at all until something actually names it: the user
renaming it (including by double-clicking the title row) or the running
program setting a title. Upstream always shows an auto-generated 'Pane #N'.
Implemented by tracking whether pane_title is still the constructor's
placeholder, NOT by matching the string: a program that legitimately titles
itself 'Pane #3' must not be mistaken for an unnamed pane. set_title clears
the flag, since something then chose that title.
current_title reads the option at render time rather than baking an empty
title in at construction, so it hot-reloads and turning the option back off
restores the placeholder on existing panes.
Display precedence is unchanged and now explicit in the code:
user-given name > title set by the running program > placeholder
render_terminal_title already fell back to an empty string rather than the
placeholder, so the terminal WINDOW title is unaffected.
Empty titles were already handled by the frame renderer
(render_title_left_side returns None for an empty title), including the
sl0p centred/right alignment path.
Baselines held: zellij-server 1436/30, zellij-utils 436/5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`ui { pane_frames { inactive_grayscale 0-100 } }`, default 0 (off).
Real colour transformation, not the SGR 'faint' attribute zellij already uses
for frames: faint is terminal-dependent, several terminals only apply it to
the 16 ANSI colours, and it does not remove colour. Each cell's colour is
resolved to RGB and blended toward its own Rec.709 luminance, so an unfocused
pane keeps its brightness but drains of colour.
Colour resolution covers the cases that actually occur: RgbCode directly,
ColorIndex via eightbit_to_rgb, and NamedColor by mapping to its 0-15 index.
Cells with NO explicit colour are deliberately left alone - they render in the
terminal default, which is already unsaturated, and rewriting them would mean
emitting an explicit colour for nearly every cell for no visible gain.
Two non-obvious pieces were needed to make it actually work:
- The output buffer only emits CHANGED cells, so an idle pane would keep its
old colours indefinitely after losing focus. Pane::refresh_for_focus_change
forces a full repaint, called from ActivePanes::insert (the single place
focus changes). It is a no-op when the feature is off, so it costs nothing
by default.
- Terminal panes render through the SHARED multi-client path. Focus is per
client, so once unfocused panes are desaturated two clients may need
different output for the same pane. Both tiled and floating panes now fall
back to per-client rendering when grayscale is enabled - the reason this is
opt-in.
Known limitation: sixel/kitty image chunks are not colour-transformed and stay
in colour.
Hot-reloads like the other sl0p options. Tests cover the transform: full
strength yields a true neutral grey, partial sits between original and grey,
indexed and named colours resolve rather than being skipped, luminance is
perceptual (green brighter than blue) not a flat average, already-grey is
unchanged, and non-colour codes are untouched.
Baselines held: zellij-server 1436/30, zellij-utils 436/5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Double-click-to-rename trapped the client: once in RenamePane, Esc and Enter
did nothing and there was no way out without killing the session.
Cause: Screen::change_mode() does `mode_info.base_mode = base_mode`, i.e. it
OVERWRITES the client's base mode with whatever it is handed. The
double-click path passed None, believing that meant 'leave it alone'. It does
not - ModeInfo::change_to_default_mode() is:
if let Some(base_mode) = self.base_mode { self.mode = base_mode; }
so a None base_mode makes leaving ANY temporary mode a silent no-op. The
legitimate caller in lib.rs passes Some(base_mode) from the session config;
this path had no equivalent.
Fix: Tab::client_base_mode() reads the client's existing base_mode (falling
back to the tab default) and the rename path passes it through, so entering
rename via the mouse leaves the return path exactly as the keybind does.
Added a test on ModeInfo pinning the trap itself: change_to_default_mode is a
no-op with base_mode None, and returns to the base mode when it is set. That
is the property that makes passing None dangerous, and it now fails loudly if
anyone assumes otherwise.
Immediate escape hatch for a session stuck on an older build:
zellij action switch-mode locked
(that goes through the server path, which sets base_mode correctly)
Baselines held: zellij-server 1429/30, zellij-utils 436/5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`padding` now accepts:
padding 1 -> all four sides
padding 1 2 -> up/down = 1, left/right = 2
padding 1 2 3 4 -> up=1 down=2 left=3 right=4
NOTE the 4-value order is UP DOWN LEFT RIGHT as requested, deliberately NOT
CSS's clockwise top-right-bottom-left. Documented on PanePadding and at the
parser, since the similarity to CSS invites the wrong assumption.
3 or 5 values are rejected with a message naming the accepted forms, rather
than being silently reinterpreted. Negative values clamp to 0.
ASPECT: only the 1-value form is aspect-corrected (left/right multiplied by
gap_aspect) so `padding 1` looks square on a non-square cell grid. The 2- and
4-value forms are literal: specifying axes separately means the caller is
already controlling horizontal size, and silently doubling it would surprise.
Style now carries a PanePadding {top,bottom,left,right} instead of an x/y
pair, which also collapses the reconfigure plumbing back to a single field
rather than growing another parameter per side.
to_kdl always emits the explicit 4-value form: emitting the 1-value form
would re-apply gap_aspect on the next read and silently double the padding
on every config round-trip.
Baselines held: zellij-utils 435/5, zellij-server 1421/30.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two changes to how the gap is defined.
1. UNIFORM GAP. Previously every pane inset every side by `gap`, so two
neighbours each gave up a cell and the space between them was 2*gap while
the screen edge got only gap. Now a pane insets RIGHT and BOTTOM always,
and LEFT/TOP only where that edge is the viewport boundary. Two neighbours
contribute exactly one gap between them, and the outer screen edges get
the same gap. `gap 1` now means 1 everywhere.
The per-side offset is computed in tiled_panes, which is the layer that
knows the viewport, and pushed to panes via Pane::set_gap_offset before the
content-offset sweep. Floating panes keep a zero gap: they are positioned
explicitly by the user, so an inter-pane gap does not apply to them (they
still get padding).
2. ASPECT CORRECTION. Terminal cells are roughly twice as tall as they are
wide, so an equal number of rows and columns does not look like an equal
distance. `gap` and `padding` are now expressed in ROWS, and columns are
multiplied by `gap_aspect` (default 2, minimum 1). Style carries the
resolved pane_gap_x/y and pane_padding_x/y.
FrameConfig::Default is now hand-written rather than derived: gap_aspect must
default to 2 to match the KDL parser, otherwise a parsed config disagrees with
a default-constructed one and it surfaces as config round-trip diffs (this
broke can_define_ui_configuration_in_configfile and both
bare_config_from_default_assets_to_string tests).
Tests assert the actual invariants: stacked and side-by-side neighbours get
exactly one gap between them, screen edges get the same gap as between panes,
and inset_by refuses to collapse a pane too small to survive it.
Baselines held: zellij-server 1421/30, zellij-utils 433/5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
REGRESSION from 30782cbb: with `gap 1` the zjstatus footer disappeared.
A status bar is a size=1 borderless pane. set_content_offset expanded it by
the gap on every side, giving top = bottom = 1 and therefore
rows - (top + bottom) = 0 content rows, so it rendered nothing at all. The
same applied to the tab bar and any other chrome pane sized exactly to its
content.
Two independent fixes, because either alone would leave a sharp edge:
1. Callers skip the inset for `borderless` panes. Chrome is sized exactly to
its content and is not part of the tiled pane grid the gap is meant to
separate, so it should never be inset.
2. Offset::expanded_by_within() refuses any expansion that would leave a pane
with less than one row or column of content, and returns the original
offset instead. Panes can be resized down to almost nothing at runtime, so
the geometry must not depend on callers being careful.
Tests encode the exact regression (a 1-row pane must keep >= 1 content row)
plus an exhaustive sweep over pane sizes 1..12 x gaps 0..4 asserting that
expansion never reduces content below what the incoming offset already
allowed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adds `ui { pane_frames { gap N; padding N } }`, both defaulting to 0 so
stock configs are unaffected.
gap - cells each pane gives up on every side, leaving real empty
terminal between neighbouring panes (WM-style outer gaps).
Two adjacent panes therefore show 2*gap between them.
padding - cells between a pane's frame and its content; breathing room
inside the pane without moving the frame.
Design: the pane KEEPS its allocated PaneGeom. Only two things move:
1. the drawn frame, via Viewport::inset(gap), reusing the existing
frame_geom_override mechanism already used for stacked panes;
2. the content, via Offset::expanded_by(gap + padding) inside
set_content_offset.
Because the allocation is untouched, the kasuari resize solver and mouse
hit-testing need no changes at all, and a click landing in a gap still
resolves to the owning pane. Content position, size and cursor are all
derived from content_offset, so widening it in one place keeps every
consumer consistent.
Viewport::inset refuses to shrink a pane below a frame's minimum viable
extent (two borders + one content cell) rather than emitting a degenerate
or zero-sized frame. Negative KDL values clamp to 0 instead of wrapping.
Tests cover: inset identity/shrink/recentre, the containment invariant
(an inset frame never escapes its allocation), small-pane refusal, and
the key safety property that content inset by gap+border+padding lands
strictly inside the frame drawn at inset(gap).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adds `ui { pane_frames { title_alignment "left"|"center"|"right" } }`.
Full-frame panes previously hardcoded the title flush left, while frameless
panes (pane_frame_style "titles") already centered it via
compose_bracketed_title. This brings full frames up to parity and makes the
position configurable.
Implementation notes:
- The value rides on `Style`, which is already threaded to FrameParams, so no
new plumbing through Tab/PaneContentsAndUi was needed.
- It is deliberately NOT round-tripped over the plugin protobuf boundary:
plugins have no use for frame title alignment and adding a proto field
would force regenerating the committed prost assets in assets/prost/.
- Left keeps the original code path untouched, so default configs render
exactly as before.
- The geometry loop clamps the title start against the right-side indicators
and breaks on `>=` rather than `==`, so a mis-clamped segment cannot spin.
Tests: alignment ordering, frame width preservation across widths, and
narrow-pane safety (cols 1..24 must not panic or corrupt width).
|
|
|
Squashed import of upstream zellij at 0e6e4404 ("Support the Kitty Image
Protocol (#5428)"), the merge-base our fork was developed against. Upstream's
3311 commits are deliberately NOT replayed here; only our own fork commits are,
so the monorepo stays small and `git log sl0ppij/` shows sl0p work, not zellij's
entire history.
Upstream provenance is tracked in FORKS.md and can always be re-fetched from
https://github.com/zellij-org/zellij.
|