|
|
|
@@ -0,0 +1,666 @@
|
|
|
|
|
#![cfg(unix)]
|
|
|
|
|
|
|
|
|
|
use zellij_integration_tests::{
|
|
|
|
|
claim_first_terminal_and_wait_for_prompt, col, keys, split_down_and_wait_for_prompt,
|
|
|
|
|
split_right_and_wait_for_prompt, start_zellij, FakePtyHandle, TestRunner, TestSession,
|
|
|
|
|
TERMINAL_SIZE,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const KITTY_PROBE_ACK: &[u8] = b"\x1b_Gi=31;OK\x1b\\";
|
|
|
|
|
const TRANSMIT_HEADER: &[u8] = b"\x1b_Ga=t,q=2,f=32,t=d,i=2000000000,s=2,v=2,m=0;";
|
|
|
|
|
const PLACEMENT: &[u8] = b"\x1b_Ga=p,q=2,i=2000000000,p=1,x=0,y=0,w=2,h=2,X=0,Y=0,z=0,C=1\x1b\\";
|
|
|
|
|
const IMAGE_FREE_DELETE: &[u8] = b"\x1b_Ga=d,q=2,d=I,i=2000000000\x1b\\";
|
|
|
|
|
const PLACEMENT_DELETE: &[u8] = b"\x1b_Ga=d,q=2,d=i,i=2000000000,p=1\x1b\\";
|
|
|
|
|
const RGB_2X2_A_T: &[u8] = b"\x1b_Ga=T,q=2,f=24,s=2,v=2,m=0;////////////////\x1b\\";
|
|
|
|
|
fn rgb_tall_transmit_and_display() -> Vec<u8> {
|
|
|
|
|
let width = 2usize;
|
|
|
|
|
let height = 48usize;
|
|
|
|
|
let raster = vec![0xffu8; width * height * 3];
|
|
|
|
|
let payload = base64_encode(&raster);
|
|
|
|
|
let mut out = format!("\x1b_Ga=T,q=2,f=24,s={},v={},C=1,m=0;", width, height).into_bytes();
|
|
|
|
|
out.extend_from_slice(payload.as_bytes());
|
|
|
|
|
out.extend_from_slice(b"\x1b\\");
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn base64_encode(bytes: &[u8]) -> String {
|
|
|
|
|
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
|
let mut out = String::new();
|
|
|
|
|
for chunk in bytes.chunks(3) {
|
|
|
|
|
let b0 = chunk[0] as u32;
|
|
|
|
|
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
|
|
|
|
|
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
|
|
|
|
|
let triple = (b0 << 16) | (b1 << 8) | b2;
|
|
|
|
|
out.push(TABLE[((triple >> 18) & 0x3f) as usize] as char);
|
|
|
|
|
out.push(TABLE[((triple >> 12) & 0x3f) as usize] as char);
|
|
|
|
|
if chunk.len() > 1 {
|
|
|
|
|
out.push(TABLE[((triple >> 6) & 0x3f) as usize] as char);
|
|
|
|
|
} else {
|
|
|
|
|
out.push('=');
|
|
|
|
|
}
|
|
|
|
|
if chunk.len() > 2 {
|
|
|
|
|
out.push(TABLE[(triple & 0x3f) as usize] as char);
|
|
|
|
|
} else {
|
|
|
|
|
out.push('=');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
|
|
|
|
|
haystack
|
|
|
|
|
.windows(needle.len())
|
|
|
|
|
.any(|window| window == needle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
|
|
|
|
haystack
|
|
|
|
|
.windows(needle.len())
|
|
|
|
|
.position(|window| window == needle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn setup_kitty_host(zellij: &TestSession, terminal: &FakePtyHandle) {
|
|
|
|
|
terminal.disable_echo();
|
|
|
|
|
zellij.send_stdin(b"\x1b[6;21;8t");
|
|
|
|
|
zellij.send_stdin(b"\x1b_Gi=31;OK\x1b\\");
|
|
|
|
|
zellij.send_stdin(b"Z");
|
|
|
|
|
terminal.wait_for_stdin(
|
|
|
|
|
"kitty handshake barrier keystroke reached the pane",
|
|
|
|
|
|stdin_bytes| stdin_bytes.contains(&b'Z'),
|
|
|
|
|
);
|
|
|
|
|
terminal.output(b"\x1b_Ga=q,i=31,s=1,v=1,t=d,f=24;AAAA\x1b\\");
|
|
|
|
|
terminal.wait_for_stdin("kitty probe acknowledged", |stdin_bytes| {
|
|
|
|
|
contains_bytes(stdin_bytes, KITTY_PROBE_ACK)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pane_kitty_image_reaches_client_with_transmit_and_placement() {
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &terminal);
|
|
|
|
|
|
|
|
|
|
terminal.output(RGB_2X2_A_T);
|
|
|
|
|
|
|
|
|
|
let bytes = zellij
|
|
|
|
|
.wait_until_raw_output("kitty transmit and placement reach the client", |bytes| {
|
|
|
|
|
contains_bytes(bytes, TRANSMIT_HEADER) && contains_bytes(bytes, PLACEMENT)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let placement_position = find_bytes(&bytes, PLACEMENT).unwrap();
|
|
|
|
|
let before_placement = &bytes[..placement_position];
|
|
|
|
|
let prompt_cursor_zero_indexed = (2usize, 1usize);
|
|
|
|
|
let expected_goto_row = prompt_cursor_zero_indexed.1 + 1;
|
|
|
|
|
let expected_goto_column = prompt_cursor_zero_indexed.0 + 1;
|
|
|
|
|
let expected_prefix = format!("\x1b[{};{}H\x1b[m", expected_goto_row, expected_goto_column);
|
|
|
|
|
assert!(
|
|
|
|
|
before_placement.ends_with(expected_prefix.as_bytes()),
|
|
|
|
|
"placement must be immediately preceded by a goto to the prompt cursor position {:?}, got trailing bytes: {:?}",
|
|
|
|
|
expected_prefix.as_bytes(),
|
|
|
|
|
&before_placement[before_placement.len().saturating_sub(16)..]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn kitty_image_at_bottom_row_scrolls_and_reaches_client() {
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &terminal);
|
|
|
|
|
|
|
|
|
|
let mut fill = Vec::new();
|
|
|
|
|
for _ in 0..64 {
|
|
|
|
|
fill.extend_from_slice(b"\r\n");
|
|
|
|
|
}
|
|
|
|
|
terminal.output(&fill);
|
|
|
|
|
terminal.output(RGB_2X2_A_T);
|
|
|
|
|
|
|
|
|
|
let bytes = zellij.wait_until_raw_output(
|
|
|
|
|
"kitty placement at bottom row reaches the client after scrolling",
|
|
|
|
|
|bytes| contains_bytes(bytes, TRANSMIT_HEADER) && contains_bytes(bytes, PLACEMENT),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let placement_position = find_bytes(&bytes, PLACEMENT).unwrap();
|
|
|
|
|
let before_placement = &bytes[..placement_position];
|
|
|
|
|
let goto_marker = find_bytes(before_placement, b"\x1b[").is_some();
|
|
|
|
|
assert!(
|
|
|
|
|
goto_marker,
|
|
|
|
|
"placement must be preceded by a cursor goto after scrolling to the bottom row"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn clear_screen_deletes_kitty_images_on_host() {
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &terminal);
|
|
|
|
|
|
|
|
|
|
terminal.output(b"\x1b[H");
|
|
|
|
|
terminal.output(&rgb_tall_transmit_and_display());
|
|
|
|
|
zellij.wait_until_raw_output("kitty placement reaches the client", |bytes| {
|
|
|
|
|
contains_bytes(bytes, b"\x1b_Ga=p,q=2,i=2000000000,p=1,")
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
terminal.output(b"\x1b[2J");
|
|
|
|
|
zellij.wait_until_raw_output("host-side image delete after clear screen", |bytes| {
|
|
|
|
|
contains_bytes(bytes, IMAGE_FREE_DELETE)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn closing_pane_flushes_kitty_deletes_and_host_ids_are_never_reused() {
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let first_terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &first_terminal);
|
|
|
|
|
let second_terminal = split_right_and_wait_for_prompt(&zellij);
|
|
|
|
|
|
|
|
|
|
second_terminal.output(RGB_2X2_A_T);
|
|
|
|
|
zellij.wait_until_raw_output("kitty placement from the second pane", |bytes| {
|
|
|
|
|
contains_bytes(bytes, PLACEMENT)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
zellij.send_stdin(&keys::ctrl('p'));
|
|
|
|
|
zellij.send_stdin(&keys::key('x'));
|
|
|
|
|
|
|
|
|
|
zellij.wait_until_raw_output("host-side image delete after pane close", |bytes| {
|
|
|
|
|
contains_bytes(bytes, IMAGE_FREE_DELETE)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
|
|
|
|
|
let bytes = zellij.raw_bytes();
|
|
|
|
|
let delete_position = find_bytes(&bytes, IMAGE_FREE_DELETE).unwrap();
|
|
|
|
|
let tail = &bytes[delete_position + IMAGE_FREE_DELETE.len()..];
|
|
|
|
|
assert!(
|
|
|
|
|
!contains_bytes(tail, b"i=2000000000"),
|
|
|
|
|
"host image id 2000000000 was referenced after its delete"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn opening_new_pane_keeps_existing_pane_image_on_host() {
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let first_terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &first_terminal);
|
|
|
|
|
|
|
|
|
|
first_terminal.output(b"\x1b[H");
|
|
|
|
|
first_terminal.output(&rgb_tall_transmit_and_display());
|
|
|
|
|
let bytes = zellij.wait_until_raw_output("first pane placement reaches the host", |bytes| {
|
|
|
|
|
contains_bytes(bytes, b"\x1b_Ga=p,q=2,i=2000000000,p=1,")
|
|
|
|
|
});
|
|
|
|
|
let baseline_len = bytes.len();
|
|
|
|
|
|
|
|
|
|
let _second_terminal = split_down_and_wait_for_prompt(&zellij);
|
|
|
|
|
|
|
|
|
|
let bytes = zellij.wait_until_raw_output(
|
|
|
|
|
"existing pane image is re-transmitted and re-placed after the layout clears the host display",
|
|
|
|
|
|bytes| {
|
|
|
|
|
let after_split = &bytes[baseline_len..];
|
|
|
|
|
contains_bytes(after_split, b"\x1b_Ga=t") && contains_bytes(after_split, b"\x1b_Ga=p,q=2,")
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
let after_split = &bytes[baseline_len..];
|
|
|
|
|
assert!(
|
|
|
|
|
contains_bytes(after_split, b"\x1b[2J"),
|
|
|
|
|
"opening a new pane must clear the host display"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
contains_bytes(after_split, b"\x1b_Ga=t"),
|
|
|
|
|
"existing pane image was not re-transmitted after the host display was cleared"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
contains_bytes(after_split, b"\x1b_Ga=p,q=2,"),
|
|
|
|
|
"existing pane image was not re-placed after the host display was cleared"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CELL_WIDTH: usize = 8;
|
|
|
|
|
const CELL_HEIGHT: usize = 21;
|
|
|
|
|
const IMAGE_CELL_X: usize = 20;
|
|
|
|
|
const IMAGE_CELL_Y: usize = 3;
|
|
|
|
|
const IMAGE_CELL_WIDTH: usize = 30;
|
|
|
|
|
const IMAGE_CELL_HEIGHT: usize = 7;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
|
|
|
struct CellRect {
|
|
|
|
|
cell_y: usize,
|
|
|
|
|
cell_x: usize,
|
|
|
|
|
cell_width: usize,
|
|
|
|
|
cell_height: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CellRect {
|
|
|
|
|
fn intersects(&self, other: &CellRect) -> bool {
|
|
|
|
|
self.cell_x < other.cell_x + other.cell_width
|
|
|
|
|
&& other.cell_x < self.cell_x + self.cell_width
|
|
|
|
|
&& self.cell_y < other.cell_y + other.cell_height
|
|
|
|
|
&& other.cell_y < self.cell_y + self.cell_height
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn scaled_rgb_transmit_and_display(cell_columns: usize, cell_rows: usize) -> Vec<u8> {
|
|
|
|
|
let raster = vec![0xffu8; 2 * 2 * 3];
|
|
|
|
|
let mut out = format!(
|
|
|
|
|
"\x1b_Ga=T,q=2,f=24,s=2,v=2,c={},r={},C=1,m=0;",
|
|
|
|
|
cell_columns, cell_rows
|
|
|
|
|
)
|
|
|
|
|
.into_bytes();
|
|
|
|
|
out.extend_from_slice(base64_encode(&raster).as_bytes());
|
|
|
|
|
out.extend_from_slice(b"\x1b\\");
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn host_placements(bytes: &[u8]) -> Vec<(CellRect, u32)> {
|
|
|
|
|
let text = String::from_utf8_lossy(bytes).to_string();
|
|
|
|
|
let marker = "\u{1b}_Ga=p,q=2,i=2000000000,p=";
|
|
|
|
|
let control_prefix = "\u{1b}_Ga=p,q=2,";
|
|
|
|
|
let mut placements = vec![];
|
|
|
|
|
let mut search_start = 0;
|
|
|
|
|
while let Some(position) = text[search_start..].find(marker) {
|
|
|
|
|
let start = search_start + position;
|
|
|
|
|
let end = start
|
|
|
|
|
+ text[start..]
|
|
|
|
|
.find("\u{1b}\\")
|
|
|
|
|
.expect("unterminated placement");
|
|
|
|
|
let mut placement_id = 0u32;
|
|
|
|
|
let mut source_width = 0usize;
|
|
|
|
|
let mut source_height = 0usize;
|
|
|
|
|
for field in text[start + control_prefix.len()..end].split(',') {
|
|
|
|
|
let mut parts = field.splitn(2, '=');
|
|
|
|
|
let key = parts.next().unwrap_or("");
|
|
|
|
|
let value = parts.next().unwrap_or("");
|
|
|
|
|
match key {
|
|
|
|
|
"p" => placement_id = value.parse().expect("placement id"),
|
|
|
|
|
"w" => source_width = value.parse().expect("placement width"),
|
|
|
|
|
"h" => source_height = value.parse().expect("placement height"),
|
|
|
|
|
_ => {},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let goto_end = text[..start].rfind('H').expect("placement goto");
|
|
|
|
|
let goto_start = text[..goto_end]
|
|
|
|
|
.rfind("\u{1b}[")
|
|
|
|
|
.expect("placement goto start");
|
|
|
|
|
let mut coordinates = text[goto_start + 2..goto_end].split(';');
|
|
|
|
|
let row: usize = coordinates.next().unwrap().parse().expect("goto row");
|
|
|
|
|
let column: usize = coordinates.next().unwrap().parse().expect("goto column");
|
|
|
|
|
placements.push((
|
|
|
|
|
CellRect {
|
|
|
|
|
cell_y: row - 1,
|
|
|
|
|
cell_x: column - 1,
|
|
|
|
|
cell_width: source_width / CELL_WIDTH,
|
|
|
|
|
cell_height: source_height / CELL_HEIGHT,
|
|
|
|
|
},
|
|
|
|
|
placement_id,
|
|
|
|
|
));
|
|
|
|
|
search_start = end;
|
|
|
|
|
}
|
|
|
|
|
placements
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn floating_pane_rect(grid_snapshot: &zellij_integration_tests::GridSnapshot) -> CellRect {
|
|
|
|
|
let mut top = None;
|
|
|
|
|
let mut bottom = 0usize;
|
|
|
|
|
let mut left = 0usize;
|
|
|
|
|
let mut right = 0usize;
|
|
|
|
|
for (row, line) in grid_snapshot.lines().iter().enumerate() {
|
|
|
|
|
let characters: Vec<char> = line.chars().collect();
|
|
|
|
|
let first = characters
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|c| *c == '\u{250c}' || *c == '\u{2502}' || *c == '\u{2514}');
|
|
|
|
|
let last = characters
|
|
|
|
|
.iter()
|
|
|
|
|
.rposition(|c| *c == '\u{2510}' || *c == '\u{2502}' || *c == '\u{2518}');
|
|
|
|
|
if let (Some(first), Some(last)) = (first, last) {
|
|
|
|
|
if top.is_none() {
|
|
|
|
|
top = Some(row);
|
|
|
|
|
left = first;
|
|
|
|
|
right = last;
|
|
|
|
|
}
|
|
|
|
|
bottom = row;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let top = top.expect("the floating pane frame must be visible");
|
|
|
|
|
CellRect {
|
|
|
|
|
cell_y: top,
|
|
|
|
|
cell_x: left,
|
|
|
|
|
cell_width: right - left + 1,
|
|
|
|
|
cell_height: bottom - top + 1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn uncovered_parts(image: CellRect, cover: CellRect) -> Vec<CellRect> {
|
|
|
|
|
if !image.intersects(&cover) {
|
|
|
|
|
return vec![image];
|
|
|
|
|
}
|
|
|
|
|
let image_bottom = image.cell_y + image.cell_height;
|
|
|
|
|
let image_right = image.cell_x + image.cell_width;
|
|
|
|
|
let cover_bottom = cover.cell_y + cover.cell_height;
|
|
|
|
|
let cover_right = cover.cell_x + cover.cell_width;
|
|
|
|
|
let mut parts = vec![];
|
|
|
|
|
if cover.cell_y > image.cell_y {
|
|
|
|
|
parts.push(CellRect {
|
|
|
|
|
cell_y: image.cell_y,
|
|
|
|
|
cell_x: image.cell_x,
|
|
|
|
|
cell_width: image.cell_width,
|
|
|
|
|
cell_height: cover.cell_y - image.cell_y,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
let middle_top = std::cmp::max(image.cell_y, cover.cell_y);
|
|
|
|
|
let middle_bottom = std::cmp::min(image_bottom, cover_bottom);
|
|
|
|
|
let middle_height = middle_bottom - middle_top;
|
|
|
|
|
if cover.cell_x > image.cell_x {
|
|
|
|
|
parts.push(CellRect {
|
|
|
|
|
cell_y: middle_top,
|
|
|
|
|
cell_x: image.cell_x,
|
|
|
|
|
cell_width: cover.cell_x - image.cell_x,
|
|
|
|
|
cell_height: middle_height,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if cover_right < image_right {
|
|
|
|
|
parts.push(CellRect {
|
|
|
|
|
cell_y: middle_top,
|
|
|
|
|
cell_x: cover_right,
|
|
|
|
|
cell_width: image_right - cover_right,
|
|
|
|
|
cell_height: middle_height,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if cover_bottom < image_bottom {
|
|
|
|
|
parts.push(CellRect {
|
|
|
|
|
cell_y: cover_bottom,
|
|
|
|
|
cell_x: image.cell_x,
|
|
|
|
|
cell_width: image.cell_width,
|
|
|
|
|
cell_height: image_bottom - cover_bottom,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
parts.sort();
|
|
|
|
|
parts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn floating_pane_occludes_kitty_image_and_restores_it_without_retransmit() {
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &terminal);
|
|
|
|
|
|
|
|
|
|
let image_rect = CellRect {
|
|
|
|
|
cell_y: IMAGE_CELL_Y,
|
|
|
|
|
cell_x: IMAGE_CELL_X,
|
|
|
|
|
cell_width: IMAGE_CELL_WIDTH,
|
|
|
|
|
cell_height: IMAGE_CELL_HEIGHT,
|
|
|
|
|
};
|
|
|
|
|
terminal.output(format!("\x1b[{};{}H", IMAGE_CELL_Y, IMAGE_CELL_X + 1).as_bytes());
|
|
|
|
|
terminal.output(&scaled_rgb_transmit_and_display(
|
|
|
|
|
IMAGE_CELL_WIDTH,
|
|
|
|
|
IMAGE_CELL_HEIGHT,
|
|
|
|
|
));
|
|
|
|
|
let bytes = zellij
|
|
|
|
|
.wait_until_raw_output("the full image placement reaches the host", |bytes| {
|
|
|
|
|
!host_placements(bytes).is_empty()
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(
|
|
|
|
|
host_placements(&bytes),
|
|
|
|
|
vec![(image_rect, 1)],
|
|
|
|
|
"the unoccluded image must be emitted as a single full-size placement"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
bytes
|
|
|
|
|
.windows(b"\x1b_Ga=t".len())
|
|
|
|
|
.filter(|window| *window == b"\x1b_Ga=t")
|
|
|
|
|
.count(),
|
|
|
|
|
1,
|
|
|
|
|
"the image pixels must be transmitted exactly once"
|
|
|
|
|
);
|
|
|
|
|
let baseline = bytes.len();
|
|
|
|
|
|
|
|
|
|
zellij.send_stdin(&keys::ctrl('p'));
|
|
|
|
|
zellij.send_stdin(&keys::key('w'));
|
|
|
|
|
let floating_terminal = zellij.expect_pty_spawn();
|
|
|
|
|
floating_terminal.output(b"$ ");
|
|
|
|
|
zellij.wait_until(
|
|
|
|
|
"the floating pane covers part of the image",
|
|
|
|
|
|grid_snapshot| grid_snapshot.contains("Pane #2") && grid_snapshot.contains("\u{250c}"),
|
|
|
|
|
);
|
|
|
|
|
let bytes = zellij.wait_until_raw_output("the occluded placements reach the host", |bytes| {
|
|
|
|
|
host_placements(&bytes[baseline..]).len() >= 2
|
|
|
|
|
});
|
|
|
|
|
let float_rect = floating_pane_rect(&zellij.snapshot());
|
|
|
|
|
assert!(
|
|
|
|
|
float_rect.intersects(&image_rect),
|
|
|
|
|
"the floating pane must overlap the image for this test to be meaningful"
|
|
|
|
|
);
|
|
|
|
|
let occluded_placements = host_placements(&bytes[baseline..]);
|
|
|
|
|
for (rect, placement_id) in &occluded_placements {
|
|
|
|
|
assert!(
|
|
|
|
|
!rect.intersects(&float_rect),
|
|
|
|
|
"placement {} at {:?} covers cells inside the floating pane rect {:?}",
|
|
|
|
|
placement_id,
|
|
|
|
|
rect,
|
|
|
|
|
float_rect
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let mut occluded_rects: Vec<CellRect> =
|
|
|
|
|
occluded_placements.iter().map(|(rect, _)| *rect).collect();
|
|
|
|
|
occluded_rects.sort();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
occluded_rects,
|
|
|
|
|
uncovered_parts(image_rect, float_rect),
|
|
|
|
|
"the emitted placements must be exactly the uncovered sub-rectangles of the image"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
!occluded_rects.contains(&image_rect),
|
|
|
|
|
"the full-image placement must no longer be emitted while the floating pane covers it"
|
|
|
|
|
);
|
|
|
|
|
let surplus_placement_ids: Vec<u32> = occluded_placements
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(_, placement_id)| *placement_id)
|
|
|
|
|
.filter(|placement_id| *placement_id != 1)
|
|
|
|
|
.collect();
|
|
|
|
|
assert!(
|
|
|
|
|
!surplus_placement_ids.is_empty(),
|
|
|
|
|
"occlusion must emit at least one additional host placement id"
|
|
|
|
|
);
|
|
|
|
|
let baseline_after_occlusion = bytes.len();
|
|
|
|
|
|
|
|
|
|
zellij.run_cli_action(zellij_utils::cli::CliAction::HideFloatingPanes { tab_id: None });
|
|
|
|
|
let bytes = zellij.wait_until_raw_output(
|
|
|
|
|
"the image is restored after the floating pane goes away",
|
|
|
|
|
|bytes| host_placements(&bytes[baseline_after_occlusion..]).contains(&(image_rect, 1)),
|
|
|
|
|
);
|
|
|
|
|
let after_restore = &bytes[baseline_after_occlusion..];
|
|
|
|
|
for placement_id in surplus_placement_ids {
|
|
|
|
|
let delete = format!("\x1b_Ga=d,q=2,d=i,i=2000000000,p={}\x1b\\", placement_id);
|
|
|
|
|
assert!(
|
|
|
|
|
contains_bytes(after_restore, delete.as_bytes()),
|
|
|
|
|
"the sub-placement {} must be deleted when the image is no longer occluded",
|
|
|
|
|
placement_id
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
assert!(
|
|
|
|
|
!contains_bytes(after_restore, b"\x1b_Ga=t"),
|
|
|
|
|
"restoring the image must not retransmit its pixels"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn disabling_the_protocol_keeps_kitty_bytes_away_from_the_client_and_the_pane() {
|
|
|
|
|
let mut zellij = TestRunner::new(TERMINAL_SIZE)
|
|
|
|
|
.with_config("support_kitty_graphics_protocol false")
|
|
|
|
|
.start();
|
|
|
|
|
let terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
terminal.disable_echo();
|
|
|
|
|
|
|
|
|
|
terminal.output(b"\x1b_Ga=q,i=31,s=1,v=1,t=d,f=24;AAAA\x1b\\");
|
|
|
|
|
terminal.output(RGB_2X2_A_T);
|
|
|
|
|
terminal.output(b"done");
|
|
|
|
|
|
|
|
|
|
zellij.wait_until("the text after the image sequences is rendered", |grid| {
|
|
|
|
|
grid.contains("done")
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let bytes = zellij.raw_bytes();
|
|
|
|
|
assert!(
|
|
|
|
|
!contains_bytes(&bytes, b"\x1b_G"),
|
|
|
|
|
"no kitty graphics sequence may reach the client when the protocol is disabled"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
terminal.stdin_bytes().is_empty(),
|
|
|
|
|
"a disabled protocol must not reply to the application at all, got: {:?}",
|
|
|
|
|
terminal.stdin_bytes()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pinned_floating_pane_keeps_its_image_when_floating_panes_are_toggled_off() {
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &terminal);
|
|
|
|
|
|
|
|
|
|
zellij.send_stdin(&keys::ctrl('p'));
|
|
|
|
|
zellij.send_stdin(&keys::key('w'));
|
|
|
|
|
let floating_terminal = zellij.expect_pty_spawn();
|
|
|
|
|
floating_terminal.disable_echo();
|
|
|
|
|
floating_terminal.output(b"$ ");
|
|
|
|
|
zellij.wait_until("floating pane appeared", |grid_snapshot| {
|
|
|
|
|
grid_snapshot.contains("PIN [ ]")
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
floating_terminal.output(RGB_2X2_A_T);
|
|
|
|
|
zellij.wait_until_raw_output("the floating pane image reaches the client", |bytes| {
|
|
|
|
|
contains_bytes(bytes, TRANSMIT_HEADER) && contains_bytes(bytes, PLACEMENT)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
zellij.send_stdin(&keys::ctrl('p'));
|
|
|
|
|
zellij.send_stdin(&keys::key('i'));
|
|
|
|
|
zellij.wait_until("floating pane became pinned", |grid_snapshot| {
|
|
|
|
|
grid_snapshot.contains("PIN [+]")
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let bytes_before_toggle = zellij.raw_bytes().len();
|
|
|
|
|
|
|
|
|
|
zellij.send_stdin(&keys::ctrl('p'));
|
|
|
|
|
zellij.send_stdin(&keys::key('w'));
|
|
|
|
|
zellij.wait_until(
|
|
|
|
|
"focus settled on the tiled pane while the pinned pane stays up",
|
|
|
|
|
|grid_snapshot| {
|
|
|
|
|
grid_snapshot.status_bar_appears()
|
|
|
|
|
&& grid_snapshot.contains("PIN [+]")
|
|
|
|
|
&& grid_snapshot.cursor_is_at(col(2).row(1))
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let bytes = zellij.raw_bytes();
|
|
|
|
|
let after_toggle = &bytes[bytes_before_toggle..];
|
|
|
|
|
assert!(
|
|
|
|
|
!contains_bytes(after_toggle, PLACEMENT_DELETE),
|
|
|
|
|
"the image of a pinned floating pane must survive hiding the floating panes"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
!contains_bytes(after_toggle, IMAGE_FREE_DELETE),
|
|
|
|
|
"the image data of a pinned floating pane must not be freed on the host"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn temp_file_transmission_probe_and_image_reach_the_client() {
|
|
|
|
|
use std::io::Write as _;
|
|
|
|
|
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &terminal);
|
|
|
|
|
|
|
|
|
|
terminal.output(b"\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c");
|
|
|
|
|
terminal.wait_for_stdin("viu remote support probe answered", |stdin_bytes| {
|
|
|
|
|
contains_bytes(stdin_bytes, KITTY_PROBE_ACK)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut probe_file = tempfile::Builder::new()
|
|
|
|
|
.prefix(".tty-graphics-protocol.viuer.")
|
|
|
|
|
.rand_bytes(1)
|
|
|
|
|
.tempfile()
|
|
|
|
|
.unwrap();
|
|
|
|
|
probe_file.write_all(&[0u8, 0, 0, 0]).unwrap();
|
|
|
|
|
probe_file.flush().unwrap();
|
|
|
|
|
let probe_path = probe_file.path().to_str().unwrap().to_string();
|
|
|
|
|
terminal.output(
|
|
|
|
|
format!(
|
|
|
|
|
"\x1b_Gi=31,s=1,v=1,a=q,t=t;{}\x1b\\",
|
|
|
|
|
base64_encode(probe_path.as_bytes())
|
|
|
|
|
)
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
);
|
|
|
|
|
terminal.wait_for_stdin("viu local support probe answered", |stdin_bytes| {
|
|
|
|
|
stdin_bytes
|
|
|
|
|
.windows(KITTY_PROBE_ACK.len())
|
|
|
|
|
.filter(|window| *window == KITTY_PROBE_ACK)
|
|
|
|
|
.count()
|
|
|
|
|
>= 2
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut image_file = tempfile::Builder::new()
|
|
|
|
|
.prefix(".tty-graphics-protocol.viuer.")
|
|
|
|
|
.rand_bytes(1)
|
|
|
|
|
.tempfile()
|
|
|
|
|
.unwrap();
|
|
|
|
|
image_file.write_all(&vec![0xffu8; 2 * 2 * 4]).unwrap();
|
|
|
|
|
image_file.flush().unwrap();
|
|
|
|
|
let image_path = image_file.path().to_str().unwrap().to_string();
|
|
|
|
|
terminal.output(
|
|
|
|
|
format!(
|
|
|
|
|
"\x1b_Gf=32,s=2,v=2,c=2,r=1,a=T,t=t;{}\x1b\\\x1b[5n",
|
|
|
|
|
base64_encode(image_path.as_bytes())
|
|
|
|
|
)
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
zellij.wait_until_raw_output("the viu image reaches the client", |bytes| {
|
|
|
|
|
contains_bytes(bytes, b"\x1b_Ga=t,q=2,f=32,t=d,i=2000000000")
|
|
|
|
|
&& contains_bytes(bytes, b"\x1b_Ga=p,q=2,i=2000000000,p=1,")
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn fullscreen_removes_the_images_of_the_panes_it_hides() {
|
|
|
|
|
let mut zellij = start_zellij();
|
|
|
|
|
let terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
|
|
|
|
|
setup_kitty_host(&zellij, &terminal);
|
|
|
|
|
|
|
|
|
|
terminal.output(RGB_2X2_A_T);
|
|
|
|
|
zellij.wait_until_raw_output("the first pane's image reaches the client", |bytes| {
|
|
|
|
|
contains_bytes(bytes, TRANSMIT_HEADER) && contains_bytes(bytes, PLACEMENT)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let _second_terminal = split_right_and_wait_for_prompt(&zellij);
|
|
|
|
|
let bytes_before_fullscreen = zellij.raw_bytes().len();
|
|
|
|
|
|
|
|
|
|
zellij.send_stdin(&keys::ctrl('p'));
|
|
|
|
|
zellij.send_stdin(&keys::key('f'));
|
|
|
|
|
zellij.wait_until("the focused pane is fullscreen", |grid_snapshot| {
|
|
|
|
|
grid_snapshot.contains("(FULLSCREEN)")
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let bytes = zellij.wait_until_raw_output(
|
|
|
|
|
"the hidden pane's image is deleted from the host",
|
|
|
|
|
|bytes| contains_bytes(&bytes[bytes_before_fullscreen..], b"\x1b_Ga=d,q=2,d=i,"),
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
contains_bytes(&bytes[bytes_before_fullscreen..], b"\x1b_Ga=d,q=2,d=i,"),
|
|
|
|
|
"a pane hidden by fullscreen must have its placements removed from the host"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
zellij.quit();
|
|
|
|
|
}
|