| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adding a pane left old borders on screen until a resize forced a full redraw.
Two causes. The picker draws each entry as its own chunk, so the space between
and around the entries was never painted and kept the previous frame's
contents; and adding a pane really does re-flow the whole grid, because a
freshly created pane has no title yet and its "Pane #N" fallback is far wider
than a named one, which widens every cell and drops the column count.
Separately, refresh_or_dissolve_stack_list changed the reserved row count
without forcing a clear, so when the list got shorter the visible pane moved up
and the rows it vacated kept their old contents.
Blanks only the spans no cell covers, never a cell's own columns: chunks are
NOT composited last-wins, so blanking the whole region erased the entries
instead of the stale contents under them -- the first attempt at this fix drew
exactly one entry.
stack_list_row_gaps is a free function so the covering invariant is testable:
gaps and cells must tile each row exactly, with no overlap, across a sweep of
row caps, label widths and member counts.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`stack_list_layout "grid"` packs picker entries several per row, which turns
stack_list_max_rows from a count into a space budget: eight panes in a wide
terminal now fit on one row instead of spending three and hiding five.
The entry renderer already drew each entry as a single chunk at its own x/y,
sized by its geom and centring its own label, so a grid needed no rendering
changes at all -- only the loop assigning every entry the same x and the full
width. Column count is derived from the widest label rather than configured, so
it adapts to the titles; one column is the floor and reproduces the previous
full-width rows byte for byte.
The row -> member mapping now has a column axis, and it had already caused one
wrong-pane bug when two consumers disagreed. So the layout is computed once,
by one function, into explicit cells; the renderer, the reserved-row budget and
the mouse hit-test all read those same cells rather than each doing their own
arithmetic. Windowing is by row, so a grid scrolls a whole row at a time and
degrades to the old per-member window when there is one column.
Cells are separated by a two-column gap: without it the focused entry's "> "
marker sits flush against the previous cell and reads as belonging to it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The cap made the picker a scrolling viewport -- everything stays reachable by
moving focus -- but there was no way to see the whole stack at once.
Clicking "+N more" expands it to every member; the indicator becomes "- fewer"
and collapses it again. Collapse gets its own label because an invisible way
back out is not an affordance.
Hangs off the indicator row because that row is already reserved, already
rendered, and resolves to no member, so it is free real estate. It is also why
this needs no new keybinding: mouse handling is entirely server-side, while
Action is a protobuf enum we cannot extend without protoc.
A click rather than hover, as expanding changes the reserved row count and so
resizes the visible pane's PTY -- acceptable once per deliberate click, not on
every mouse move.
All three consumers of the row -> member mapping (renderer, reserved-row
budget, mouse hit-test) now go through stack_list_window_for, so expansion
cannot desync them the way the cap desynced the hit-test in 844617b4.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Regression from the picker row cap. The row -> member mapping lives in TWO
places: the renderer, and the hit-test that resolves mouse events to a pane.
Capping made the renderer draw a WINDOW of the members, so screen row 0 is
members[window_start], but the hit-test still resolved row N to members[N].
Any click or hover on a capped stack whose window had scrolled therefore
landed on the wrong pane -- with 8 members focused on the last, clicking the
top row selected member 0 instead of member 5.
Both sites now go through stack_list_window(). Clicks on the "+N more"
indicator resolve to no member rather than to a bogus one.
Caught by asking how a user reaches the panes past the cutoff, not by the
tests: the previous commit tested the window arithmetic and the rendering, but
nothing covered this second consumer of the same mapping.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A stack reserves one row per member plus a padding row, so a deep stack spends
most of the screen listing itself. `stack_list_max_rows` caps that; the list
windows around the focused member and reports the rest as "+N more".
The indicator goes in the padding row the list already reserves, so it costs
no extra row and never displaces a real entry. Entries keep their real rank
rather than renumbering from 1, otherwise the numbering stops meaning anything
once the window has scrolled. The window slides on ordinary focus movement,
which is also why this needs no new keybinding -- Action is a protobuf enum
and the prost assets cannot be regenerated here (no protoc).
A fixed cap rather than hover-to-expand on purpose: reserved_top_rows feeds
pane geometry, so growing the list on hover would resize the pane's PTY and
reflow whatever is running in it on every mouse move. A real expand/collapse
has to be an overlay that reserves nothing, which is a much larger change to
the render path.
reserved_top_rows and the renderer both go through stack_list_window(); if
they disagreed the list would draw into rows the geometry never reserved, i.e.
over the pane beneath it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A `children` placeholder inserts every pane it receives as a flat sibling.
That single hardcoded policy is why a layout can only express "N equal
columns", "N equal rows" or "one stack" without hand-writing a variant per
pane count -- the max_panes=5 / max_panes=8 / max_panes=12 ladders in a swap
layout file exist purely to work around it.
`pane arrangement="grid" { children; }` builds a tree for N panes instead.
Two to start: grid (ceil(sqrt(n)) columns, rows filled left to right) and
master-stack (one 60%-wide master, the rest beside it).
Generates a TiledPaneLayout rather than geometry, so everything downstream
works unchanged: split_space, the minimum-size feasibility check,
LayoutApplier, swap-layout damage tracking, and the responsive collapse. It
composes with that last one for free -- a grid is simply infeasible on a
narrow terminal, so it is skipped and the collapse takes over.
The arrangement builds ONE self-contained subtree rather than splicing a
variable number of nodes in among the placeholder's siblings, so it stays well
defined next to the pre-existing "children node with sibling nodes" TODO
rather than leaning on it.
Like default_fg/default_bg, the field has no protobuf representation, so it is
defaulted at both plugin-API boundaries and does not survive a round trip.
This is why it is a field rather than a LayoutConstraint variant: a new enum
variant would have no representation when converting TO protobuf, and the
committed prost assets cannot be regenerated here (no protoc).
The 48 snapshot updates are the new field appearing in TiledPaneLayout debug
dumps (324 lines, all `children_arrangement: None`). No snapshot content was
removed. The `expression:` metadata churn in those files is pre-existing drift
that regeneration swept up: the stored snapshots recorded the expression
before normalize_layout_debug was introduced.
|
| |
|
|
|
|
|
|
|
|
| |
The tree carried 30 failures in zellij-server and 5 in zellij-utils from the
first sl0p commit until 5f2c28ab. They were never real bugs: aa51848d changed
the scroll indicator and frame-title rendering without refreshing the insta
snapshots. Documents the current green counts so a future context treats any
failure as its own, plus the two traps that hid this for so long -- cargo
stops at the first failing test binary, and insta aborts a test at its first
snapshot mismatch -- and how to regenerate snapshots without cargo-insta.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two problems made the stack list unusable for panes nothing had named yet,
which is the common case right after opening them -- and the responsive
collapse walks straight into it, since collapsing is exactly when the list
becomes the only way to pick a pane.
`hide_default_pane_names` no longer applies to stack list entries. In a frame
an empty title is fine: the pane is still visible, sized and positioned, so
you can tell what it is by looking at it. In the stack list the label is the
ENTIRE affordance, so blanking it leaves rows that cannot be identified or
told apart. Entries fall back to the "Pane #N" placeholder even when frames
are configured to hide it.
Entries are also prefixed with their 1-based position, so they stay countable
and addressable even when several share a title or have none. The prefix is
built by one shared helper because the renderer and Tab's column-width
calculation must agree on its width or the list misaligns.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Splitting a window into thirds is great until the window is 70 columns wide
and each pane gets 23. `responsive_min_cols` collapses the tab into the swap
layout named by `responsive_layout` when it gets narrower than that, and
restores the previously active layout when it grows back.
Selects a swap layout BY NAME rather than adding a size-aware
LayoutConstraint: that type crosses both the plugin protobuf API and the
client/server contract, and the committed prost assets cannot be regenerated
here (no protoc).
Restoring uses a 10-column hysteresis band. Without a dead band, dragging a
window edge that lands exactly on the threshold flips the layout on every
motion event, and every flip resizes every PTY in the tab.
"Did the user rearrange panes by hand" is judged from the damaged flag as
sampled BEFORE each resize, not the live one: resize_whole_tab's own
auto-layout pass sets that flag itself and clears it again only if it found a
layout that fits, so on a terminal too narrow for any of them it latches true
although nothing was rearranged. And once collapsed, restore stays reachable
regardless of the flag, so damage occurring while collapsed cannot strand the
tab in the narrow layout.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
aa51848d replaced "SCROLL: 42/1000" with a compact "^42" glyph and stopped
showing any indicator when the pane is not actually scrolled up, but did not
refresh the insta snapshots that encode the old upstream frame rendering.
That left 30 failing tests in zellij-server from the very first sl0p commit
onward, which in turn masked 5 more in zellij-utils (cargo stops at the first
failing test binary, so zellij-utils never ran).
Bisected to aa51848d; the upstream fork point 0e6e4404 is green.
zellij-server (44 snapshots): frame titles lose the "SCROLL: 0/N" segment when
not scrolled, and render "^N" where they previously rendered "SCROLL: N/M".
Verified every diff is scroll-only, and that the genuinely-scrolled cases
(2/13, 3/33, 6/33) still render as ^2, ^3 and ^6 rather than disappearing.
zellij-utils (5 snapshots): serialized Config gained the sl0p FrameConfig
fields (title_alignment, gap, padding, gap_aspect, inactive_grayscale,
hide_default_pane_names), all at their existing defaults. Additions only.
Several tests assert multiple snapshots and insta aborts at the first
mismatch, so this took four regenerate/verify passes to fully converge.
zellij-server: 1471 passed, 0 failed.
zellij-utils: 441 passed, 0 failed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Entering RenameTab left the existing name in place and appended to it, so
renaming a tab meant clearing it by hand first. The first printable keystroke
now REPLACES it, the way a text field with its contents selected behaves.
Armed when a client enters RenameTab and disarmed when it leaves, so a
keystroke that arrives later cannot wipe a name the user was not editing.
Backspace and delete deliberately do NOT replace: reaching for them means
editing the existing name, not discarding it.
The flag is taken out of the set BEFORE borrowing the active tab, since
get_active_tab_mut borrows self mutably and every branch consumes the flag
anyway.
Applies to renames from any entry point - the keybind and the sl0pstatus
double-click-a-tab both go through Screen::change_mode.
Pane rename is deliberately left alone for now; say the word and it can get
the same treatment.
Baselines held: zellij-server 1441/30.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
A `git add -A` swept in the per-job files bgrun writes beside SPEED.md,
including a 37KB build log, none of which belong in the repo.
SPEED.md stays tracked on purpose: the narrow test commands, measured timings
and the pre-existing test-failure baselines are useful to anyone working on
this fork, not just to an agent.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Enter/Esc still did not leave rename mode after entering it by double-click.
The previous attempt fixed a real trap (a None base_mode makes
change_to_default_mode a silent no-op) but not this one. The actual cause:
zellij tracks the client's input mode in TWO places.
- Screen's ModeInfo, which drives what is rendered.
- SessionData::current_input_modes on the server, which is what ROUTES key
input to the right handler.
The double-click path sent ScreenInstruction::ChangeMode directly, so only the
first was updated: the rename prompt appeared and accepted text, while the
server still interpreted keys in the previous mode - so Enter and Esc were
never treated as rename-mode keys and there was no way out.
Action::SwitchToMode (the rename keybind) sends ServerInstruction::ChangeMode
instead, and the server handler updates current_input_modes AND derives
base_mode from the session config before forwarding to Screen. The mouse path
now sends exactly that, so it is the same code path as the keybind rather than
an imitation of it.
Tab::client_base_mode is dropped: the server supplies base_mode correctly, so
it is no longer needed.
Lesson worth keeping: 'send the instruction the existing feature sends' beats
reconstructing its effects downstream. Both bugs here came from entering the
system one layer too low.
Baselines held: zellij-server 1429/30.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Double-click-to-rename trapped the client: once in RenamePane, Esc and Enter
did nothing and there was no way out without killing the session.
Cause: Screen::change_mode() does `mode_info.base_mode = base_mode`, i.e. it
OVERWRITES the client's base mode with whatever it is handed. The
double-click path passed None, believing that meant 'leave it alone'. It does
not - ModeInfo::change_to_default_mode() is:
if let Some(base_mode) = self.base_mode { self.mode = base_mode; }
so a None base_mode makes leaving ANY temporary mode a silent no-op. The
legitimate caller in lib.rs passes Some(base_mode) from the session config;
this path had no equivalent.
Fix: Tab::client_base_mode() reads the client's existing base_mode (falling
back to the tab default) and the rename path passes it through, so entering
rename via the mouse leaves the return path exactly as the keybind does.
Added a test on ModeInfo pinning the trap itself: change_to_default_mode is a
no-op with base_mode None, and returns to the base mode when it is set. That
is the property that makes passing None dangerous, and it now fails loudly if
anyone assumes otherwise.
Immediate escape hatch for a session stuck on an older build:
zellij action switch-mode locked
(that goes through the server path, which sets base_mode correctly)
Baselines held: zellij-server 1429/30, zellij-utils 436/5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reported after double-click-to-rename landed: rename one pane, then hovering
over other panes puts them in 'edit title' state too.
Cause is focus_follows_mouse, not the rename itself. RenamePane is a CLIENT
mode that acts on whatever pane is ACTIVE, so with focus-follows-mouse enabled
every pane the mouse passes over becomes active and inherits the prompt. The
double-click entry point just made it easy to hit, since you finish the
gesture with the mouse sitting on a pane frame.
Focus-follows-mouse is now suppressed for a client that is in a modal
text-entry mode: RenamePane, RenameTab, EnterSearch or Prompt. Those all act
on the active pane/tab, so moving focus underneath them is never what the user
meant. Keyboard focus movement is untouched.
Baselines held: zellij-server 1429/30.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two independent bugs, either of which alone made the feature dead code.
1. WRONG EDGE. The check was `details.edge == Some(PaneEdge::Top)`, but
get_edge_at_position() quadrant-splits a pane and only ever returns the four
CORNER variants - PaneEdge::Top exists in the enum and is never produced. A
title-row click reports as TopLeft/TopRight, so the condition was never true.
Replaced with Pane::position_is_on_title_row(), which tests the geometry
directly the same way position_is_on_frame() does for the top border.
2. RESET ON RELEASE. Click recording reset the tracker on any event that was
not a frame press - which includes the RELEASE between the two clicks of a
double-click. The sequence is press/release/press, so the count went
1 -> reset -> 1 and never reached 2.
The tests passed anyway because they hand-built a MouseEventContext with
edge: Some(PaneEdge::Top) and called the decision function directly. They
asserted my assumption rather than the behaviour, and exercised neither the
edge lookup nor the click sequence.
Click recording is now a small pure helper (record_frame_click) covered by
tests that replay real event sequences: press/release/press is a double-click,
motion in between does not break it, a press off the frame does break it, and
two clicks at different positions are not a double-click. The decision tests
now use the corner edge value that is actually produced.
Baselines held: zellij-server 1428/30 (7 new tests).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Building this fork for local use took ~3 minutes per one-line change:
[profile.release] sets lto = true and codegen-units = 1, so every rebuild
redid whole-program optimization on a single core. That is right for a
shipped artifact and pure waste for a binary we just want to look at in a
terminal - zellij is bound on a human, not on codegen.
[profile.fastdev] inherits release, keeps opt-level 3, drops LTO, uses 16
codegen units and keeps the incremental cache:
cargo build --profile fastdev -p zellij ~2.8s incremental (was ~3min)
[profile.release] is deliberately untouched, so shipping/benchmarking builds
are unchanged.
.fastfeedback/SPEED.md records the narrow commands, measured timings, the
deploy loop, and - importantly - the PRE-EXISTING test failure baselines
(30 in zellij-server, 5 in zellij-utils) so a future session does not waste
time chasing failures it did not cause.
Rejected: the gold linker (rustc warns it is deprecated with known Rust
bugs) and clang+lld (no lld binary installed). Dropping LTO removed the
dominant cost anyway.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| | |
|
| |
|
|
|
|
|
|
|
| |
- pane_boundaries_frame: replace 'SCROLL: 42/1000' with a single-glyph
indicator ('up-arrow'42), and only show it when actually scrolled up
rather than whenever scrollback exists.
- tab-bar: rebrand the prefix to sl0p.foo.
Pre-existing local work, committed as the base of the sl0p fork.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
* 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
|
| |
|
|
|
|
|
| |
passthrough, remove clippy (#5426)
* fix(build): remove manpage, honor CARGO_TARGET_DIR, add cargo argument passthrough, remove clippy
* add pr
|
| |
|
|
|
| |
* Upgrade axum-extra 0.12, axum-server 0.8, rcgen 0.14
* rustfmt
|
| |
|
|
|
|
|
|
|
|
|
| |
* upgrade insta, rand, sha2, strum, thiserror, tokio-tungstenite, toml, which; drop unused semver and typetag
* upgrade sysinfo to 0.39.5, bump toolchain (msrv) to 1.95.0
* remote legacy yaml config converter and thus drop serde_yaml
* upgrade unicode-width to 0.2.2
* fix(tests): run ipc roundtrip test on a larger stack
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
* 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
|
| |
|
|
|
|
|
|
|
|
|
| |
* Switch from the unmaintained daemonize crate to a maintained fork
c.f. https://rustsec.org/advisories/RUSTSEC-2025-0069.html
* add pr
---------
Co-authored-by: Aram Drevekenin <aram@poor.dev>
|
| |
|
|
|
| |
* chore: replace unmaintained cassowary with maintained fork kasuari
* add pr
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
`find_executable` used to short-circuit on the first exists()-and-is_file
match and only consult PATHEXT if no bare file was present. That returned
the wrong file for tools whose Windows installer ships both a Unix-style
launcher and a `.bat` alongside it. The reported case is Composer, whose
setup places bare `composer` (a Unix launcher, invalid PE on Windows) next
to `composer.bat`. The bare file was resolved and handed to CreateProcessW,
which rejected it with ERROR_BAD_EXE_FORMAT.
On Windows, when the candidate has no user-supplied extension, walk PATHEXT
first and only fall back to the bare match if nothing matches. This mirrors
cmd.exe's and powershell's resolution.
|
| |
|
|
|
|
|
| |
* fix: mouse scroll resize stack issue
* regression test
* cleanups
|
| | |
|
| |
|
|
| |
into their respective events (#5323)
|
| | |
|