Stacked lists (#5331)

* initial implementation

* improve ui

* fix: stacked resize interaction

* title list boundaries fix

* fix BEL

* mouse parity

* float/embed + bulk-op features

* fix in-place bug

* improve ui

* config live reload

* improve ui

* fix issues, add exit status

* fix for serialization

* fix tests

* integration tests

* comments

* refactor: duplicate title logic

* refactor: consolidate logic

* fix: flaky tests

* add a stacked swap layout

* rustfmt

* fix e2e

* add pr
This commit is contained in:
Aram Drevekenin
2026-07-04 16:27:08 +02:00
committed by GitHub
parent 0dd324bdef
commit 82c4a24d70
64 changed files with 2431 additions and 99 deletions
+1
View File
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: new title-frames UI (https://github.com/zellij-org/zellij/pull/5318)
* chore: upgrade wasmi to 1.1.0 (https://github.com/zellij-org/zellij/pull/5319)
* fix: fragmented STDIN causing "garbage" input on slow SSH connections (https://github.com/zellij-org/zellij/pull/5320)
* feat: stacked lists - new stack UI (https://github.com/zellij-org/zellij/pull/5331)
## [0.44.3] - 2026-05-13
* fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139)
+5
View File
@@ -507,6 +507,11 @@ load_plugins {
//
// stacked_resize false
// Whether stacked panes display as a list with the expanded pane pinned to the bottom
// Default: true
//
// stacked_pane_list false
// Whether to show release notes on first version run
// Default: true
//
+6 -6
View File
@@ -125,12 +125,12 @@ fn previous_swap_layout() {
zellij.send_stdin(&keys::alt('['));
let grid_snapshot =
zellij.wait_until("swap layout moved back to horizontal", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("HORIZONTAL")
&& !grid_snapshot.contains("BASE")
});
let grid_snapshot = zellij.wait_until("swap layout moved back to stacked", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("STACKED")
&& !grid_snapshot.contains("HALF-STACKED")
&& !grid_snapshot.contains("BASE")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
@@ -1,11 +1,10 @@
---
source: zellij-integration-tests/tests/misc.rs
assertion_line: 135
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
$
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Pane #2
Pane #1
<↓↑> [ Pane #2 ]
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -3,7 +3,7 @@ source: zellij-integration-tests/tests/panes.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
──────────────────────────────────────────────────────── Pane #1 ───────────────────────────────────────────────────────
──────────────────────────────────────────────────────── Pane #2 ───────────────────────────────────────────────────────
Pane #1
<↓↑> [ Pane #2 ]
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -3,8 +3,9 @@ source: zellij-integration-tests/tests/panes.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
┌ Pane #2 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Pane #1
<↓↑> [ Pane #2 ]
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│$ █ │
│ │
│ │
@@ -23,6 +24,5 @@ expression: normalized(&grid_snapshot)
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,9 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
<↓↑> [ Pane #1 ]
member-command [ EXIT CODE: 2 ]
█
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,10 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
<↓↑> [ Pane #3 ]
$ third-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,10 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
<↓↑> [ Pane #1 ]
Pane #2
Pane #3
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,8 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
$ first-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,10 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
Pane #2
<↓↑> [ Pane #3 ]
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,10 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  Tab #2  + 
Pane #1
<↓↑> [ Pane #2 ]
$ second-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,11 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&swapped_in)
---
Zellij (test)  Tab #1  + 
<↓↑> [ Pane #1 ]
Pane #2
Pane #3
$ first-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,10 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
<↓↑> [ Pane #2 ]
$ second-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,9 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
──────────────────────────────────────────────────────── Pane #1 ────────────────────────────────────── [ SCROLL: 0/1 ]
──────────────────────────────────────────────────────── Pane #2 ───────────────────────────────────────────────────────
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,9 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
<↓↑> [ Pane #1 ]
member-command
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,21 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
<↓↑> [ Pane #2 ]
$ second-member
$
┌ Pane #3 ──────────────────────────────────────── PIN [ ] ┐
│$ third-member │
│$ █ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,11 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
Pane #2
<↓↑> [ Pane #3 ]
$ third-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,14 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
$ first-member
$
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
<↓↑> [ Pane #3 ]
Pane #2
$ third-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,13 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
$ first-member
$
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Pane #2
<↓↑> [ Pane #3 ]
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,14 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
Pane #2
<↓↑> [ Pane #3 ]
$ third-member
$
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Pane #4
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,11 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
Pane #2
<↓↑> [ Pane #3 ]
$ third-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,28 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&grid_snapshot)
---
Zellij (test)  Tab #1  + 
Pane #1
<↓↑> [ Pane #2 ] [ SCROLL: 21/21 ]
$ line0
line1
line2
line3
line4
line5
line6
line7
line8
line9
line10
line11
line12
line13
line14
line15
line16
line17
line18
█ine19
Ctrl + <s> SEARCH   s  Search  ↓↑  Scroll  PgDn|PgUp  Scroll  d|u  Scroll  e  Edit  ENTER  Select 
@@ -0,0 +1,11 @@
---
source: zellij-integration-tests/tests/stacked_pane_list.rs
expression: normalized(&middle_member_selected)
---
Zellij (test)  Tab #1  + 
Pane #1
<↓↑> [ Pane #2 ]
Pane #3
$ second-member
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  ...
@@ -0,0 +1,762 @@
#![cfg(unix)]
use insta::assert_snapshot;
use zellij_integration_tests::{
claim_first_terminal_and_wait_for_prompt, col, keys, normalized,
split_down_and_wait_for_prompt, start_zellij, FakePtyHandle, GridSnapshot, LayoutInfo, Size,
TestRunner, TestSession, PROMPT, TERMINAL_SIZE,
};
use zellij_utils::input::command::TerminalAction;
const STACKED_LAYOUT: &str = r#"
layout {
default_tab_template {
pane size=1 borderless=true {
plugin location="tab-bar"
}
children
pane size=1 borderless=true {
plugin location="status-bar"
}
}
tab {
pane stacked=true {
pane
pane
pane
}
}
}
"#;
const STACK_WITH_COMMAND_MEMBER_LAYOUT: &str = r#"
layout {
default_tab_template {
pane size=1 borderless=true {
plugin location="tab-bar"
}
children
pane size=1 borderless=true {
plugin location="status-bar"
}
}
tab {
pane stacked=true {
pane
pane command="member-command"
}
}
}
"#;
fn add_stacked_pane_and_wait_for_selected_entry(
zellij: &TestSession,
selected_entry_title: &str,
) -> FakePtyHandle {
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('s'));
let terminal = zellij.expect_pty_spawn();
terminal.output(PROMPT);
let bracketed_entry = format!("[ {} ]", selected_entry_title);
zellij.wait_until("stacked pane added and selected", |grid_snapshot| {
grid_snapshot.status_bar_appears() && grid_snapshot.contains(&bracketed_entry)
});
terminal
}
fn build_three_member_stack(zellij: &TestSession) -> [FakePtyHandle; 3] {
let first_member = claim_first_terminal_and_wait_for_prompt(zellij);
first_member.output(b"first-member\r\n$ ");
zellij.wait_until("first member content rendered", |grid_snapshot| {
grid_snapshot.contains("first-member")
});
let second_member = add_stacked_pane_and_wait_for_selected_entry(zellij, "Pane #2");
second_member.output(b"second-member\r\n$ ");
zellij.wait_until("second member content rendered", |grid_snapshot| {
grid_snapshot.contains("second-member")
});
let third_member = add_stacked_pane_and_wait_for_selected_entry(zellij, "Pane #3");
third_member.output(b"third-member\r\n$ ");
zellij.wait_until("third member content rendered", |grid_snapshot| {
grid_snapshot.contains("third-member")
});
[first_member, second_member, third_member]
}
fn sgr_left_click(column: usize, line: usize) -> Vec<u8> {
format!(
"\u{1b}[<0;{};{}M\u{1b}[<0;{};{}m",
column, line, column, line
)
.into_bytes()
}
fn sgr_alt_left_click(column: usize, line: usize) -> Vec<u8> {
format!(
"\u{1b}[<8;{};{}M\u{1b}[<8;{};{}m",
column, line, column, line
)
.into_bytes()
}
fn display_column_of(line: &str, needle: &str) -> Option<usize> {
line.find(needle)
.map(|byte_offset| line[..byte_offset].chars().count())
}
fn entry_click_coordinates(grid_snapshot: &GridSnapshot, entry_title: &str) -> (usize, usize) {
let lines = grid_snapshot.lines();
let (line_index, line) = lines
.iter()
.enumerate()
.find(|(_, line)| line.contains(entry_title))
.expect("entry title is on a header row");
let column = display_column_of(line, entry_title).expect("entry title is on the line") + 1;
(column, line_index + 1)
}
#[test]
fn stack_renders_as_title_list_above_the_visible_pane() {
let mut zellij = start_zellij();
build_three_member_stack(&zellij);
let grid_snapshot = zellij.wait_until("stack list settled", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("Pane #1")
&& grid_snapshot.contains("Pane #2")
&& grid_snapshot.contains("[ Pane #3 ]")
&& grid_snapshot.contains("third-member")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn a_layout_stack_renders_as_a_title_list() {
let mut zellij = TestRunner::new(TERMINAL_SIZE)
.with_layout(LayoutInfo::Stringified(STACKED_LAYOUT.to_string()))
.start();
for _ in 0..3 {
let member = zellij.expect_pty_spawn();
member.output(PROMPT);
}
let grid_snapshot = zellij.wait_until("layout stack rendered as a list", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("[ Pane #1 ]")
&& grid_snapshot.contains("Pane #2")
&& grid_snapshot.contains("Pane #3")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn disabling_stacked_pane_list_restores_the_classic_stack() {
let mut zellij = TestRunner::new(TERMINAL_SIZE)
.with_config("stacked_pane_list false")
.start();
let first_member = claim_first_terminal_and_wait_for_prompt(&zellij);
first_member.output(b"first-member\r\n$ ");
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('s'));
let second_member = zellij.expect_pty_spawn();
second_member.output(PROMPT);
let grid_snapshot = zellij.wait_until("classic stack rendered", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("Pane #1")
&& grid_snapshot.contains("Pane #2")
&& !grid_snapshot.contains("[ Pane #2 ]")
&& grid_snapshot.cursor_is_at(col(2).row(3))
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn up_and_down_move_the_selection_through_the_list() {
let mut zellij = start_zellij();
build_three_member_stack(&zellij);
zellij.send_stdin(&keys::alt('k'));
let middle_member_selected = zellij.wait_until("middle member swapped in", |grid_snapshot| {
grid_snapshot.contains("<g> LOCK")
&& grid_snapshot.contains("[ Pane #2 ]")
&& grid_snapshot.contains("second-member")
&& !grid_snapshot.contains("[ Pane #3 ]")
});
assert_snapshot!(normalized(&middle_member_selected));
zellij.send_stdin(&keys::alt('k'));
zellij.wait_until("top member swapped in", |grid_snapshot| {
grid_snapshot.contains("[ Pane #1 ]") && grid_snapshot.contains("first-member")
});
zellij.send_stdin(&keys::alt('j'));
zellij.wait_until("selection moved back down", |grid_snapshot| {
grid_snapshot.contains("[ Pane #2 ]") && grid_snapshot.contains("second-member")
});
zellij.quit();
}
#[test]
fn navigating_past_the_top_entry_leaves_the_stack() {
let mut zellij = start_zellij();
claim_first_terminal_and_wait_for_prompt(&zellij);
let _lower_terminal = split_down_and_wait_for_prompt(&zellij);
add_stacked_pane_and_wait_for_selected_entry(&zellij, "Pane #3");
add_stacked_pane_and_wait_for_selected_entry(&zellij, "Pane #4");
zellij.send_stdin(&keys::alt('k'));
zellij.wait_until("selection on the middle entry", |grid_snapshot| {
grid_snapshot.contains("[ Pane #3 ]")
});
zellij.send_stdin(&keys::alt('k'));
zellij.wait_until("selection on the top entry", |grid_snapshot| {
grid_snapshot.contains("[ Pane #2 ]")
});
zellij.send_stdin(&keys::alt('k'));
zellij.wait_until("focus left the stack to the pane above", |grid_snapshot| {
grid_snapshot.contains("[ Pane #2 ]") && grid_snapshot.cursor_is_at(col(2).row(2))
});
zellij.send_stdin(&keys::alt('j'));
zellij.wait_until("focus re-entered the stack", |grid_snapshot| {
grid_snapshot.contains("[ Pane #2 ]") && grid_snapshot.cursor_is_at(col(2).row(15))
});
zellij.quit();
}
#[test]
fn closing_the_visible_member_promotes_its_neighbor() {
let mut zellij = start_zellij();
build_three_member_stack(&zellij);
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('x'));
let grid_snapshot =
zellij.wait_until("third member closed, second promoted", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& !grid_snapshot.contains("Pane #3")
&& grid_snapshot.contains("[ Pane #2 ]")
&& grid_snapshot.contains("second-member")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn a_stack_list_dissolves_into_a_normal_pane_at_one_member() {
let mut zellij = start_zellij();
let first_member = claim_first_terminal_and_wait_for_prompt(&zellij);
first_member.output(b"first-member\r\n$ ");
add_stacked_pane_and_wait_for_selected_entry(&zellij, "Pane #2");
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('x'));
let grid_snapshot = zellij.wait_until("list dissolved to a single pane", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& !grid_snapshot.contains("Pane #2")
&& !grid_snapshot.contains("Pane #1")
&& grid_snapshot.contains("first-member")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn a_hidden_member_exiting_removes_its_entry() {
let mut zellij = start_zellij();
let [_first_member, second_member, _third_member] = build_three_member_stack(&zellij);
second_member.exit(Some(0));
let grid_snapshot = zellij.wait_until("hidden member removed from the list", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& !grid_snapshot.contains("Pane #2")
&& grid_snapshot.contains("Pane #1")
&& grid_snapshot.contains("[ Pane #3 ]")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn breaking_the_visible_member_out_promotes_and_moves_it_to_a_new_tab() {
let mut zellij = start_zellij();
build_three_member_stack(&zellij);
zellij.send_stdin(&keys::ctrl('t'));
zellij.send_stdin(&keys::key('b'));
zellij.wait_until("visible member moved to a new tab", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("Tab #2")
&& grid_snapshot.contains("third-member")
&& !grid_snapshot.contains("Pane #1")
});
zellij.send_stdin(&keys::alt('h'));
let grid_snapshot = zellij.wait_until("stack shrank to two members", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("Pane #1")
&& grid_snapshot.contains("[ Pane #2 ]")
&& !grid_snapshot.contains("Pane #3")
&& grid_snapshot.contains("second-member")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn floating_the_visible_member_promotes_its_neighbor() {
let mut zellij = start_zellij();
build_three_member_stack(&zellij);
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('e'));
let grid_snapshot = zellij.wait_until("member floated over the stack", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("[ Pane #2 ]")
&& grid_snapshot.contains("Pane #3")
&& grid_snapshot.contains("third-member")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn fullscreen_suspends_the_list_and_restores_it() {
let mut zellij = start_zellij();
build_three_member_stack(&zellij);
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('f'));
zellij.wait_until("stack member fullscreened", |grid_snapshot| {
grid_snapshot.contains("(FULLSCREEN)") && !grid_snapshot.contains("[ Pane #3 ]")
});
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('f'));
let grid_snapshot = zellij.wait_until("list restored after fullscreen", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& !grid_snapshot.contains("(FULLSCREEN)")
&& grid_snapshot.contains("Pane #1")
&& grid_snapshot.contains("Pane #2")
&& grid_snapshot.contains("[ Pane #3 ]")
&& grid_snapshot.contains("third-member")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn clicking_a_hidden_entry_swaps_it_in() {
let mut zellij = TestRunner::new(TERMINAL_SIZE)
.with_config("mouse_mode true")
.start();
build_three_member_stack(&zellij);
let grid_snapshot = zellij.wait_until("stack list rendered", |grid_snapshot| {
grid_snapshot.contains("Pane #1") && grid_snapshot.contains("[ Pane #3 ]")
});
let (entry_column, entry_line) = entry_click_coordinates(&grid_snapshot, "Pane #1");
zellij.send_stdin(&sgr_left_click(entry_column, entry_line));
let swapped_in = zellij.wait_until("clicked member swapped in", |grid_snapshot| {
grid_snapshot.contains("<g> LOCK")
&& grid_snapshot.contains("[ Pane #1 ]")
&& grid_snapshot.contains("first-member")
});
assert_snapshot!(normalized(&swapped_in));
zellij.quit();
}
#[test]
fn alt_clicking_a_hidden_entry_marks_it_without_selecting() {
let mut zellij = TestRunner::new(TERMINAL_SIZE)
.with_config("mouse_mode true\nadvanced_mouse_actions true")
.start();
build_three_member_stack(&zellij);
let grid_snapshot = zellij.wait_until("stack list rendered", |grid_snapshot| {
grid_snapshot.contains("Pane #1") && grid_snapshot.contains("[ Pane #3 ]")
});
let (entry_column, entry_line) = entry_click_coordinates(&grid_snapshot, "Pane #1");
zellij.send_stdin(&sgr_alt_left_click(entry_column, entry_line));
zellij.wait_until(
"hidden member marked, selection unchanged",
|grid_snapshot| {
grid_snapshot.contains("GROUP ACTIONS") && grid_snapshot.contains("[ Pane #3 ]")
},
);
zellij.quit();
}
#[test]
fn synced_input_reaches_hidden_members() {
let mut zellij = start_zellij();
let members = build_three_member_stack(&zellij);
zellij.send_stdin(&keys::ctrl('t'));
zellij.send_stdin(&keys::key('s'));
zellij.wait_until("tab marked as syncing", |grid_snapshot| {
grid_snapshot.status_bar_appears() && grid_snapshot.contains("SYNC")
});
zellij.send_stdin(&keys::ENTER);
zellij.send_stdin(b"synced-input");
for member in &members {
member.wait_for_stdin("synced input reached member", |stdin| {
stdin.windows(12).any(|window| window == b"synced-input")
});
}
zellij.quit();
}
#[test]
fn entries_share_a_uniform_width_sized_by_the_widest_title() {
let mut zellij = TestRunner::new(TERMINAL_SIZE)
.with_layout(LayoutInfo::Stringified(
STACK_WITH_COMMAND_MEMBER_LAYOUT.to_string(),
))
.start();
for _ in 0..2 {
let member = zellij.expect_pty_spawn();
member.output(PROMPT);
}
let grid_snapshot = zellij.wait_until("mixed-width entries rendered", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("member-command")
&& grid_snapshot.contains("[ Pane #1")
});
let lines = grid_snapshot.lines();
let selected_entry_line = lines
.iter()
.find(|line| line.contains("[ Pane #1"))
.expect("selected entry is on a header row");
let hidden_entry_line = lines
.iter()
.find(|line| line.contains("member-command"))
.expect("hidden entry is on a header row");
let selected_title_column =
display_column_of(selected_entry_line, "Pane #1").expect("selected title is on the line");
let hidden_title_column = display_column_of(hidden_entry_line, "member-command")
.expect("hidden title is on the line");
assert_eq!(selected_title_column, hidden_title_column);
let widest_title_width = "member-command".chars().count();
let closing_bracket_column =
display_column_of(selected_entry_line, "]").expect("selected entry has a closing bracket");
assert_eq!(
closing_bracket_column,
selected_title_column + widest_title_width + 1
);
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn a_hidden_held_members_exit_code_shows_on_its_entry() {
let mut zellij = TestRunner::new(TERMINAL_SIZE)
.with_layout(LayoutInfo::Stringified(
STACK_WITH_COMMAND_MEMBER_LAYOUT.to_string(),
))
.start();
let first_spawned = zellij.expect_pty_spawn();
let second_spawned = zellij.expect_pty_spawn();
let command_member = [&first_spawned, &second_spawned]
.into_iter()
.find(|terminal| match terminal.terminal_action() {
Some(TerminalAction::RunCommand(run_command)) => run_command
.command
.to_string_lossy()
.contains("member-command"),
_ => false,
})
.expect("one member runs a command")
.clone();
zellij.wait_until(
"stack list rendered with the command entry",
|grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("member-command")
&& grid_snapshot.contains("[ Pane #1")
},
);
command_member.exit(Some(2));
let grid_snapshot =
zellij.wait_until("exit code rendered on the hidden entry", |grid_snapshot| {
grid_snapshot.contains("EXIT CODE: 2") && grid_snapshot.contains("[ Pane #1")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn the_visible_members_scroll_state_shows_on_its_entry() {
let mut zellij = start_zellij();
claim_first_terminal_and_wait_for_prompt(&zellij);
let second_member = add_stacked_pane_and_wait_for_selected_entry(&zellij, "Pane #2");
let mut scrollback = String::new();
for line_number in 0..40 {
scrollback.push_str(&format!("line{}\r\n", line_number));
}
second_member.output(scrollback.as_bytes());
zellij.wait_until("scrollback filled", |grid_snapshot| {
grid_snapshot.contains("line39")
});
zellij.send_stdin(&keys::ctrl('s'));
zellij.send_stdin(&keys::ctrl('b'));
let grid_snapshot = zellij.wait_until("scroll indicator on the entry row", |grid_snapshot| {
grid_snapshot.contains("SCROLL: ")
&& !grid_snapshot.contains("SCROLL: 0/")
&& grid_snapshot.contains("[ Pane #2 ]")
&& grid_snapshot.contains("PgDn|PgUp")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn hidden_members_follow_whole_tab_resizes() {
let mut zellij = start_zellij();
let [first_member, second_member, third_member] = build_three_member_stack(&zellij);
zellij.resize(Size {
cols: 100,
rows: 20,
});
let (resized_cols, resized_rows) =
third_member.wait_for_size("visible member resized", |cols, _rows| cols <= 100);
for hidden_member in [&first_member, &second_member] {
hidden_member.wait_for_size("hidden member resized with the tab", |cols, rows| {
(cols, rows) == (resized_cols, resized_rows)
});
}
zellij.quit();
}
fn build_two_member_stack_with_scrollback_editor() -> (TestSession, FakePtyHandle) {
let zellij = TestRunner::new(TERMINAL_SIZE)
.with_config("scrollback_editor \"fake-editor\"")
.start();
let first_member = claim_first_terminal_and_wait_for_prompt(&zellij);
first_member.output(b"first-member\r\n$ ");
let second_member = add_stacked_pane_and_wait_for_selected_entry(&zellij, "Pane #2");
second_member.output(b"second-member\r\n$ ");
zellij.wait_until("second member content rendered", |grid_snapshot| {
grid_snapshot.contains("second-member")
});
zellij.send_stdin(&keys::ctrl('s'));
zellij.send_stdin(&keys::key('e'));
let editor = zellij.expect_pty_spawn();
editor.output(b"editor-open");
zellij.wait_until(
"scrollback editor opened on the visible member",
|grid_snapshot| grid_snapshot.contains("editor-open"),
);
(zellij, editor)
}
#[test]
fn a_scrollback_editor_follows_its_member_through_selection_changes() {
let (mut zellij, editor) = build_two_member_stack_with_scrollback_editor();
zellij.send_stdin(&keys::alt('k'));
zellij.wait_until("editor member swapped out", |grid_snapshot| {
grid_snapshot.contains("first-member") && !grid_snapshot.contains("editor-open")
});
zellij.send_stdin(&keys::alt('j'));
zellij.wait_until("editor member swapped back in", |grid_snapshot| {
grid_snapshot.contains("editor-open")
});
editor.exit(Some(0));
zellij.wait_until("editor closed back into its member", |grid_snapshot| {
grid_snapshot.contains("[ Pane #2 ]")
&& grid_snapshot.contains("second-member")
&& !grid_snapshot.contains("editor-open")
});
zellij.quit();
}
#[test]
fn a_hidden_scrollback_editor_pair_survives_fullscreen() {
let (mut zellij, editor) = build_two_member_stack_with_scrollback_editor();
zellij.send_stdin(&keys::alt('k'));
zellij.wait_until("editor member swapped out", |grid_snapshot| {
grid_snapshot.contains("first-member") && !grid_snapshot.contains("editor-open")
});
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('f'));
zellij.wait_until("member fullscreened over the list", |grid_snapshot| {
grid_snapshot.contains("(FULLSCREEN)")
});
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('f'));
zellij.wait_until("list re-formed after fullscreen", |grid_snapshot| {
grid_snapshot.contains("[ Pane #1") && grid_snapshot.contains("first-member")
});
zellij.send_stdin(&keys::alt('j'));
zellij.wait_until("editor member swapped back in", |grid_snapshot| {
grid_snapshot.contains("editor-open")
});
editor.exit(Some(0));
zellij.wait_until("editor closed back into its member", |grid_snapshot| {
grid_snapshot.contains("[ Pane #2 ]") && grid_snapshot.contains("second-member")
});
zellij.quit();
}
#[test]
fn closing_a_hidden_editor_member_substitutes_its_parked_pane() {
let (mut zellij, editor) = build_two_member_stack_with_scrollback_editor();
zellij.send_stdin(&keys::alt('k'));
zellij.wait_until("editor member swapped out", |grid_snapshot| {
grid_snapshot.contains("first-member") && !grid_snapshot.contains("editor-open")
});
editor.exit(Some(0));
zellij.wait_until("parked pane substituted on the entry", |grid_snapshot| {
grid_snapshot.contains("Pane #2") && !grid_snapshot.contains("fake-editor")
});
zellij.send_stdin(&keys::alt('j'));
zellij.wait_until("substituted member swapped in", |grid_snapshot| {
grid_snapshot.contains("[ Pane #2 ]") && grid_snapshot.contains("second-member")
});
zellij.quit();
}
#[test]
fn splitting_a_new_pane_reforms_the_list_around_it() {
let mut zellij = start_zellij();
build_three_member_stack(&zellij);
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('d'));
let new_pane = zellij.expect_pty_spawn();
new_pane.output(PROMPT);
let grid_snapshot = zellij.wait_until("list re-formed above the new pane", |grid_snapshot| {
grid_snapshot.contains("<g> LOCK")
&& grid_snapshot.contains("Pane #1")
&& grid_snapshot.contains("Pane #2")
&& grid_snapshot.contains("[ Pane #3 ]")
&& grid_snapshot.contains("Pane #4")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn resizing_the_visible_member_keeps_the_list() {
let mut zellij = start_zellij();
let first_member = claim_first_terminal_and_wait_for_prompt(&zellij);
first_member.output(b"first-member\r\n$ ");
let _lower_terminal = split_down_and_wait_for_prompt(&zellij);
add_stacked_pane_and_wait_for_selected_entry(&zellij, "Pane #3");
let before_resize = zellij.wait_until("list settled before resize", |grid_snapshot| {
grid_snapshot.contains("<g> LOCK") && grid_snapshot.contains("[ Pane #3 ]")
});
let entry_row_before = before_resize
.lines()
.iter()
.position(|line| line.contains("[ Pane #3 ]"))
.expect("selected entry is on a header row");
zellij.send_stdin(&keys::ctrl('n'));
zellij.send_stdin(&keys::key('k'));
zellij.send_stdin(&keys::ctrl('n'));
let grid_snapshot = zellij.wait_until("stack region grew upward", move |grid_snapshot| {
grid_snapshot.contains("<g> LOCK")
&& grid_snapshot.contains("Pane #2")
&& grid_snapshot
.lines()
.iter()
.position(|line| line.contains("[ Pane #3 ]"))
.map_or(false, |entry_row| entry_row < entry_row_before)
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn moving_the_visible_member_reorders_the_list() {
let mut zellij = start_zellij();
let first_member = claim_first_terminal_and_wait_for_prompt(&zellij);
first_member.output(b"first-member\r\n$ ");
let _lower_terminal = split_down_and_wait_for_prompt(&zellij);
let third_member = add_stacked_pane_and_wait_for_selected_entry(&zellij, "Pane #3");
third_member.output(b"third-member\r\n$ ");
zellij.wait_until("third member content rendered", |grid_snapshot| {
grid_snapshot.contains("third-member")
});
zellij.send_stdin(&keys::ctrl('h'));
zellij.send_stdin(&keys::key('k'));
zellij.send_stdin(&keys::ctrl('h'));
let grid_snapshot =
zellij.wait_until("visible member moved above its neighbor", |grid_snapshot| {
let lines = grid_snapshot.lines();
let row_of = |needle: &str| lines.iter().position(|line| line.contains(needle));
grid_snapshot.contains("<g> LOCK")
&& grid_snapshot.contains("third-member")
&& matches!(
(row_of("[ Pane #3 ]"), row_of("Pane #2")),
(Some(selected_row), Some(hidden_row)) if selected_row < hidden_row
)
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
#[test]
fn a_stack_list_survives_session_resurrection() {
let mut zellij = TestRunner::new(TERMINAL_SIZE)
.with_config("session_serialization true")
.start();
build_three_member_stack(&zellij);
zellij.save_session();
zellij.wait_for_serialized_session();
zellij.quit();
zellij.resurrect(TERMINAL_SIZE);
for _ in 0..3 {
let member = zellij.expect_pty_spawn();
member.output(PROMPT);
}
let grid_snapshot = zellij.wait_until("stack list resurrected", |grid_snapshot| {
grid_snapshot.status_bar_appears()
&& grid_snapshot.contains("Pane #1")
&& grid_snapshot.contains("Pane #2")
&& grid_snapshot.contains("[ Pane #3 ]")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
}
+13 -3
View File
@@ -308,11 +308,21 @@ fn undo_rename_tab() {
zellij.send_stdin(&keys::ctrl('t'));
zellij.send_stdin(&keys::key('r'));
zellij.send_stdin(b"aa");
zellij.send_stdin(&keys::ESC);
zellij.send_stdin(&keys::ESC);
zellij.wait_until("typed name applied in rename mode", |grid_snapshot| {
grid_snapshot.contains("aa") && grid_snapshot.contains("RENAMING TAB")
});
zellij.send_stdin(&keys::ESC);
zellij.wait_until("rename undone, back in tab mode", |grid_snapshot| {
grid_snapshot.contains("Tab #1") && !grid_snapshot.contains("aa")
});
zellij.send_stdin(&keys::ESC);
let grid_snapshot = zellij.wait_until("tab name reverted to default", |grid_snapshot| {
grid_snapshot.contains("Tab #1") && grid_snapshot.contains("LOCK")
grid_snapshot.contains("Tab #1")
&& !grid_snapshot.contains("aa")
&& grid_snapshot.contains("LOCK")
});
assert_snapshot!(normalized(&grid_snapshot));
zellij.quit();
@@ -43,7 +43,7 @@ fn single_pane_scroll_shows_on_the_tab_line() {
grid_snapshot.text
);
zellij.send_stdin(&keys::ESC);
zellij.send_stdin(&keys::ENTER);
zellij.wait_until("back in the base mode after scrolling", |grid_snapshot| {
grid_snapshot.status_bar_appears()
});
+1
View File
@@ -427,6 +427,7 @@ impl SessionMetaData {
rounded_corners: new_config.ui.pane_frames.rounded_corners,
hide_session_name: new_config.ui.pane_frames.hide_session_name,
stacked_resize: new_config.options.stacked_resize.unwrap_or(true),
stacked_pane_list: new_config.options.stacked_pane_list.unwrap_or(true),
default_editor: new_config.options.scrollback_editor.clone(),
advanced_mouse_actions: new_config
.options
+6 -2
View File
@@ -428,10 +428,14 @@ impl Pane for PluginPane {
if self.borderless {
return Ok(None);
}
let frame_geom = self.current_geom();
let frame_geom = frame_params
.frame_geom_override
.unwrap_or_else(|| self.current_geom());
let grid = get_or_create_grid!(self, client_id);
let err_context = || format!("failed to render frame for client {client_id}");
let pane_title = if let Some(text_color_override) = self
let pane_title = if frame_params.blank_title {
String::new()
} else if let Some(text_color_override) = self
.pane_frame_color_override
.as_ref()
.and_then(|(_color, text)| text.as_ref())
+18 -11
View File
@@ -414,27 +414,24 @@ impl Pane for TerminalPane {
modifier_text.push(']');
}
format!("SEARCHING: {}{}", self.search_term, modifier_text)
} else if self.pane_name.is_empty() {
self.grid
.title
.clone()
.unwrap_or_else(|| self.pane_title.clone())
} else {
self.pane_name.clone()
self.current_title()
};
let pane_title = if let Some(text_color_override) = self
let pane_title = if frame_params.blank_title {
String::new()
} else if let Some(text_color_override) = self
.pane_frame_color_override
.as_ref()
.and_then(|(_color, text)| text.as_ref())
{
text_color_override.into()
} else if self.has_bell_notification {
format!("{} [!]", normal_title)
} else {
normal_title
self.title_with_bell_indicator(normal_title)
};
let frame_geom = self.current_geom();
let frame_geom = frame_params
.frame_geom_override
.unwrap_or_else(|| self.current_geom());
let is_pinned = frame_geom.is_pinned;
let mut frame = PaneFrame::new(
frame_geom.into(),
@@ -897,6 +894,9 @@ impl Pane for TerminalPane {
self.pane_name.to_owned()
}
}
fn stack_list_entry_label(&self) -> String {
self.title_with_bell_indicator(self.current_title())
}
fn custom_title(&self) -> Option<String> {
if self.pane_name.is_empty() {
None
@@ -1079,6 +1079,13 @@ impl Pane for TerminalPane {
}
impl TerminalPane {
fn title_with_bell_indicator(&self, title: String) -> String {
if self.has_bell_notification {
format!("{} [!]", title)
} else {
title
}
}
#[allow(clippy::too_many_arguments)]
pub fn new(
pid: u32,
+57 -3
View File
@@ -63,6 +63,8 @@ pub struct TiledPanes {
mode_info: Rc<RefCell<HashMap<ClientId, ModeInfo>>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
stacked_resize: Rc<RefCell<bool>>,
stacked_pane_list: Rc<RefCell<bool>>,
reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>>,
default_mode_info: ModeInfo,
style: Style,
session_is_mirrored: bool,
@@ -87,6 +89,8 @@ impl TiledPanes {
mode_info: Rc<RefCell<HashMap<ClientId, ModeInfo>>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
stacked_resize: Rc<RefCell<bool>>,
stacked_pane_list: Rc<RefCell<bool>>,
reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>>,
session_is_mirrored: bool,
pane_frame_style: PaneFrameStyle,
default_mode_info: ModeInfo,
@@ -103,6 +107,8 @@ impl TiledPanes {
mode_info,
character_cell_size,
stacked_resize,
stacked_pane_list,
reserved_top_rows,
default_mode_info,
style,
session_is_mirrored,
@@ -570,13 +576,40 @@ impl TiledPanes {
.positions_and_sizes_of_all_stacks()
.unwrap_or_else(|| Default::default())
};
let reserved_top_rows = if self.fullscreen_is_active.is_some() {
HashMap::new()
} else {
self.reserved_top_rows.borrow().clone()
};
for pane in self.panes.values_mut() {
if !pane.borderless() {
pane.set_frame(draws_full_frames);
}
let reserved_rows = reserved_top_rows.get(&pane.pid()).copied().unwrap_or(0);
#[allow(clippy::if_same_then_else)]
if draws_full_frames && !pane.borderless() {
if reserved_rows > 0 {
if draws_full_frames && !pane.borderless() {
pane.set_content_offset(Offset {
top: 1 + reserved_rows,
bottom: 1,
left: 1,
right: 1,
});
} else {
let position_and_size = pane.current_geom();
let (pane_columns_offset, pane_rows_offset) =
pane_content_offset(&position_and_size, &viewport);
let visible_member_title_rows = if draws_full_frames { 1 } else { 0 };
pane.set_content_offset(Offset {
top: visible_member_title_rows + reserved_rows,
bottom: pane_rows_offset,
left: 0,
right: pane_columns_offset,
});
}
} else if draws_full_frames && !pane.borderless() {
pane.set_content_offset(Offset::frame(1));
} else if draws_full_frames && pane.borderless() {
pane.set_content_offset(Offset::default());
@@ -1124,6 +1157,19 @@ impl TiledPanes {
&& help_text_visible.get(client_id).copied().unwrap_or(false)
}) && selectable_pane_count > 1
&& self.fullscreen_is_active.is_none();
let reserved_rows_for_pane = if self.fullscreen_is_active.is_some() {
0
} else {
{ self.reserved_top_rows.borrow().get(&pane.pid()).copied() }.unwrap_or(0)
};
let visible_member_frame_override = if reserved_rows_for_pane > 0 {
let mut geom = pane.current_geom();
geom.y += reserved_rows_for_pane;
geom.rows.decrease_inner(reserved_rows_for_pane);
Some(geom)
} else {
None
};
let mut pane_contents_and_ui = PaneContentsAndUi::new(
pane,
output,
@@ -1137,8 +1183,10 @@ impl TiledPanes {
&mouse_hover_pane_id,
current_pane_group.clone(),
show_help_text,
omit_pane_title,
omit_pane_title && reserved_rows_for_pane == 0,
);
pane_contents_and_ui.set_frame_geom_override(visible_member_frame_override);
pane_contents_and_ui.set_blank_title(reserved_rows_for_pane > 0);
for client_id in &connected_clients {
let client_mode = self
.mode_info
@@ -1166,7 +1214,9 @@ impl TiledPanes {
pane_is_selectable,
)
.with_context(err_context)?;
} else if self.pane_frame_style.draws_titles() || pane_is_stacked {
} else if (self.pane_frame_style.draws_titles() || pane_is_stacked)
&& reserved_rows_for_pane == 0
{
pane_contents_and_ui
.render_pane_frame(
*client_id,
@@ -1241,6 +1291,10 @@ impl TiledPanes {
pub fn get_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> {
self.panes.iter()
}
pub fn position_and_size_of_stack(&mut self, pane_id: &PaneId) -> Option<PaneGeom> {
StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide)
.position_and_size_of_stack(pane_id)
}
pub fn set_geom_for_pane_with_run(
&mut self,
run: Option<Run>,
@@ -1372,6 +1372,112 @@ Some(
"horizontal",
),
),
(
{
MinPanes(
3,
): TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: None,
borderless: None,
focus: None,
external_children_index: Some(
0,
),
children_are_stacked: true,
is_expanded_in_stack: false,
exclude_from_sync: None,
run_instructions_to_ignore: [],
hide_floating_panes: false,
pane_initial_contents: None,
default_fg: None,
default_bg: None,
},
],
split_size: None,
run: None,
borderless: None,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
run_instructions_to_ignore: [],
hide_floating_panes: false,
pane_initial_contents: None,
default_fg: None,
default_bg: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: Some(
Fixed(
1,
),
),
run: Some(
Plugin(
Alias(
PluginAlias {
name: "compact-bar",
configuration: Some(
PluginUserConfiguration(
{},
),
),
initial_cwd: None,
run_plugin: None,
},
),
),
),
borderless: Some(
true,
),
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
run_instructions_to_ignore: [],
hide_floating_panes: false,
pane_initial_contents: None,
default_fg: None,
default_bg: None,
},
],
split_size: None,
run: None,
borderless: None,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
run_instructions_to_ignore: [],
hide_floating_panes: false,
pane_initial_contents: None,
default_fg: None,
default_bg: None,
},
},
Some(
"stacked",
),
),
(
{
MinPanes(
@@ -1512,7 +1618,7 @@ Some(
},
},
Some(
"stacked",
"half-stacked",
),
),
],
+38 -10
View File
@@ -744,6 +744,7 @@ pub enum ScreenInstruction {
rounded_corners: bool,
hide_session_name: bool,
stacked_resize: bool,
stacked_pane_list: bool,
default_editor: Option<PathBuf>,
advanced_mouse_actions: bool,
mouse_hover_effects: bool,
@@ -1411,6 +1412,7 @@ pub(crate) struct Screen {
pixel_dimensions: PixelDimensions,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
stacked_resize: Rc<RefCell<bool>>,
stacked_pane_list: Rc<RefCell<bool>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
@@ -1549,6 +1551,7 @@ impl Screen {
layout_dir: Option<PathBuf>,
explicitly_disable_kitty_keyboard_protocol: bool,
stacked_resize: bool,
stacked_pane_list: bool,
default_editor: Option<PathBuf>,
web_clients_allowed: bool,
web_sharing: WebSharing,
@@ -1573,6 +1576,7 @@ impl Screen {
pixel_dimensions: Default::default(),
character_cell_size: Rc::new(RefCell::new(None)),
stacked_resize: Rc::new(RefCell::new(stacked_resize)),
stacked_pane_list: Rc::new(RefCell::new(stacked_pane_list)),
sixel_image_store: Rc::new(RefCell::new(SixelImageStore::default())),
style: client_attributes.style,
connected_clients: Rc::new(RefCell::new(HashMap::new())),
@@ -2022,6 +2026,9 @@ impl Screen {
.with_context(err_context)?;
}
destination_tab.set_force_render();
if destination_tab.has_stack_lists() {
destination_tab.set_should_clear_display_before_rendering();
}
destination_tab.visible(true).with_context(err_context)?;
}
Ok(())
@@ -3305,6 +3312,7 @@ impl Screen {
self.size,
self.character_cell_size.clone(),
self.stacked_resize.clone(),
self.stacked_pane_list.clone(),
self.sixel_image_store.clone(),
self.bus
.os_input
@@ -4604,12 +4612,13 @@ impl Screen {
) -> Result<()> {
let err_context = || "failed break pane out of tab".to_string();
let active_tab = self.get_active_tab_mut(client_id)?;
let active_pane_id = active_tab
.get_active_pane_id(client_id)
.with_context(err_context)?;
if active_tab.get_selectable_tiled_panes_count() > 1
|| active_tab.get_visible_selectable_floating_panes_count() > 0
|| active_tab.pane_is_stack_list_member(&active_pane_id)
{
let active_pane_id = active_tab
.get_active_pane_id(client_id)
.with_context(err_context)?;
let active_pane = active_tab
.extract_pane(active_pane_id, false)
.with_context(err_context)?;
@@ -4650,9 +4659,6 @@ impl Screen {
None,
))?;
} else {
let active_pane_id = active_tab
.get_active_pane_id(client_id)
.with_context(err_context)?;
self.bus
.senders
.send_to_background_jobs(BackgroundJob::DisplayPaneError(
@@ -5027,6 +5033,7 @@ impl Screen {
rounded_corners: bool,
hide_session_name: bool,
stacked_resize: bool,
stacked_pane_list: bool,
default_editor: Option<PathBuf>,
advanced_mouse_actions: bool,
mouse_hover_effects: bool,
@@ -5065,6 +5072,9 @@ impl Screen {
{
*self.stacked_resize.borrow_mut() = stacked_resize;
}
{
*self.stacked_pane_list.borrow_mut() = stacked_pane_list;
}
if let Some(copy_to_clipboard) = copy_to_clipboard {
self.copy_options.clipboard = copy_to_clipboard;
}
@@ -5081,6 +5091,7 @@ impl Screen {
tab.update_mouse_hover_effects(mouse_hover_effects);
tab.update_focus_follows_mouse(focus_follows_mouse);
tab.update_mouse_click_through(mouse_click_through);
tab.sync_stacked_pane_list_mode();
}
// Clear hover state when disabled
@@ -5527,6 +5538,7 @@ impl Screen {
);
}
let stack_list_geoms = tab.stack_list_serialization_geoms();
let tiled_panes: Vec<PaneLayoutMetadata> = tab
.get_tiled_panes()
.map(|(pane_id, p)| {
@@ -5535,14 +5547,21 @@ impl Screen {
// is currently only the case the scrollback editing panes, and
// when dumping the layout we want the "real" pane and not the
// editor pane
match suppressed_panes.remove(pane_id) {
let geom_override = stack_list_geoms.get(pane_id).copied();
let (pane_id, p) = match suppressed_panes.remove(pane_id) {
Some((is_scrollback_editor, suppressed_pane)) if *is_scrollback_editor => {
(suppressed_pane.pid(), suppressed_pane)
},
_ => (*pane_id, p),
}
};
(
pane_id,
p,
geom_override.unwrap_or_else(|| p.position_and_size()),
)
})
.map(|(pane_id, p)| {
.chain(tab.hidden_stack_list_members_for_serialization())
.map(|(pane_id, p, geom)| {
let focused_clients: Vec<ClientId> = active_pane_ids
.iter()
.filter_map(|(c_id, p_id)| {
@@ -5552,7 +5571,7 @@ impl Screen {
let (default_fg, default_bg) = p.get_pane_default_colors();
PaneLayoutMetadata::new(
pane_id,
p.position_and_size(),
geom,
p.borderless(),
p.invoked_with().clone(),
p.custom_title(),
@@ -6076,6 +6095,11 @@ fn find_already_running_panes(
let running_tiled_instructions: Vec<Option<Run>> = active_tab
.get_tiled_panes()
.map(|(_, pane)| pane.invoked_with().clone())
.chain(
active_tab
.suppressed_stack_list_members()
.map(|(_, pane)| pane.invoked_with().clone()),
)
.collect();
let mut tiled_to_ignore = Vec::new();
@@ -6191,6 +6215,7 @@ pub(crate) fn screen_thread_main(
.unwrap_or(false); // by default, we try to support this if the terminal supports it and
// the program running inside a pane requests it
let stacked_resize = config_options.stacked_resize.unwrap_or(true);
let stacked_pane_list = config_options.stacked_pane_list.unwrap_or(true);
let web_clients_allowed = config_options
.web_sharing
.map(|s| s.web_clients_allowed())
@@ -6234,6 +6259,7 @@ pub(crate) fn screen_thread_main(
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
stacked_resize,
stacked_pane_list,
default_editor,
web_clients_allowed,
web_sharing,
@@ -9379,6 +9405,7 @@ pub(crate) fn screen_thread_main(
rounded_corners,
hide_session_name,
stacked_resize,
stacked_pane_list,
default_editor,
advanced_mouse_actions,
mouse_hover_effects,
@@ -9403,6 +9430,7 @@ pub(crate) fn screen_thread_main(
rounded_corners,
hide_session_name,
stacked_resize,
stacked_pane_list,
default_editor,
advanced_mouse_actions,
mouse_hover_effects,
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -417,9 +417,10 @@ impl MouseHandler {
client_id: ClientId,
) -> Option<ClickedPaneDetails> {
let is_floating = tab.floating_panes.panes_contain(&pane_id);
let is_hidden_stack_list_member = tab.pane_is_hidden_stack_list_member(&pane_id);
let pane = Self::get_pane_at(tab, position, false).ok()??;
let on_frame = pane.position_is_on_frame(position);
let on_frame = !is_hidden_stack_list_member && pane.position_is_on_frame(position);
let frame_intercepted = on_frame && pane.intercept_mouse_event_on_frame(event, client_id);
let edge = if on_frame {
pane.get_edge_at_position(position)
@@ -1053,6 +1054,10 @@ impl MouseHandler {
return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id);
}
if tab.pane_is_hidden_stack_list_member(&pane_id) {
return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id);
}
let active_pane_id = tab.get_active_pane_id(client_id);
if active_pane_id == Some(pane_id) {
return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id);
@@ -1543,7 +1548,9 @@ impl MouseHandler {
}
}
if let Some(clicked_pane) = tab.get_pane_id_at(point, true).with_context(err_context)? {
tab.tiled_panes.focus_pane(clicked_pane, client_id);
if !tab.focus_hidden_stack_list_member(clicked_pane, client_id) {
tab.tiled_panes.focus_pane(clicked_pane, client_id);
}
tab.set_pane_active_at(clicked_pane);
if tab.floating_panes.panes_are_visible() {
tab.hide_floating_panes();
@@ -1737,7 +1744,7 @@ impl MouseHandler {
.get_pane_id_at(point, search_selectable)
.with_context(err_context)?
{
Ok(tab.tiled_panes.get_pane_mut(pane_id))
Ok(tab.get_pane_with_id_mut(pane_id))
} else {
Ok(None)
}
@@ -187,6 +187,8 @@ fn create_layout_applier_fixtures(
let connected_clients_set = Rc::new(RefCell::new(HashSet::from([client_id])));
let mode_info = Rc::new(RefCell::new(HashMap::new()));
let stacked_resize = Rc::new(RefCell::new(false));
let stacked_pane_list = Rc::new(RefCell::new(false));
let reserved_top_rows = Rc::new(RefCell::new(HashMap::new()));
let session_is_mirrored = true;
let draw_pane_frames = PaneFrameStyle::Full;
let default_mode_info = ModeInfo::default();
@@ -199,6 +201,8 @@ fn create_layout_applier_fixtures(
mode_info.clone(),
character_cell_size.clone(),
stacked_resize,
stacked_pane_list,
reserved_top_rows,
session_is_mirrored,
draw_pane_frames,
default_mode_info.clone(),
@@ -317,6 +321,8 @@ fn create_layout_applier_fixtures_with_receivers(
let connected_clients_set = Rc::new(RefCell::new(HashSet::from([client_id])));
let mode_info = Rc::new(RefCell::new(HashMap::new()));
let stacked_resize = Rc::new(RefCell::new(false));
let stacked_pane_list = Rc::new(RefCell::new(false));
let reserved_top_rows = Rc::new(RefCell::new(HashMap::new()));
let session_is_mirrored = true;
let draw_pane_frames = PaneFrameStyle::Full;
let default_mode_info = ModeInfo::default();
@@ -329,6 +335,8 @@ fn create_layout_applier_fixtures_with_receivers(
mode_info.clone(),
character_cell_size.clone(),
stacked_resize,
stacked_pane_list,
reserved_top_rows,
session_is_mirrored,
draw_pane_frames,
default_mode_info.clone(),
@@ -246,6 +246,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -334,6 +335,7 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -437,6 +439,7 @@ fn create_new_tab_with_swap_layouts(
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -545,6 +548,7 @@ fn create_new_tab_with_os_api(
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -635,6 +639,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -739,6 +744,7 @@ fn create_new_tab_with_mock_pty_writer(
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -834,6 +840,7 @@ fn create_new_tab_with_sixel_support(
size,
character_cell_size,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -12727,6 +12734,7 @@ fn create_new_tab_with_plugin_receiver(
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -14512,6 +14520,7 @@ fn create_new_tab_with_server_receiver(
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
+3
View File
@@ -188,6 +188,7 @@ fn create_new_tab(size: Size, stacked_resize: bool) -> Tab {
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -275,6 +276,7 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab {
size,
character_cell_info,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -368,6 +370,7 @@ fn create_new_tab_with_cell_size(
size,
character_cell_size,
stacked_resize,
Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
+146 -32
View File
@@ -1,5 +1,7 @@
use crate::output::CharacterChunk;
use crate::panes::{AnsiCode, RcCharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER};
use crate::panes::{
AnsiCode, CharacterStyles, RcCharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
};
use crate::ui::boundaries::boundary_type;
use crate::ui::hint_text::{
exit_code_segments, hover_segments, rerun_segments, HintExitStatus, HintLevel, HintSegment,
@@ -8,45 +10,53 @@ use crate::ui::hint_text::{
use crate::ClientId;
use zellij_utils::data::{client_id_to_colors, PaletteColor, Style};
use zellij_utils::errors::prelude::*;
use zellij_utils::pane_size::{Offset, Viewport};
use zellij_utils::pane_size::{Offset, PaneGeom, Viewport};
use zellij_utils::position::Position;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
fn styled_characters(
characters: &str,
style_character: impl Fn(&mut CharacterStyles),
) -> Vec<TerminalCharacter> {
characters
.chars()
.map(|character| {
let mut styles = RcCharacterStyles::reset();
styles.update(|styles| style_character(styles));
TerminalCharacter::new_styled(character, styles)
})
.collect()
}
fn foreground_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
let mut colored_string = Vec::new();
for character in characters.chars() {
let mut styles = RcCharacterStyles::reset();
styles.update(|styles| {
styles.bold = Some(AnsiCode::On);
match color {
Some(palette_color) => {
styles.foreground = Some(AnsiCode::from(palette_color));
},
None => {},
}
});
let terminal_character = TerminalCharacter::new_styled(character, styles);
colored_string.push(terminal_character);
}
colored_string
styled_characters(characters, |styles| {
styles.bold = Some(AnsiCode::On);
if let Some(palette_color) = color {
styles.foreground = Some(AnsiCode::from(palette_color));
}
})
}
fn dimmed_foreground_color(
characters: &str,
color: Option<PaletteColor>,
) -> Vec<TerminalCharacter> {
styled_characters(characters, |styles| {
styles.dim = Some(AnsiCode::On);
if let Some(palette_color) = color {
styles.foreground = Some(AnsiCode::from(palette_color));
}
})
}
fn background_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
let mut colored_string = Vec::new();
for character in characters.chars() {
let mut styles = RcCharacterStyles::reset();
styles.update(|styles| match color {
Some(palette_color) => {
styles.background = Some(AnsiCode::from(palette_color));
styles.bold(Some(AnsiCode::On));
},
None => {},
});
let terminal_character = TerminalCharacter::new_styled(character, styles);
colored_string.push(terminal_character);
}
colored_string
styled_characters(characters, |styles| {
if let Some(palette_color) = color {
styles.background = Some(AnsiCode::from(palette_color));
styles.bold(Some(AnsiCode::On));
}
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -55,6 +65,14 @@ enum ExitStatus {
Exited,
}
#[derive(Clone, PartialEq)]
pub struct StackListEntry {
pub width: usize,
pub label: String,
pub is_selected: bool,
pub is_emphasized: bool,
}
pub struct FrameParams {
pub focused_client: Option<ClientId>,
pub is_main_client: bool, // more accurately: is_focused_for_main_client
@@ -73,6 +91,9 @@ pub struct FrameParams {
pub show_help_text: bool,
pub highlight_tooltip: Option<String>,
pub omit_title: bool,
pub frame_geom_override: Option<PaneGeom>,
pub stack_list_entry: Option<StackListEntry>,
pub blank_title: bool,
}
#[derive(Default, PartialEq)]
@@ -100,6 +121,8 @@ pub struct PaneFrame {
show_help_text: bool,
highlight_tooltip: Option<String>,
omit_title: bool,
stack_list_entry: Option<StackListEntry>,
color_override: Option<PaletteColor>,
}
impl PaneFrame {
@@ -133,6 +156,8 @@ impl PaneFrame {
show_help_text: frame_params.show_help_text,
highlight_tooltip: frame_params.highlight_tooltip,
omit_title: frame_params.omit_title,
stack_list_entry: frame_params.stack_list_entry,
color_override: None,
}
}
pub fn is_pinned(mut self, is_pinned: bool) -> Self {
@@ -150,6 +175,7 @@ impl PaneFrame {
}
pub fn override_color(&mut self, color: PaletteColor) {
self.color = Some(color);
self.color_override = Some(color);
}
fn client_cursor(&self, client_id: ClientId) -> Vec<TerminalCharacter> {
let color = client_id_to_colors(client_id, self.style.colors.multiplayer_user_colors);
@@ -715,6 +741,86 @@ impl PaneFrame {
.or_else(|| Some(self.title_line_without_middle()))
.with_context(|| format!("failed to render title '{}'", self.title))
}
fn render_stack_list_entry(&self, entry: &StackListEntry) -> Vec<TerminalCharacter> {
let usable_cols = self.geom.cols.saturating_sub(2);
let bracket_overhead = "[ ]".width();
let inner_width = entry
.width
.min(usable_cols.saturating_sub(bracket_overhead));
let content = if entry.label.width() <= inner_width {
entry.label.clone()
} else {
let truncation_budget = inner_width.saturating_sub(1);
let mut truncated = String::new();
for character in entry.label.chars() {
if truncated.width() + character.width().unwrap_or(0) > truncation_budget {
break;
}
truncated.push(character);
}
truncated.push('…');
truncated
};
let padding = " ".repeat(inner_width.saturating_sub(content.width()));
let padded_content = format!("{}{}", content, padding);
let left_bracket = "[ ";
let right_bracket = " ]";
let entry_length = left_bracket.width() + padded_content.width() + right_bracket.width();
let entry_start = usable_cols.saturating_sub(entry_length) / 2;
let selection_sign = "<↓↑> ";
let selection_sign_width = if entry.is_selected && entry_start >= selection_sign.width() {
selection_sign.width()
} else {
0
};
let left_budget = entry_start.saturating_sub(selection_sign_width);
let (mut focus_part, focus_length) = self
.bracketed_focus_indicator(left_budget)
.unwrap_or((vec![], 0));
let mut line = Vec::with_capacity(usable_cols);
line.append(&mut focus_part);
for _ in focus_length..left_budget {
line.push(EMPTY_TERMINAL_CHARACTER);
}
if selection_sign_width > 0 {
line.append(&mut foreground_color(selection_sign, self.color));
}
let content_color = if entry.is_emphasized {
self.color
} else {
self.color_override
};
if entry.is_selected {
line.append(&mut foreground_color(left_bracket, self.color_override));
line.append(&mut foreground_color(&padded_content, self.color));
line.append(&mut foreground_color(right_bracket, self.color_override));
} else {
let unbracketed = format!(" {} ", padded_content);
if entry.is_emphasized {
line.append(&mut foreground_color(&unbracketed, content_color));
} else {
line.append(&mut dimmed_foreground_color(&unbracketed, content_color));
}
}
let mut occupied_columns = entry_start + entry_length;
let (mut scroll_part, scroll_length) = self
.bracketed_scroll_indicator(usable_cols.saturating_sub(occupied_columns))
.unwrap_or((vec![], 0));
if self.exit_status.is_some() {
let (mut exit_part, exit_length) = self.first_exited_held_title_part_full();
if occupied_columns + 1 + exit_length + scroll_length <= usable_cols {
line.push(EMPTY_TERMINAL_CHARACTER);
line.append(&mut exit_part);
occupied_columns += 1 + exit_length;
}
}
while occupied_columns < usable_cols.saturating_sub(scroll_length) {
line.push(EMPTY_TERMINAL_CHARACTER);
occupied_columns += 1;
}
line.append(&mut scroll_part);
line
}
fn render_one_line_title(&self) -> Result<Vec<TerminalCharacter>> {
if self.should_draw_pane_frames {
let total_title_length = self.geom.cols.saturating_sub(2);
@@ -1168,6 +1274,14 @@ impl PaneFrame {
if self.omit_title {
return Ok((character_chunks, None));
}
if let Some(entry) = &self.stack_list_entry {
character_chunks.push(CharacterChunk::new(
self.render_stack_list_entry(entry),
self.geom.x + 1,
self.geom.y,
));
return Ok((character_chunks, None));
}
if self.geom.rows == 1 || !self.should_draw_pane_frames {
// we do this explicitly when not drawing pane frames because this should only happen
// if this is a stacked pane with pane frames off (and it doesn't necessarily have only
+45 -1
View File
@@ -2,11 +2,12 @@ use crate::output::Output;
use crate::panes::PaneId;
use crate::tab::Pane;
use crate::ui::boundaries::Boundaries;
use crate::ui::pane_boundaries_frame::FrameParams;
use crate::ui::pane_boundaries_frame::{FrameParams, StackListEntry};
use crate::ClientId;
use std::collections::{HashMap, HashSet};
use zellij_utils::data::{client_id_to_colors, InputMode, PaletteColor, Style};
use zellij_utils::errors::prelude::*;
use zellij_utils::pane_size::PaneGeom;
pub struct PaneContentsAndUi<'a> {
pane: &'a mut Box<dyn Pane>,
output: &'a mut Output,
@@ -21,6 +22,10 @@ pub struct PaneContentsAndUi<'a> {
current_pane_group: HashMap<ClientId, Vec<PaneId>>,
show_help_text: bool,
omit_title: bool,
frame_geom_override: Option<PaneGeom>,
stack_list_entry_width: Option<usize>,
stack_list_entry_is_selected: bool,
blank_title: bool,
}
impl<'a> PaneContentsAndUi<'a> {
@@ -69,8 +74,26 @@ impl<'a> PaneContentsAndUi<'a> {
current_pane_group,
show_help_text,
omit_title,
frame_geom_override: None,
stack_list_entry_width: None,
stack_list_entry_is_selected: false,
blank_title: false,
}
}
pub fn set_frame_geom_override(&mut self, frame_geom_override: Option<PaneGeom>) {
self.frame_geom_override = frame_geom_override;
}
pub fn set_blank_title(&mut self, blank_title: bool) {
self.blank_title = blank_title;
}
pub fn set_stack_list_entry(
&mut self,
stack_list_entry_width: Option<usize>,
stack_list_entry_is_selected: bool,
) {
self.stack_list_entry_width = stack_list_entry_width;
self.stack_list_entry_is_selected = stack_list_entry_is_selected;
}
pub fn render_pane_contents_to_multiple_clients(
&mut self,
clients: impl Iterator<Item = ClientId>,
@@ -243,6 +266,21 @@ impl<'a> PaneContentsAndUi<'a> {
None
};
let pane_is_stacked = self.pane.current_geom().is_stacked();
let pane_is_in_group = self
.current_pane_group
.get(&client_id)
.map(|p| p.contains(&self.pane.pid()))
.unwrap_or(false);
let stack_list_entry = self.stack_list_entry_width.map(|width| StackListEntry {
width,
label: self.pane.stack_list_entry_label(),
is_selected: self.stack_list_entry_is_selected,
is_emphasized: pane_is_in_group
|| (self
.mouse_is_hovering_over_pane_for_clients
.contains(&client_id)
&& !pane_focused_for_client_id),
});
let frame_params = if session_is_mirrored {
FrameParams {
focused_client,
@@ -264,6 +302,9 @@ impl<'a> PaneContentsAndUi<'a> {
show_help_text: self.show_help_text,
highlight_tooltip: highlight_tooltip.clone(),
omit_title: self.omit_title,
frame_geom_override: self.frame_geom_override,
stack_list_entry: stack_list_entry.clone(),
blank_title: self.blank_title,
}
} else {
FrameParams {
@@ -286,6 +327,9 @@ impl<'a> PaneContentsAndUi<'a> {
show_help_text: self.show_help_text,
highlight_tooltip,
omit_title: self.omit_title,
frame_geom_override: self.frame_geom_override,
stack_list_entry,
blank_title: self.blank_title,
}
};
+6
View File
@@ -306,6 +306,7 @@ fn create_new_screen(
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
stacked_resize,
false,
None,
false,
web_sharing,
@@ -695,6 +696,7 @@ impl MockScreen {
let config_options = Options::default();
let mut config = Config::default();
config.options.pane_frame_style = Some(PaneFrameStyle::Full);
config.options.stacked_pane_list = Some(false);
let main_client_id = 1;
std::thread::Builder::new()
@@ -5425,6 +5427,7 @@ fn create_new_screen_with_message_capture(
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
stacked_resize,
false,
None,
false,
web_sharing,
@@ -8499,6 +8502,7 @@ fn create_new_screen_with_forward_capture(size: Size) -> (Screen, ForwardCapture
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
stacked_resize,
false,
None,
false,
web_sharing,
@@ -9082,6 +9086,7 @@ fn create_new_screen_with_theme_capture(size: Size) -> (Screen, ThemeCapture) {
None,
false,
true,
false,
None,
false,
web_sharing,
@@ -9561,6 +9566,7 @@ fn create_non_mirrored_screen(size: Size) -> Screen {
None, // layout_dir
false, // explicitly_disable_kitty_keyboard_protocol
true, // stacked_resize
false,
None,
false,
WebSharing::Off,
+5
View File
@@ -505,6 +505,11 @@ load_plugins {
//
// stacked_resize false
// Whether stacked panes display as a list with the expanded pane pinned to the bottom
// Default: true
//
// stacked_pane_list false
// Whether to show release notes on first version run
// Default: true
//
@@ -55,6 +55,12 @@ swap_tiled_layout name="horizontal" {
}
swap_tiled_layout name="stacked" {
ui min_panes=4 {
pane stacked=true { children; }
}
}
swap_tiled_layout name="half-stacked" {
ui min_panes=5 {
pane split_direction="vertical" {
pane
@@ -48,6 +48,12 @@ swap_tiled_layout name="horizontal" {
}
swap_tiled_layout name="stacked" {
ui min_panes=3 {
pane stacked=true { children; }
}
}
swap_tiled_layout name="half-stacked" {
ui min_panes=4 {
pane split_direction="vertical" {
pane
@@ -51,6 +51,12 @@ swap_tiled_layout name="horizontal" {
}
swap_tiled_layout name="stacked" {
ui min_panes=4 {
pane stacked=true { children; }
}
}
swap_tiled_layout name="half-stacked" {
ui min_panes=5 {
pane split_direction="vertical" {
pane
@@ -59,6 +59,12 @@ swap_tiled_layout name="horizontal" {
}
swap_tiled_layout name="stacked" {
ui min_panes=5 {
pane stacked=true { children; }
}
}
swap_tiled_layout name="half-stacked" {
ui min_panes=6 {
pane split_direction="vertical" {
pane
@@ -1981,6 +1981,8 @@ pub struct Options {
pub mobile_threshold_rows: ::core::option::Option<u32>,
#[prost(string, optional, tag="51")]
pub pane_frame_style: ::core::option::Option<::prost::alloc::string::String>,
#[prost(bool, optional, tag="52")]
pub stacked_pane_list: ::core::option::Option<bool>,
}
/// Pane-targeting action messages
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -1188,6 +1188,7 @@ message Options {
optional uint32 mobile_threshold_cols = 49;
optional uint32 mobile_threshold_rows = 50;
optional string pane_frame_style = 51;
optional bool stacked_pane_list = 52;
}
enum OnForceClose {
+8
View File
@@ -329,6 +329,10 @@ pub struct Options {
#[serde(default)]
pub stacked_resize: Option<bool>,
#[clap(long, value_parser)]
#[serde(default)]
pub stacked_pane_list: Option<bool>,
/// Whether to show startup tips when starting a new session
/// default is true
#[clap(long, value_parser)]
@@ -490,6 +494,7 @@ impl Options {
let web_server = other.web_server.or(self.web_server);
let web_sharing = other.web_sharing.or(self.web_sharing);
let stacked_resize = other.stacked_resize.or(self.stacked_resize);
let stacked_pane_list = other.stacked_pane_list.or(self.stacked_pane_list);
let show_startup_tips = other.show_startup_tips.or(self.show_startup_tips);
let show_release_notes = other.show_release_notes.or(self.show_release_notes);
let advanced_mouse_actions = other.advanced_mouse_actions.or(self.advanced_mouse_actions);
@@ -551,6 +556,7 @@ impl Options {
web_server,
web_sharing,
stacked_resize,
stacked_pane_list,
show_startup_tips,
show_release_notes,
advanced_mouse_actions,
@@ -633,6 +639,7 @@ impl Options {
let web_server = other.web_server.or(self.web_server);
let web_sharing = other.web_sharing.or(self.web_sharing);
let stacked_resize = other.stacked_resize.or(self.stacked_resize);
let stacked_pane_list = other.stacked_pane_list.or(self.stacked_pane_list);
let show_startup_tips = other.show_startup_tips.or(self.show_startup_tips);
let show_release_notes = other.show_release_notes.or(self.show_release_notes);
let advanced_mouse_actions = other.advanced_mouse_actions.or(self.advanced_mouse_actions);
@@ -694,6 +701,7 @@ impl Options {
web_server,
web_sharing,
stacked_resize,
stacked_pane_list,
show_startup_tips,
show_release_notes,
advanced_mouse_actions,
@@ -739,6 +739,7 @@ impl From<crate::input::options::Options>
crate::data::WebSharing::Disabled => ProtoWebSharing::Disabled as i32,
}),
stacked_resize: options.stacked_resize,
stacked_pane_list: options.stacked_pane_list,
show_startup_tips: options.show_startup_tips,
show_release_notes: options.show_release_notes,
advanced_mouse_actions: options.advanced_mouse_actions,
@@ -858,6 +859,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Options>
})
.transpose()?,
stacked_resize: options.stacked_resize,
stacked_pane_list: options.stacked_pane_list,
show_startup_tips: options.show_startup_tips,
show_release_notes: options.show_release_notes,
advanced_mouse_actions: options.advanced_mouse_actions,
@@ -468,6 +468,7 @@ fn test_client_messages() {
web_server: Some(true),
web_sharing: Some(WebSharing::On),
stacked_resize: Some(true),
stacked_pane_list: Some(true),
show_startup_tips: Some(true),
show_release_notes: Some(true),
advanced_mouse_actions: Some(true),
+35
View File
@@ -2809,6 +2809,9 @@ impl Options {
};
let stacked_resize =
kdl_property_first_arg_as_bool_or_error!(kdl_options, "stacked_resize").map(|(v, _)| v);
let stacked_pane_list =
kdl_property_first_arg_as_bool_or_error!(kdl_options, "stacked_pane_list")
.map(|(v, _)| v);
let show_startup_tips =
kdl_property_first_arg_as_bool_or_error!(kdl_options, "show_startup_tips")
.map(|(v, _)| v);
@@ -2938,6 +2941,7 @@ impl Options {
web_server,
web_sharing,
stacked_resize,
stacked_pane_list,
show_startup_tips,
show_release_notes,
advanced_mouse_actions,
@@ -4069,6 +4073,34 @@ impl Options {
None
}
}
fn stacked_pane_list_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
let comment_text = format!(
"{}\n{}\n{}\n{}",
" ",
"// Whether stacked panes display as a list with the expanded pane pinned to the bottom",
"// Default: true",
"// ",
);
let create_node = |node_value: bool| -> KdlNode {
let mut node = KdlNode::new("stacked_pane_list");
node.push(KdlValue::Bool(node_value));
node
};
if let Some(stacked_pane_list) = self.stacked_pane_list {
let mut node = create_node(stacked_pane_list);
if add_comments {
node.set_leading(format!("{}\n", comment_text));
}
Some(node)
} else if add_comments {
let mut node = create_node(false);
node.set_leading(format!("{}\n// ", comment_text));
Some(node)
} else {
None
}
}
fn show_startup_tips_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
let comment_text = format!(
"{}\n{}\n{}\n{}",
@@ -4571,6 +4603,9 @@ impl Options {
if let Some(stacked_resize) = self.stacked_resize_to_kdl(add_comments) {
nodes.push(stacked_resize);
}
if let Some(stacked_pane_list) = self.stacked_pane_list_to_kdl(add_comments) {
nodes.push(stacked_pane_list);
}
if let Some(show_startup_tips) = self.show_startup_tips_to_kdl(add_comments) {
nodes.push(show_startup_tips);
}
@@ -537,6 +537,11 @@ web_client {
//
// stacked_resize false
// Whether stacked panes display as a list with the expanded pane pinned to the bottom
// Default: true
//
// stacked_pane_list false
// Whether to show tips on startup
// Default: true
//
@@ -255,6 +255,11 @@ web_sharing "disabled"
//
// stacked_resize false
// Whether stacked panes display as a list with the expanded pane pinned to the bottom
// Default: true
//
// stacked_pane_list false
// Whether to show tips on startup
// Default: true
//
+2 -1
View File
@@ -703,7 +703,8 @@ fn stack_layout_from_manifest(
}
}
let mut stack_nodes = vec![];
for (_stack_id, stacked_panes) in children_stacks.into_iter() {
for (_stack_id, mut stacked_panes) in children_stacks.into_iter() {
stacked_panes.sort_by_key(|p| p.geom.y);
stack_nodes.push(TiledPaneLayout {
split_size,
children: stacked_panes
@@ -39,6 +39,7 @@ Options {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,
@@ -39,6 +39,7 @@ Options {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,
@@ -37,6 +37,7 @@ Options {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,
@@ -6015,6 +6015,7 @@ Config {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,
@@ -6015,6 +6015,7 @@ Config {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,
@@ -124,6 +124,7 @@ Config {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,
@@ -39,6 +39,7 @@ Options {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,
@@ -6015,6 +6015,7 @@ Config {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,
@@ -6015,6 +6015,7 @@ Config {
web_server: None,
web_sharing: None,
stacked_resize: None,
stacked_pane_list: None,
show_startup_tips: None,
show_release_notes: None,
advanced_mouse_actions: None,