aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-08 20:05:45 +0200
committeruser <user@clank>2026-08-08 20:05:45 +0200
commite118b6977086f5043422ab5763cda91ce6cae44f (patch)
treed75862e2c3407006edd6ef6b09d2d9466f3a0f50
parentsl0p: configurable pane gap (outer) and padding (inner) (diff)
downloadsl0ppij-e118b6977086f5043422ab5763cda91ce6cae44f.tar.gz
sl0ppij-e118b6977086f5043422ab5763cda91ce6cae44f.tar.xz
sl0ppij-e118b6977086f5043422ab5763cda91ce6cae44f.zip
sl0p: fix status bar vanishing when pane gap is set
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.
-rw-r--r--zellij-server/src/panes/plugin_pane.rs10
-rw-r--r--zellij-server/src/panes/terminal_pane.rs10
-rw-r--r--zellij-utils/src/pane_size.rs74
3 files changed, 92 insertions, 2 deletions
diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs
index 4d30cb4b..534a149c 100644
--- a/zellij-server/src/panes/plugin_pane.rs
+++ b/zellij-server/src/panes/plugin_pane.rs
@@ -715,7 +715,15 @@ impl Pane for PluginPane {
self.frame.clear();
}
fn set_content_offset(&mut self, offset: Offset) {
- self.content_offset = offset.expanded_by(self.style.pane_gap + self.style.pane_padding);
+ // see terminal_pane.rs: borderless panes are chrome (this is the one
+ // that matters in practice - zjstatus/status-bar/tab-bar are plugins)
+ let inset = if self.borderless {
+ 0
+ } else {
+ self.style.pane_gap + self.style.pane_padding
+ };
+ self.content_offset =
+ offset.expanded_by_within(inset, self.cols(), self.rows());
self.resize_grids();
}
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs
index ab17c7c5..709c3dfd 100644
--- a/zellij-server/src/panes/terminal_pane.rs
+++ b/zellij-server/src/panes/terminal_pane.rs
@@ -935,7 +935,15 @@ impl Pane for TerminalPane {
}
fn set_content_offset(&mut self, offset: Offset) {
- self.content_offset = offset.expanded_by(self.style.pane_gap + self.style.pane_padding);
+ // borderless panes are UI chrome (status bar, tab bar): they are sized
+ // exactly to their content, so insetting them makes them vanish
+ let inset = if self.borderless {
+ 0
+ } else {
+ self.style.pane_gap + self.style.pane_padding
+ };
+ self.content_offset =
+ offset.expanded_by_within(inset, self.get_columns(), self.get_rows());
self.reflow_lines();
}
diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs
index 1326a857..5afedd7a 100644
--- a/zellij-utils/src/pane_size.rs
+++ b/zellij-utils/src/pane_size.rs
@@ -472,6 +472,29 @@ impl Offset {
}
}
+ /// As [`Offset::expanded_by`], but refuses to expand when doing so would
+ /// leave a pane of `cols` x `rows` with no room for content.
+ ///
+ /// This exists because of a real regression: a one-row `borderless` status
+ /// bar pane expanded by a gap of 1 ends up with top = bottom = 1 and thus
+ /// zero content rows, i.e. it silently disappears. Chrome panes are now
+ /// excluded by their callers, but any pane can be resized down to nothing,
+ /// so the geometry refuses to collapse itself regardless of caller.
+ pub fn expanded_by_within(&self, by: usize, cols: usize, rows: usize) -> Self {
+ let expanded = self.expanded_by(by);
+ let fits = cols
+ .checked_sub(expanded.left + expanded.right)
+ .is_some_and(|c| c >= 1)
+ && rows
+ .checked_sub(expanded.top + expanded.bottom)
+ .is_some_and(|r| r >= 1);
+ if fits {
+ expanded
+ } else {
+ *self
+ }
+ }
+
pub fn frame(size: usize) -> Self {
Self {
top: size,
@@ -622,6 +645,57 @@ mod gap_and_padding_tests {
}
#[test]
+ fn expanded_by_within_never_collapses_a_one_row_status_bar() {
+ // REGRESSION: `gap 1` made the zjstatus footer disappear. A size=1
+ // borderless pane expanded by 1 gets top=bottom=1 -> 0 content rows.
+ let bar = Offset::default();
+ let got = bar.expanded_by_within(1, 80, 1);
+ assert_eq!(
+ (got.top, got.bottom),
+ (0, 0),
+ "a 1-row pane must not be inset vertically"
+ );
+ let rows_left = 1usize.saturating_sub(got.top + got.bottom);
+ assert!(rows_left >= 1, "status bar must keep at least one content row");
+ }
+
+ #[test]
+ fn expanded_by_within_applies_when_there_is_room() {
+ let got = Offset::frame(1).expanded_by_within(1, 80, 24);
+ assert_eq!((got.top, got.bottom, got.left, got.right), (2, 2, 2, 2));
+ }
+
+ #[test]
+ fn expanded_by_within_leaves_content_for_any_pane_size() {
+ // no pane size may ever be reduced to zero content in either axis
+ for rows in 1..12usize {
+ for cols in 1..12usize {
+ for by in 0..4usize {
+ let got = Offset::frame(1).expanded_by_within(by, cols, rows);
+ let c = cols.saturating_sub(got.left + got.right);
+ let r = rows.saturating_sub(got.top + got.bottom);
+ // Offset::frame(1) itself can exceed a tiny pane; what we
+ // assert is that expansion never makes things worse than
+ // the offset we were handed.
+ let base = Offset::frame(1);
+ let base_c = cols.saturating_sub(base.left + base.right);
+ let base_r = rows.saturating_sub(base.top + base.bottom);
+ assert!(
+ c >= base_c.min(1) || base_c == 0,
+ "cols {} rows {} by {}: content cols {} < base {}",
+ cols, rows, by, c, base_c
+ );
+ assert!(
+ r >= base_r.min(1) || base_r == 0,
+ "cols {} rows {} by {}: content rows {} < base {}",
+ cols, rows, by, r, base_r
+ );
+ }
+ }
+ }
+ }
+
+ #[test]
fn frame_and_content_insets_agree() {
// The invariant that makes gaps safe: content inset by (gap + border +
// padding) must land strictly inside the frame drawn at inset(gap).