aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server/src/panes/unit
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-07-27 18:24:37 +0200
committerGitHub <noreply@github.com>2026-07-27 18:24:37 +0200
commitea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d (patch)
tree4a8fac8a9b27f2a8b32b3af61a3b5419b9937c81 /zellij-server/src/panes/unit
parentSwitch from the unmaintained daemonize crate to a maintained fork (#4512) (diff)
downloadsl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.tar.gz
sl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.tar.xz
sl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.zip
Nested session handling (#5417)
* fix: theme query and updated pixel dimensions * add no-ui-fullscreen key/action/capability * protocol core * integration test harness * fix integration tests * fix flaky tests * nested ui (dimming) and directional focus initial implementation * integration tests for nested ui dimming and directional focus * initial full screen nesteed implementation * fix: keep hidden floating panes hidden across nested no-ui fullscreen * fix integration tests * no ui fullscreen integration tests * hardening behavior/tests * fix(nested): re-derive descend/dim on host tab switch and new-tab so clicking dimmed chrome ascends * initial modal implementation * initial ui improvements * fix integration tests * improve nested selection ui * nested session modal refactoring and tests * improve integration test speed somewhat * some ui adjustments * simplify full/auto nested session handling * hints in compact-bar * guest stdin leak fix * allow floating panes to be fullscreen * make nested session handling configurable * protocol reannounce * refactor: naming confusion * improve ux somewhat * rustfmt * merge fixes * improve integration test rate limiting (fix ci) * add pr
Diffstat (limited to 'zellij-server/src/panes/unit')
-rw-r--r--zellij-server/src/panes/unit/grid_tests.rs182
-rw-r--r--zellij-server/src/panes/unit/nested_session_modal_tests.rs360
-rw-r--r--zellij-server/src/panes/unit/terminal_pane_tests.rs147
3 files changed, 689 insertions, 0 deletions
diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs
index e21ea867..2029a4b4 100644
--- a/zellij-server/src/panes/unit/grid_tests.rs
+++ b/zellij-server/src/panes/unit/grid_tests.rs
@@ -6289,3 +6289,185 @@ fn csi_5n_status_query_still_handled_locally() {
"DSR 5 must still produce its local 'all good' reply"
);
}
+
+fn new_grid_for_nested_frames() -> Grid {
+ let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
+ let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
+ Grid::new(
+ 41,
+ 110,
+ Rc::new(RefCell::new(Palette::default())),
+ terminal_emulator_color_codes,
+ Rc::new(RefCell::new(LinkHandler::new())),
+ Rc::new(RefCell::new(None)),
+ sixel_image_store,
+ Style::default(),
+ false,
+ true,
+ true,
+ true,
+ false,
+ )
+}
+
+fn nested_announce_message() -> zellij_utils::nested_session::NestedSessionMessage {
+ zellij_utils::nested_session::NestedSessionMessage::Announce {
+ session_name: "guest-session".to_owned(),
+ capabilities: vec![zellij_utils::nested_session::NestedSessionCapability::NestedControl],
+ }
+}
+
+#[test]
+fn nested_session_frame_is_staged_for_dispatch() {
+ let mut vte_parser = vte::Parser::new();
+ let mut grid = new_grid_for_nested_frames();
+ let message = nested_announce_message();
+ for byte in zellij_utils::nested_session::encode_frame(&message) {
+ vte_parser.advance(&mut grid, byte);
+ }
+ assert_eq!(grid.pending_nested_session_messages, vec![message]);
+}
+
+#[test]
+fn nested_session_frame_split_across_feeds_is_staged() {
+ let mut vte_parser = vte::Parser::new();
+ let mut grid = new_grid_for_nested_frames();
+ let message = nested_announce_message();
+ let frame = zellij_utils::nested_session::encode_frame(&message);
+ let (first_half, second_half) = frame.split_at(frame.len() / 2);
+ for byte in first_half {
+ vte_parser.advance(&mut grid, *byte);
+ }
+ assert!(grid.pending_nested_session_messages.is_empty());
+ for byte in second_half {
+ vte_parser.advance(&mut grid, *byte);
+ }
+ assert_eq!(grid.pending_nested_session_messages, vec![message]);
+}
+
+#[test]
+fn dcs_with_wrong_magic_param_is_not_staged() {
+ let mut vte_parser = vte::Parser::new();
+ let mut grid = new_grid_for_nested_frames();
+ for byte in b"\x1bP26662nAAAA\x1b\\" {
+ vte_parser.advance(&mut grid, *byte);
+ }
+ assert!(grid.pending_nested_session_messages.is_empty());
+}
+
+#[test]
+fn dcs_with_intermediates_is_not_staged() {
+ let mut vte_parser = vte::Parser::new();
+ let mut grid = new_grid_for_nested_frames();
+ for byte in b"\x1bP26661$nAAAA\x1b\\" {
+ vte_parser.advance(&mut grid, *byte);
+ }
+ assert!(grid.pending_nested_session_messages.is_empty());
+}
+
+#[test]
+fn dcs_n_without_params_is_not_staged() {
+ let mut vte_parser = vte::Parser::new();
+ let mut grid = new_grid_for_nested_frames();
+ for byte in b"\x1bPnAAAA\x1b\\" {
+ vte_parser.advance(&mut grid, *byte);
+ }
+ assert!(grid.pending_nested_session_messages.is_empty());
+}
+
+#[test]
+fn nested_session_frame_with_garbage_payload_is_dropped() {
+ let mut vte_parser = vte::Parser::new();
+ let mut grid = new_grid_for_nested_frames();
+ for byte in b"\x1bP26661n!!!not-base64!!!\x1b\\" {
+ vte_parser.advance(&mut grid, *byte);
+ }
+ assert!(grid.pending_nested_session_messages.is_empty());
+}
+
+fn serialize_text_bytes(text: &str) -> String {
+ text.as_bytes()
+ .iter()
+ .map(|byte| byte.to_string())
+ .collect::<Vec<_>>()
+ .join(",")
+}
+
+fn render_ui_component(component_name: &str, serialized_params: &str) -> Grid {
+ let mut vte_parser = vte::Parser::new();
+ let mut grid = new_grid_for_nested_frames();
+ let dcs = format!("\u{1b}Pz{};{}\u{1b}\\", component_name, serialized_params);
+ for byte in dcs.bytes() {
+ vte_parser.advance(&mut grid, byte);
+ }
+ grid
+}
+
+fn styled_cells_of(
+ grid: &Grid,
+ needle: char,
+) -> Vec<crate::panes::terminal_character::CharacterStyles> {
+ let mut styles = vec![];
+ for line in grid.as_character_lines() {
+ for terminal_character in line {
+ if terminal_character.character == needle {
+ styles.push(*terminal_character.styles);
+ }
+ }
+ }
+ styles
+}
+
+#[test]
+fn disabled_ribbon_renders_italic_non_bold_unselected() {
+ use crate::panes::terminal_character::AnsiCode;
+ let grid = render_ui_component("ribbon", &format!("d{}", serialize_text_bytes("RES")));
+ let letter_styles = styled_cells_of(&grid, 'R');
+ assert!(!letter_styles.is_empty());
+ for style in letter_styles {
+ assert_eq!(style.italic, Some(AnsiCode::On));
+ assert_ne!(style.bold, Some(AnsiCode::On));
+ }
+}
+
+#[test]
+fn disabled_text_with_opaque_flag_keeps_a_background() {
+ use crate::panes::terminal_character::AnsiCode;
+ let grid = render_ui_component("text", &format!("zd{}", serialize_text_bytes("RES")));
+ let letter_styles = styled_cells_of(&grid, 'R');
+ assert!(!letter_styles.is_empty());
+ for style in letter_styles {
+ assert_eq!(style.italic, Some(AnsiCode::On));
+ assert_ne!(style.bold, Some(AnsiCode::On));
+ assert!(style.background.is_some());
+ }
+}
+
+#[test]
+fn disabled_text_renders_italic_non_bold() {
+ use crate::panes::terminal_character::AnsiCode;
+ let grid = render_ui_component("text", &format!("d{}", serialize_text_bytes("RES")));
+ let letter_styles = styled_cells_of(&grid, 'R');
+ assert!(!letter_styles.is_empty());
+ for style in letter_styles {
+ assert_eq!(style.italic, Some(AnsiCode::On));
+ assert_ne!(style.bold, Some(AnsiCode::On));
+ }
+}
+
+#[test]
+fn ui_component_flag_prefixes_parse_order_independently() {
+ let baseline = render_ui_component("text", &format!("xzd{}", serialize_text_bytes("RES")));
+ let baseline_styles = styled_cells_of(&baseline, 'R');
+ for prefix in ["xdz", "zxd", "zdx", "dxz", "dzx"] {
+ let grid = render_ui_component(
+ "text",
+ &format!("{}{}", prefix, serialize_text_bytes("RES")),
+ );
+ assert_eq!(
+ styled_cells_of(&grid, 'R'),
+ baseline_styles,
+ "prefix {prefix} should parse the same flags as xzd"
+ );
+ }
+}
diff --git a/zellij-server/src/panes/unit/nested_session_modal_tests.rs b/zellij-server/src/panes/unit/nested_session_modal_tests.rs
new file mode 100644
index 00000000..4d0a14ee
--- /dev/null
+++ b/zellij-server/src/panes/unit/nested_session_modal_tests.rs
@@ -0,0 +1,360 @@
+use super::*;
+use zellij_utils::data::{PaletteColor, Style};
+
+fn style_with_distinct_selected_background() -> Style {
+ let mut style = Style::default();
+ style.colors.text_selected.background = PaletteColor::EightBit(200);
+ style.colors.text_selected.base = PaletteColor::EightBit(201);
+ style.colors.text_selected.emphasis_1 = PaletteColor::EightBit(202);
+ style.colors.text_selected.emphasis_3 = PaletteColor::EightBit(203);
+ style
+}
+
+fn sample_shortcuts() -> GuestModalShortcuts {
+ GuestModalShortcuts {
+ zoom: vec!["Ctrl g".to_string(), "o".to_string(), "f".to_string()],
+ ascend: vec!["Ctrl o".to_string()],
+ descend: vec!["Ctrl o".to_string(), "d".to_string()],
+ }
+}
+
+fn render_lines(chunks: &[CharacterChunk]) -> Vec<String> {
+ let mut lines: Vec<String> = Vec::new();
+ for chunk in chunks {
+ let text: String = chunk
+ .terminal_characters
+ .iter()
+ .map(|c| c.character)
+ .collect();
+ lines.push(text);
+ }
+ lines
+}
+
+fn rendered_text(chunks: &[CharacterChunk]) -> String {
+ chunks
+ .iter()
+ .flat_map(|chunk| chunk.terminal_characters.iter().map(|c| c.character))
+ .collect()
+}
+
+#[test]
+fn renders_all_expected_content() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+ let chunks = guest_modal_chunks(80, 30, 0, 0, &style, "my-session", 1, &shortcuts);
+ assert_eq!(chunks.len(), 30);
+ let rendered = rendered_text(&chunks);
+ assert!(rendered.contains("Nested Zellij session detected: my-session"));
+ assert!(rendered.contains("What would you like to do?"));
+ assert!(rendered.contains("Zoom in and control this session"));
+ assert!(rendered.contains("Control this session on focus"));
+ assert!(rendered.contains("<↓↑> select"));
+ assert!(rendered.contains("<Enter> confirm"));
+ assert!(rendered.contains("<Esc> dismiss"));
+}
+
+#[test]
+fn renders_resolved_keybindings_as_joined_tokens() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+ let chunks = guest_modal_chunks(80, 30, 0, 0, &style, "s", 0, &shortcuts);
+ let rendered = rendered_text(&chunks);
+ assert!(rendered.contains("<Ctrl g> + <o> + <f>"));
+ assert!(rendered.contains("<Ctrl o>"));
+}
+
+#[test]
+fn renders_unbound_when_no_keys() {
+ let style = Style::default();
+ let shortcuts = GuestModalShortcuts::default();
+ let chunks = guest_modal_chunks(80, 30, 0, 0, &style, "s", 0, &shortcuts);
+ let rendered = rendered_text(&chunks);
+ assert!(rendered.contains("<unbound>"));
+}
+
+#[test]
+fn selection_marker_moves_with_selected_index() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+
+ let first = render_lines(&guest_modal_chunks(
+ 80, 30, 0, 0, &style, "s", 0, &shortcuts,
+ ));
+ assert!(first
+ .iter()
+ .any(|line| line.trim_start().starts_with("> 1.")));
+ assert!(first.iter().any(|line| line.trim_start().starts_with("2.")));
+ assert!(!first
+ .iter()
+ .any(|line| line.trim_start().starts_with("> 2.")));
+
+ let second = render_lines(&guest_modal_chunks(
+ 80, 30, 0, 0, &style, "s", 1, &shortcuts,
+ ));
+ assert!(second
+ .iter()
+ .any(|line| line.trim_start().starts_with("> 2.")));
+ assert!(!second
+ .iter()
+ .any(|line| line.trim_start().starts_with("> 1.")));
+}
+
+fn number_column(line: &str, number: &str) -> usize {
+ line.find(number).expect("option number present")
+}
+
+#[test]
+fn options_are_left_justified_within_the_centered_block() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+ let chunks = guest_modal_chunks(80, 30, 0, 0, &style, "s", 0, &shortcuts);
+ let lines = render_lines(&chunks);
+ let selected_line = lines
+ .iter()
+ .find(|line| line.trim_start().starts_with("> 1."))
+ .expect("selected option line present");
+ let selected_number_column = number_column(selected_line, "1.");
+ assert!(
+ selected_number_column > 2,
+ "block must be centered with a left pad before the marker"
+ );
+
+ for (number, needle) in [("2.", "2.")] {
+ let line = lines
+ .iter()
+ .find(|line| line.trim_start().starts_with(needle))
+ .expect("unselected option line present");
+ assert_eq!(
+ number_column(line, number),
+ selected_number_column,
+ "the option number column is stable across selection"
+ );
+ }
+}
+
+#[test]
+fn small_columns_render_the_fallback() {
+ let style = Style::default();
+ let shortcuts = GuestModalShortcuts::default();
+ let chunks = guest_modal_chunks(18, 20, 0, 0, &style, "s", 0, &shortcuts);
+ assert_eq!(chunks.len(), 20);
+ let rendered = rendered_text(&chunks);
+ assert!(rendered.contains("Nested"));
+ assert!(!rendered.contains("What would you like to do?"));
+}
+
+#[test]
+fn small_rows_render_the_fallback() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+ let chunks = guest_modal_chunks(80, 5, 0, 0, &style, "s", 0, &shortcuts);
+ assert_eq!(chunks.len(), 5);
+ let rendered = rendered_text(&chunks);
+ assert!(rendered.contains("Nested Zellij session"));
+ assert!(!rendered.contains("What would you like to do?"));
+}
+
+#[test]
+fn tiny_geometry_does_not_panic() {
+ let style = Style::default();
+ let shortcuts = GuestModalShortcuts::default();
+ let chunks = guest_modal_chunks(3, 2, 0, 0, &style, "session", 0, &shortcuts);
+ assert_eq!(chunks.len(), 2);
+ let empty = guest_modal_chunks(0, 0, 0, 0, &style, "session", 0, &shortcuts);
+ assert!(empty.is_empty());
+}
+
+#[test]
+fn every_rendered_row_spans_the_full_width() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+ let columns = 80;
+ let chunks = guest_modal_chunks(columns, 30, 0, 0, &style, "s", 0, &shortcuts);
+ for chunk in &chunks {
+ let width: usize = chunk
+ .terminal_characters
+ .iter()
+ .map(|c| c.character.width().unwrap_or(0).max(1))
+ .sum();
+ assert_eq!(width, columns);
+ }
+}
+
+#[test]
+fn hit_test_round_trips_the_options() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+ let rows = 40;
+ let columns = 80;
+ let mut hit_rows: Vec<usize> = Vec::new();
+ for row in 0..rows {
+ if let Some(option) =
+ guest_modal_option_at_content_row(rows, columns, row, &style, "session", 0, &shortcuts)
+ {
+ assert_eq!(hit_rows.len(), option);
+ hit_rows.push(row);
+ }
+ }
+ assert_eq!(hit_rows.len(), 2);
+}
+
+#[test]
+fn hit_test_returns_none_off_option_rows_and_in_fallback() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+ assert_eq!(
+ guest_modal_option_at_content_row(40, 10, 0, &style, "session", 0, &shortcuts),
+ None
+ );
+ assert_eq!(
+ guest_modal_option_at_content_row(4, 10, 2, &style, "session", 0, &shortcuts),
+ None
+ );
+ assert_eq!(
+ guest_modal_option_at_content_row(40, 80, 0, &style, "session", 0, &shortcuts),
+ None
+ );
+}
+
+#[test]
+fn hit_test_matches_rendered_marker_rows() {
+ let style = Style::default();
+ let shortcuts = sample_shortcuts();
+ let rows = 40;
+ let columns = 80;
+ let selected = 1;
+ let lines = render_lines(&guest_modal_chunks(
+ columns, rows, 0, 0, &style, "session", selected, &shortcuts,
+ ));
+ for (row, line) in lines.iter().enumerate() {
+ let hit = guest_modal_option_at_content_row(
+ rows, columns, row, &style, "session", selected, &shortcuts,
+ );
+ let trimmed = line.trim_start();
+ let is_option_line =
+ trimmed.starts_with("> ") || trimmed.starts_with("1.") || trimmed.starts_with("2.");
+ assert_eq!(hit.is_some(), is_option_line, "row {} mismatch", row);
+ }
+}
+
+#[test]
+fn selected_rows_carry_the_selected_background_rectangle() {
+ let style = style_with_distinct_selected_background();
+ let shortcuts = sample_shortcuts();
+ let selected = 0;
+ let columns = 80;
+ let rows = 40;
+ let styles = guest_modal_styles(&style);
+ let layout = guest_modal_layout(columns, rows, &styles, "session", selected, &shortcuts);
+ let selected_option_offset = layout.option_rows[selected];
+
+ let chunks = guest_modal_chunks(columns, rows, 0, 0, &style, "session", selected, &shortcuts);
+ let selected_row_index = layout.start_row + selected_option_offset;
+ let selected_chunk = &chunks[selected_row_index];
+
+ let selected_background = styles.selected_fill.background;
+ let base_background = styles.fill.background;
+ assert_ne!(
+ selected_background, base_background,
+ "the test style must give the selected background a distinct value"
+ );
+
+ let block_start = layout.left_pad;
+ let block_end = layout.left_pad + layout.block_width;
+ let mut block_cells = 0;
+ for (column, character) in selected_chunk.terminal_characters.iter().enumerate() {
+ if column >= block_start && column < block_end {
+ assert_eq!(
+ character.styles.background, selected_background,
+ "block cell {} on the selected row must carry the selected background",
+ column
+ );
+ block_cells += 1;
+ } else {
+ assert_eq!(
+ character.styles.background, base_background,
+ "outer padding cell {} must keep the base background",
+ column
+ );
+ }
+ }
+ assert_eq!(
+ block_cells, layout.block_width,
+ "the selected rectangle spans the whole block width"
+ );
+}
+
+#[test]
+fn unselected_rows_do_not_use_the_selected_background() {
+ let style = style_with_distinct_selected_background();
+ let shortcuts = sample_shortcuts();
+ let selected = 0;
+ let columns = 80;
+ let rows = 40;
+ let styles = guest_modal_styles(&style);
+ let layout = guest_modal_layout(columns, rows, &styles, "session", selected, &shortcuts);
+ let other_option_offset = layout.option_rows[1];
+
+ let selected_background = styles.selected_fill.background;
+ let chunks = guest_modal_chunks(columns, rows, 0, 0, &style, "session", selected, &shortcuts);
+ let other_row = &chunks[layout.start_row + other_option_offset];
+ for character in &other_row.terminal_characters {
+ assert_ne!(
+ character.styles.background, selected_background,
+ "unselected option row must not carry the selected background"
+ );
+ }
+}
+
+#[test]
+fn keycode_words_join_with_plus_separators() {
+ let style = Style::default();
+ let styles = guest_modal_styles(&style);
+ let keys = vec!["Ctrl g".to_string(), "o".to_string()];
+ let words = guest_modal_keycode_words(&keys, styles.fill.clone(), styles.keycode.clone());
+ let joined: String = words.iter().map(|(text, _)| text.as_str()).collect();
+ assert_eq!(joined, "<Ctrl g>+<o>");
+ assert_eq!(words[0].1, styles.keycode);
+ assert_eq!(words[1].1, styles.fill);
+ assert_eq!(words[2].1, styles.keycode);
+}
+
+#[test]
+fn keycode_words_render_unbound_when_empty() {
+ let style = Style::default();
+ let styles = guest_modal_styles(&style);
+ let words = guest_modal_keycode_words(&[], styles.fill.clone(), styles.keycode.clone());
+ assert_eq!(words.len(), 1);
+ assert_eq!(words[0].0, "<unbound>");
+ assert_eq!(words[0].1, styles.fill);
+}
+
+#[test]
+fn wrap_words_respects_width_and_indent() {
+ let style = Style::default();
+ let styles = guest_modal_styles(&style);
+ let words = guest_modal_text_words("one two three four five", styles.fill.clone());
+ let indent = 4;
+ let width = 14;
+ let lines = guest_modal_wrap_words(&words, indent, styles.fill.clone(), width);
+ assert!(lines.len() > 1, "text wider than the width must wrap");
+ for line in &lines {
+ let indent_segment = &line[0];
+ assert_eq!(indent_segment.0, " ".repeat(indent));
+ let rendered_width: usize = line.iter().map(|(text, _)| text.width()).sum();
+ assert!(
+ rendered_width <= width,
+ "wrapped line width {} exceeds {}",
+ rendered_width,
+ width
+ );
+ }
+}
+
+#[test]
+fn center_pad_centers_and_clamps() {
+ assert_eq!(center_modal_pad(10, 30), 10);
+ assert_eq!(center_modal_pad(30, 30), 0);
+ assert_eq!(center_modal_pad(40, 30), 0);
+}
diff --git a/zellij-server/src/panes/unit/terminal_pane_tests.rs b/zellij-server/src/panes/unit/terminal_pane_tests.rs
index 2e34b273..a6234844 100644
--- a/zellij-server/src/panes/unit/terminal_pane_tests.rs
+++ b/zellij-server/src/panes/unit/terminal_pane_tests.rs
@@ -873,3 +873,150 @@ pub fn frameless_pane_position_is_on_frame() {
assert!(!terminal_pane.position_is_on_frame(&Position::new(30, 130)));
assert!(!terminal_pane.position_is_on_frame(&Position::new(30, 131)));
}
+
+fn create_guest_modal_pane() -> TerminalPane {
+ let mut fake_win_size = PaneGeom::default();
+ fake_win_size.cols.set_inner(80);
+ fake_win_size.rows.set_inner(24);
+ let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
+ let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
+ let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
+ TerminalPane::new(
+ 1,
+ fake_win_size,
+ Style::default(),
+ 0,
+ String::new(),
+ Rc::new(RefCell::new(LinkHandler::new())),
+ Rc::new(RefCell::new(None)),
+ sixel_image_store,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ None,
+ None,
+ false,
+ true,
+ true,
+ true,
+ false,
+ None,
+ )
+}
+
+fn press_key(
+ pane: &mut TerminalPane,
+ bare_key: zellij_utils::data::BareKey,
+ client_id: u16,
+) -> Option<crate::tab::AdjustedInput> {
+ let key = zellij_utils::data::KeyWithModifier::new(bare_key);
+ pane.adjust_input_to_terminal(&Some(key), vec![], false, Some(client_id))
+}
+
+#[test]
+pub fn guest_modal_navigation_wraps() {
+ use zellij_utils::data::BareKey;
+ let mut pane = create_guest_modal_pane();
+ let client_id = 1;
+ pane.set_guest_modal(&[client_id]);
+ assert_eq!(pane.guest_modal_selection(client_id), Some(0));
+ press_key(&mut pane, BareKey::Up, client_id);
+ assert_eq!(pane.guest_modal_selection(client_id), Some(1));
+ press_key(&mut pane, BareKey::Down, client_id);
+ assert_eq!(pane.guest_modal_selection(client_id), Some(0));
+ press_key(&mut pane, BareKey::Down, client_id);
+ assert_eq!(pane.guest_modal_selection(client_id), Some(1));
+ press_key(&mut pane, BareKey::Up, client_id);
+ assert_eq!(pane.guest_modal_selection(client_id), Some(0));
+}
+
+#[test]
+pub fn guest_modal_tab_is_ignored() {
+ use zellij_utils::data::{BareKey, KeyWithModifier};
+ let mut pane = create_guest_modal_pane();
+ let client_id = 1;
+ pane.set_guest_modal(&[client_id]);
+ press_key(&mut pane, BareKey::Tab, client_id);
+ assert_eq!(pane.guest_modal_selection(client_id), Some(0));
+ let key = KeyWithModifier::new(BareKey::Tab).with_shift_modifier();
+ pane.adjust_input_to_terminal(&Some(key), vec![], false, Some(client_id));
+ assert_eq!(pane.guest_modal_selection(client_id), Some(0));
+}
+
+#[test]
+pub fn guest_modal_enter_confirms_selection() {
+ use crate::tab::AdjustedInput;
+ use zellij_utils::data::BareKey;
+ let mut pane = create_guest_modal_pane();
+ let client_id = 1;
+
+ pane.set_guest_modal(&[client_id]);
+ let outcome = press_key(&mut pane, BareKey::Enter, client_id);
+ assert!(matches!(outcome, Some(AdjustedInput::GuestModalZoom)));
+
+ pane.set_guest_modal(&[client_id]);
+ press_key(&mut pane, BareKey::Down, client_id);
+ let outcome = press_key(&mut pane, BareKey::Enter, client_id);
+ assert!(matches!(outcome, Some(AdjustedInput::GuestModalDescend)));
+}
+
+#[test]
+pub fn guest_modal_digit_shortcuts() {
+ use crate::tab::AdjustedInput;
+ use zellij_utils::data::BareKey;
+ let mut pane = create_guest_modal_pane();
+ let client_id = 1;
+
+ pane.set_guest_modal(&[client_id]);
+ let outcome = press_key(&mut pane, BareKey::Char('1'), client_id);
+ assert!(matches!(outcome, Some(AdjustedInput::GuestModalZoom)));
+
+ pane.set_guest_modal(&[client_id]);
+ let outcome = press_key(&mut pane, BareKey::Char('2'), client_id);
+ assert!(matches!(outcome, Some(AdjustedInput::GuestModalDescend)));
+}
+
+#[test]
+pub fn guest_modal_esc_descends() {
+ use crate::tab::AdjustedInput;
+ use zellij_utils::data::BareKey;
+ let mut pane = create_guest_modal_pane();
+ let client_id = 1;
+ pane.set_guest_modal(&[client_id]);
+ let outcome = press_key(&mut pane, BareKey::Esc, client_id);
+ assert!(matches!(outcome, Some(AdjustedInput::GuestModalDescend)));
+}
+
+#[test]
+pub fn guest_modal_swallows_other_input() {
+ use zellij_utils::data::BareKey;
+ let mut pane = create_guest_modal_pane();
+ let client_id = 1;
+ pane.set_guest_modal(&[client_id]);
+ let outcome = press_key(&mut pane, BareKey::Char('x'), client_id);
+ assert!(outcome.is_none());
+ assert_eq!(pane.guest_modal_selection(client_id), Some(0));
+}
+
+#[test]
+pub fn guest_modal_is_per_client() {
+ use zellij_utils::data::BareKey;
+ let mut pane = create_guest_modal_pane();
+ let client_a = 1;
+ let client_b = 2;
+ pane.set_guest_modal(&[client_a, client_b]);
+ press_key(&mut pane, BareKey::Down, client_a);
+ assert_eq!(pane.guest_modal_selection(client_a), Some(1));
+ assert_eq!(pane.guest_modal_selection(client_b), Some(0));
+}
+
+#[test]
+pub fn guest_modal_no_modal_passes_input_through() {
+ use zellij_utils::data::BareKey;
+ let mut pane = create_guest_modal_pane();
+ let client_id = 1;
+ let outcome = press_key(&mut pane, BareKey::Char('x'), client_id);
+ assert!(matches!(
+ outcome,
+ Some(crate::tab::AdjustedInput::WriteBytesToTerminal(_))
+ ));
+}