aboutsummaryrefslogtreecommitdiffstats
path: root/README.md (follow)
Commit message (Collapse)AuthorAge
* sl0p: document --purpose and tab purposesHEADmainuser2026-08-12
|
* sl0p: document workspace hints for consumersuser2026-08-12
|
* sl0p: hot-reload status_bar_button_styleuser2026-08-12
| | | | | | | | | | | | | | | | 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.
* 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: document the pane status bar protocoluser2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | 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.
* 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: per-pane status bar via OSC 5577 (experimental)user2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* 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: bring zj-pane-finder into the repouser2026-08-11
| | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: stop hide_default_pane_names blanking the plugin APIuser2026-08-11
| | | | | | | | | | | | | | | | | | | | | | | | `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.
* 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: resolve responsive layouts from current state, not a remembered positionuser2026-08-11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* 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: click the picker's indicator row to expand a capped stackuser2026-08-11
| | | | | | | | | | | | | | | | | | | | | | 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.
* 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: first keystroke replaces the old tab name when renaminguser2026-08-09
| | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: preview pane reordering live while dragginguser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Dragging showed only a green border on the target. The swap is now applied as you drag, so the real layout IS the preview and releasing just keeps it. Dragging elsewhere undoes it and previews the new arrangement; dragging off all panes restores the original. Because swap_panes is symmetric, applying it twice is the identity, so 'undo the preview' is just the same call again. Drop needs no work at all now - the swap already happened - which also removes the old drop-time swap. THE TRAP, which a naive version hits immediately: once the swap is applied the dragged pane sits under the cursor, so the next motion computes the dragged pane as its own target, undoes the swap, which puts the original pane back under the cursor, which re-applies it - oscillating on every motion event and spamming both PTYs with resizes. Avoided by treating two cases as 'the layout already shows what you would get' and doing nothing: - the cursor is over the dragged pane, wherever the preview has moved it; - the cursor is over the pane it is currently swapped with, which after a preview sits at the dragged pane's original position. So a swap is only re-applied when the cursor reaches a THIRD pane, which also keeps PTY resizes down to one pair per target change rather than per event. The dragged pane is kept highlighted rather than the target, so it stays identifiable while the layout moves around it. Note: the state machine lives in the executor and needs a real Tab, so it is reasoned through in the commit and README rather than unit-tested; the decision-level tests cover only which action is produced. Baselines held: zellij-server 1441/30.
* sl0p: highlight the drop target while dragging a paneuser2026-08-08
| | | | | | | | | | | | | | | | | | | | Dragging a pane by its title was blind until release. The pane a drop would land on is now highlighted with the theme's frame_highlight colour. Reuses the existing hover highlight rather than adding a second highlighting mechanism, but sets tab.mouse_hover_pane_id directly instead of going through execute_update_hover, which is gated on the mouse_hover_effects option. During a drag this is essential feedback rather than decoration, so it must not be switchable off. The pane being dragged is deliberately NOT highlighted when hovered: dropping onto itself is a no-op, so showing no highlight is the honest signal that releasing there changes nothing. The highlight is cleared on drop, so a completed drag does not leave a pane looking hovered. Baselines held: zellij-server 1441/30.
* sl0p: rearrange tiled panes by dragging their title baruser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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).
* 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: double-click a pane title to rename ituser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: ignore gap/padding when a pane draws no frameuser2026-08-08
| | | | | | | | | | | | | | | | | | | | | | | | 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.
* sl0p: tag fork builds as 0.45.0+sl0p1user2026-08-08
| | | | | | | | | | | | | | | | | | | | | `zellij --version` was identical to upstream, so there was no way to tell a fork binary from a stock one. Uses BUILD METADATA (+sl0p1) rather than a prerelease (-sl0p1): cargo ignores build metadata when matching version requirements, so the intra-workspace `zellij-utils = "0.45.0"` dependencies still resolve. The prerelease form was tried first and fails to resolve - it would require updating every member manifest to depend on the exact prerelease. Checked against the places VERSION is actually consumed: - shared::version_number() yields 4500 for both 0.45.0 and 0.45.0+sl0p1, so terminal version reporting is byte-identical, and still orders below 0.46.0. - client/server compatibility keys off CLIENT_SERVER_CONTRACT_VERSION, not this, so existing sessions are unaffected. - ZELLIJ_PLUGIN_ARTIFACT_DIR is keyed by VERSION, so the bundled wasm plugins are recompiled once on first run after a bump. One-time, harmless. Baselines held: zellij-server 1421/30, zellij-utils 435/5.
* docs: prepend sl0ppij fork header to READMEuser2026-08-08
| | | | | | | | | | | | | | | | | | | | 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.
* adjust badges location in readmeAram Drevekenin2026-03-19
|
* fix sponsored links truncationAram Drevekenin2026-03-19
|
* Add sponsor badgeAram Drevekenin2026-02-21
|
* docs(readme): description and roadmap updateAram Drevekenin2025-08-08
|
* docs(readme): add https to curl downloadAram Drevekenin2024-10-25
|
* docs(readme): update sponsorsAram Drevekenin2024-09-16
|
* docs(readme): about issuesAram Drevekenin2024-07-09
|
* docs(readme): warning about pre-release versionsAram Drevekenin2024-07-05
|
* docs(readme): add name origin section (#3391)djalal2024-06-14
| | | | | | | | | * add name section in readme * Adjust wording --------- Co-authored-by: Aram Drevekenin <aram@poor.dev>
* docs(readme): update roadmap and remove extraneous section (#3252)Aram Drevekenin2024-04-10
|
* chore(readme): add sponsorAram Drevekenin2024-03-27
|
* docs(readme): add sponsorAram Drevekenin2024-02-04
|
* Add 'launch in xonsh' in README (#2323)Andy Kipp2023-03-30
| | | Updated the quick launch commands to include xonsh.
* docs(readme): fix roadmap linkAram Drevekenin2023-03-29
|
* docs(readme): update roadmap image (#2333)Aram Drevekenin2023-03-29
|
* chore(repo): version 0.35.0 (#2230)Aram Drevekenin2023-03-07
| | | | | | | | | | | | | | | * chore(assets): new readme animation * chore(assets): increase animation resolution * chore(assets): improve animation * chore(assets): improve animation * docs(readme): some cleanups and clarifications * docs(readme): fix links and wording * chore(version): bump development version
* chore(repo): remove nix support (#2038)Aram Drevekenin2022-12-20
|
* WIP: Use `xtask` as build system (#2012)har7an2022-12-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * xtask: Implement a new build system xtask is a cargo alias that is used to extend the cargo build system with custom commands. For an introduction to xtask, see here: https://github.com/matklad/cargo-xtask/ The idea is that instead of writing makefiles, xtask requires no additional dependencies except `cargo` and `rustc`, which must be available to build the project anyway. This commit provides a basic implementation of the `build` and `test` subcommands. * xtask/deps: Add 'which' * xtask/test: Handle error when cargo not found * xtask/flags: Add more commands to perform different useful tasks. Includes: - clippy - format - "make" (composite) - "install" (composite) Also add more options to `build` to selectively compile plugins or leave them out entirely. * xtask/main: Return error when cargo not found * xtask/build: Add more subtasks - `wasm_opt_plugins` and - `manpage` that perform other build commands. Add thorough documentation on what each of these does and also handle the new `build` cli flags appropriately. * xtask/clippy: Add job to run clippy * xtask/format: Add job to run rustfmt * xtask/pipeline: Add composite commands that perform multiple atomic xtask commands sequentially in a pipeline sort of fashion. * xtask/deps: Pin dependencies * xtask/main: Integrate new jobs and add documentation. * xtask: Implement 'dist' which performs an 'install' and copies the resulting zellij binary along with some other assets to a `target/dist` folder. * cargo: Update xflags version * xtask: Measure task time, update tty title * xtask: Update various tasks * xtask: wasm-opt plugins in release builds automatically. * xtask/build: Copy debug plugins to assets folder * xtask: Add 'run' subcommand * xtask: Add arbitrary args to test and run * xtask: Rearrange CLI commands in help * xtask: Add deprecation notice * docs: Replace `cargo make` with `xtask` * github: Use `xtask` in workflows. * xtask: Add support for CI commands * xtask: Streamline error handling * github: Use new xtask commands in CI * xtask: Add 'publish' job * xtask/publish: Add retry when publish fails * xtask: Apply rustfmt * xtask: Refine 'make' deprecation warning * xtask: add task to build manpage * contributing: Fix e2e commands * xtask/run: Add missing `--` to pass all arguments following `xtask run` directly to the zellij binary being run. * xtask: Stay in invocation dir and make all tasks that need it change to the project root dir themselves. * xtask/run: Add `--data-dir` flag which will allow very quick iterations when not changing the plugins between builds. * xtask/ci: Install dependencies without asking * utils: Allow including plugins from target folder * utils/assets: Reduce asset map complexity * utils/consts: Update asset map docs * xtask: Fix plugin includes * xtask/test: Build plugins first because the zellij binary needs to include the plugins. * xtask/test: Fix formatting * xtask: Add notice on how to disable it
* made the README more aesthetic (#1885)Rhythm Bansal2022-11-08
|