| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Dragging a tiled pane by its title bar and dropping it on another pane swaps
the two. Previously the only way to rearrange panes was MOVE mode.
The underlying operation already existed: the directional move_pane_* functions
are all the same geometry swap, differing only in how they pick the other pane.
Extracted as TiledPanes::swap_panes(a, b) so a pane can be swapped with an
arbitrary target - the one a drag was dropped onto - which also de-duplicates
what was four copies of the same swap.
Gesture: the title row EXCLUDING its corners. Corners stay resize handles, so
two-axis resize from the top is preserved. Plain top-edge resize is gone and
costs nothing: every boundary is shared, so the boundary above a pane is the
bottom edge of the pane above and can be dragged from there. A pane at the top
of the viewport has the screen edge above it, which was never resizable.
Ordering in determine_mouse_action matters three ways:
- an in-progress drag is checked FIRST, so motion cannot be reinterpreted as
text selection or a resize;
- the drag check sits AFTER the double-click check, so the second click of a
rename is not swallowed;
- and BEFORE the resize branch, which would otherwise claim the title row.
Two behaviours had to be preserved deliberately:
- Dropping a pane on itself is a no-op, which is what keeps a plain click and
a double-click working on the title bar.
- Starting a drag focuses the pane. Pressing a title used to start a resize,
which focused it as a side effect; without this a plain click on the title
would do nothing at all.
Stacked panes are excluded from the handle. Their title row is the affordance
for focusing/expanding a stack member, and intercepting it broke
focus_stacked_pane_under_flexible_pane_with_the_mouse - caught by the suite,
not by hand.
Baselines held: zellij-server 1440/30 (the 30 are pre-existing).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reported: grayscale worked for most panes but not one running neovim.
Nothing to do with neovim specifically - it was the only pane sitting IDLE.
refresh_for_focus_change called render_full_viewport(), which marks the grid's
output BUFFER so the whole viewport is emitted rather than a diff. But
Pane::render() early-returns unless grid.should_render is set, and
render_full_viewport() does not set it. So an idle pane never re-rendered at
all and kept its previous colours indefinitely.
Shell panes hid the bug: they set should_render constantly on their own
(prompt redraws, cursor), so they picked up the change almost immediately and
looked correct. A pane parked in neovim with no output produced nothing, so it
never repainted.
Both calls are now made and the reason each is needed is documented at the
call site, because 'mark the buffer' and 'schedule a render' being two
separate flags is exactly the kind of thing that reads as redundant and gets
'cleaned up' later.
Baselines held: zellij-server 1436/30.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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-clicking the top edge of a pane's frame focuses that pane and switches
the client into RenamePane mode.
Everything needed already existed; this is wiring, not new machinery:
- grid.rs already tracks repeated clicks (Click, 400ms threshold). It is now
pub and reused rather than inventing a second notion of 'double click'
that could drift from the one used for text selection. A frame click never
reaches the grid, so Tab tracks frame clicks per client separately.
- pane.position_is_on_frame() / get_edge_at_position() already give the hit
edge, and mouse_handler already computes both.
- Screen::change_mode() already calls store_pane_name() when entering
RenamePane, so issuing ChangeMode(RenamePane) does the whole setup exactly
as the keybind path does. No rename logic is duplicated.
Placement in determine_mouse_action matters twice:
- AFTER the floating 'start moving' branch, so floating panes keep dragging
by their title (that is their move handle). Tiled panes only for now.
- BEFORE the resize branch, which would otherwise consume the click as a
top-edge resize drag.
Only PaneEdge::Top renames; corners stay resize handles. The pane is focused
first because RenamePane acts on the ACTIVE pane, so renaming an unfocused
pane would otherwise rename the wrong one.
Tests cover all three: double-click on Top renames, single click on Top still
resizes, and double-click on any other edge or corner does not rename.
Baselines held: zellij-server 1424/30 (3 new tests), zellij-utils 435/5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Closes the frameless-mode hole documented in the README.
In frameless mode (pane_frame_style "titles" or "none") the cells around a
pane's content are owned by the inter-pane boundary renderer, not the pane.
The ghost-fix commit correctly stopped panes from BLANKING that region -
blanking it erased the boundaries and broke three tests - but nothing stopped
them from CREATING it: padding still inset the content, so the vacated cells
had no owner and kept whatever was drawn there before.
Half the job done in each direction produced exactly the ghosting the blanking
was meant to prevent, just in a mode we do not use by default.
Fix: a pane that is not drawing a frame is not inset at all - no gap, no
padding, no moat, nothing unowned. Pane::set_frame() already received the
right boolean from both the tiled and floating containers and was throwing it
away; it now records it.
Config values are preserved, they simply take effect only with full frames.
README updated to describe this as a defined limit rather than a known bug.
Baselines held: zellij-server 1421/30, zellij-utils 435/5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Anyone cloning this repo got stock Zellij's README and no indication the
binary behaves differently, with the new options discoverable only from
commit messages.
Documents the four config options, which changes apply live vs need a new
session, the fastdev build profile, and the known rough edges.
Two corrections while writing it:
- The frameless-mode caveat is stated honestly. `padding` DOES still inset
content when pane frames are off, but the vacated cells are not repainted
because ui/boundaries.rs owns that region, so stale glyphs are expected on
resize. That is a real limitation, not the deliberate design the first
draft claimed. gap/padding are documented as full-frame only for now.
- Deduplicated the borderless-pane comment in both panes, which scripted
patching had left repeated three times.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The previous ghosting fix only covered ONE of the two regions the insets
carve out. With `padding N` there is a second unowned region: the cells
between the frame border and the content rect. Nothing wrote them, so when
a pane grew, cells that had previously held another pane's border or title
kept those glyphs - the reported 'old pane title/border repeated in the gap
space'.
Both rings are now blanked at every frame-render site:
1. allocation \ drawn frame (outer gap)
2. frame inner \ content rect (inner padding)
Frameless mode is explicitly excluded from ring 2. With pane frames off the
cells around the content belong to ui/boundaries.rs, which draws the
inter-pane boundaries there, and content_offset deliberately leaves room for
them. Blanking that region erased the boundaries and broke three tests
(render_stacks_without_pane_frames,
swap_tiled_layout_with_stacked_children_and_no_pane_frames,
send_cli_toggle_pane_frames_action). Collapsing the interior onto the
content rect emits no ring at all in that mode.
gap_ring_chunks is now robust to any rect relationship: spans are clamped to
`outer`, so a mis-clamped caller cannot make it write outside the pane's own
allocation. Tested with inner rects that cover, miss, straddle and degenerate
against outer.
Suite back to baseline: 1421 passed / 30 pre-existing failures.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
With a gap configured, PaneFrame::render() emits CharacterChunks only for
the INSET frame. The gap ring - the cells between a pane's allocated
geometry and the smaller frame drawn inside it - was written by nobody.
On a fresh paint that looks correct, but only because the screen had just
been cleared. As soon as panes are moved or resized, whatever was
previously drawn in those cells survives, which is the ghosting/artifacts
seen while dragging pane borders.
gap_ring_chunks() emits explicit blanks for that ring, restoring the
renderer's existing invariant that every cell of a pane's allocation is
written by exactly one pane on every render. It is appended at all four
frame-render sites (tiled + plugin panes, cached and uncached paths), and
is a no-op when gap is 0 so the default path is untouched.
Tests assert the ring never overlaps itself, never escapes the allocation,
never overdraws the frame, has exactly (outer area - inner area) cells, and
that ring + frame together tile the allocation with no holes - a hole being
exactly what produces the artifacts.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The three sl0p frame options were read once at session start: zellij's
ScreenInstruction::Reconfigure carries an explicit, hand-maintained field
list (pane_frame_style, rounded_corners, hide_session_name, ...) and ours
were not in it, so editing config.kdl did nothing until a new session.
Wires them through the existing reconfigure path:
lib.rs -> ScreenInstruction::Reconfigure -> Screen::reconfigure
-> Tab::update_frame_layout -> {tiled,floating}_panes fanout
-> Pane::update_frame_layout
Two ordering/coverage details that are easy to get wrong:
- Tab::update_frame_layout must run BEFORE set_pane_frames. gap/padding are
baked into content_offset when it is set, not read live at render time, so
the values must be in place before the sweep that recomputes offsets.
- FloatingPanes::set_pane_frame_style only re-runs that sweep when a floating
pane is fullscreen, so ordinary floating panes would have kept their old
inset until the next resize. The floating fanout now recomputes explicitly.
Title alignment needs no recompute (it is read at render time) but rides the
same path for consistency; panes clear their frame cache so it repaints.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* initial implementation - kinda working
* tests and fixes
* fix bounds
* tests and compatibility fixes
* fix: cursor scroll, clears, deletes, key tolerance, and host id range
* fix: keep images alive across resize, relayout and history scroll
* anchor to lines, not pixels
* fix image/text alignments
* config to allow disabling
* fix pinned pane issue
* never let host graphics replies reach the keyboard parser
* fix nested session
* properly handle fullscreening a pane
* rustfmt
* cleanup of ignored test
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
* chore(deps): upgrade base64 0.22, colored 3.1, crossterm 0.29, dialoguer 0.11, directories 6.0, highway 1.2, prost 0.12.6, strip-ansi-escapes 0.2.1, suggest 0.5.1
* chore(deps): upgrade miette 7.6, nix 0.30, notify 8.2, notify-debouncer-full 0.5
* vte 0.15
* clap 4.6
* workspaceify plugin deps
* don't send empty fs watch events
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* fix: theme query and updated pixel dimensions
* add no-ui-fullscreen key/action/capability
* protocol core
* integration test harness
* fix integration tests
* fix flaky tests
* nested ui (dimming) and directional focus initial implementation
* integration tests for nested ui dimming and directional focus
* initial full screen nesteed implementation
* fix: keep hidden floating panes hidden across nested no-ui fullscreen
* fix integration tests
* no ui fullscreen integration tests
* hardening behavior/tests
* fix(nested): re-derive descend/dim on host tab switch and new-tab so clicking dimmed chrome ascends
* initial modal implementation
* initial ui improvements
* fix integration tests
* improve nested selection ui
* nested session modal refactoring and tests
* improve integration test speed somewhat
* some ui adjustments
* simplify full/auto nested session handling
* hints in compact-bar
* guest stdin leak fix
* allow floating panes to be fullscreen
* make nested session handling configurable
* protocol reannounce
* refactor: naming confusion
* improve ux somewhat
* rustfmt
* merge fixes
* improve integration test rate limiting (fix ci)
* add pr
|
| |
|
|
|
| |
* chore: replace unmaintained cassowary with maintained fork kasuari
* add pr
|
| |
|
|
|
| |
* fix(grid): properly pad rows in CUF
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* fix(grid): scroll the region when a line wraps at its bottom margin
Wrapping at the bottom margin of a DECSTBM scroll region used to move
the cursor below the region instead of scrolling it, after which no
subsequent output could scroll through the region (or reach scrollback
for top-anchored regions) anymore.
line_wrap() now mirrors add_canonical_line(): the region scrolls up one
line, the cursor stays on its bottom row, and the new row is a wrapped
(non-canonical) row. Top-anchored partial regions preserve the
scrolled-off line in scrollback (#4941), other regions discard it, and
the alternate screen never transfers.
* docs(changelog): line wrap scroll region fix
* fix(grid): keep hyperlink tracking aligned when wrap scrolls an inner region
When a line wraps at the bottom of a scroll region that does not start
at the top of the screen, the region's rows shift up but the hyperlink
tracker's recorded positions did not, so a URL wrapping there lost its
clickable anchors. Offset the tracked positions for the rows inside the
region, the way the full-screen and top-anchored paths already do via
offset_cursor_lines.
* refactor shared logic and adjust changelog
---------
Co-authored-by: Aram Drevekenin <aram@poor.dev>
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
| |
* feat: allow opening panes and tabs from the cli without focusing them
* rustfmt
* fix flake
* rustfmt
* add pr
|
| |
|
|
|
|
|
| |
* fix: correctness issues with focus-last-pane
* rustfmt
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* initial implementation
* improve ui
* fix: stacked resize interaction
* title list boundaries fix
* fix BEL
* mouse parity
* float/embed + bulk-op features
* fix in-place bug
* improve ui
* config live reload
* improve ui
* fix issues, add exit status
* fix for serialization
* fix tests
* integration tests
* comments
* refactor: duplicate title logic
* refactor: consolidate logic
* fix: flaky tests
* add a stacked swap layout
* rustfmt
* fix e2e
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* initial implementation
* improve single line title design
* hint text plugin infra
* add proto regen task
* display alt text in status bar
* don't display text with full frames
* style alt hover text
* fix + style control own pane hover
* make multiplayer indications responsive
* update snapshots
* plugin api
* single pane as tab
* better scroll styling
* fix tests
* tab-bar plus hover
* plugin surface tests
* integration tests
* adjust all integration tests
* reduce hover noise
* add pane id to in place
* more integration tests
* style: rustfmt
* update plugin assets
* update assets and snapshots
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* feat(panes): focus last pane (#4142)
* add tests
* add client id parameter to `add_floating_pane`
* add action for floating panes
* add tests for floating panes
* add keybinds to configs
* rebase fixes
* simplify focus logic when adding stacked pane
* formatting fix
* formatting fix 2
* fix: make sure expanding pane always focuses it for all clients in stack
* add pr
* fix rebase
---------
Co-authored-by: Aram Drevekenin <aram@poor.dev>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* phase 2+3
* initial mobile plugin (phases 4 + 5 + 6)
* some bugfixes
* implement stage 7
* mobile interface improvements
* improve mobile pane/tab picker
* more ui improvements and fixes
* various mobile fixes
* some mobile fixes
* mobile keyboard triggering fix
* some cleanups
* initial plugin keyboard implementation
* improve keyboard hit zones
* improve keyboard
* pinch resize
* pinching fixes
* implement scrolling + panning
* initial keyboard resizing
* fix: mobile first render issues
* keyboard responsiveness
* fix some issues with the keyboard
* implement fit
* fix mobile scrolling
* add tilde cell to compact
* improve mobile keyboard
* fix starting mobile terminal font to natural keyboard size
* fix mobile resize blank screen flash
* shadow focus for mobile
* more keyboard fixes
* improve mobile status bar
* fix: render plugins to mobile clients
* allow opening new panes/tabs from the mobile plugin
* allow configuring
* fix dot slash
* improve mobile native soft keyboard interaction
* switch to native keyboard - initial implementation
* dirty hacks, but phone keyboard works now
* adjust keyboard help strip
* fix cursor in mobile
* fix keyboard and help line
* rename keyboard to modifier bar
* allow starting new session
* make clicking session name bring us to new session menu
* allow switching to desktop from the mobile ui
* mobile welcome screen initial implementation
* mobile welcome screen fixes
* various welcome screen fixes
* more mobile fixes
* moar mobile fixes
* mobile ui redesign
* fix: swallowed backspace bug
* switch session screen improvement
* touch up some interfaces
* refactor: move mobile state to own struct
* refactor: move fit resizing to the server
* remove dead flash code
* some cleanups
* refactor: clear fit on close
* refactor: improve fit api
* refactor: pane sync functions
* refactor: mouse
* refactor: break into modules
* some cleanups for the sessions screen
* fix scrolling and other stuff
* refactor: sessions screen
* refactor: panes screen
* refactor: new session screen
* refactor: menu screen
* refactor: render
* cleanups
* refactor: plugin structure
* remove unused welcome screen path
* Rename api
* refactor input.js
* cleanup useless comments
* fit/mobile refactoring
* take out the garbage
* remove more comments
* remove mobile flash on startup/attach
* fix: various mobile startup issues
* fix space bugs
* cleanup
* rustfmt
* fix windows build
* adjust modifier bar ui
* add pr
|
| |
|
|
|
|
|
| |
* fix: properly handle resize while pane is in fullscreen
* fix: re-update fullscreen after closing editor pane
* rustfmt
|
| |
|
|
|
|
|
| |
* fix: block pty byte processing while waiting for host terminal reply
* rustfmt
* add pr
|
| |
|
|
|
| |
* implement DECQRM for CSI2031
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
stale color queries (#5105)
* initial phase 1 implementation
* various fies
* some fixes
* fix fragmented stdin cases
* some fixes
* renaming
* guard against old clients
* implement csi 2031
* allow toggling theme with actions
* rustfmt
* fix: finalize trailing characters in ansi parser as well
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* initial implementation
* fix mouse cpu spam
* prevent excess metadata write and prune sessions on startup
* consolidate CommandChanged and PaneCommandChanged
* tests
* rustfmt
* add pr
|
| |
|
|
|
|
|
| |
* bump patch version
* fix: preserve row background when scrolling
* add pr
|
| |
|
|
|
|
|
| |
As Zellij always handles OSC 52 from inner programs and then forwards
it appropriately (`copy_command`, `copy_clipboard`), I think it makes
sense to unconditionally report OSC 52 support.
Co-authored-by: Michael Hoang <enzime@users.noreply.github.com>
|
| |
|
| |
Co-authored-by: kazimazi <attila.koszo.official@gmail.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* fix: position host cursor for CJK IME even when app hides cursor
When apps like Claude Code (ink/React library) hide the cursor during
rendering, Zellij's render_cursor() was not sending a CUP sequence to
the host terminal. This caused IME composition candidates to appear at
the wrong position, breaking CJK input in these apps.
Change cursor_coordinates() to return the cursor position unconditionally
(as long as it is within bounds) along with a bool indicating visibility.
render_cursor() now positions the host cursor even when the app has hidden
it, so the IME subsystem can read the correct cursor position.
* add test
* rustfmt
---------
Co-authored-by: Aram Drevekenin <aram@poor.dev>
|
| |
|
|
|
|
|
| |
* fix bce and other issues
* rustfmt
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* Preserve scrollback for partial scroll regions starting at top
When a partial scroll region starts at row 0 (the top of the viewport),
lines scrolled off the top were silently discarded via viewport.remove(0)
instead of being transferred to lines_above (the scrollback buffer).
This affected both add_canonical_line and rotate_scroll_region_down.
Applications like Codex CLI use partial scroll regions to push rendered
content into terminal scrollback (via DECSTBM + newlines). In tmux,
these lines are preserved in the scrollback buffer and accessible via
copy-mode scrolling. In Zellij, they were lost, causing mouse wheel
scrolling to show only stale pre-application content.
Transfer lines to lines_above when scroll_region_top == 0 and alternate
screen is not active, matching the behavior of tmux.
* add selection handling to CSI S, add matching handling for CSI M, add tests
* add pr
---------
Co-authored-by: Aram Drevekenin <aram@poor.dev>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* initial implementation
* tests
* fix: always report to zellij, only report to app if it wants a report
* rustfmt
* fix: always report capabilities to panes requesting them
* rustfmt
* add pr
|
| |
|
|
|
|
|
|
|
| |
* release notes
* fix some link issues
* rustfmt
* fix click wording
|