mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-12 02:27:24 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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
-3
@@ -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 ...
|
||||
|
||||
+9
@@ -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 ...
|
||||
+10
@@ -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 ...
|
||||
+10
@@ -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 ...
|
||||
+8
@@ -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 ...
|
||||
+10
@@ -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 ...
|
||||
+10
@@ -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 ...
|
||||
+11
@@ -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 ...
|
||||
+10
@@ -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 ...
|
||||
+9
@@ -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 ...
|
||||
+9
@@ -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 ...
|
||||
+21
@@ -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 ...
|
||||
+11
@@ -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 ...
|
||||
+14
@@ -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 ...
|
||||
+13
@@ -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 ...
|
||||
+14
@@ -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 ...
|
||||
+11
@@ -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 ...
|
||||
+28
@@ -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
|
||||
+11
@@ -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();
|
||||
}
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
+107
-1
@@ -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
@@ -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,
|
||||
|
||||
+863
-16
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+5
@@ -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
|
||||
//
|
||||
|
||||
+5
@@ -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
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user