aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-utils/src/input (follow)
Commit message (Collapse)AuthorAge
* sl0p: --purpose flag and tab-level purposeuser2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: per-pane workspace hints (purpose=)user2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: add an indexed style for status bar buttonsuser2026-08-12
| | | | | | | | | | | | | | | | | | | | | `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.
* sl0p: clickable action buttons in the pane status baruser2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: serialise the pane status bar for resurrectionuser2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: separate the hover resize hint from the hover highlightuser2026-08-12
| | | | | | | | | | | | | | | | | | 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.
* sl0p: add close_if_focused to LaunchOrFocusPluginuser2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: keep plugin panes pinned when synthesizing a layoutuser2026-08-11
| | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: add a minimal style for the stacked-pane pickeruser2026-08-11
| | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: keep expanding possible after the pane count changesuser2026-08-11
| | | | | | | | | | | | | | | | | 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".
* sl0p: synthesize a stack layout when none exists to collapse intouser2026-08-11
| | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: optionally render the stacked-pane picker as a griduser2026-08-11
| | | | | | | | | | | | | | | | | | | | | | | `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.
* sl0p: cap how many rows the stacked-pane picker takesuser2026-08-11
| | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: add `arrangement` to build a tree for N panesuser2026-08-11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: collapse to a swap layout when the terminal gets narrowuser2026-08-11
| | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: optionally hide the 'Pane #N' placeholder titleuser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | `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.
* sl0p: grayscale the contents of unfocused panesuser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `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.
* sl0p: CSS-style 1/2/4-value shorthand for inner paddinguser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | `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.
* sl0p: uniform pane gap + aspect-corrected horizontal gapuser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: configurable pane gap (outer) and padding (inner)user2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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).
* sl0p: configurable pane title alignmentuser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | Adds `ui { pane_frames { title_alignment "left"|"center"|"right" } }`. Full-frame panes previously hardcoded the title flush left, while frameless panes (pane_frame_style "titles") already centered it via compose_bracketed_title. This brings full frames up to parity and makes the position configurable. Implementation notes: - The value rides on `Style`, which is already threaded to FrameParams, so no new plumbing through Tab/PaneContentsAndUi was needed. - It is deliberately NOT round-tripped over the plugin protobuf boundary: plugins have no use for frame title alignment and adding a proto field would force regenerating the committed prost assets in assets/prost/. - Left keeps the original code path untouched, so default configs render exactly as before. - The geometry loop clamps the title start against the right-side indicators and breaks on `>=` rather than `==`, so a mis-clamped segment cannot spin. Tests: alignment ordering, frame width preservation across widths, and narrow-pane safety (cols 1..24 must not panic or corrupt width).
* Support the Kitty Image Protocol (#5428)Aram Drevekenin2026-07-31
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * 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
* Dep upgrades for debian (#5420)Aram Drevekenin2026-07-28
| | | | | | | | | | | | | * 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
* Nested session handling (#5417)Aram Drevekenin2026-07-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * 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
* feat(config): add mouse_scroll_resize option (#5283)WeZZard2026-07-17
|
* Allow creating panes/tabs from the CLI without changing focus to them (#5346)Aram Drevekenin2026-07-09
| | | | | | | | | | | * feat: allow opening panes and tabs from the cli without focusing them * rustfmt * fix flake * rustfmt * add pr
* Stacked lists (#5331)Aram Drevekenin2026-07-04
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * 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
* Title frames (#5318)Aram Drevekenin2026-06-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * 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: reload config on theme change (#5135)Hannah Fluch2026-06-26
| | | | | | | | | | | * feat: reload config on theme change * fix: small TOCTOU window and some logging * add pr --------- Co-authored-by: Aram Drevekenin <aram@poor.dev>
* feat(panes): `FocusLastPane` action (#4241)DawidPietrykowski2026-06-25
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * 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>
* Mobile layout (#5241)Aram Drevekenin2026-06-10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * 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
* Support CSI2031 (dark/light theme switching) + remove STDIN cache to avoid ↵Aram Drevekenin2026-04-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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
* fix(windows): skip modifier re-encoding for Char keys in cast_crossterm_key ↵divens2026-04-15
| | | | | | | | (#5014) PR #4967 added VT modifier encoding for non-Char keys but the guard condition also matched Char keys, re-encoding Ctrl+C from [0x03] to CSI u sequence \x1b[3;5u which shells don't understand. Skip the encoding block for Char keys since they already handle modifiers.
* fix: respect --name flag when creating new panes (#4979)divens2026-04-06
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * fix: respect --name flag when creating and renaming panes Three issues fixed: 1. Pane creation: --name was silently discarded for default shell panes because use_terminal_title was true. Now user-provided names take priority. Also pass initial_pane_title as pane_name to TerminalPane constructor (was hardcoded to empty string). 2. CLI rename without --pane-id: was routed through PaneNameInput which appends to existing name. Now uses a dedicated RenameActivePane action that fully replaces the name, working for any name length including single characters. 3. Interactive rename: entering rename mode preserves the existing name for editing (append/backspace). Esc reverts to the name before rename mode was entered. Adds 28 tests covering rename semantics, interactive editing, undo, OSC title interaction, and a wire-level IPC round-trip test. * refactor: route active-pane rename via Option<PaneId> Replace the dedicated Action::RenameActivePane variant with an Option<PaneId> on the existing Action::RenamePaneByPaneId. route.rs dispatches None to the server-internal ScreenInstruction::RenameActivePane and Some(id) to the existing RenamePaneWithPaneId path. Drops all proto/conversion boilerplate for the removed variant: plugin API additions, IPC tag 137, and both directions of the wire conversion. --------- Co-authored-by: divens <divens.dev@gmail.com>
* fix(windows): encode modifiers in raw bytes for non-Char keys (#4967)divens2026-04-03
| | | | | | | | | | | | | | | | On the native console input path, crossterm captures modifier flags from INPUT_RECORD but the raw bytes written to the PTY don't include them. This causes inner applications to never see ALT, SHIFT, or CTRL modifiers for keys like Enter, Tab, Backspace, arrows, and function keys. Encode modifiers using standard VT conventions: - Simple keys (Enter, Tab, Backspace): ALT uses ESC prefix, other modifiers use CSI u encoding - CSI letter-final keys (arrows, Home, End): xterm modifier parameter - CSI tilde keys (PageUp, Delete, F5+): modifier before tilde - SS3 keys (F1-F4): convert to CSI with modifier Co-authored-by: divens <divens.dev@gmail.com>
* Proper error when providing layout name with a tab (#4958)Aram Drevekenin2026-03-30
| | | | | * proper error when providing layout name with a tab * rustfmt
* Layout string flag (#4956)Aram Drevekenin2026-03-30
| | | | | | | | | * allow sending a stringified layout with --layout-string * add --tab-id to all the things * fix windows tests * add pr
* CLI focus-pane-id and are-floating-panes-visible actions (#4926)Aram Drevekenin2026-03-27
| | | | | | | | | * add zellij action focus-pane-id * zellij action are-floating-panes-visible * fix e2e tests * add pr
* fix: user themes from `~/.config/zellij/themes` not loaded on session start ↵Luca Pattocchio2026-03-26
| | | | | | | | | | | (#4892) * Fix user themes in config folder not loaded on session start * preserve layout config dir --------- Co-authored-by: Aram Drevekenin <aram@poor.dev>
* Fix Windows config/data paths and consolidate path helpers (#4852)divens2026-03-19
| | | | | | | | | | | | | | | | | | | | | | | | * Fix Windows config/data paths and consolidate path helpers Move platform-specific path logic into home_unix.rs / home_windows.rs behind a shared interface. Consolidate duplicated path helpers from setup.rs into home.rs. Use Display formatting for paths in setup info output to avoid escaped backslashes on Windows. - Remove organization from ProjectDirs to get clean Zellij\ paths - Add Windows-specific ProgramData paths for system config/data - Remove duplicated find_default_config_dir, get_default_data_dir, xdg_config_dir, xdg_data_dir, home_config_dir, get_layout_dir, get_theme_dir from setup.rs (now in home.rs) * Restore quoted paths in setup --check and org qualifier on non-Windows Restore quotation marks around paths in setup --check output, and keep the "org", "Zellij Contributors" qualifier in ProjectDirs on non-Windows platforms to preserve existing config directory locations. --------- Co-authored-by: divens <divens.dev@gmail.com>
* Minor fixes and updates (#4861)Aram Drevekenin2026-03-17
| | | | | * add all blocking flags to action new-pane * security updates
* Warning and duplication cleanups (#4857)Aram Drevekenin2026-03-16
| | | | | * cleanup compilation warnings * consolidate EditScrollbackRaw into EditScrollback
* Ansi dump support (#4848)Aram Drevekenin2026-03-14
| | | | | | | | | | | | | | | | | | | | | * add missing pane-id and tab-id arguments to all cli actions * rustfmt * dump-screen --ansi * edit-scrollback --ansi * add --ansi to subscribe * add tests for ansi additions * add ansi render report to plugins * rustfmt * fix e2e tests * add pr
* Add `--pane-id` and `--tab-id` to all relevant CLI commands (#4846)Aram Drevekenin2026-03-13
| | | | | | | | | | | * add missing pane-id and tab-id arguments to all cli actions * rustfmt * fix tests * update e2e snapshots * add pr
* Focus follows mouse and mouse clickthrough (#4836)Aram Drevekenin2026-03-11
| | | | | | | | | * implement focus follows mouse * mouse click through * rustfmt * add pr
* allow dumping screen to STDOUT as well (#4823)Aram Drevekenin2026-03-09
|
* feat(cli): add --pane-id option to dump-screen action (#4598)Yacine Hmito2026-03-09
| | | | | | | | | | | | | | | | | * feat(cli): add --pane-id option to dump-screen action Adds the ability to dump a specific terminal pane's content by passing `--pane-id <ID>` to the `dump-screen` CLI action. When omitted, the existing behavior of dumping the focused pane is preserved. This enables coding agents running inside Zellij to inspect the content of other panes programmatically, without needing complex workarounds. Prior art: https://github.com/theslyprofessor/zellij-pane-tracker, whose design complexity is due to current limitations that this PR addresses. * address review * improve man page entry
* Add cli paste action (#4817)Aram Drevekenin2026-03-08
| | | | | * add cli paste action * add pr
* Fix partial sequences parsing (#4623)Jonathan Giannuzzi2026-03-06
| | | | | | | | | | | | | | | | | * Vendor termwiz 0.23.3 * Apply fix for parsing partial SGR mouse sequences to termwiz * Add stdin debounce for fragmented input handling Parse with maybe_more=true and send events immediately. Finalization is deferred until 50ms of inactivity, avoiding latency while correctly handling escape sequences split across multiple reads (e.g., over SSH). * Remove unused parts of termwiz --------- Co-authored-by: Aram Drevekenin <aram@poor.dev>
* Click file paths to open in editor, allow plugins to change styles and hover ↵Aram Drevekenin2026-03-05
| | | | | | | | | | | | | | | | | | | | | | | | | | | of viewport (#4802) * initial implementation * implement hover * add styles and foreground/background emphases, fix upsert hover collisions * initial implementation of link plugin * suppress highlighting + clicks on unfocused and mouse mode panes * also make italic * fix env var and ~ expansions * fix regex issues and add defensive validations * fix tests and add plugin assets * add tests * refactor all the things * add pr