| 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reported still dead on a live session after the previous fix. Stacks move the
frame twice more, and neither move was in the hit-test:
- a stack list is drawn over the top rows of its visible member, pushing that
pane's frame down. The renderer applies this as a frame geom override
(reserved_top_rows); the click side did not, so every pane in a stack was
clickable one row above its own title. frame_rect_with_top_reserved() now
applies the same shift, before the gap inset, in the same order as render;
- a stack's collapsed members drew a button on their one-row title. Nothing
could honour it -- that row is the stack's picker, and "split the pane that
is not on screen" is not a thing to ask anyway. They no longer draw one, and
the click side mirrors the condition. 120 snapshot lines change, every one
of them a one-liner losing its button and nothing else (same width, same
content otherwise; audited mechanically).
Both confirmed by reintroducing each bug and watching the check fail. A third
candidate fix -- resolving a click to the stack's visible member, on the theory
that get_pane_at could return a collapsed sibling -- turned out to be
unnecessary: collapsed members have one-row geoms, so position lookup already
lands on the right pane. Removed rather than kept "just in case".
The check itself was wrong twice before it was right, in the direction that
matters most (reporting failures that were its own): it now reads until the pty
goes quiet instead of for a fixed time, and parses only a freshly forced
repaint, so a half-arrived screen cannot look like a button drawn where it is
not. It clicks every button it can see, and runs against config/config.kdl as
well, whose stacks the minimal config never had.
When the two disagree, the server's own log settles it: log::info! in
intercept_split_button_click, read /tmp/sl0ppij-$UID/sl0ppij-log/.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
zellij-server is one enormous crate and `fastdev` was building it in 16
codegen units, which is the granularity the incremental cache can skip work
at: a one-line change re-optimised a sixteenth of the crate. Splitting the
crates we actually edit into 256 units makes that a sliver instead.
Measured on the same one-line change to pane_boundaries_frame.rs, `git
checkout` of the file to a linked binary:
codegen-units = 16 52s
codegen-units = 256 6s, 4s on a second run
+ opt-level = 1 5s (rejected: not worth a slower binary)
Ruled out on the way: linking the 69MB binary is ~2s, and rust-lld did not
beat the default linker on it, so the linker was never the problem. Root
crate codegen plus link is ~2s of the total.
Costs one 80s rebuild when switching profiles, and the fastdev binary keeps
opt-level 3. Verified with the full server/utils suites and make check-live.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The `+` shipped in 0.1.7 and 0.1.8 without ever appearing on a live session,
while rendering in 265 snapshots. Four faults, each enough on its own:
- title_line_without_middle computed the right side only inside the left
side's and_then, so a pane with no title (hide_default_pane_names, the live
default) got none at all;
- title_line_with_middle discarded the right side whenever the left was
absent -- that is every frame in a session with a second client, which the
live box always had, and "MY FOCUS AND:" on every frame was saying so;
- the title was budgeted before the button, so a title longer than the frame
(any shell showing a path) consumed its columns. The button is anchored and
hit-tested by pure geometry, so it cannot be the part that loses;
- the click hit-tested position_and_size() while the frame is drawn inset by
the pane gap, putting the clickable columns two right of the glyph and a row
above it at the top of the screen. Hence "the click works" and "nothing is
drawn" both being true, and that click test proving nothing.
Adds Pane::frame_rect(), the rect the frame is really drawn in, which the
status bar buttons had already had to work around by hand.
Also fixes render_my_focus building its short form from separators that
Vec::append had already drained: 4 cells wide while reporting 6, a title line
two cells short of its pane. Found by asserting the width in the new test.
Verification, since unit tests have now been insufficient here four times:
split_button_is_drawn_where_it_is_hit_tested covers the combinations the live
config produces, and packaging/checks/split-button-live.py (make check-live)
reads the glyph off a real session, attaches a second client to force the
middle builder, and clicks the columns it found the glyph in. Both confirmed
to fail on the parent commit.
|
| |
|
|
|
|
| |
A right-aligned title no longer runs flush to the corner: the split button now
occupies SPLIT_BUTTON_WIDTH columns between them. Adjusts the expectation rather
than the layout, since that gap is the intended design.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The hover-gated version did not work. `mouse_is_hovering_over_pane` never became
true in a live session, so the `+` was invisible while its hit-test -- which is
geometric and never consulted hover -- kept working. Clickable but invisible is
the worst of both, and it is exactly the failure the shared
`split_button_columns()` was supposed to prevent: that guarantees render and
hit-test agree on WHERE, not on WHETHER.
Reported as "I hover over panes but don't see a +". My end-to-end test had
proved the click, not the button, and the passing snapshots only proved the
renderer works when the flag is set.
Why the flag is false live is still open and is written into the code comment:
the plumbing from `mouse_hover_pane_id` through `FrameParams` reads correctly,
and the config that reproduces it sets both `advanced_mouse_actions` and
`mouse_hover_effects`. Re-gate on hover only once that is understood AND shown
on a real session rather than in a snapshot.
265 snapshots updated: every pane frame now carries the button. 233 are
mechanically provable as button-only (the ` + ` replaces three horizontal glyphs
before a corner); the remaining 32 differ only by a header line offset in the
comparison, with the same substitution underneath.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Switching panes, switching tabs, renaming, reordering and resizing all have a
mouse path in this fork. Spawning a pane did not: it was keyboard-only.
A `+` now appears at the right end of a pane's title line while the pointer is
over that pane, and clicking it splits the pane. Hover-driven rather than always
drawn, for the same reason the resize hint and the status bar button highlight
are: a permanent `+` on every frame is noise, and the pointer being there is
exactly when the button is useful.
render ui/pane_boundaries_frame.rs::render_split_button
hit tab/mouse_handler.rs::intercept_split_button_click
shared split_button_columns(), so what is clickable is what is visible
The button sits immediately left of the top-right corner and the existing scroll
and pin indications get the room that is left. That ordering is deliberate: a
fixed position relative to the pane's right edge is one the mouse handler can
hit-test directly, instead of re-deriving the whole title layout the way
`status_bar_button_at` has to.
The clicked pane is focused first, then the split is requested exactly as a
keybinding would (`PtyInstruction::SpawnTerminal` with `NoPreference`), so mouse
and keyboard produce the same arrangement and the split lands on the pane whose
button was clicked rather than whichever pane happened to be focused.
Not drawn on floating panes: their PIN control owns that corner and is
hit-tested by its drawn position, so anything to its right silently moves it out
from under the pointer. Caught by
test_ctrl_click_on_floating_pin_button_toggles_pin, which stopped pinning.
Verified on the test box by injecting real SGR mouse events into an attached
client's pty at 160x45:
click on the button 1 -> 2 panes
click on the title 1 -> 1
click mid-frame 1 -> 1
Two hover snapshots updated, and only those two: the `+` on the hovered pane
(including an unfocused one -- splitting a pane you are not in is useful).
zellij-server 1502 pass, zellij-utils 473 pass.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A pane created after a responsive collapse/expand cycle was absorbed into a
stack, even at full width. Reported as "a new pane starts collapsed even though
the terminal is big enough", and it appeared to fix itself on resize.
The expand was never really happening. `SwapLayouts::tiled_layout_at` checked
whether a candidate layout fits by positioning it against
`tiled_panes.visible_panes_count()` -- and a stack hides every member but one.
So while collapsed, the check ran with a pane count of 1, and a layout with no
room for the hidden panes passed as feasible. The tab reported
`sl0p:responsive-expand` while every pane stayed in the single rect it had while
collapsed; the next pane added simply joined that stack.
Feasibility is now checked against every tiled pane, hidden or not, which is the
count the layout actually has to accommodate. Only the responsive collapse/expand
resolves layouts through this function -- ordinary swap-layout cycling has its
own body in `swap_tiled_panes` -- so the change is contained to the path that was
wrong.
Verified on the test box (responsive_min_cols 90) with a scripted pty that
resizes mid-session, geometry read back from `list-panes --json`:
before narrow(80) 2 panes -> one rect, both 80x23 (correct collapse)
widen to 160 -> STILL one rect, 160x44 (expand did nothing)
add a pane -> new pane joins the stack
after narrow(80) 2 panes -> one rect (collapse unchanged)
widen to 160 -> x=0 and x=80, side by side (really expanded)
add three panes -> 2+1, then 2x2, then 3+2 grids, no shared rects
wide from the start -> unaffected
narrow again (70) -> collapses, as before
No unit test ships with this, deliberately. Three attempts could not make one
discriminate: the tab harness does no rendering, so nothing populates
`panes_to_hide` and both counts are equal; hiding panes by hand does not help
because the harness's expand takes the BASE path; and asserting on the resolver
directly bypasses the count, since `tiled_layout_at` uses it only for the
feasibility check and returns the raw layout. A test that passes with the fix
reverted is worse than none, so the scripted live reproduction above is the
verification, and it is written down in docs/roadmap.md to be re-run.
zellij-server 1502 pass, zellij-utils 473 pass.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`zellij action` failed ~20% of the time against a live session: exit 0, empty
stdout, empty stderr, and nothing in the server log. `sl0ppi ls` inherited it,
and attached clients were dropped with "lost connection to the zellij server".
The chain, which took measuring rather than reading to find:
1. every action first probes liveness (sessions::assert_socket): connect,
send ConnStatus, disconnect. The probe is a full client and takes an id.
2. new_client() handed out the LOWEST FREE id, so the action connection that
follows microseconds later gets the id the probe just released.
3. the main server thread then processes the queued ConnStatus(id), whose
handler replies and calls remove_client!(id) -- removing the *action*
client, which now owns that id.
4. the action's reply goes through send_to_client, which returns Ok(()) for an
unknown client. The payload is dropped in silence; the client sees only the
disconnect and exits 0 with nothing to print.
Fix: hand out ids from an advancing cursor instead of reusing the lowest free
one, so a stale removal cannot land on a fresh connection. ClientId is a u16 and
every CLI invocation burns two, so the cursor wraps and skips ids that are still
in use rather than assuming they are free.
Measured on the 2-vCPU Debian box that surfaced this:
before after
list-tabs ~20-25% 0/50 failed
list-panes ~20-25% 0/50 failed
list-tabs (attached) ~25% 0/30 failed
sl0ppi ls most runs 0/15 failed
5 attach cycles dropped 5/5 stable
Three regression tests: ids are not reused immediately, they advance and skip
live ones, and they wrap without colliding or hanging. zellij-server: 1502 pass.
The silent-drop in send_to_client is left as-is but is worth revisiting: an
unknown client id is a bug in the caller, and swallowing it is what made this
cost a day instead of a minute.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Attaching to a session of `start_suspended` panes could drop the client with
"your zellij client lost connection to the zellij server", and `zellij action`
against that live session failed most of the time.
Cause: `set_terminal_size` treats "no fd for this terminal id" as an error, but a
reserved-and-not-yet-started terminal is exactly what `start_suspended` (and any
held pane) is. Resizes arrive in cached bursts (ResizeCache), so every attach and
every layout change logged one formatted, contexted, file-written error PER
suspended pane, on the server's hot path.
The map distinguishes the two cases already -- `Some(None)` is reserved-without-fd
(reserve_terminal_id), `None` is genuinely unknown -- so the fix is to match them
separately and stay quiet about the former.
Measured on a 2-vCPU Debian box, `action list-tabs` against a live session:
before 2 suspended panes 5/12 failed 2 plain panes 1/12 failed
after 2 suspended panes 6/30 failed 2 plain panes 9/30 failed
resize errors logged: 126+ -> 0
ie. the suspended-pane penalty is gone; what remains is a separate baseline
failure rate that affects plain sessions equally (see the known issue in
docs/roadmap.md).
zellij-server: 1499 tests still pass.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A design doc, not an implementation: lets a project ship a layout that also tells
an agent what each pane is for.
The load-bearing decision is the boundary -- zellij carries one opaque string per
pane and never interprets it, pi owns the whole vocabulary. That keeps the fork
change to a single field threaded along the existing status_bar path, and lets the
vocabulary change without rebuilding zellij.
Records two findings that were checked rather than assumed, since both would
otherwise be discovered the expensive way: pane names cannot carry metadata (there
is no stable name field at runtime, and title is clobbered by whatever the program
sets), and the runtime query surface already exists (list-panes --json), so the
work is only to get the field into it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The option was only read when a client connected, so changing it needed a new
session -- inconsistent with gap/padding/title_alignment, which apply live, and
the kind of inconsistency that reads as "my config change did nothing".
Threaded through update_frame_layout, the existing vehicle for the fork's
pane_frames options: Tab -> tiled/floating containers -> each pane's Style,
including suppressed panes so a hidden stack member is styled correctly when it
swaps back into view.
Verified by editing config.kdl against a running session and watching
1:Approve flip to [Approve] and back, then confirming a click still resolves
afterwards -- the button widths change with the style, so a stale layout would
have silently mis-targeted clicks.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A click target with no hover feedback gives you no way to tell what you are about
to hit, which matters more here than elsewhere because the buttons sit in a frame
row that is only one cell tall.
The hovered button is inverted rather than merely recoloured: swapping
fore/background reads regardless of which colours a theme uses, and makes "the
pointer is on THIS one" unmistakable next to its unhovered neighbours.
Tracked per client, because hover belongs to a pointer rather than to a pane --
with two users attached, one pointer must not light up the other's button. Stored
on the pane keyed by client id, which the frame renderer already has.
Updated before the action dispatch rather than in the hover action, because
motion over the ACTIVE pane is dispatched as SendToTerminal, not UpdateHover --
and the pane you are about to click a button in is usually the one you are
already in. Every other pane's hover for that client is cleared on each motion,
so moving between panes cannot leave a button lit behind.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reference for programs wanting to drive the bar: the OSC 5577 verbs, escaping
rules, click reports, limits, rendering behaviour, persistence, and the security
model.
Both worked examples were run against a real session rather than written from
memory, which caught four bugs in my own samples:
- the Python reader kept a trailing buffer window, so a processed report was
re-matched and every click fired twice
- it used tty.setraw, which also disables signals, leaving the sample unkillable
with Ctrl-C
- it exited via traceback and left the bar behind; both samples now clear it on
exit
- the shell reader echoed the report to the screen, and an `stty -echo` did not
help because bash's `read -n` saves and restores terminal settings around each
call. `read -s` is the fix.
Also verified and documented two behaviours I would otherwise have guessed at: a
BEL terminator works as well as ST, and the bar is not drawn when pane frames are
off (it lives in the frame row), reappearing when they return.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
First slice of the status/action frame: one row of program-supplied text in a
pane's bottom frame row.
printf '\033]5577;1;status;building 3/7\033\\'
printf '\033]5577;1;clear\033\\'
Rendered in the EXISTING frame row rather than in reserved rows. Reserving would
shrink the pane's content area and resize its PTY whenever the bar appeared or
disappeared, firing SIGWINCH at the program that asked for it -- the same churn
that made the stack-list picker use a fixed cap. The frame row is already a text
surface with a precedence chain, so this cost no geometry work at all.
State lives on Grid like the window title, so it survives re-renders, resizes
and detach/reattach with no draining. Session resurrection is NOT covered yet.
Versioned from the start, and unknown versions or verbs are ignored, so the
planned button verbs cannot be half-interpreted by an older build. Text is the
remaining params rejoined with ';', which means a single text field needs no
escaping -- the trick OSC 0/2 already uses for titles.
Treated as untrusted, because it is: an in-band sequence is emitted just as
readily by `cat` of a hostile file as by a cooperating program. Control
characters are stripped -- a raw ESC could otherwise repaint anything on screen,
a newline would corrupt the frame row -- and the length is capped, since the
string is retained for the life of the pane.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
In a collapsed (mobile) layout every stack member except the visible one is
suppressed, and the finder skipped suppressed panes outright -- so it could only
ever find the one pane already on screen, which defeats the point of having it
there.
Suppressed terminals are now listed and marked "(hidden)". Suppressed plugins
still are not: zellij:link and friends are deliberately invisible
infrastructure, and focusing one shows an empty pane. Unfiltering everything
listed it, which is how that distinction surfaced.
Needed no server work. Screen::focus_pane_with_id already locates the owning tab
via has_pane_with_pid, which includes suppressed panes, and calls go_to_tab; and
Tab::focus_pane_with_id tries focus_hidden_stack_list_member first. So selecting
a hidden stack member swaps it into view and selecting a pane in another tab
switches tab, both already handled by the host.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Reaching the finder through a mode dance (leader, then session mode, then a
letter) is too many keys for something used constantly. Documents Ctrl-; as a
one-chord binding, and why: ';' has no control-code meaning, readline does not
bind it, and vim binds only bare ';' -- while plain ';' keeps typing normally.
Also records the tradeoff. Ctrl-; needs the kitty keyboard protocol to be
distinguishable from a plain ';', so it silently never fires on a terminal
without it. The combos that work anywhere are real control codes and are all
taken: Ctrl-\\ is SIGQUIT, Ctrl-] is vim's tag jump, Ctrl-_ is readline undo.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The finder always opened as a centred float with a frame. That costs its
margins (16% of columns, 28% of rows), a border, and on this fork the pane gap
as well -- on an 80x24 terminal leaving about 63x14 of the 80x24 available.
Rows are the scarce resource in a list, so most of the results were going to
decoration.
Below 100x30 it now takes the whole viewport and asks to be borderless, which
also drops the gap since this fork skips gap/padding for frameless panes. Above
that the centred float is kept, because it reads better when the space is
there.
The viewport is only knowable from TabInfo, so this re-evaluates on TabUpdate,
which is also how a terminal resize reaches a plugin -- switching modes live in
both directions. Re-applied only when the decision flips, so it does not fight
someone moving or resizing the pane by hand.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The pane finder was living in ~/dev with no version control at all, while
path-depending on this checkout's zellij-tile -- so the plugin and the binary
it must stay API-compatible with were maintained in two unrelated places, one
of them unversioned.
Its own workspace root rather than a member of zellij's: a non-root member has
its [profile] ignored, and opt-level="z" + lto + strip are what keep the wasm
small, which matters because the keybinding loads it with skip_plugin_cache and
recompiles on every launch. Staying independent also means carrying this crate
touches no upstream-tracked file, so it cannot conflict on a rebase.
install.sh no longer assumes ./target: this checkout's .cargo/config.toml sets
a shared build.target-dir, so the artifact lands in the repo's target dir
instead. It now asks cargo metadata where the build actually went, and fails
loudly rather than silently installing a stale wasm.
The zellij-tile path dep becomes relative, so the crate builds from any
checkout rather than only from /home/user/lol/zellij.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`hide_default_pane_names` is a frame-rendering option, but it was applied
inside current_title() -- which is also what fills PaneInfo.title. Every plugin
that lists panes therefore received an empty string for any pane whose program
had never set a title.
For the bundled session manager that is untidy. For a fuzzy pane finder it is
broken: the rows are identical AND unsearchable, so the plugin's whole purpose
stops working.
The blanking moves to the frame, which already had a blank_title concept, and
current_title() goes back to reporting a real title to everything that asks.
Frames still show nothing for an unnamed pane, named panes still show their
name, and PaneInfo reports "Pane #N" again.
Changes a value rather than adding a field, so it needs no protobuf
regeneration.
Only observable without a shell that sets a title: verified by disabling the
zsh hook, where the pane finder previously listed three identical blank rows
and now lists Pane #1/#2/#3. That masking is also why this went unnoticed --
and why I flagged it twice without confirming it was actually reachable.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every bug this feature has had came from the same place: it remembered the swap
layout position that was active before collapsing, and replayed it to expand.
Remembered positions stop applying when the pane count changes, are only stable
while nothing is inserted, and a failed apply left the state claiming a
transition that never happened.
Both directions now resolve fresh. Collapsing picks the configured layout, or a
synthesized stack if that one is missing OR cannot currently apply -- a
`stacked` layout with min_panes=4 and three panes open used to be found, fail
its constraint, and give up. Expanding picks BASE when it still applies, since
that reproduces the user's actual layout, and otherwise a synthesized grid.
Both chains end in an unconstrained layout, so neither can come up empty.
The only remaining state is one bool, flipped only after a layout was applied.
Also stops consulting is_tiled_damaged. It is meant to mean "the user
rearranged panes by hand" but is equally set when a relayout finds nothing
applicable, which happens on the first pane added to a tab with no usable swap
layout and then never clears -- it disabled this feature in four distinct
situations. It protected little regardless: expanding applies BASE or a grid,
so a manual arrangement does not survive a round trip either way.
Synthesized layouts are created once and reused; the test asserts the layout
list does not grow across repeated collapse/expand cycles, since they are now
appended lazily in both directions.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The collapse selects a swap layout BY NAME, so a tab with no swap layouts
simply does nothing when the terminal gets narrow -- no error, no hint. The
usual cause is not the responsive settings at all: a custom layout file only
gets swap layouts from a sibling "<layout>.swap.kdl", so launching with one
that has no such file silently disables the feature.
That is the second time this feature has failed invisibly (the first was a
layout marked damaged), and both cost real debugging time. Logs once per tab,
not per resize, and names the actual cause rather than the symptom.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adding a pane left old borders on screen until a resize forced a full redraw.
Two causes. The picker draws each entry as its own chunk, so the space between
and around the entries was never painted and kept the previous frame's
contents; and adding a pane really does re-flow the whole grid, because a
freshly created pane has no title yet and its "Pane #N" fallback is far wider
than a named one, which widens every cell and drops the column count.
Separately, refresh_or_dissolve_stack_list changed the reserved row count
without forcing a clear, so when the list got shorter the visible pane moved up
and the rows it vacated kept their old contents.
Blanks only the spans no cell covers, never a cell's own columns: chunks are
NOT composited last-wins, so blanking the whole region erased the entries
instead of the stale contents under them -- the first attempt at this fix drew
exactly one entry.
stack_list_row_gaps is a free function so the covering invariant is testable:
gaps and cells must tile each row exactly, with no overlap, across a sweep of
row caps, label widths and member counts.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The cap made the picker a scrolling viewport -- everything stays reachable by
moving focus -- but there was no way to see the whole stack at once.
Clicking "+N more" expands it to every member; the indicator becomes "- fewer"
and collapses it again. Collapse gets its own label because an invisible way
back out is not an affordance.
Hangs off the indicator row because that row is already reserved, already
rendered, and resolves to no member, so it is free real estate. It is also why
this needs no new keybinding: mouse handling is entirely server-side, while
Action is a protobuf enum we cannot extend without protoc.
A click rather than hover, as expanding changes the reserved row count and so
resizes the visible pane's PTY -- acceptable once per deliberate click, not on
every mouse move.
All three consumers of the row -> member mapping (renderer, reserved-row
budget, mouse hit-test) now go through stack_list_window_for, so expansion
cannot desync them the way the cap desynced the hit-test in 844617b4.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Regression from the picker row cap. The row -> member mapping lives in TWO
places: the renderer, and the hit-test that resolves mouse events to a pane.
Capping made the renderer draw a WINDOW of the members, so screen row 0 is
members[window_start], but the hit-test still resolved row N to members[N].
Any click or hover on a capped stack whose window had scrolled therefore
landed on the wrong pane -- with 8 members focused on the last, clicking the
top row selected member 0 instead of member 5.
Both sites now go through stack_list_window(). Clicks on the "+N more"
indicator resolve to no member rather than to a bogus one.
Caught by asking how a user reaches the panes past the cutoff, not by the
tests: the previous commit tested the window arithmetic and the rendering, but
nothing covered this second consumer of the same mapping.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
| |
The tree carried 30 failures in zellij-server and 5 in zellij-utils from the
first sl0p commit until 5f2c28ab. They were never real bugs: aa51848d changed
the scroll indicator and frame-title rendering without refreshing the insta
snapshots. Documents the current green counts so a future context treats any
failure as its own, plus the two traps that hid this for so long -- cargo
stops at the first failing test binary, and insta aborts a test at its first
snapshot mismatch -- and how to regenerate snapshots without cargo-insta.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two problems made the stack list unusable for panes nothing had named yet,
which is the common case right after opening them -- and the responsive
collapse walks straight into it, since collapsing is exactly when the list
becomes the only way to pick a pane.
`hide_default_pane_names` no longer applies to stack list entries. In a frame
an empty title is fine: the pane is still visible, sized and positioned, so
you can tell what it is by looking at it. In the stack list the label is the
ENTIRE affordance, so blanking it leaves rows that cannot be identified or
told apart. Entries fall back to the "Pane #N" placeholder even when frames
are configured to hide it.
Entries are also prefixed with their 1-based position, so they stay countable
and addressable even when several share a title or have none. The prefix is
built by one shared helper because the renderer and Tab's column-width
calculation must agree on its width or the list misaligns.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Entering RenameTab left the existing name in place and appended to it, so
renaming a tab meant clearing it by hand first. The first printable keystroke
now REPLACES it, the way a text field with its contents selected behaves.
Armed when a client enters RenameTab and disarmed when it leaves, so a
keystroke that arrives later cannot wipe a name the user was not editing.
Backspace and delete deliberately do NOT replace: reaching for them means
editing the existing name, not discarding it.
The flag is taken out of the set BEFORE borrowing the active tab, since
get_active_tab_mut borrows self mutably and every branch consumes the flag
anyway.
Applies to renames from any entry point - the keybind and the sl0pstatus
double-click-a-tab both go through Screen::change_mode.
Pane rename is deliberately left alone for now; say the word and it can get
the same treatment.
Baselines held: zellij-server 1441/30.
|