feat(terminal): sixel support (#1557)

* work

* work

* work

* work

* work

* more work

* work

* work

* work

* hack around stdin repeater

* refactor(sixel): rename sixel structs

* feat(sixel): render text above images

* fix(sixel): reap images once they're past the end of the scrollbuffer

* fix(sixel): display images in the middle of the line

* fix(sixel): render crash

* fix(sixel): react to SIGWINCH

* fix(sixel): behave properly in alternate screen mode

* fix(sixel): reap images on terminal reset

* feat(sixel): handle DECSDM

* fix(terminal): properly respond to XTSMGRAPHICS and device attributes with Sixel

* Add comment

* fix(sixel): hack for unknown event overflow until we fix the api

* feat(input): query terminal for all OSC 4 colors and respond to them in a buggy way

* fix(sixel): do not render corrupted image

* feat(input): improve STDIN queries

* fix(client): mistake in clear terminal attributes string

* fix(ansi): report correct number of supported color registers

* fix(sixel): reap images that are completely covered

* style(comment): fix name

* test(sixel): infra

* test(sixel): cases and fixes

* fix(sixel): forward dcs bytes to sixel parser

* refactor(client): ansi stdin parser

* refactor(output): cleanup

* some refactorings

* fix test

* refactor(grid): sixel-grid / sixel-image-store

* refactor(grid): grid debug method

* refactor(grid): move various logic to sixel.rs

* refactor(grid): remove unused methods

* fix(sixel): work with multiple users

* refactor(pane): remove unused z_index

* style(fmt): prepend unused variable

* style(fmt): rustfmt

* fix(tests): various apis

* chore(dependencies): use published version of sixel crates

* style(fmt): rustfmt

* style(fmt): rustfmt

* style(lint): make clippy happy

* style(lint): make clippy happy... again

* style(lint): make clippy happy... again (chapter 2)

* style(comment): remove unused

* fix(colors): export COLORTERM and respond to XTVERSION

* fix(test): color register count

* fix(stdin): adjust STDIN sleep times
This commit is contained in:
Aram Drevekenin 2022-07-08 17:19:42 +02:00 committed by GitHub
parent 61deca80ed
commit c89b416d76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 4385 additions and 1068 deletions

34
Cargo.lock generated
View file

@ -53,6 +53,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "async-channel"
version = "1.6.1"
@ -1144,9 +1150,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.14.1"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc3e639bcba360d9237acabd22014c16f3df772db463b7446cd81b070714767"
checksum = "689960f187c43c01650c805fb6bc6f55ab944499d86d4ffe9474ad78991d8e94"
dependencies = [
"console",
"once_cell",
@ -2213,6 +2219,25 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "sixel-image"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84880b5da046b87741b9bad6ee916919d5548cca182f009b3a240e20a7f7c99f"
dependencies = [
"sixel-tokenizer",
]
[[package]]
name = "sixel-tokenizer"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71b7f6629ac964d60179c1fb00dfb80da265fc3465a17245e26c1eaf678d476"
dependencies = [
"arrayvec 0.7.2",
"thiserror",
]
[[package]]
name = "slab"
version = "0.4.6"
@ -2764,7 +2789,7 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
dependencies = [
"arrayvec",
"arrayvec 0.5.2",
"utf8parse",
"vte_generate_state_changes",
]
@ -3273,6 +3298,7 @@ name = "zellij-server"
version = "0.31.0"
dependencies = [
"ansi_term",
"arrayvec 0.7.2",
"async-trait",
"base64",
"byteorder",
@ -3284,6 +3310,8 @@ dependencies = [
"insta",
"log",
"serde_json",
"sixel-image",
"sixel-tokenizer",
"sysinfo",
"typetag",
"unicode-width",

View file

@ -50,7 +50,6 @@ pub fn get_cached_tip_name() -> String {
}
let quicknav_show_count = local_cache.get_cached_data().get("quicknav").unwrap_or(&0);
eprintln!("quicknav_show_count: {:?}", quicknav_show_count);
if quicknav_show_count <= &MAX_CACHE_HITS {
let _ = local_cache.caching("quicknav");
return String::from("quicknav");

View file

@ -1,9 +1,11 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use zellij_server::panes::sixel::SixelImageStore;
use zellij_server::panes::{LinkHandler, TerminalPane};
use zellij_utils::data::{Palette, Style};
use zellij_utils::pane_size::{Dimension, PaneGeom, Size};
use zellij_utils::pane_size::{Dimension, PaneGeom, Size, SizeInPixels};
use zellij_utils::vte;
use ssh2::Session;
@ -76,6 +78,7 @@ fn start_zellij(channel: &mut ssh2::Channel) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) {
@ -90,6 +93,7 @@ fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirrored: bool) {
@ -108,6 +112,7 @@ fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirr
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
@ -121,6 +126,7 @@ fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_without_frames(channel: &mut ssh2::Channel) {
@ -135,6 +141,7 @@ fn start_zellij_without_frames(channel: &mut ssh2::Channel) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_with_layout(channel: &mut ssh2::Channel, layout_path: &str) {
@ -153,6 +160,7 @@ fn start_zellij_with_layout(channel: &mut ssh2::Channel, layout_path: &str) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn read_from_channel(
@ -174,6 +182,11 @@ fn read_from_channel(
let mut retries_left = 3;
let mut should_sleep = false;
let mut vte_parser = vte::Parser::new();
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
height: 21,
width: 8,
})));
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let mut terminal_output = TerminalPane::new(
0,
pane_geom,
@ -181,8 +194,10 @@ fn read_from_channel(
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
Rc::new(RefCell::new(None)),
character_cell_size,
sixel_image_store,
Rc::new(RefCell::new(Palette::default())),
Rc::new(RefCell::new(HashMap::new())),
); // 0 is the pane index
loop {
if !should_keep_running.load(Ordering::SeqCst) {
@ -322,6 +337,7 @@ impl RemoteTerminal {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
pub fn load_fixture(&mut self, name: &str) {
let mut channel = self.channel.lock().unwrap();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,12 +2,13 @@
//! and dispatch actions, that are specified through the command line.
//! Multiple actions at the same time can be dispatched.
use log::debug;
use std::sync::{Arc, Mutex};
use std::{fs, path::PathBuf, thread};
use crate::{
command_is_executing::CommandIsExecuting, input_handler::input_actions,
os_input_output::ClientOsApi, stdin_handler::stdin_loop, ClientInfo, ClientInstruction,
InputInstruction,
os_input_output::ClientOsApi, stdin_ansi_parser::StdinAnsiParser, stdin_handler::stdin_loop,
ClientInfo, ClientInstruction, InputInstruction,
};
use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext},
@ -82,12 +83,13 @@ pub fn start_fake_client(
})
});
let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));
let _stdin_thread = thread::Builder::new()
.name("stdin_handler".to_string())
.spawn({
let os_input = os_input.clone();
let send_input_instructions = send_input_instructions.clone();
move || stdin_loop(os_input, send_input_instructions)
move || stdin_loop(os_input, send_input_instructions, stdin_ansi_parser)
});
let clients: Vec<ClientId>;

View file

@ -1,8 +1,7 @@
//! Main input logic.
use crate::{
os_input_output::ClientOsApi,
stdin_ansi_parser::{AnsiStdinInstructionOrKeys, StdinAnsiParser},
ClientId, ClientInstruction, CommandIsExecuting, InputInstruction,
os_input_output::ClientOsApi, stdin_ansi_parser::AnsiStdinInstruction, ClientId,
ClientInstruction, CommandIsExecuting, InputInstruction,
};
use zellij_utils::{
channels::{Receiver, SenderWithContext, OPENCALLS},
@ -69,19 +68,6 @@ impl InputHandler {
if self.options.mouse_mode.unwrap_or(true) {
self.os_input.enable_mouse();
}
// <ESC>[14t => get text area size in pixels,
// <ESC>[16t => get character cell size in pixels
// <ESC>]11;?<ESC>\ => get background color
// <ESC>]10;?<ESC>\ => get foreground color
let get_cell_pixel_info =
"\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}";
let _ = self
.os_input
.get_stdout_writer()
.write(get_cell_pixel_info.as_bytes())
.unwrap();
let mut ansi_stdin_parser = StdinAnsiParser::new();
ansi_stdin_parser.increment_expected_ansi_instructions(4);
loop {
if self.should_exit {
break;
@ -91,13 +77,7 @@ impl InputHandler {
match input_event {
InputEvent::Key(key_event) => {
let key = cast_termwiz_key(key_event, &raw_bytes);
if ansi_stdin_parser.expected_instructions() > 0 {
self.handle_possible_pixel_instruction(
ansi_stdin_parser.parse(key, raw_bytes),
);
} else {
self.handle_key(&key, raw_bytes);
}
self.handle_key(&key, raw_bytes);
},
InputEvent::Mouse(mouse_event) => {
let mouse_event =
@ -126,13 +106,13 @@ impl InputHandler {
Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => {
self.mode = input_mode;
},
Ok((InputInstruction::PossiblePixelRatioChange, _error_context)) => {
let _ = self
.os_input
.get_stdout_writer()
.write(get_cell_pixel_info.as_bytes())
.unwrap();
ansi_stdin_parser.increment_expected_ansi_instructions(4);
Ok((
InputInstruction::AnsiStdinInstructions(ansi_stdin_instructions),
_error_context,
)) => {
for ansi_instruction in ansi_stdin_instructions {
self.handle_stdin_ansi_instruction(ansi_instruction);
}
},
Err(err) => panic!("Encountered read error: {:?}", err),
}
@ -147,33 +127,28 @@ impl InputHandler {
}
}
}
fn handle_possible_pixel_instruction(
&mut self,
pixel_instruction_or_keys: Option<AnsiStdinInstructionOrKeys>,
) {
match pixel_instruction_or_keys {
Some(AnsiStdinInstructionOrKeys::PixelDimensions(pixel_dimensions)) => {
fn handle_stdin_ansi_instruction(&mut self, ansi_stdin_instructions: AnsiStdinInstruction) {
match ansi_stdin_instructions {
AnsiStdinInstruction::PixelDimensions(pixel_dimensions) => {
self.os_input
.send_to_server(ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions));
},
Some(AnsiStdinInstructionOrKeys::BackgroundColor(background_color_instruction)) => {
AnsiStdinInstruction::BackgroundColor(background_color_instruction) => {
self.os_input
.send_to_server(ClientToServerMsg::BackgroundColor(
background_color_instruction,
));
},
Some(AnsiStdinInstructionOrKeys::ForegroundColor(foreground_color_instruction)) => {
AnsiStdinInstruction::ForegroundColor(foreground_color_instruction) => {
self.os_input
.send_to_server(ClientToServerMsg::ForegroundColor(
foreground_color_instruction,
));
},
Some(AnsiStdinInstructionOrKeys::Keys(keys)) => {
for (key, raw_bytes) in keys {
self.handle_key(&key, raw_bytes);
}
AnsiStdinInstruction::ColorRegisters(color_registers) => {
self.os_input
.send_to_server(ClientToServerMsg::ColorRegisters(color_registers));
},
None => {},
}
}
fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) {
@ -352,7 +327,6 @@ pub(crate) fn input_loop(
)
.handle_input();
}
/// Entry point to the module. Instantiates an [`InputHandler`] and starts
/// its [`InputHandler::handle_input()`] loop.
#[allow(clippy::too_many_arguments)]
@ -379,7 +353,3 @@ pub(crate) fn input_actions(
)
.handle_actions(actions, &session_name, clients);
}
#[cfg(test)]
#[path = "./unit/input_handler_tests.rs"]
mod input_handler_tests;

View file

@ -13,8 +13,10 @@ use std::env::current_exe;
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::thread;
use crate::stdin_ansi_parser::{AnsiStdinInstruction, StdinAnsiParser};
use crate::{
command_is_executing::CommandIsExecuting, input_handler::input_loop,
os_input_output::ClientOsApi, stdin_handler::stdin_loop,
@ -114,7 +116,7 @@ impl ClientInfo {
pub(crate) enum InputInstruction {
KeyEvent(InputEvent, Vec<u8>),
SwitchToMode(InputMode),
PossiblePixelRatioChange,
AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
}
pub fn start_client(
@ -126,7 +128,7 @@ pub fn start_client(
layout: Option<LayoutFromYaml>,
) {
info!("Starting Zellij client!");
let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}12l\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l";
let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l";
let take_snapshot = "\u{1b}[?1049h";
let bracketed_paste = "\u{1b}[?2004h";
os_input.unset_raw_mode(0).unwrap();
@ -162,11 +164,13 @@ pub fn start_client(
let first_msg = match info {
ClientInfo::Attach(name, config_options) => {
envs::set_session_name(name);
envs::set_initial_environment_vars();
ClientToServerMsg::AttachClient(client_attributes, config_options)
},
ClientInfo::New(name) => {
envs::set_session_name(name);
envs::set_initial_environment_vars();
spawn_server(&*ZELLIJ_IPC_PIPE).unwrap();
@ -214,13 +218,15 @@ pub fn start_client(
});
let on_force_close = config_options.on_force_close.unwrap_or_default();
let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));
let _stdin_thread = thread::Builder::new()
.name("stdin_handler".to_string())
.spawn({
let os_input = os_input.clone();
let send_input_instructions = send_input_instructions.clone();
move || stdin_loop(os_input, send_input_instructions)
let stdin_ansi_parser = stdin_ansi_parser.clone();
move || stdin_loop(os_input, send_input_instructions, stdin_ansi_parser)
});
let _input_thread = thread::Builder::new()
@ -246,7 +252,6 @@ pub fn start_client(
let _signal_thread = thread::Builder::new()
.name("signal_listener".to_string())
.spawn({
let send_input_instructions = send_input_instructions.clone();
let os_input = os_input.clone();
move || {
os_input.handle_signals(
@ -256,8 +261,16 @@ pub fn start_client(
os_api.send_to_server(ClientToServerMsg::TerminalResize(
os_api.get_terminal_size_using_fd(0),
));
let _ = send_input_instructions
.send(InputInstruction::PossiblePixelRatioChange);
// send a query to the terminal emulator in case the font size changed
// as well - we'll parse the response through STDIN
let terminal_emulator_query_string = stdin_ansi_parser
.lock()
.unwrap()
.window_size_change_query_string();
let _ = os_api
.get_stdout_writer()
.write(terminal_emulator_query_string.as_bytes())
.unwrap();
}
}),
Box::new({
@ -385,3 +398,7 @@ pub fn start_client(
let _ = stdout.write(goodbye_message.as_bytes()).unwrap();
stdout.flush().unwrap();
}
#[cfg(test)]
#[path = "./unit/stdin_tests.rs"]
mod stdin_tests;

View file

@ -94,7 +94,7 @@ pub trait ClientOsApi: Send + Sync {
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
fn get_stdin_reader(&self) -> Box<dyn io::Read>;
/// Returns the raw contents of standard input.
fn read_from_stdin(&self) -> Vec<u8>;
fn read_from_stdin(&mut self) -> Vec<u8>;
/// Returns a [`Box`] pointer to this [`ClientOsApi`] struct.
fn box_clone(&self) -> Box<dyn ClientOsApi>;
/// Sends a message to the server.
@ -126,7 +126,7 @@ impl ClientOsApi for ClientOsInputOutput {
fn box_clone(&self) -> Box<dyn ClientOsApi> {
Box::new((*self).clone())
}
fn read_from_stdin(&self) -> Vec<u8> {
fn read_from_stdin(&mut self) -> Vec<u8> {
let stdin = std::io::stdin();
let mut stdin = stdin.lock();
let buffer = stdin.fill_buf().unwrap();

View file

@ -1,200 +1,234 @@
use std::time::{Duration, Instant};
const STARTUP_PARSE_DEADLINE_MS: u64 = 500;
const SIGWINCH_PARSE_DEADLINE_MS: u64 = 200;
use zellij_utils::{
data::{CharOrArrow, Key},
ipc::PixelDimensions,
lazy_static::lazy_static,
pane_size::SizeInPixels,
regex::Regex,
ipc::PixelDimensions, lazy_static::lazy_static, pane_size::SizeInPixels, regex::Regex,
};
#[derive(Debug)]
pub struct StdinAnsiParser {
expected_ansi_instructions: usize,
current_buffer: Vec<(Key, Vec<u8>)>,
raw_buffer: Vec<u8>,
pending_color_sequences: Vec<(usize, String)>,
pending_events: Vec<AnsiStdinInstruction>,
parse_deadline: Option<Instant>,
}
impl StdinAnsiParser {
pub fn new() -> Self {
StdinAnsiParser {
expected_ansi_instructions: 0,
current_buffer: vec![],
raw_buffer: vec![],
pending_color_sequences: vec![],
pending_events: vec![],
parse_deadline: None,
}
}
pub fn increment_expected_ansi_instructions(&mut self, to: usize) {
self.expected_ansi_instructions += to;
pub fn terminal_emulator_query_string(&mut self) -> String {
// note that this assumes the String will be sent to the terminal emulator and so starts a
// deadline timeout (self.parse_deadline)
// <ESC>[14t => get text area size in pixels,
// <ESC>[16t => get character cell size in pixels
// <ESC>]11;?<ESC>\ => get background color
// <ESC>]10;?<ESC>\ => get foreground color
let mut query_string =
String::from("\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}");
// query colors
// eg. <ESC>]4;5;?<ESC>\ => query color register number 5
for i in 0..256 {
query_string.push_str(&format!("\u{1b}]4;{};?\u{1b}\u{5c}", i));
}
self.parse_deadline =
Some(Instant::now() + Duration::from_millis(STARTUP_PARSE_DEADLINE_MS));
query_string
}
pub fn decrement_expected_ansi_instructions(&mut self, by: usize) {
self.expected_ansi_instructions = self.expected_ansi_instructions.saturating_sub(by);
pub fn window_size_change_query_string(&mut self) -> String {
// note that this assumes the String will be sent to the terminal emulator and so starts a
// deadline timeout (self.parse_deadline)
// <ESC>[14t => get text area size in pixels,
// <ESC>[16t => get character cell size in pixels
let query_string = String::from("\u{1b}[14t\u{1b}[16t");
self.parse_deadline =
Some(Instant::now() + Duration::from_millis(SIGWINCH_PARSE_DEADLINE_MS));
query_string
}
pub fn expected_instructions(&self) -> usize {
self.expected_ansi_instructions
}
pub fn parse(&mut self, key: Key, raw_bytes: Vec<u8>) -> Option<AnsiStdinInstructionOrKeys> {
if self.current_buffer.is_empty()
&& (key != Key::Esc && key != Key::Alt(CharOrArrow::Char(']')))
fn drain_pending_events(&mut self) -> Vec<AnsiStdinInstruction> {
let mut events = vec![];
events.append(&mut self.pending_events);
if let Some(color_registers) =
AnsiStdinInstruction::color_registers_from_bytes(&mut self.pending_color_sequences)
{
// the first key of a sequence is always Esc, but termwiz interprets esc + ] as Alt+]
self.current_buffer.push((key, raw_bytes));
self.expected_ansi_instructions = 0;
return Some(AnsiStdinInstructionOrKeys::Keys(
self.current_buffer.drain(..).collect(),
));
}
if let Key::Char('t') = key {
self.current_buffer.push((key, raw_bytes));
match AnsiStdinInstructionOrKeys::pixel_dimensions_from_keys(&self.current_buffer) {
Ok(pixel_instruction) => {
self.decrement_expected_ansi_instructions(1);
self.current_buffer.clear();
Some(pixel_instruction)
},
Err(_) => {
self.expected_ansi_instructions = 0;
Some(AnsiStdinInstructionOrKeys::Keys(
self.current_buffer.drain(..).collect(),
))
},
}
} else if let Key::Alt(CharOrArrow::Char('\\')) | Key::Ctrl('g') = key {
match AnsiStdinInstructionOrKeys::color_sequence_from_keys(&self.current_buffer) {
Ok(color_instruction) => {
self.decrement_expected_ansi_instructions(1);
self.current_buffer.clear();
Some(color_instruction)
},
Err(_) => {
self.expected_ansi_instructions = 0;
Some(AnsiStdinInstructionOrKeys::Keys(
self.current_buffer.drain(..).collect(),
))
},
}
} else if self.key_is_valid(key) {
self.current_buffer.push((key, raw_bytes));
None
} else {
self.current_buffer.push((key, raw_bytes));
self.expected_ansi_instructions = 0;
Some(AnsiStdinInstructionOrKeys::Keys(
self.current_buffer.drain(..).collect(),
))
events.push(color_registers);
}
events
}
fn key_is_valid(&self, key: Key) -> bool {
match key {
Key::Esc => {
// this is a UX improvement
// in case the user's terminal doesn't support one or more of these signals,
// if they spam ESC they need to be able to get back to normal mode and not "us
// waiting for ansi instructions" mode
!self.current_buffer.iter().any(|(key, _)| *key == Key::Esc)
},
Key::Char(';')
| Key::Char('[')
| Key::Char(']')
| Key::Char('r')
| Key::Char('g')
| Key::Char('b')
| Key::Char('\\')
| Key::Char(':')
| Key::Char('/') => true,
Key::Alt(CharOrArrow::Char(']')) => true,
Key::Alt(CharOrArrow::Char('\\')) => true,
Key::Char(c) => {
matches!(c, '0'..='9' | 'a'..='f')
},
_ => false,
pub fn should_parse(&self) -> bool {
if let Some(parse_deadline) = self.parse_deadline {
if parse_deadline >= Instant::now() {
return true;
}
}
false
}
pub fn parse(&mut self, mut raw_bytes: Vec<u8>) -> Vec<AnsiStdinInstruction> {
for byte in raw_bytes.drain(..) {
self.parse_byte(byte);
}
self.drain_pending_events()
}
fn parse_byte(&mut self, byte: u8) {
if byte == b't' {
self.raw_buffer.push(byte);
match AnsiStdinInstruction::pixel_dimensions_from_bytes(&self.raw_buffer) {
Ok(ansi_sequence) => {
self.pending_events.push(ansi_sequence);
self.raw_buffer.clear();
},
Err(_) => {
self.raw_buffer.clear();
},
}
} else if byte == b'\\' {
self.raw_buffer.push(byte);
if let Ok(ansi_sequence) = AnsiStdinInstruction::bg_or_fg_from_bytes(&self.raw_buffer) {
self.pending_events.push(ansi_sequence);
self.raw_buffer.clear();
} else if let Ok((color_register, color_sequence)) =
color_sequence_from_bytes(&self.raw_buffer)
{
self.raw_buffer.clear();
self.pending_color_sequences
.push((color_register, color_sequence));
} else {
self.raw_buffer.clear();
}
} else {
self.raw_buffer.push(byte);
}
}
}
#[derive(Debug)]
pub enum AnsiStdinInstructionOrKeys {
#[derive(Debug, Clone)]
pub enum AnsiStdinInstruction {
PixelDimensions(PixelDimensions),
BackgroundColor(String),
ForegroundColor(String),
Keys(Vec<(Key, Vec<u8>)>),
ColorRegisters(Vec<(usize, String)>),
}
impl AnsiStdinInstructionOrKeys {
pub fn pixel_dimensions_from_keys(keys: &[(Key, Vec<u8>)]) -> Result<Self, &'static str> {
impl AnsiStdinInstruction {
pub fn pixel_dimensions_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
// eg. <ESC>[4;21;8t
lazy_static! {
static ref RE: Regex = Regex::new(r"^\u{1b}\[(\d+);(\d+);(\d+)t$").unwrap();
}
let key_sequence: Vec<Option<char>> = keys
.iter()
.map(|(key, _)| match key {
Key::Char(c) => Some(*c),
Key::Esc => Some('\u{1b}'),
_ => None,
})
.collect();
if key_sequence.iter().all(|k| k.is_some()) {
let key_string: String = key_sequence.iter().map(|k| k.unwrap()).collect();
let captures = RE
.captures_iter(&key_string)
.next()
.ok_or("invalid_instruction")?;
let csi_index = captures[1].parse::<usize>();
let first_field = captures[2].parse::<usize>();
let second_field = captures[3].parse::<usize>();
if csi_index.is_err() || first_field.is_err() || second_field.is_err() {
return Err("invalid_instruction");
}
match csi_index {
Ok(4) => {
// text area size
Ok(AnsiStdinInstructionOrKeys::PixelDimensions(
PixelDimensions {
character_cell_size: None,
text_area_size: Some(SizeInPixels {
height: first_field.unwrap(),
width: second_field.unwrap(),
}),
},
))
},
Ok(6) => {
// character cell size
Ok(AnsiStdinInstructionOrKeys::PixelDimensions(
PixelDimensions {
character_cell_size: Some(SizeInPixels {
height: first_field.unwrap(),
width: second_field.unwrap(),
}),
text_area_size: None,
},
))
},
_ => Err("invalid sequence"),
}
} else {
Err("invalid sequence")
let key_string = String::from_utf8_lossy(bytes); // TODO: handle error
let captures = RE
.captures_iter(&key_string)
.next()
.ok_or("invalid_instruction")?;
let csi_index = captures[1].parse::<usize>();
let first_field = captures[2].parse::<usize>();
let second_field = captures[3].parse::<usize>();
if csi_index.is_err() || first_field.is_err() || second_field.is_err() {
return Err("invalid_instruction");
}
match csi_index {
Ok(4) => {
// text area size
Ok(AnsiStdinInstruction::PixelDimensions(PixelDimensions {
character_cell_size: None,
text_area_size: Some(SizeInPixels {
height: first_field.unwrap(),
width: second_field.unwrap(),
}),
}))
},
Ok(6) => {
// character cell size
Ok(AnsiStdinInstruction::PixelDimensions(PixelDimensions {
character_cell_size: Some(SizeInPixels {
height: first_field.unwrap(),
width: second_field.unwrap(),
}),
text_area_size: None,
}))
},
_ => Err("invalid sequence"),
}
}
pub fn color_sequence_from_keys(keys: &[(Key, Vec<u8>)]) -> Result<Self, &'static str> {
pub fn bg_or_fg_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
// eg. <ESC>]11;rgb:0000/0000/0000\
lazy_static! {
static ref BACKGROUND_RE: Regex = Regex::new(r"11;(.*)$").unwrap();
static ref BACKGROUND_RE: Regex = Regex::new(r"\]11;(.*)\u{1b}\\$").unwrap();
}
// eg. <ESC>]10;rgb:ffff/ffff/ffff\
lazy_static! {
static ref FOREGROUND_RE: Regex = Regex::new(r"10;(.*)$").unwrap();
static ref FOREGROUND_RE: Regex = Regex::new(r"\]10;(.*)\u{1b}\\$").unwrap();
}
let key_string = keys.iter().fold(String::new(), |mut acc, (key, _)| {
if let Key::Char(c) = key {
acc.push(*c)
};
acc
});
let key_string = String::from_utf8_lossy(bytes);
if let Some(captures) = BACKGROUND_RE.captures_iter(&key_string).next() {
let background_query_response = captures[1].parse::<String>();
Ok(AnsiStdinInstructionOrKeys::BackgroundColor(
background_query_response.unwrap(),
))
match background_query_response {
Ok(background_query_response) => Ok(AnsiStdinInstruction::BackgroundColor(
background_query_response,
)),
_ => Err("invalid_instruction"),
}
} else if let Some(captures) = FOREGROUND_RE.captures_iter(&key_string).next() {
let foreground_query_response = captures[1].parse::<String>();
Ok(AnsiStdinInstructionOrKeys::ForegroundColor(
foreground_query_response.unwrap(),
))
match foreground_query_response {
Ok(foreground_query_response) => Ok(AnsiStdinInstruction::ForegroundColor(
foreground_query_response,
)),
_ => Err("invalid_instruction"),
}
} else {
Err("invalid_instruction")
}
}
pub fn color_registers_from_bytes(color_sequences: &mut Vec<(usize, String)>) -> Option<Self> {
if color_sequences.is_empty() {
return None;
}
let mut registers = vec![];
for (color_register, color_sequence) in color_sequences.drain(..) {
registers.push((color_register, color_sequence));
}
Some(AnsiStdinInstruction::ColorRegisters(registers))
}
}
fn color_sequence_from_bytes(bytes: &[u8]) -> Result<(usize, String), &'static str> {
lazy_static! {
static ref COLOR_REGISTER_RE: Regex = Regex::new(r"\]4;(.*);(.*)\u{1b}\\$").unwrap();
}
lazy_static! {
// this form is used by eg. Alacritty, where the leading 4 is dropped in the response
static ref ALTERNATIVE_COLOR_REGISTER_RE: Regex = Regex::new(r"\](.*);(.*)\u{1b}\\$").unwrap();
}
let key_string = String::from_utf8_lossy(bytes);
if let Some(captures) = COLOR_REGISTER_RE.captures_iter(&key_string).next() {
let color_register_response = captures[1].parse::<usize>();
let color_response = captures[2].parse::<String>();
match (color_register_response, color_response) {
(Ok(crr), Ok(cr)) => Ok((crr, cr)),
_ => Err("invalid_instruction"),
}
} else if let Some(captures) = ALTERNATIVE_COLOR_REGISTER_RE
.captures_iter(&key_string)
.next()
{
let color_register_response = captures[1].parse::<usize>();
let color_response = captures[2].parse::<String>();
match (color_register_response, color_response) {
(Ok(crr), Ok(cr)) => Ok((crr, cr)),
_ => Err("invalid_instruction"),
}
} else {
Err("invalid_instruction")
}
}

View file

@ -1,17 +1,46 @@
use crate::os_input_output::ClientOsApi;
use crate::stdin_ansi_parser::StdinAnsiParser;
use crate::InputInstruction;
use std::sync::{Arc, Mutex};
use zellij_utils::channels::SenderWithContext;
use zellij_utils::termwiz::input::{InputEvent, InputParser, MouseButtons};
pub(crate) fn stdin_loop(
os_input: Box<dyn ClientOsApi>,
mut os_input: Box<dyn ClientOsApi>,
send_input_instructions: SenderWithContext<InputInstruction>,
stdin_ansi_parser: Arc<Mutex<StdinAnsiParser>>,
) {
let mut holding_mouse = false;
let mut input_parser = InputParser::new();
let mut current_buffer = vec![];
// on startup we send a query to the terminal emulator for stuff like the pixel size and colors
// we get a response through STDIN, so it makes sense to do this here
let terminal_emulator_query_string = stdin_ansi_parser
.lock()
.unwrap()
.terminal_emulator_query_string();
let _ = os_input
.get_stdout_writer()
.write(terminal_emulator_query_string.as_bytes())
.unwrap();
loop {
let buf = os_input.read_from_stdin();
{
// here we check if we need to parse specialized ANSI instructions sent over STDIN
// this happens either on startup (see above) or on SIGWINCH
//
// if we need to parse them, we do so with an internal timeout - anything else we
// receive on STDIN during that timeout is unceremoniously dropped
let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap();
if stdin_ansi_parser.should_parse() {
let events = stdin_ansi_parser.parse(buf);
if !events.is_empty() {
let _ = send_input_instructions
.send(InputInstruction::AnsiStdinInstructions(events));
}
continue;
}
}
current_buffer.append(&mut buf.to_vec());
let maybe_more = false; // read_from_stdin should (hopefully) always empty the STDIN buffer completely
let mut events = vec![];

View file

@ -1,733 +0,0 @@
use super::input_loop;
use zellij_utils::data::{InputMode, Palette};
use zellij_utils::input::actions::{Action, Direction};
use zellij_utils::input::config::Config;
use zellij_utils::input::options::Options;
use zellij_utils::nix;
use zellij_utils::pane_size::{Size, SizeInPixels};
use zellij_utils::termwiz::input::{InputEvent, KeyCode, KeyEvent, Modifiers};
use crate::InputInstruction;
use crate::{
os_input_output::{ClientOsApi, StdinPoller},
ClientInstruction, CommandIsExecuting,
};
use std::path::Path;
use std::io;
use std::os::unix::io::RawFd;
use std::sync::{Arc, Mutex};
use zellij_utils::{
errors::ErrorContext,
ipc::{ClientToServerMsg, PixelDimensions, ServerToClientMsg},
};
use zellij_utils::channels::{self, ChannelWithContext, SenderWithContext};
#[allow(unused)]
pub mod commands {
pub const QUIT: [u8; 1] = [17]; // ctrl-q
pub const ESC: [u8; 1] = [27];
pub const ENTER: [u8; 1] = [10]; // char '\n'
pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h
pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l
pub const PANE_MODE: [u8; 1] = [16]; // ctrl-p
pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [110]; // n
pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p
pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d
pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r
pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f
pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x
pub const MOVE_FOCUS_DOWN_IN_PANE_MODE: [u8; 1] = [106]; // j
pub const MOVE_FOCUS_UP_IN_PANE_MODE: [u8; 1] = [107]; // k
pub const MOVE_FOCUS_LEFT_IN_PANE_MODE: [u8; 1] = [104]; // h
pub const MOVE_FOCUS_RIGHT_IN_PANE_MODE: [u8; 1] = [108]; // l
pub const SCROLL_MODE: [u8; 1] = [19]; // ctrl-s
pub const SCROLL_UP_IN_SCROLL_MODE: [u8; 1] = [107]; // k
pub const SCROLL_DOWN_IN_SCROLL_MODE: [u8; 1] = [106]; // j
pub const SCROLL_PAGE_UP_IN_SCROLL_MODE: [u8; 1] = [2]; // ctrl-b
pub const SCROLL_PAGE_DOWN_IN_SCROLL_MODE: [u8; 1] = [6]; // ctrl-f
pub const RESIZE_MODE: [u8; 1] = [18]; // ctrl-r
pub const RESIZE_DOWN_IN_RESIZE_MODE: [u8; 1] = [106]; // j
pub const RESIZE_UP_IN_RESIZE_MODE: [u8; 1] = [107]; // k
pub const RESIZE_LEFT_IN_RESIZE_MODE: [u8; 1] = [104]; // h
pub const RESIZE_RIGHT_IN_RESIZE_MODE: [u8; 1] = [108]; // l
pub const TAB_MODE: [u8; 1] = [20]; // ctrl-t
pub const NEW_TAB_IN_TAB_MODE: [u8; 1] = [110]; // n
pub const SWITCH_NEXT_TAB_IN_TAB_MODE: [u8; 1] = [108]; // l
pub const SWITCH_PREV_TAB_IN_TAB_MODE: [u8; 1] = [104]; // h
pub const CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [120]; // x
pub const BRACKETED_PASTE_START: [u8; 6] = [27, 91, 50, 48, 48, 126]; // \u{1b}[200~
pub const BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201
pub const SLEEP: [u8; 0] = [];
}
#[derive(Default, Clone)]
struct FakeStdoutWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl FakeStdoutWriter {
pub fn new(buffer: Arc<Mutex<Vec<u8>>>) -> Self {
FakeStdoutWriter { buffer }
}
}
impl io::Write for FakeStdoutWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
self.buffer.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
}
#[derive(Clone)]
struct FakeClientOsApi {
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
command_is_executing: Arc<Mutex<CommandIsExecuting>>,
stdout_buffer: Arc<Mutex<Vec<u8>>>,
}
impl FakeClientOsApi {
pub fn new(
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
command_is_executing: CommandIsExecuting,
) -> Self {
// while command_is_executing itself is implemented with an Arc<Mutex>, we have to have an
// Arc<Mutex> here because we need interior mutability, otherwise we'll have to change the
// ClientOsApi trait, and that will cause a lot of havoc
let command_is_executing = Arc::new(Mutex::new(command_is_executing));
let stdout_buffer = Arc::new(Mutex::new(vec![]));
FakeClientOsApi {
events_sent_to_server,
command_is_executing,
stdout_buffer,
}
}
pub fn stdout_buffer(&self) -> Vec<u8> {
self.stdout_buffer.lock().unwrap().drain(..).collect()
}
}
impl ClientOsApi for FakeClientOsApi {
fn get_terminal_size_using_fd(&self, _fd: RawFd) -> Size {
unimplemented!()
}
fn set_raw_mode(&mut self, _fd: RawFd) {
unimplemented!()
}
fn unset_raw_mode(&self, _fd: RawFd) -> Result<(), nix::Error> {
unimplemented!()
}
fn get_stdout_writer(&self) -> Box<dyn io::Write> {
let fake_stdout_writer = FakeStdoutWriter::new(self.stdout_buffer.clone());
Box::new(fake_stdout_writer)
}
fn get_stdin_reader(&self) -> Box<dyn io::Read> {
unimplemented!()
}
fn read_from_stdin(&self) -> Vec<u8> {
unimplemented!()
}
fn box_clone(&self) -> Box<dyn ClientOsApi> {
unimplemented!()
}
fn send_to_server(&self, msg: ClientToServerMsg) {
{
let mut events_sent_to_server = self.events_sent_to_server.lock().unwrap();
events_sent_to_server.push(msg);
}
{
let mut command_is_executing = self.command_is_executing.lock().unwrap();
command_is_executing.unblock_input_thread();
}
}
fn recv_from_server(&self) -> Option<(ServerToClientMsg, ErrorContext)> {
unimplemented!()
}
fn handle_signals(&self, _sigwinch_cb: Box<dyn Fn()>, _quit_cb: Box<dyn Fn()>) {
unimplemented!()
}
fn connect_to_server(&self, _path: &Path) {
unimplemented!()
}
fn load_palette(&self) -> Palette {
unimplemented!()
}
fn enable_mouse(&self) {}
fn disable_mouse(&self) {}
fn stdin_poller(&self) -> StdinPoller {
unimplemented!()
}
}
fn extract_actions_sent_to_server(
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
) -> Vec<Action> {
let events_sent_to_server = events_sent_to_server.lock().unwrap();
events_sent_to_server.iter().fold(vec![], |mut acc, event| {
if let ClientToServerMsg::Action(action, None) = event {
acc.push(action.clone());
}
acc
})
}
fn extract_pixel_events_sent_to_server(
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
) -> Vec<PixelDimensions> {
let events_sent_to_server = events_sent_to_server.lock().unwrap();
events_sent_to_server.iter().fold(vec![], |mut acc, event| {
if let ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions) = event {
acc.push(*pixel_dimensions);
}
acc
})
}
#[test]
pub fn quit_breaks_input_loop() {
let stdin_events = vec![(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
)];
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api = Box::new(FakeClientOsApi::new(
events_sent_to_server.clone(),
command_is_executing.clone(),
));
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
for event in stdin_events {
send_input_instructions
.send(InputInstruction::KeyEvent(event.1, event.0))
.unwrap();
}
let default_mode = InputMode::Normal;
input_loop(
client_os_api,
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
);
let expected_actions_sent_to_server = vec![Action::Quit];
let received_actions = extract_actions_sent_to_server(events_sent_to_server);
assert_eq!(
expected_actions_sent_to_server, received_actions,
"All actions sent to server properly"
);
}
#[test]
pub fn move_focus_left_in_normal_mode() {
let stdin_events = vec![
(
commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('h'),
modifiers: Modifiers::ALT,
}),
),
(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
),
];
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api = Box::new(FakeClientOsApi::new(
events_sent_to_server.clone(),
command_is_executing.clone(),
));
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
for event in stdin_events {
send_input_instructions
.send(InputInstruction::KeyEvent(event.1, event.0))
.unwrap();
}
let default_mode = InputMode::Normal;
input_loop(
client_os_api,
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
);
let expected_actions_sent_to_server =
vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit];
let received_actions = extract_actions_sent_to_server(events_sent_to_server);
assert_eq!(
expected_actions_sent_to_server, received_actions,
"All actions sent to server properly"
);
}
#[test]
pub fn pixel_info_queried_from_terminal_emulator() {
let stdin_events = vec![(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
)];
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api = FakeClientOsApi::new(events_sent_to_server, command_is_executing.clone());
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
for event in stdin_events {
send_input_instructions
.send(InputInstruction::KeyEvent(event.1, event.0))
.unwrap();
}
let default_mode = InputMode::Normal;
let client_os_api_clone = client_os_api.clone();
input_loop(
Box::new(client_os_api),
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
);
let extracted_stdout_buffer = client_os_api_clone.stdout_buffer();
assert_eq!(
String::from_utf8(extracted_stdout_buffer),
Ok(String::from(
"\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\\\u{1b}]10;?\u{1b}\\"
)),
);
}
#[test]
pub fn pixel_info_sent_to_server() {
let stdin_events = vec![
(
vec![27],
InputEvent::Key(KeyEvent {
key: KeyCode::Escape,
modifiers: Modifiers::NONE,
}),
),
(
"[".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('['),
modifiers: Modifiers::NONE,
}),
),
(
"6".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('6'),
modifiers: Modifiers::NONE,
}),
),
(
";".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char(';'),
modifiers: Modifiers::NONE,
}),
),
(
"1".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('1'),
modifiers: Modifiers::NONE,
}),
),
(
"0".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('0'),
modifiers: Modifiers::NONE,
}),
),
(
";".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char(';'),
modifiers: Modifiers::NONE,
}),
),
(
"5".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('5'),
modifiers: Modifiers::NONE,
}),
),
(
"t".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('t'),
modifiers: Modifiers::NONE,
}),
),
(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
),
];
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api =
FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone());
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
for event in stdin_events {
send_input_instructions
.send(InputInstruction::KeyEvent(event.1, event.0))
.unwrap();
}
let default_mode = InputMode::Normal;
input_loop(
Box::new(client_os_api),
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
);
let actions_sent_to_server = extract_actions_sent_to_server(events_sent_to_server.clone());
let pixel_events_sent_to_server = extract_pixel_events_sent_to_server(events_sent_to_server);
assert_eq!(actions_sent_to_server, vec![Action::Quit]);
assert_eq!(
pixel_events_sent_to_server,
vec![PixelDimensions {
character_cell_size: Some(SizeInPixels {
height: 10,
width: 5
}),
text_area_size: None
}],
);
}
#[test]
pub fn corrupted_pixel_info_sent_as_key_events() {
let stdin_events = vec![
(
vec![27],
InputEvent::Key(KeyEvent {
key: KeyCode::Escape,
modifiers: Modifiers::NONE,
}),
),
(
"[".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('['),
modifiers: Modifiers::NONE,
}),
),
(
"f".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('f'),
modifiers: Modifiers::NONE,
}),
),
(
";".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char(';'),
modifiers: Modifiers::NONE,
}),
),
(
"1".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('1'),
modifiers: Modifiers::NONE,
}),
),
(
"0".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('0'),
modifiers: Modifiers::NONE,
}),
),
(
";".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char(';'),
modifiers: Modifiers::NONE,
}),
),
(
"5".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('5'),
modifiers: Modifiers::NONE,
}),
),
(
"t".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('t'),
modifiers: Modifiers::NONE,
}),
),
(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
),
];
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api =
FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone());
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
for event in stdin_events {
send_input_instructions
.send(InputInstruction::KeyEvent(event.1, event.0))
.unwrap();
}
let default_mode = InputMode::Normal;
input_loop(
Box::new(client_os_api),
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
);
let actions_sent_to_server = extract_actions_sent_to_server(events_sent_to_server.clone());
let pixel_events_sent_to_server = extract_pixel_events_sent_to_server(events_sent_to_server);
assert_eq!(
actions_sent_to_server,
vec![
Action::Write(vec![27]),
Action::Write(vec![b'[']),
Action::Write(vec![b'f']),
Action::Write(vec![b';']),
Action::Write(vec![b'1']),
Action::Write(vec![b'0']),
Action::Write(vec![b';']),
Action::Write(vec![b'5']),
Action::Write(vec![b't']),
Action::Quit
]
);
assert_eq!(pixel_events_sent_to_server, vec![],);
}
#[test]
pub fn esc_in_the_middle_of_pixelinfo_breaks_out_of_it() {
let stdin_events = vec![
(
vec![27],
InputEvent::Key(KeyEvent {
key: KeyCode::Escape,
modifiers: Modifiers::NONE,
}),
),
(
"[".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('['),
modifiers: Modifiers::NONE,
}),
),
(
vec![27],
InputEvent::Key(KeyEvent {
key: KeyCode::Escape,
modifiers: Modifiers::NONE,
}),
),
(
";".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char(';'),
modifiers: Modifiers::NONE,
}),
),
(
"1".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('1'),
modifiers: Modifiers::NONE,
}),
),
(
"0".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('0'),
modifiers: Modifiers::NONE,
}),
),
(
";".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char(';'),
modifiers: Modifiers::NONE,
}),
),
(
"5".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('5'),
modifiers: Modifiers::NONE,
}),
),
(
"t".as_bytes().to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('t'),
modifiers: Modifiers::NONE,
}),
),
(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
),
];
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api =
FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone());
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
for event in stdin_events {
send_input_instructions
.send(InputInstruction::KeyEvent(event.1, event.0))
.unwrap();
}
let default_mode = InputMode::Normal;
input_loop(
Box::new(client_os_api),
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
);
let actions_sent_to_server = extract_actions_sent_to_server(events_sent_to_server.clone());
let pixel_events_sent_to_server = extract_pixel_events_sent_to_server(events_sent_to_server);
assert_eq!(
actions_sent_to_server,
vec![
Action::Write(vec![27]),
Action::Write(vec![b'[']),
Action::Write(vec![27]),
Action::Write(vec![b';']),
Action::Write(vec![b'1']),
Action::Write(vec![b'0']),
Action::Write(vec![b';']),
Action::Write(vec![b'5']),
Action::Write(vec![b't']),
Action::Quit
]
);
assert_eq!(pixel_events_sent_to_server, vec![],);
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,409 @@
use super::input_loop;
use crate::stdin_ansi_parser::StdinAnsiParser;
use crate::stdin_loop;
use zellij_utils::data::{InputMode, Palette};
use zellij_utils::input::actions::{Action, Direction};
use zellij_utils::input::config::Config;
use zellij_utils::input::options::Options;
use zellij_utils::nix;
use zellij_utils::pane_size::Size;
use zellij_utils::termwiz::input::{InputEvent, KeyCode, KeyEvent, Modifiers};
use crate::InputInstruction;
use crate::{
os_input_output::{ClientOsApi, StdinPoller},
ClientInstruction, CommandIsExecuting,
};
use ::insta::assert_snapshot;
use std::path::Path;
use std::io;
use std::os::unix::io::RawFd;
use std::sync::{Arc, Mutex};
use std::thread;
use zellij_utils::{
errors::ErrorContext,
ipc::{ClientToServerMsg, ServerToClientMsg},
};
use zellij_utils::channels::{self, ChannelWithContext, SenderWithContext};
fn read_fixture(fixture_name: &str) -> Vec<u8> {
let mut path_to_file = std::path::PathBuf::new();
path_to_file.push("../src");
path_to_file.push("tests");
path_to_file.push("fixtures");
path_to_file.push(fixture_name);
std::fs::read(path_to_file)
.unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name))
}
#[allow(unused)]
pub mod commands {
pub const QUIT: [u8; 1] = [17]; // ctrl-q
pub const ESC: [u8; 1] = [27];
pub const ENTER: [u8; 1] = [10]; // char '\n'
pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h
pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l
pub const PANE_MODE: [u8; 1] = [16]; // ctrl-p
pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [110]; // n
pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p
pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d
pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r
pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f
pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x
pub const MOVE_FOCUS_DOWN_IN_PANE_MODE: [u8; 1] = [106]; // j
pub const MOVE_FOCUS_UP_IN_PANE_MODE: [u8; 1] = [107]; // k
pub const MOVE_FOCUS_LEFT_IN_PANE_MODE: [u8; 1] = [104]; // h
pub const MOVE_FOCUS_RIGHT_IN_PANE_MODE: [u8; 1] = [108]; // l
pub const SCROLL_MODE: [u8; 1] = [19]; // ctrl-s
pub const SCROLL_UP_IN_SCROLL_MODE: [u8; 1] = [107]; // k
pub const SCROLL_DOWN_IN_SCROLL_MODE: [u8; 1] = [106]; // j
pub const SCROLL_PAGE_UP_IN_SCROLL_MODE: [u8; 1] = [2]; // ctrl-b
pub const SCROLL_PAGE_DOWN_IN_SCROLL_MODE: [u8; 1] = [6]; // ctrl-f
pub const RESIZE_MODE: [u8; 1] = [18]; // ctrl-r
pub const RESIZE_DOWN_IN_RESIZE_MODE: [u8; 1] = [106]; // j
pub const RESIZE_UP_IN_RESIZE_MODE: [u8; 1] = [107]; // k
pub const RESIZE_LEFT_IN_RESIZE_MODE: [u8; 1] = [104]; // h
pub const RESIZE_RIGHT_IN_RESIZE_MODE: [u8; 1] = [108]; // l
pub const TAB_MODE: [u8; 1] = [20]; // ctrl-t
pub const NEW_TAB_IN_TAB_MODE: [u8; 1] = [110]; // n
pub const SWITCH_NEXT_TAB_IN_TAB_MODE: [u8; 1] = [108]; // l
pub const SWITCH_PREV_TAB_IN_TAB_MODE: [u8; 1] = [104]; // h
pub const CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [120]; // x
pub const BRACKETED_PASTE_START: [u8; 6] = [27, 91, 50, 48, 48, 126]; // \u{1b}[200~
pub const BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201
pub const SLEEP: [u8; 0] = [];
}
#[derive(Default, Clone)]
struct FakeStdoutWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl FakeStdoutWriter {
pub fn new(buffer: Arc<Mutex<Vec<u8>>>) -> Self {
FakeStdoutWriter { buffer }
}
}
impl io::Write for FakeStdoutWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
self.buffer.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
}
#[derive(Clone)]
struct FakeClientOsApi {
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
command_is_executing: Arc<Mutex<CommandIsExecuting>>,
stdout_buffer: Arc<Mutex<Vec<u8>>>,
stdin_buffer: Vec<u8>,
}
impl FakeClientOsApi {
pub fn new(
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
command_is_executing: CommandIsExecuting,
) -> Self {
// while command_is_executing itself is implemented with an Arc<Mutex>, we have to have an
// Arc<Mutex> here because we need interior mutability, otherwise we'll have to change the
// ClientOsApi trait, and that will cause a lot of havoc
let command_is_executing = Arc::new(Mutex::new(command_is_executing));
let stdout_buffer = Arc::new(Mutex::new(vec![]));
FakeClientOsApi {
events_sent_to_server,
command_is_executing,
stdout_buffer,
stdin_buffer: vec![],
}
}
pub fn with_stdin_buffer(mut self, stdin_buffer: Vec<u8>) -> Self {
self.stdin_buffer = stdin_buffer;
self
}
pub fn stdout_buffer(&self) -> Vec<u8> {
self.stdout_buffer.lock().unwrap().drain(..).collect()
}
}
impl ClientOsApi for FakeClientOsApi {
fn get_terminal_size_using_fd(&self, _fd: RawFd) -> Size {
unimplemented!()
}
fn set_raw_mode(&mut self, _fd: RawFd) {
unimplemented!()
}
fn unset_raw_mode(&self, _fd: RawFd) -> Result<(), nix::Error> {
unimplemented!()
}
fn get_stdout_writer(&self) -> Box<dyn io::Write> {
let fake_stdout_writer = FakeStdoutWriter::new(self.stdout_buffer.clone());
Box::new(fake_stdout_writer)
}
fn get_stdin_reader(&self) -> Box<dyn io::Read> {
unimplemented!()
}
fn read_from_stdin(&mut self) -> Vec<u8> {
self.stdin_buffer.drain(..).collect()
}
fn box_clone(&self) -> Box<dyn ClientOsApi> {
unimplemented!()
}
fn send_to_server(&self, msg: ClientToServerMsg) {
{
let mut events_sent_to_server = self.events_sent_to_server.lock().unwrap();
events_sent_to_server.push(msg);
}
{
let mut command_is_executing = self.command_is_executing.lock().unwrap();
command_is_executing.unblock_input_thread();
}
}
fn recv_from_server(&self) -> Option<(ServerToClientMsg, ErrorContext)> {
unimplemented!()
}
fn handle_signals(&self, _sigwinch_cb: Box<dyn Fn()>, _quit_cb: Box<dyn Fn()>) {
unimplemented!()
}
fn connect_to_server(&self, _path: &Path) {
unimplemented!()
}
fn load_palette(&self) -> Palette {
unimplemented!()
}
fn enable_mouse(&self) {}
fn disable_mouse(&self) {}
fn stdin_poller(&self) -> StdinPoller {
unimplemented!()
}
}
fn extract_actions_sent_to_server(
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
) -> Vec<Action> {
let events_sent_to_server = events_sent_to_server.lock().unwrap();
events_sent_to_server.iter().fold(vec![], |mut acc, event| {
if let ClientToServerMsg::Action(action, None) = event {
acc.push(action.clone());
}
acc
})
}
#[test]
pub fn quit_breaks_input_loop() {
let stdin_events = vec![(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
)];
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api = Box::new(FakeClientOsApi::new(
events_sent_to_server.clone(),
command_is_executing.clone(),
));
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
for event in stdin_events {
send_input_instructions
.send(InputInstruction::KeyEvent(event.1, event.0))
.unwrap();
}
let default_mode = InputMode::Normal;
input_loop(
client_os_api,
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
);
let expected_actions_sent_to_server = vec![Action::Quit];
let received_actions = extract_actions_sent_to_server(events_sent_to_server);
assert_eq!(
expected_actions_sent_to_server, received_actions,
"All actions sent to server properly"
);
}
#[test]
pub fn move_focus_left_in_normal_mode() {
let stdin_events = vec![
(
commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('h'),
modifiers: Modifiers::ALT,
}),
),
(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
),
];
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api = Box::new(FakeClientOsApi::new(
events_sent_to_server.clone(),
command_is_executing.clone(),
));
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
for event in stdin_events {
send_input_instructions
.send(InputInstruction::KeyEvent(event.1, event.0))
.unwrap();
}
let default_mode = InputMode::Normal;
input_loop(
client_os_api,
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
);
let expected_actions_sent_to_server =
vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit];
let received_actions = extract_actions_sent_to_server(events_sent_to_server);
assert_eq!(
expected_actions_sent_to_server, received_actions,
"All actions sent to server properly"
);
}
#[test]
pub fn terminal_info_queried_from_terminal_emulator() {
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api = FakeClientOsApi::new(events_sent_to_server, command_is_executing);
let client_os_api_clone = client_os_api.clone();
let (send_input_instructions, _receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));
let stdin_thread = thread::Builder::new()
.name("stdin_handler".to_string())
.spawn({
move || {
stdin_loop(
Box::new(client_os_api),
send_input_instructions,
stdin_ansi_parser,
)
}
});
std::thread::sleep(std::time::Duration::from_millis(500)); // wait for initial query to be sent
let extracted_stdout_buffer = client_os_api_clone.stdout_buffer();
let mut expected_query =
String::from("\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}");
for i in 0..256 {
expected_query.push_str(&format!("\u{1b}]4;{};?\u{1b}\u{5c}", i));
}
assert_eq!(
String::from_utf8(extracted_stdout_buffer),
Ok(expected_query),
);
drop(stdin_thread);
}
#[test]
pub fn pixel_info_sent_to_server() {
let fake_stdin_buffer = read_fixture("terminal_emulator_startup_response");
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api =
FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone())
.with_stdin_buffer(fake_stdin_buffer);
let config = Config::from_default_assets().unwrap();
let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));
let stdin_thread = thread::Builder::new()
.name("stdin_handler".to_string())
.spawn({
let client_os_api = client_os_api.clone();
move || {
stdin_loop(
Box::new(client_os_api),
send_input_instructions,
stdin_ansi_parser,
)
}
});
let default_mode = InputMode::Normal;
let input_thread = thread::Builder::new()
.name("input_handler".to_string())
.spawn({
move || {
input_loop(
Box::new(client_os_api),
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
)
}
});
std::thread::sleep(std::time::Duration::from_millis(1000)); // wait for initial query to be sent
assert_snapshot!(*format!("{:?}", events_sent_to_server.lock().unwrap()));
drop(stdin_thread);
drop(input_thread);
}

View file

@ -27,6 +27,9 @@ typetag = "0.1.7"
chrono = "0.4.19"
close_fds = "0.3.2"
sysinfo = "0.22.5"
sixel-tokenizer = "0.1.0"
sixel-image = "0.1.0"
arrayvec = "0.7.2"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
[dev-dependencies]

View file

@ -4,6 +4,7 @@ use crate::panes::selection::Selection;
use crate::panes::Row;
use crate::{
panes::sixel::SixelImageStore,
panes::terminal_character::{AnsiCode, CharacterStyles},
panes::{LinkHandler, TerminalCharacter, EMPTY_TERMINAL_CHARACTER},
ClientId,
@ -16,6 +17,7 @@ use std::{
str,
};
use zellij_utils::pane_size::PaneGeom;
use zellij_utils::pane_size::SizeInPixels;
fn vte_goto_instruction(x_coords: usize, y_coords: usize, vte_output: &mut String) {
write!(
@ -54,7 +56,6 @@ fn write_changed_styles(
if let Some(new_styles) =
character_styles.update_and_return_diff(&current_character_styles, chunk_changed_colors)
{
// if let Some(osc8_link) = link_handler.as_ref().and_then(|l_h| l_h.borrow().output_osc8(new_styles.link_anchor)) {
if let Some(osc8_link) =
link_handler.and_then(|l_h| l_h.output_osc8(new_styles.link_anchor))
{
@ -65,11 +66,14 @@ fn write_changed_styles(
}
}
fn serialize_character_chunks(
fn serialize_chunks(
character_chunks: Vec<CharacterChunk>,
sixel_chunks: Option<&Vec<SixelImageChunk>>,
link_handler: Option<&mut Rc<RefCell<LinkHandler>>>,
sixel_image_store: &mut SixelImageStore,
) -> String {
let mut vte_output = String::new(); // TODO: preallocate character_chunks.len()?
let mut vte_output = String::new();
let mut sixel_vte: Option<String> = None;
let link_handler = link_handler.map(|l_h| l_h.borrow());
for character_chunk in character_chunks {
let chunk_selection_and_background_color = character_chunk.selection_and_background_color();
@ -96,6 +100,32 @@ fn serialize_character_chunks(
}
character_styles.clear();
}
if let Some(sixel_chunks) = sixel_chunks {
for sixel_chunk in sixel_chunks {
let serialized_sixel_image = sixel_image_store.serialize_image(
sixel_chunk.sixel_image_id,
sixel_chunk.sixel_image_pixel_x,
sixel_chunk.sixel_image_pixel_y,
sixel_chunk.sixel_image_pixel_width,
sixel_chunk.sixel_image_pixel_height,
);
if let Some(serialized_sixel_image) = serialized_sixel_image {
let sixel_vte = sixel_vte.get_or_insert_with(String::new);
vte_goto_instruction(sixel_chunk.cell_x, sixel_chunk.cell_y, sixel_vte);
sixel_vte.push_str(&serialized_sixel_image);
}
}
}
if let Some(ref sixel_vte) = sixel_vte {
// we do this at the end because of the implied z-index,
// images should be above text unless the text was explicitly inserted after them (the
// latter being a case we handle in our own internal state and not in the output)
let save_cursor_position = "\u{1b}[s";
let restore_cursor_position = "\u{1b}[u";
vte_output.push_str(save_cursor_position);
vte_output.push_str(sixel_vte);
vte_output.push_str(restore_cursor_position);
}
vte_output
}
@ -148,11 +178,24 @@ pub struct Output {
pre_vte_instructions: HashMap<ClientId, Vec<String>>,
post_vte_instructions: HashMap<ClientId, Vec<String>>,
client_character_chunks: HashMap<ClientId, Vec<CharacterChunk>>,
sixel_chunks: HashMap<ClientId, Vec<SixelImageChunk>>,
link_handler: Option<Rc<RefCell<LinkHandler>>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
floating_panes_stack: Option<FloatingPanesStack>,
}
impl Output {
pub fn new(
sixel_image_store: Rc<RefCell<SixelImageStore>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
) -> Self {
Output {
sixel_image_store,
character_cell_size,
..Default::default()
}
}
pub fn add_clients(
&mut self,
client_ids: &HashSet<ClientId>,
@ -240,6 +283,48 @@ impl Output {
.or_insert_with(Vec::new);
entry.push(String::from(vte_instruction));
}
pub fn add_sixel_image_chunks_to_client(
&mut self,
client_id: ClientId,
sixel_image_chunks: Vec<SixelImageChunk>,
z_index: Option<usize>,
) {
if let Some(character_cell_size) = *self.character_cell_size.borrow() {
let mut sixel_chunks = if let Some(floating_panes_stack) = &self.floating_panes_stack {
floating_panes_stack.visible_sixel_image_chunks(
sixel_image_chunks,
z_index,
&character_cell_size,
)
} else {
sixel_image_chunks
};
let entry = self.sixel_chunks.entry(client_id).or_insert_with(Vec::new);
entry.append(&mut sixel_chunks);
}
}
pub fn add_sixel_image_chunks_to_multiple_clients(
&mut self,
sixel_image_chunks: Vec<SixelImageChunk>,
client_ids: impl Iterator<Item = ClientId>,
z_index: Option<usize>,
) {
if let Some(character_cell_size) = *self.character_cell_size.borrow() {
let sixel_chunks = if let Some(floating_panes_stack) = &self.floating_panes_stack {
floating_panes_stack.visible_sixel_image_chunks(
sixel_image_chunks,
z_index,
&character_cell_size,
)
} else {
sixel_image_chunks
};
for client_id in client_ids {
let entry = self.sixel_chunks.entry(client_id).or_insert_with(Vec::new);
entry.append(&mut sixel_chunks.clone());
}
}
}
pub fn serialize(&mut self) -> HashMap<ClientId, String> {
let mut serialized_render_instructions = HashMap::new();
@ -256,9 +341,11 @@ impl Output {
}
// append the actual vte
client_serialized_render_instructions.push_str(&serialize_character_chunks(
client_serialized_render_instructions.push_str(&serialize_chunks(
client_character_chunks,
self.sixel_chunks.get(&client_id),
self.link_handler.as_mut(),
&mut self.sixel_image_store.borrow_mut(),
)); // TODO: less allocations?
// append post-vte instructions for this client
@ -319,6 +406,26 @@ impl FloatingPanesStack {
}
visible_chunks
}
pub fn visible_sixel_image_chunks(
&self,
mut sixel_image_chunks: Vec<SixelImageChunk>,
z_index: Option<usize>,
character_cell_size: &SizeInPixels,
) -> Vec<SixelImageChunk> {
let z_index = z_index.unwrap_or(0);
let mut chunks_to_check: Vec<SixelImageChunk> = sixel_image_chunks.drain(..).collect();
let panes_to_check = self.layers.iter().skip(z_index);
for pane_geom in panes_to_check {
let chunks_to_check_against_this_pane: Vec<SixelImageChunk> =
chunks_to_check.drain(..).collect();
for s_chunk in chunks_to_check_against_this_pane {
let mut uncovered_chunks =
self.remove_covered_sixel_parts(pane_geom, &s_chunk, character_cell_size);
chunks_to_check.append(&mut uncovered_chunks);
}
}
chunks_to_check
}
fn remove_covered_parts(
&self,
pane_geom: &PaneGeom,
@ -368,6 +475,165 @@ impl FloatingPanesStack {
};
None
}
fn remove_covered_sixel_parts(
&self,
pane_geom: &PaneGeom,
s_chunk: &SixelImageChunk,
character_cell_size: &SizeInPixels,
) -> Vec<SixelImageChunk> {
// round these up to the nearest cell edge
let rounded_sixel_image_pixel_height =
if s_chunk.sixel_image_pixel_height % character_cell_size.height > 0 {
let modulus = s_chunk.sixel_image_pixel_height % character_cell_size.height;
s_chunk.sixel_image_pixel_height + (character_cell_size.height - modulus)
} else {
s_chunk.sixel_image_pixel_height
};
let rounded_sixel_image_pixel_width =
if s_chunk.sixel_image_pixel_width % character_cell_size.width > 0 {
let modulus = s_chunk.sixel_image_pixel_width % character_cell_size.width;
s_chunk.sixel_image_pixel_width + (character_cell_size.width - modulus)
} else {
s_chunk.sixel_image_pixel_width
};
let pane_top_edge = pane_geom.y * character_cell_size.height;
let pane_left_edge = pane_geom.x * character_cell_size.width;
let pane_bottom_edge = (pane_geom.y + pane_geom.rows.as_usize().saturating_sub(1))
* character_cell_size.height;
let pane_right_edge =
(pane_geom.x + pane_geom.cols.as_usize().saturating_sub(1)) * character_cell_size.width;
let s_chunk_top_edge = s_chunk.cell_y * character_cell_size.height;
let s_chunk_bottom_edge = s_chunk_top_edge + rounded_sixel_image_pixel_height;
let s_chunk_left_edge = s_chunk.cell_x * character_cell_size.width;
let s_chunk_right_edge = s_chunk_left_edge + rounded_sixel_image_pixel_width;
let mut uncovered_chunks = vec![];
let pane_covers_chunk_completely = pane_top_edge <= s_chunk_top_edge
&& pane_bottom_edge >= s_chunk_bottom_edge
&& pane_left_edge <= s_chunk_left_edge
&& pane_right_edge >= s_chunk_right_edge;
let pane_intersects_with_chunk_vertically = (pane_left_edge >= s_chunk_left_edge
&& pane_left_edge <= s_chunk_right_edge)
|| (pane_right_edge >= s_chunk_left_edge && pane_right_edge <= s_chunk_right_edge)
|| (pane_left_edge <= s_chunk_left_edge && pane_right_edge >= s_chunk_right_edge);
let pane_intersects_with_chunk_horizontally = (pane_top_edge >= s_chunk_top_edge
&& pane_top_edge <= s_chunk_bottom_edge)
|| (pane_bottom_edge >= s_chunk_top_edge && pane_bottom_edge <= s_chunk_bottom_edge)
|| (pane_top_edge <= s_chunk_top_edge && pane_bottom_edge >= s_chunk_bottom_edge);
if pane_covers_chunk_completely {
return uncovered_chunks;
}
if pane_top_edge >= s_chunk_top_edge
&& pane_top_edge <= s_chunk_bottom_edge
&& pane_intersects_with_chunk_vertically
{
// pane covers image bottom
let top_image_chunk = SixelImageChunk {
cell_x: s_chunk.cell_x,
cell_y: s_chunk.cell_y,
sixel_image_pixel_x: s_chunk.sixel_image_pixel_x,
sixel_image_pixel_y: s_chunk.sixel_image_pixel_y,
sixel_image_pixel_width: rounded_sixel_image_pixel_width,
sixel_image_pixel_height: pane_top_edge - s_chunk_top_edge,
sixel_image_id: s_chunk.sixel_image_id,
};
uncovered_chunks.push(top_image_chunk);
}
if pane_bottom_edge <= s_chunk_bottom_edge
&& pane_bottom_edge >= s_chunk_top_edge
&& pane_intersects_with_chunk_vertically
{
// pane covers image top
let bottom_image_chunk = SixelImageChunk {
cell_x: s_chunk.cell_x,
cell_y: (pane_bottom_edge / character_cell_size.height) + 1,
sixel_image_pixel_x: s_chunk.sixel_image_pixel_x,
sixel_image_pixel_y: s_chunk.sixel_image_pixel_y
+ (pane_bottom_edge - s_chunk_top_edge)
+ character_cell_size.height,
sixel_image_pixel_width: rounded_sixel_image_pixel_width,
sixel_image_pixel_height: (rounded_sixel_image_pixel_height
- (pane_bottom_edge - s_chunk_top_edge))
.saturating_sub(character_cell_size.height),
sixel_image_id: s_chunk.sixel_image_id,
};
uncovered_chunks.push(bottom_image_chunk);
}
if pane_left_edge >= s_chunk_left_edge
&& pane_left_edge <= s_chunk_right_edge
&& pane_intersects_with_chunk_horizontally
{
// pane covers image right
let sixel_image_pixel_y = if s_chunk_top_edge < pane_top_edge {
s_chunk.sixel_image_pixel_y + (pane_top_edge - s_chunk_top_edge)
} else {
s_chunk.sixel_image_pixel_y
};
let max_image_height = if s_chunk_top_edge < pane_top_edge {
rounded_sixel_image_pixel_height.saturating_sub(pane_top_edge - s_chunk_top_edge)
} else {
rounded_sixel_image_pixel_height
};
let left_image_chunk = SixelImageChunk {
cell_x: s_chunk.cell_x,
// if the pane_top_edge is lower than the image, we want to start there, because we
// already cut that part above when checking if the pane covered the chunk bottom
cell_y: std::cmp::max(s_chunk.cell_y, pane_top_edge / character_cell_size.height),
sixel_image_pixel_x: s_chunk.sixel_image_pixel_x,
sixel_image_pixel_y,
sixel_image_pixel_width: rounded_sixel_image_pixel_width
.saturating_sub(s_chunk_right_edge.saturating_sub(pane_left_edge)),
sixel_image_pixel_height: std::cmp::min(
pane_bottom_edge - pane_top_edge + character_cell_size.height,
max_image_height,
),
sixel_image_id: s_chunk.sixel_image_id,
};
uncovered_chunks.push(left_image_chunk);
}
if pane_right_edge <= s_chunk_right_edge
&& pane_right_edge >= s_chunk_left_edge
&& pane_intersects_with_chunk_horizontally
{
// pane covers image left
let sixel_image_pixel_y = if s_chunk_top_edge < pane_top_edge {
s_chunk.sixel_image_pixel_y + (pane_top_edge - s_chunk_top_edge)
} else {
s_chunk.sixel_image_pixel_y
};
let max_image_height = if s_chunk_top_edge < pane_top_edge {
rounded_sixel_image_pixel_height.saturating_sub(pane_top_edge - s_chunk_top_edge)
} else {
rounded_sixel_image_pixel_height
};
let sixel_image_pixel_x = s_chunk.sixel_image_pixel_x
+ (pane_right_edge - s_chunk_left_edge)
+ character_cell_size.width;
let right_image_chunk = SixelImageChunk {
cell_x: (pane_right_edge / character_cell_size.width) + 1,
// if the pane_top_edge is lower than the image, we want to start there, because we
// already cut that part above when checking if the pane covered the chunk bottom
cell_y: std::cmp::max(s_chunk.cell_y, pane_top_edge / character_cell_size.height),
sixel_image_pixel_x,
sixel_image_pixel_y,
sixel_image_pixel_width: (rounded_sixel_image_pixel_width
.saturating_sub(pane_right_edge - s_chunk_left_edge))
.saturating_sub(character_cell_size.width),
sixel_image_pixel_height: std::cmp::min(
pane_bottom_edge - pane_top_edge + character_cell_size.height,
max_image_height,
),
sixel_image_id: s_chunk.sixel_image_id,
};
uncovered_chunks.push(right_image_chunk);
}
if uncovered_chunks.is_empty() {
// the pane doesn't cover the chunk at all, so we return it as is
uncovered_chunks.push(*s_chunk);
}
uncovered_chunks
}
}
#[derive(Debug, Clone, Default)]
@ -379,6 +645,17 @@ pub struct CharacterChunk {
selection_and_background_color: Option<(Selection, AnsiCode)>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SixelImageChunk {
pub cell_x: usize,
pub cell_y: usize,
pub sixel_image_pixel_x: usize,
pub sixel_image_pixel_y: usize,
pub sixel_image_pixel_width: usize,
pub sixel_image_pixel_height: usize,
pub sixel_image_id: usize,
}
impl CharacterChunk {
pub fn new(terminal_characters: Vec<TerminalCharacter>, x: usize, y: usize) -> Self {
CharacterChunk {
@ -480,8 +757,8 @@ impl CharacterChunk {
#[derive(Clone, Debug)]
pub struct OutputBuffer {
changed_lines: Vec<usize>, // line index
should_update_all_lines: bool,
pub changed_lines: Vec<usize>, // line index
pub should_update_all_lines: bool,
}
impl Default for OutputBuffer {
@ -520,6 +797,7 @@ impl OutputBuffer {
for line_index in 0..viewport_height {
let terminal_characters =
self.extract_line_from_viewport(line_index, viewport, viewport_width);
let x = x_offset; // right now we only buffer full lines as this doesn't seem to have a huge impact on performance, but the infra is here if we want to change this
let y = line_index + y_offset;
changed_chunks.push(CharacterChunk::new(terminal_characters, x, y));
@ -568,4 +846,42 @@ impl OutputBuffer {
},
}
}
pub fn changed_rects_in_viewport(&self, viewport_height: usize) -> HashMap<usize, usize> {
// group the changed lines into "changed_rects", which indicate where the line starts (the
// hashmap key) and how many lines are in there (its value)
let mut changed_rects: HashMap<usize, usize> = HashMap::new(); // <start_line_index, line_count>
let mut last_changed_line_index: Option<usize> = None;
let mut changed_line_count = 0;
let mut add_changed_line = |line_index| match last_changed_line_index.as_mut() {
Some(changed_line_index) => {
if *changed_line_index + changed_line_count == line_index {
changed_line_count += 1
} else {
changed_rects.insert(*changed_line_index, changed_line_count);
last_changed_line_index = Some(line_index);
changed_line_count = 1;
}
},
None => {
last_changed_line_index = Some(line_index);
changed_line_count = 1;
},
};
// TODO: move this whole thing to output_buffer
if self.should_update_all_lines {
// for line_index in 0..self.viewport.len() {
for line_index in 0..viewport_height {
add_changed_line(line_index);
}
} else {
for line_index in self.changed_lines.iter().copied() {
add_changed_line(line_index);
}
}
if let Some(changed_line_index) = last_changed_line_index {
changed_rects.insert(changed_line_index, changed_line_count);
}
changed_rects
}
}

View file

@ -1,4 +1,6 @@
use super::sixel::{PixelRect, SixelGrid, SixelImageStore};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use unicode_width::UnicodeWidthChar;
use zellij_utils::regex::Regex;
@ -24,7 +26,7 @@ pub const MAX_TITLE_STACK_SIZE: usize = 1000;
use vte::{Params, Perform};
use zellij_utils::{consts::VERSION, shared::version_number};
use crate::output::{CharacterChunk, OutputBuffer};
use crate::output::{CharacterChunk, OutputBuffer, SixelImageChunk};
use crate::panes::alacritty_functions::{parse_number, xparse_color};
use crate::panes::link_handler::LinkHandler;
use crate::panes::selection::Selection;
@ -112,6 +114,7 @@ fn get_top_canonical_row_and_wraps(rows: &mut Vec<Row>) -> Vec<Row> {
fn transfer_rows_from_lines_above_to_viewport(
lines_above: &mut VecDeque<Row>,
viewport: &mut Vec<Row>,
sixel_grid: &mut SixelGrid,
count: usize,
max_viewport_width: usize,
) -> usize {
@ -143,7 +146,7 @@ fn transfer_rows_from_lines_above_to_viewport(
}
if !next_lines.is_empty() {
let excess_row = Row::from_rows(next_lines, 0);
bounded_push(lines_above, excess_row);
bounded_push(lines_above, sixel_grid, excess_row);
}
match usize::try_from(lines_added_to_viewport) {
Ok(n) => n,
@ -154,6 +157,7 @@ fn transfer_rows_from_lines_above_to_viewport(
fn transfer_rows_from_viewport_to_lines_above(
viewport: &mut Vec<Row>,
lines_above: &mut VecDeque<Row>,
sixel_grid: &mut SixelGrid,
count: usize,
max_viewport_width: usize,
) -> isize {
@ -176,7 +180,7 @@ fn transfer_rows_from_viewport_to_lines_above(
break; // no more rows
}
}
let dropped_line_width = bounded_push(lines_above, next_lines.remove(0));
let dropped_line_width = bounded_push(lines_above, sixel_grid, next_lines.remove(0));
if let Some(width) = dropped_line_width {
transferred_rows_count -=
calculate_row_display_height(width, max_viewport_width) as isize;
@ -232,11 +236,12 @@ fn transfer_rows_from_lines_below_to_viewport(
}
}
fn bounded_push(vec: &mut VecDeque<Row>, value: Row) -> Option<usize> {
fn bounded_push(vec: &mut VecDeque<Row>, sixel_grid: &mut SixelGrid, value: Row) -> Option<usize> {
let mut dropped_line_width = None;
if vec.len() >= *SCROLL_BUFFER_SIZE.get().unwrap() {
let line = vec.pop_front();
if let Some(line) = line {
sixel_grid.offset_grid_top();
dropped_line_width = Some(line.width());
}
}
@ -298,7 +303,7 @@ pub struct Grid {
viewport: Vec<Row>,
lines_below: Vec<Row>,
horizontal_tabstops: BTreeSet<usize>,
alternate_lines_above_viewport_and_cursor: Option<(VecDeque<Row>, Vec<Row>, Cursor)>,
alternate_screen_state: Option<AlternateScreenState>,
cursor: Cursor,
saved_cursor_position: Option<Cursor>,
// FIXME: change scroll_region to be (usize, usize) - where the top line is always the first
@ -309,14 +314,17 @@ pub struct Grid {
active_charset: CharsetIndex,
preceding_char: Option<TerminalCharacter>,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
output_buffer: OutputBuffer,
title_stack: Vec<String>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
sixel_grid: SixelGrid,
pub changed_colors: Option<[Option<AnsiCode>; 256]>,
pub should_render: bool,
pub cursor_key_mode: bool, // DECCKM - when set, cursor keys should send ANSI direction codes (eg. "OD") instead of the arrow keys (eg. "")
pub bracketed_paste_mode: bool, // when set, paste instructions to the terminal should be escaped with a special sequence
pub erasure_mode: bool, // ERM
pub sixel_scrolling: bool, // DECSDM
pub insert_mode: bool,
pub disable_linewrap: bool,
pub clear_viewport_before_rendering: bool,
@ -334,7 +342,41 @@ pub struct Grid {
impl Debug for Grid {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for (i, row) in self.viewport.iter().enumerate() {
let mut buffer: Vec<Row> = self.viewport.clone();
// pad buffer
for _ in buffer.len()..self.height {
buffer.push(Row::new(self.width).canonical());
}
// display sixel placeholder
let sixel_indication_character = |x| {
let sixel_indication_word = "Sixel";
sixel_indication_word
.chars()
.nth(x % sixel_indication_word.len())
.unwrap()
};
for image_coordinates in self
.sixel_grid
.image_cell_coordinates_in_viewport(self.height, self.lines_above.len())
{
let (image_top_edge, image_bottom_edge, image_left_edge, image_right_edge) =
image_coordinates;
for y in image_top_edge..image_bottom_edge {
let row = buffer.get_mut(y).unwrap();
for x in image_left_edge..image_right_edge {
let fake_sixel_terminal_character = TerminalCharacter {
character: sixel_indication_character(x),
width: 1,
styles: Default::default(),
};
row.add_character_at(fake_sixel_terminal_character, x);
}
}
}
// display terminal characters with stripped styles
for (i, row) in buffer.iter().enumerate() {
if row.is_canonical {
writeln!(f, "{:02?} (C): {:?}", i, row)?;
} else {
@ -350,9 +392,12 @@ impl Grid {
rows: usize,
columns: usize,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
link_handler: Rc<RefCell<LinkHandler>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
) -> Self {
let sixel_grid = SixelGrid::new(character_cell_size.clone(), sixel_image_store);
Grid {
lines_above: VecDeque::with_capacity(
// .get_or_init() is used instead of .get().unwrap() to prevent
@ -372,13 +417,15 @@ impl Grid {
cursor_key_mode: false,
bracketed_paste_mode: false,
erasure_mode: false,
sixel_scrolling: false,
insert_mode: false,
disable_linewrap: false,
alternate_lines_above_viewport_and_cursor: None,
alternate_screen_state: None,
clear_viewport_before_rendering: false,
active_charset: Default::default(),
pending_messages_to_pty: vec![],
terminal_emulator_colors,
terminal_emulator_color_codes,
output_buffer: Default::default(),
selection: Default::default(),
title_stack: vec![],
@ -390,6 +437,7 @@ impl Grid {
scrollback_buffer_lines: 0,
mouse_mode: false,
character_cell_size,
sixel_grid,
}
}
pub fn render_full_viewport(&mut self) {
@ -534,6 +582,7 @@ impl Grid {
let transferred_rows_height = transfer_rows_from_lines_above_to_viewport(
&mut self.lines_above,
&mut self.viewport,
&mut self.sixel_grid,
1,
self.width,
);
@ -560,7 +609,8 @@ impl Grid {
last_line_above
};
let dropped_line_width = bounded_push(&mut self.lines_above, line_to_push_up);
let dropped_line_width =
bounded_push(&mut self.lines_above, &mut self.sixel_grid, line_to_push_up);
if let Some(width) = dropped_line_width {
let dropped_line_height = calculate_row_display_height(width, self.width);
@ -607,7 +657,8 @@ impl Grid {
return;
}
self.selection.reset();
if new_columns != self.width && self.alternate_lines_above_viewport_and_cursor.is_none() {
self.sixel_grid.character_cell_size_possibly_changed();
if new_columns != self.width && self.alternate_screen_state.is_none() {
self.horizontal_tabstops = create_horizontal_tabstops(new_columns);
let mut cursor_canonical_line_index = self.cursor_canonical_line_index();
let cursor_index_in_canonical_line = self.cursor_index_in_canonical_line();
@ -697,6 +748,7 @@ impl Grid {
transfer_rows_from_lines_above_to_viewport(
&mut self.lines_above,
&mut self.viewport,
&mut self.sixel_grid,
row_count_to_transfer,
new_columns,
);
@ -713,6 +765,7 @@ impl Grid {
transfer_rows_from_viewport_to_lines_above(
&mut self.viewport,
&mut self.lines_above,
&mut self.sixel_grid,
row_count_to_transfer,
new_columns,
);
@ -725,9 +778,7 @@ impl Grid {
saved_cursor_position.y = new_cursor_y;
saved_cursor_position.x = new_cursor_x;
};
} else if new_columns != self.width
&& self.alternate_lines_above_viewport_and_cursor.is_some()
{
} else if new_columns != self.width && self.alternate_screen_state.is_some() {
// in alternate screen just truncate exceeding width
for row in &mut self.viewport {
if row.width() >= new_columns {
@ -744,6 +795,7 @@ impl Grid {
transfer_rows_from_lines_above_to_viewport(
&mut self.lines_above,
&mut self.viewport,
&mut self.sixel_grid,
row_count_to_transfer,
new_columns,
);
@ -763,13 +815,16 @@ impl Grid {
} else {
self.cursor.y -= row_count_to_transfer;
if let Some(saved_cursor_position) = self.saved_cursor_position.as_mut() {
saved_cursor_position.y -= row_count_to_transfer
saved_cursor_position.y = saved_cursor_position
.y
.saturating_sub(row_count_to_transfer);
};
}
if self.alternate_lines_above_viewport_and_cursor.is_none() {
if self.alternate_screen_state.is_none() {
transfer_rows_from_viewport_to_lines_above(
&mut self.viewport,
&mut self.lines_above,
&mut self.sixel_grid,
row_count_to_transfer,
new_columns,
);
@ -813,16 +868,34 @@ impl Grid {
}
lines
}
pub fn read_changes(&mut self, x_offset: usize, y_offset: usize) -> Vec<CharacterChunk> {
let changes = self.output_buffer.changed_chunks_in_viewport(
pub fn read_changes(
&mut self,
x_offset: usize,
y_offset: usize,
) -> (Vec<CharacterChunk>, Vec<SixelImageChunk>) {
let changed_character_chunks = self.output_buffer.changed_chunks_in_viewport(
&self.viewport,
self.width,
self.height,
x_offset,
y_offset,
);
let changed_rects = self
.output_buffer
.changed_rects_in_viewport(self.viewport.len());
let changed_sixel_image_chunks = self.sixel_grid.changed_sixel_chunks_in_viewport(
changed_rects,
self.lines_above.len(),
self.width,
x_offset,
y_offset,
);
if let Some(image_ids_to_reap) = self.sixel_grid.drain_image_ids_to_reap() {
self.sixel_grid.reap_images(image_ids_to_reap);
}
self.output_buffer.clear();
changes
(changed_character_chunks, changed_sixel_image_chunks)
}
pub fn cursor_coordinates(&self) -> Option<(usize, usize)> {
if self.cursor.is_hidden {
@ -901,7 +974,7 @@ impl Grid {
}
}
pub fn fill_viewport(&mut self, character: TerminalCharacter) {
if self.alternate_lines_above_viewport_and_cursor.is_some() {
if self.alternate_screen_state.is_some() {
self.viewport.clear();
} else {
self.transfer_rows_to_lines_above(self.viewport.len())
@ -927,7 +1000,7 @@ impl Grid {
return;
}
if scroll_region_bottom == self.height - 1 && scroll_region_top == 0 {
if self.alternate_lines_above_viewport_and_cursor.is_none() {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
self.viewport.remove(0);
@ -963,9 +1036,10 @@ impl Grid {
}
if self.cursor.y == self.height - 1 {
if self.scroll_region.is_none() {
if self.alternate_lines_above_viewport_and_cursor.is_none() {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
self.sixel_grid.offset_grid_top();
self.viewport.remove(0);
}
@ -997,6 +1071,27 @@ impl Grid {
} else {
row.add_character_at(terminal_character, self.cursor.x);
}
if let Some(character_cell_size) = *self.character_cell_size.borrow() {
let scrollback_size_in_pixels =
self.lines_above.len() * character_cell_size.height;
let absolute_x_in_pixels = self.cursor.x * character_cell_size.width;
let absolute_y_in_pixels =
scrollback_size_in_pixels + (self.cursor.y * character_cell_size.height);
let rect_to_cut_out = PixelRect {
x: absolute_x_in_pixels,
y: absolute_y_in_pixels as isize,
width: character_cell_size.width,
height: character_cell_size.height,
};
if let Some(images_to_cut_out) =
self.sixel_grid.cut_off_rect_from_images(rect_to_cut_out)
{
for (image_id, rect_in_image_to_cut_out) in images_to_cut_out {
self.sixel_grid
.remove_pixels_from_image(image_id, rect_in_image_to_cut_out);
}
}
}
self.output_buffer.update_line(self.cursor.y);
},
None => {
@ -1088,7 +1183,7 @@ impl Grid {
fn line_wrap(&mut self) {
self.cursor.x = 0;
if self.cursor.y == self.height - 1 {
if self.alternate_lines_above_viewport_and_cursor.is_none() {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
self.viewport.remove(0);
@ -1333,7 +1428,7 @@ impl Grid {
self.lines_above = VecDeque::with_capacity(*SCROLL_BUFFER_SIZE.get().unwrap());
self.lines_below = vec![];
self.viewport = vec![Row::new(self.width).canonical()];
self.alternate_lines_above_viewport_and_cursor = None;
self.alternate_screen_state = None;
self.cursor_key_mode = false;
self.scroll_region = None;
self.clear_viewport_before_rendering = true;
@ -1346,6 +1441,10 @@ impl Grid {
self.output_buffer.update_all_lines();
self.changed_colors = None;
self.scrollback_buffer_lines = 0;
self.sixel_scrolling = false;
if let Some(images_to_reap) = self.sixel_grid.clear() {
self.sixel_grid.reap_images(images_to_reap);
}
}
fn set_preceding_character(&mut self, terminal_character: TerminalCharacter) {
self.preceding_char = Some(terminal_character);
@ -1486,6 +1585,7 @@ impl Grid {
let transferred_rows_count = transfer_rows_from_viewport_to_lines_above(
&mut self.viewport,
&mut self.lines_above,
&mut self.sixel_grid,
count,
self.width,
);
@ -1493,6 +1593,57 @@ impl Grid {
self.scrollback_buffer_lines =
subtract_isize_from_usize(self.scrollback_buffer_lines, transferred_rows_count);
}
fn move_cursor_down_by_pixels(&mut self, pixel_count: usize) {
if let Some(character_cell_size) = {
let c = *self.character_cell_size.borrow();
c
} {
// thanks borrow checker
let pixel_height = character_cell_size.height;
let to_move = (pixel_count as f64 / pixel_height as f64).ceil() as usize;
for _ in 0..to_move {
self.add_canonical_line();
}
}
}
fn current_cursor_pixel_coordinates(&self) -> Option<(usize, usize)> {
// (x, y)
if let Some(character_cell_size) = *self.character_cell_size.borrow() {
let line_count_in_scrollback = self.lines_above.len();
let y_coordinates =
(line_count_in_scrollback + self.cursor.y) * character_cell_size.height;
let x_coordinates = self.cursor.x * character_cell_size.width;
Some((x_coordinates, y_coordinates))
} else {
None
}
}
fn create_sixel_image(&mut self) {
if let Some((x_pixel_coordinates, y_pixel_coordinates)) =
self.current_cursor_pixel_coordinates()
{
let (x_pixel_coordinates, y_pixel_coordinates) = if self.sixel_scrolling {
let scrollback_pixel_height =
self.lines_above.len() * self.character_cell_size.borrow().unwrap().height;
(0, scrollback_pixel_height)
} else {
(x_pixel_coordinates, y_pixel_coordinates)
};
let new_image_id = self.sixel_grid.next_image_id();
let new_sixel_image =
self.sixel_grid
.end_image(new_image_id, x_pixel_coordinates, y_pixel_coordinates);
if let Some(new_sixel_image) = new_sixel_image {
let (image_pixel_height, _image_pixel_width) = new_sixel_image.pixel_size();
self.sixel_grid
.new_sixel_image(new_image_id, new_sixel_image);
if !self.sixel_scrolling {
self.move_cursor_down_by_pixels(image_pixel_height);
}
self.render_full_viewport(); // TODO: this could be optimized if it's a performance bottleneck
}
}
}
}
impl Perform for Grid {
@ -1543,16 +1694,40 @@ impl Perform for Grid {
}
}
fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) {
// TBD
fn hook(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, c: char) {
if c == 'q' {
// we only process sixel images if we know the pixel size of each character cell,
// otherwise we can't reliably display them
if self.current_cursor_pixel_coordinates().is_some() {
let max_sixel_height_in_pixels = if self.sixel_scrolling {
let character_cell_height = self.character_cell_size.borrow().unwrap().height; // unwrap here is safe because `current_cursor_pixel_coordinates` above is only Some if it exists
Some(self.height * character_cell_height)
} else {
None
};
self.sixel_grid.start_image(
max_sixel_height_in_pixels,
intermediates.iter().collect(),
params.iter().collect(),
);
}
}
}
fn put(&mut self, _byte: u8) {
// TBD
fn put(&mut self, byte: u8) {
if self.sixel_grid.is_parsing() {
self.sixel_grid.handle_byte(byte);
// we explicitly set this to false here because in the context of Sixel, we only render the
// image when it's done, i.e. in the unhook method
self.should_render = false;
}
}
fn unhook(&mut self) {
// TBD
if self.sixel_grid.is_parsing() {
self.create_sixel_image();
}
self.mark_for_rerender();
}
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
@ -1588,6 +1763,18 @@ impl Perform for Grid {
}
self.changed_colors.as_mut().unwrap()[i as usize] = Some(c);
return;
} else if chunk.get(1).as_ref().and_then(|c| c.get(0)) == Some(&b'?') {
if let Some(index) = index {
let terminal_emulator_color_codes =
self.terminal_emulator_color_codes.borrow();
let color = terminal_emulator_color_codes.get(&(index as usize));
if let Some(color) = color {
let color_response_message =
format!("\u{1b}]4;{};{}{}", index, color, terminator);
self.pending_messages_to_pty
.push(color_response_message.as_bytes().to_vec());
}
}
}
}
},
@ -1808,18 +1995,22 @@ impl Perform for Grid {
self.bracketed_paste_mode = false;
},
Some(1049) => {
// leave alternate buffer
if let Some((
alternative_lines_above,
alternative_viewport,
alternative_cursor,
)) = &mut self.alternate_lines_above_viewport_and_cursor
if let Some(mut alternate_screen_state) = self.alternate_screen_state.take()
{
std::mem::swap(&mut self.lines_above, alternative_lines_above);
std::mem::swap(&mut self.viewport, alternative_viewport);
std::mem::swap(&mut self.cursor, alternative_cursor);
if let Some(image_ids_to_reap) = self.sixel_grid.clear() {
// reap images before dropping the alternate_screen_state contents
// - we can't implement a drop method for this because the store is
// outside of the alternate_screen_state struct
self.sixel_grid.reap_images(image_ids_to_reap);
}
alternate_screen_state.apply_contents_to(
&mut self.lines_above,
&mut self.viewport,
&mut self.cursor,
&mut self.sixel_grid,
);
}
self.alternate_lines_above_viewport_and_cursor = None;
self.alternate_screen_state = None;
self.clear_viewport_before_rendering = true;
self.force_change_size(self.height, self.width); // the alternative_viewport might have been of a different size...
self.mark_for_rerender();
@ -1844,6 +2035,9 @@ impl Perform for Grid {
Some(7) => {
self.disable_linewrap = true;
},
Some(80) => {
self.sixel_scrolling = false;
},
Some(1006) => {
self.mouse_mode = false;
},
@ -1878,8 +2072,17 @@ impl Perform for Grid {
vec![Row::new(self.width).canonical()],
);
let current_cursor = std::mem::replace(&mut self.cursor, Cursor::new(0, 0));
self.alternate_lines_above_viewport_and_cursor =
Some((current_lines_above, current_viewport, current_cursor));
let sixel_image_store = self.sixel_grid.sixel_image_store.clone();
let alternate_sixelgrid = std::mem::replace(
&mut self.sixel_grid,
SixelGrid::new(self.character_cell_size.clone(), sixel_image_store),
);
self.alternate_screen_state = Some(AlternateScreenState::new(
current_lines_above,
current_viewport,
current_cursor,
alternate_sixelgrid,
));
self.clear_viewport_before_rendering = true;
self.scrollback_buffer_lines = self.recalculate_scrollback_buffer_count();
self.output_buffer.update_all_lines(); // make sure the screen gets cleared in the next render
@ -1900,6 +2103,9 @@ impl Perform for Grid {
Some(7) => {
self.disable_linewrap = false;
},
Some(80) => {
self.sixel_scrolling = true;
},
Some(1006) => {
self.mouse_mode = true;
},
@ -1969,9 +2175,45 @@ impl Perform for Grid {
let line_count = next_param_or(1);
self.rotate_scroll_region_up(line_count as usize);
} else if c == 'S' {
// move scroll up
let count = next_param_or(1);
self.rotate_scroll_region_down(count);
let first_intermediate_is_questionmark = match intermediates.get(0) {
Some(b'?') => true,
None => false,
_ => false,
};
if first_intermediate_is_questionmark {
let query_type = params_iter.next();
let is_query = params_iter.next() == Some(&[1]);
if is_query {
// XTSMGRAPHICS
match query_type {
Some(&[1]) => {
// number of color registers
let response = "\u{1b}[?1;0;65536S";
self.pending_messages_to_pty
.push(response.as_bytes().to_vec());
},
Some(&[2]) => {
// Sixel graphics geometry in pixels
if let Some(character_cell_size) = *self.character_cell_size.borrow() {
let sixel_area_geometry = format!(
"\u{1b}[?2;0;{};{}S",
character_cell_size.width * self.width,
character_cell_size.height * self.height,
);
self.pending_messages_to_pty
.push(sixel_area_geometry.as_bytes().to_vec());
}
},
_ => {
// unsupported (eg. ReGIS graphics geometry)
},
}
}
} else {
// move scroll up
let count = next_param_or(1);
self.rotate_scroll_region_down(count);
}
} else if c == 's' {
self.save_cursor_position();
} else if c == 'u' {
@ -2022,6 +2264,11 @@ impl Perform for Grid {
if let Some(cursor_shape) = shape {
self.cursor.change_shape(cursor_shape);
}
} else if matches!(intermediates.get(0), Some(b'>')) {
let version = version_number(VERSION);
let xtversion = format!("\u{1b}P>|Zellij({})\u{1b}\\", version);
self.pending_messages_to_pty
.push(xtversion.as_bytes().to_vec());
}
} else if c == 'Z' {
for _ in 0..next_param_or(1) {
@ -2033,7 +2280,7 @@ impl Perform for Grid {
match intermediates.get(0) {
None | Some(0) => {
// primary device attributes
let terminal_capabilities = "\u{1b}[?6c";
let terminal_capabilities = "\u{1b}[?64;4c";
self.pending_messages_to_pty
.push(terminal_capabilities.as_bytes().to_vec());
},
@ -2175,6 +2422,41 @@ impl Perform for Grid {
}
}
#[derive(Clone)]
pub struct AlternateScreenState {
lines_above: VecDeque<Row>,
viewport: Vec<Row>,
cursor: Cursor,
sixel_grid: SixelGrid,
}
impl AlternateScreenState {
pub fn new(
lines_above: VecDeque<Row>,
viewport: Vec<Row>,
cursor: Cursor,
sixel_grid: SixelGrid,
) -> Self {
AlternateScreenState {
lines_above,
viewport,
cursor,
sixel_grid,
}
}
pub fn apply_contents_to(
&mut self,
lines_above: &mut VecDeque<Row>,
viewport: &mut Vec<Row>,
cursor: &mut Cursor,
sixel_grid: &mut SixelGrid,
) {
std::mem::swap(&mut self.lines_above, lines_above);
std::mem::swap(&mut self.viewport, viewport);
std::mem::swap(&mut self.cursor, cursor);
std::mem::swap(&mut self.sixel_grid, sixel_grid);
}
}
#[derive(Clone)]
pub struct Row {
pub columns: VecDeque<TerminalCharacter>,

View file

@ -4,6 +4,7 @@ pub mod grid;
pub mod link_handler;
mod plugin_pane;
pub mod selection;
pub mod sixel;
pub mod terminal_character;
mod terminal_pane;
mod tiled_panes;
@ -13,6 +14,7 @@ pub use floating_panes::*;
pub use grid::*;
pub use link_handler::*;
pub(crate) use plugin_pane::*;
pub use sixel::*;
pub(crate) use terminal_character::*;
pub use terminal_pane::*;
pub use tiled_panes::*;

View file

@ -2,7 +2,7 @@ use std::fmt::Write;
use std::sync::mpsc::channel;
use std::time::Instant;
use crate::output::CharacterChunk;
use crate::output::{CharacterChunk, SixelImageChunk};
use crate::panes::PaneId;
use crate::pty::VteBytes;
use crate::tab::Pane;
@ -141,7 +141,7 @@ impl Pane for PluginPane {
fn render(
&mut self,
client_id: Option<ClientId>,
) -> Option<(Vec<CharacterChunk>, Option<String>)> {
) -> Option<(Vec<CharacterChunk>, Option<String>, Vec<SixelImageChunk>)> {
// this is a bit of a hack but works in a pinch
client_id?;
let client_id = client_id.unwrap();
@ -214,7 +214,7 @@ impl Pane for PluginPane {
}
}
}
Some((vec![], Some(vte_output))) // TODO: PluginPanes should have their own grid so that we can return the non-serialized TerminalCharacters and have them participate in the render buffer
Some((vec![], Some(vte_output), vec![])) // TODO: PluginPanes should have their own grid so that we can return the non-serialized TerminalCharacters and have them participate in the render buffer
} else {
None
}

View file

@ -0,0 +1,466 @@
use crate::output::SixelImageChunk;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use sixel_image::{SixelDeserializer, SixelImage};
use sixel_tokenizer::SixelEvent;
use std::fmt::Debug;
use zellij_utils::pane_size::SizeInPixels;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct PixelRect {
pub x: usize,
pub y: isize, // this can potentially be negative (eg. when the image top has scrolled past the edge of the scrollbuffer)
pub width: usize,
pub height: usize,
}
impl PixelRect {
pub fn new(x: usize, y: usize, height: usize, width: usize) -> Self {
PixelRect {
x,
y: y as isize,
width,
height,
}
}
pub fn intersecting_rect(&self, other: &PixelRect) -> Option<PixelRect> {
// if the two rects intersect, this returns a PixelRect *relative to self*
let self_top_edge = self.y;
let self_bottom_edge = self.y + self.height as isize;
let self_left_edge = self.x;
let self_right_edge = self.x + self.width;
let other_top_edge = other.y;
let other_bottom_edge = other.y + other.height as isize;
let other_left_edge = other.x;
let other_right_edge = other.x + other.width;
let absolute_x = std::cmp::max(self_left_edge, other_left_edge);
let absolute_y = std::cmp::max(self_top_edge, other_top_edge);
let absolute_right_edge = std::cmp::min(self_right_edge, other_right_edge);
let absolute_bottom_edge = std::cmp::min(self_bottom_edge, other_bottom_edge);
let width = absolute_right_edge.saturating_sub(absolute_x);
let height = absolute_bottom_edge.saturating_sub(absolute_y);
let x = absolute_x - self.x;
let y = absolute_y - self.y;
if width > 0 && height > 0 {
Some(PixelRect {
x,
y,
width,
height: height as usize,
})
} else {
None
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SixelGrid {
sixel_image_locations: HashMap<usize, PixelRect>,
previous_cell_size: Option<SizeInPixels>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
currently_parsing: Option<SixelDeserializer>,
image_ids_to_reap: Vec<usize>,
sixel_parser: Option<sixel_tokenizer::Parser>,
pub sixel_image_store: Rc<RefCell<SixelImageStore>>,
}
impl SixelGrid {
pub fn new(
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
) -> Self {
let previous_cell_size = *character_cell_size.borrow();
SixelGrid {
previous_cell_size,
character_cell_size,
sixel_image_store,
..Default::default()
}
}
pub fn handle_byte(&mut self, byte: u8) {
self.sixel_parser
.as_mut()
.unwrap()
.advance(&byte, |sixel_event| {
if let Some(currently_parsing) = self.currently_parsing.as_mut() {
let _ = currently_parsing.handle_event(sixel_event);
}
});
}
pub fn handle_event(&mut self, sixel_event: SixelEvent) {
if let Some(currently_parsing) = self.currently_parsing.as_mut() {
let _ = currently_parsing.handle_event(sixel_event);
}
}
pub fn is_parsing(&self) -> bool {
self.sixel_parser.is_some()
}
pub fn start_image(
&mut self,
max_height_in_pixels: Option<usize>,
dcs_intermediates: Vec<&u8>,
dcs_params: Vec<&[u16]>,
) {
self.sixel_parser = Some(sixel_tokenizer::Parser::new());
match max_height_in_pixels {
Some(max_height_in_pixels) => {
self.currently_parsing =
Some(SixelDeserializer::new().max_height(max_height_in_pixels));
},
None => {
self.currently_parsing = Some(SixelDeserializer::new());
},
}
self.handle_byte(27);
self.handle_byte(b'P');
for byte in dcs_intermediates {
self.handle_byte(*byte);
}
// send DCS event to parser
for (i, param) in dcs_params.iter().enumerate() {
if i != 0 {
self.handle_byte(b';');
}
for subparam in param.iter() {
let mut b = [0; 4];
for digit in subparam.to_string().chars() {
let len = digit.encode_utf8(&mut b).len();
for byte in b.iter().take(len) {
self.handle_byte(*byte);
}
}
}
}
self.handle_byte(b'q');
}
pub fn end_image(
&mut self,
new_image_id: usize,
x_pixel_coordinates: usize,
y_pixel_coordinates: usize,
) -> Option<SixelImage> {
// usize is image_id
self.sixel_parser = None;
if let Some(sixel_deserializer) = self.currently_parsing.as_mut() {
if let Ok(sixel_image) = sixel_deserializer.create_image() {
let image_pixel_size = sixel_image.pixel_size();
let image_size_and_coordinates = PixelRect::new(
x_pixel_coordinates,
y_pixel_coordinates,
image_pixel_size.0,
image_pixel_size.1,
);
// here we remove images which this image covers completely to save on system
// resources - TODO: also do this with partial covers, eg. if several images
// together cover one image
for (image_id, pixel_rect) in &self.sixel_image_locations {
if let Some(intersecting_rect) =
pixel_rect.intersecting_rect(&image_size_and_coordinates)
{
if intersecting_rect.x == pixel_rect.x
&& intersecting_rect.y == pixel_rect.y
&& intersecting_rect.height == pixel_rect.height
&& intersecting_rect.width == pixel_rect.width
{
self.image_ids_to_reap.push(*image_id);
}
}
}
for image_id in &self.image_ids_to_reap {
self.sixel_image_locations.remove(image_id);
}
self.sixel_image_locations
.insert(new_image_id, image_size_and_coordinates);
self.currently_parsing = None;
Some(sixel_image)
} else {
None
}
} else {
None
}
}
pub fn image_coordinates(&self) -> impl Iterator<Item = (usize, &PixelRect)> {
self.sixel_image_locations
.iter()
.map(|(image_id, pixel_rect)| (*image_id, pixel_rect))
}
pub fn cut_off_rect_from_images(
&mut self,
rect_to_cut_out: PixelRect,
) -> Option<Vec<(usize, PixelRect)>> {
// if there is an image at this cursor location, this returns the image ID and the PixelRect inside the image to be removed
let mut ret = None;
for (image_id, pixel_rect) in &self.sixel_image_locations {
if let Some(intersecting_rect) = pixel_rect.intersecting_rect(&rect_to_cut_out) {
let ret = ret.get_or_insert(vec![]);
ret.push((*image_id, intersecting_rect));
}
}
ret
}
pub fn offset_grid_top(&mut self) {
if let Some(character_cell_size) = *self.character_cell_size.borrow() {
let height_to_reduce = character_cell_size.height as isize;
for (sixel_image_id, pixel_rect) in self.sixel_image_locations.iter_mut() {
pixel_rect.y -= height_to_reduce;
if pixel_rect.y + pixel_rect.height as isize <= 0 {
self.image_ids_to_reap.push(*sixel_image_id);
}
}
for image_id in &self.image_ids_to_reap {
self.sixel_image_locations.remove(image_id);
}
}
}
pub fn drain_image_ids_to_reap(&mut self) -> Option<Vec<usize>> {
let images_to_reap = self.image_ids_to_reap.drain(..);
if images_to_reap.len() > 0 {
Some(images_to_reap.collect())
} else {
None
}
}
pub fn character_cell_size_possibly_changed(&mut self) {
if let (Some(previous_cell_size), Some(character_cell_size)) =
(self.previous_cell_size, *self.character_cell_size.borrow())
{
if previous_cell_size != character_cell_size {
for (_image_id, pixel_rect) in self.sixel_image_locations.iter_mut() {
pixel_rect.x =
(pixel_rect.x / previous_cell_size.width) * character_cell_size.width;
pixel_rect.y = (pixel_rect.y / previous_cell_size.height as isize)
* character_cell_size.height as isize;
}
}
}
self.previous_cell_size = *self.character_cell_size.borrow();
}
pub fn clear(&mut self) -> Option<Vec<usize>> {
// returns image ids to reap
let mut image_ids: Vec<usize> = self
.sixel_image_locations
.drain()
.map(|(image_id, _image_rect)| image_id)
.collect();
image_ids.append(&mut self.image_ids_to_reap);
if !image_ids.is_empty() {
Some(image_ids)
} else {
None
}
}
pub fn next_image_id(&self) -> usize {
self.sixel_image_store.borrow().sixel_images.keys().len()
}
pub fn new_sixel_image(&mut self, sixel_image_id: usize, sixel_image: SixelImage) {
self.sixel_image_store
.borrow_mut()
.sixel_images
.insert(sixel_image_id, (sixel_image, HashMap::new()));
}
pub fn remove_pixels_from_image(&mut self, image_id: usize, pixel_rect: PixelRect) {
if let Some((sixel_image, sixel_image_cache)) = self
.sixel_image_store
.borrow_mut()
.sixel_images
.get_mut(&image_id)
{
sixel_image.cut_out(
pixel_rect.x,
pixel_rect.y as usize,
pixel_rect.width,
pixel_rect.height,
);
sixel_image_cache.clear(); // TODO: more intelligent cache clearing
}
}
pub fn reap_images(&mut self, ids_to_reap: Vec<usize>) {
for id in ids_to_reap {
drop(self.sixel_image_store.borrow_mut().sixel_images.remove(&id));
}
}
pub fn image_cell_coordinates_in_viewport(
&self,
viewport_height: usize,
scrollback_height: usize,
) -> Vec<(usize, usize, usize, usize)> {
match *self.character_cell_size.borrow() {
Some(character_cell_size) => self
.sixel_image_locations
.iter()
.map(|(_image_id, pixel_rect)| {
let scrollback_size_in_pixels = scrollback_height * character_cell_size.height;
let y_pixel_coordinates_in_viewport =
pixel_rect.y - scrollback_size_in_pixels as isize;
let image_y = std::cmp::max(y_pixel_coordinates_in_viewport, 0) as usize
/ character_cell_size.height;
let image_x = pixel_rect.x / character_cell_size.width;
let image_height_in_pixels = if y_pixel_coordinates_in_viewport < 0 {
pixel_rect.height as isize + y_pixel_coordinates_in_viewport
} else {
pixel_rect.height as isize
};
let image_height = image_height_in_pixels as usize / character_cell_size.height;
let image_width = pixel_rect.width / character_cell_size.width;
let height_remainder =
if image_height_in_pixels as usize % character_cell_size.height > 0 {
1
} else {
0
};
let width_remainder = if pixel_rect.width % character_cell_size.width > 0 {
1
} else {
0
};
let image_top_edge = image_y;
let image_bottom_edge =
std::cmp::min(image_y + image_height + height_remainder, viewport_height);
let image_left_edge = image_x;
let image_right_edge = image_x + image_width + width_remainder;
(
image_top_edge,
image_bottom_edge,
image_left_edge,
image_right_edge,
)
})
.collect(),
None => vec![],
}
}
pub fn changed_sixel_chunks_in_viewport(
&self,
changed_rects: HashMap<usize, usize>,
scrollback_size_in_lines: usize,
viewport_width_in_cells: usize,
viewport_x_offset: usize,
viewport_y_offset: usize,
) -> Vec<SixelImageChunk> {
let mut changed_sixel_image_chunks = vec![];
if let Some(character_cell_size) = { *self.character_cell_size.borrow() } {
for (sixel_image_id, sixel_image_pixel_rect) in self.image_coordinates() {
for (line_index, line_count) in &changed_rects {
let changed_rect_pixel_height = line_count * character_cell_size.height;
let changed_rect_top_edge = ((line_index + scrollback_size_in_lines)
* character_cell_size.height)
as isize;
let changed_rect_bottom_edge =
changed_rect_top_edge + changed_rect_pixel_height as isize;
let sixel_image_top_edge = sixel_image_pixel_rect.y;
let sixel_image_bottom_edge =
sixel_image_pixel_rect.y + sixel_image_pixel_rect.height as isize;
let cell_x_in_current_pane =
sixel_image_pixel_rect.x / character_cell_size.width;
let cell_x = viewport_x_offset + cell_x_in_current_pane;
let sixel_image_pixel_width = if sixel_image_pixel_rect.x
+ sixel_image_pixel_rect.width
<= (viewport_width_in_cells * character_cell_size.width)
{
sixel_image_pixel_rect.width
} else {
(viewport_width_in_cells * character_cell_size.width)
.saturating_sub(sixel_image_pixel_rect.x)
};
if sixel_image_pixel_width == 0 {
continue;
}
let sixel_image_cell_distance_from_scrollback_top =
sixel_image_top_edge as usize / character_cell_size.height;
// if the image is above the rect top, this will be 0
let sixel_image_cell_distance_from_changed_rect_top =
sixel_image_cell_distance_from_scrollback_top
.saturating_sub(line_index + scrollback_size_in_lines);
let cell_y = viewport_y_offset
+ line_index
+ sixel_image_cell_distance_from_changed_rect_top;
let sixel_image_pixel_x = 0;
// if the image is above the rect top, this will be 0
let sixel_image_pixel_y = (changed_rect_top_edge as usize)
.saturating_sub(sixel_image_top_edge as usize)
as usize;
let sixel_image_pixel_height = std::cmp::min(
(std::cmp::min(changed_rect_bottom_edge, sixel_image_bottom_edge)
- std::cmp::max(changed_rect_top_edge, sixel_image_top_edge))
as usize,
sixel_image_pixel_rect.height,
);
if (sixel_image_top_edge >= changed_rect_top_edge
&& sixel_image_top_edge <= changed_rect_bottom_edge)
|| (sixel_image_bottom_edge >= changed_rect_top_edge
&& sixel_image_bottom_edge <= changed_rect_bottom_edge)
|| (sixel_image_bottom_edge >= changed_rect_bottom_edge
&& sixel_image_top_edge <= changed_rect_top_edge)
{
changed_sixel_image_chunks.push(SixelImageChunk {
cell_x,
cell_y,
sixel_image_pixel_x,
sixel_image_pixel_y,
sixel_image_pixel_width,
sixel_image_pixel_height,
sixel_image_id,
});
}
}
}
}
changed_sixel_image_chunks
}
}
type SixelImageCache = HashMap<PixelRect, String>;
#[derive(Debug, Clone, Default)]
pub struct SixelImageStore {
sixel_images: HashMap<usize, (SixelImage, SixelImageCache)>,
}
impl SixelImageStore {
pub fn serialize_image(
&mut self,
image_id: usize,
pixel_x: usize,
pixel_y: usize,
pixel_width: usize,
pixel_height: usize,
) -> Option<String> {
self.sixel_images
.get_mut(&image_id)
.map(|(sixel_image, sixel_image_cache)| {
if let Some(cached_image) = sixel_image_cache.get(&PixelRect::new(
pixel_x,
pixel_y,
pixel_height,
pixel_width,
)) {
cached_image.clone()
} else {
let serialized_image =
sixel_image.serialize_range(pixel_x, pixel_y, pixel_width, pixel_height);
sixel_image_cache.insert(
PixelRect::new(pixel_x, pixel_y, pixel_height, pixel_width),
serialized_image.clone(),
);
serialized_image
}
})
}
pub fn image_count(&self) -> usize {
self.sixel_images.len()
}
}

View file

@ -1,4 +1,5 @@
use crate::output::CharacterChunk;
use crate::output::{CharacterChunk, SixelImageChunk};
use crate::panes::sixel::SixelImageStore;
use crate::panes::{
grid::Grid,
terminal_character::{CursorShape, TerminalCharacter, EMPTY_TERMINAL_CHARACTER},
@ -99,10 +100,10 @@ impl Pane for TerminalPane {
self.reflow_lines();
}
fn handle_pty_bytes(&mut self, bytes: VteBytes) {
self.set_should_render(true);
for &byte in &bytes {
self.vte_parser.advance(&mut self.grid, byte);
}
self.set_should_render(true);
}
fn cursor_coordinates(&self) -> Option<(usize, usize)> {
// (x, y)
@ -205,13 +206,14 @@ impl Pane for TerminalPane {
fn render(
&mut self,
_client_id: Option<ClientId>,
) -> Option<(Vec<CharacterChunk>, Option<String>)> {
) -> Option<(Vec<CharacterChunk>, Option<String>, Vec<SixelImageChunk>)> {
if self.should_render() {
let mut raw_vte_output = String::new();
let content_x = self.get_content_x();
let content_y = self.get_content_y();
let mut character_chunks = self.grid.read_changes(content_x, content_y);
let (mut character_chunks, sixel_image_chunks) =
self.grid.read_changes(content_x, content_y);
for character_chunk in character_chunks.iter_mut() {
character_chunk.add_changed_colors(self.grid.changed_colors);
if self
@ -237,7 +239,7 @@ impl Pane for TerminalPane {
self.grid.ring_bell = false;
}
self.set_should_render(false);
Some((character_chunks, Some(raw_vte_output)))
Some((character_chunks, Some(raw_vte_output), sixel_image_chunks))
} else {
None
}
@ -509,15 +511,19 @@ impl TerminalPane {
pane_name: String,
link_handler: Rc<RefCell<LinkHandler>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
) -> TerminalPane {
let initial_pane_title = format!("Pane #{}", pane_index);
let grid = Grid::new(
position_and_size.rows.as_usize(),
position_and_size.cols.as_usize(),
terminal_emulator_colors,
terminal_emulator_color_codes,
link_handler,
character_cell_size,
sixel_image_store,
);
TerminalPane {
frame: HashMap::new(),

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,57 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 1810
assertion_line: 2189
expression: "format!(\"{:?}\", grid)"
---
00 (C): f oo
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):
30 (C):
31 (C):
32 (C):
33 (C):
34 (C):
35 (C):
36 (C):
37 (C):
38 (C):
39 (C):
40 (C):
41 (C):
42 (C):
43 (C):
44 (C):
45 (C):
46 (C):
47 (C):
48 (C):
49 (C):
50 (C):

View file

@ -1,10 +1,34 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 1552
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/mosaic on main bash 16:00:06
02 (C): [aram@green mosaic]$ 12345678912345678912345678912345678912345678912345678912345678912345678912345678912345678912345
03 (W):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):

View file

@ -1,9 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 875
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/zellij on wide-char bash 15:35:20
02 (C): [aram@green zellij]$
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,9 +1,34 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 1486
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/mosaic on main vim some-file 15:07:22
02 (C): ⋊> ~/c/mosaic on main 15:07:29
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):

View file

@ -1,8 +1,57 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 655
expression: "format!(\"{:?}\", grid)"
---
00 (C): ffffff
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):
30 (C):
31 (C):
32 (C):
33 (C):
34 (C):
35 (C):
36 (C):
37 (C):
38 (C):
39 (C):
40 (C):
41 (C):
42 (C):
43 (C):
44 (C):
45 (C):
46 (C):
47 (C):
48 (C):
49 (C):
50 (C):

View file

@ -1,8 +1,57 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 677
expression: "format!(\"{:?}\", grid)"
---
00 (C): foo
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):
30 (C):
31 (C):
32 (C):
33 (C):
34 (C):
35 (C):
36 (C):
37 (C):
38 (C):
39 (C):
40 (C):
41 (C):
42 (C):
43 (C):
44 (C):
45 (C):
46 (C):
47 (C):
48 (C):
49 (C):
50 (C):

View file

@ -1,8 +1,57 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 699
expression: "format!(\"{:?}\", grid)"
---
00 (C): 12345678foo234567890
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):
30 (C):
31 (C):
32 (C):
33 (C):
34 (C):
35 (C):
36 (C):
37 (C):
38 (C):
39 (C):
40 (C):
41 (C):
42 (C):
43 (C):
44 (C):
45 (C):
46 (C):
47 (C):
48 (C):
49 (C):
50 (C):

View file

@ -1,7 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 831
expression: "format!(\"{:?}\", grid)"
---
00 (C): [aram@green zellij]$ 🏠🏠🏠
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,7 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 809
expression: "format!(\"{:?}\", grid)"
---
00 (C): [aram@green zellij]$ 🏠 def abc
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,8 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 941
expression: "format!(\"{:?}\", grid)"
---
00 (C): 12 4
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,7 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 897
expression: "format!(\"{:?}\", grid)"
---
00 (C): i
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,7 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 919
expression: "format!(\"{:?}\", grid)"
---
00 (C): i
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,7 +1,7 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 1508
expression: "format!(\"{:?}\", grid)"
---
00 (C):
01 (C): OS: 5.9.13-arch1-1 GNU/Linux
@ -18,4 +18,17 @@ expression: "format!(\"{:?}\", grid)"
12 (C): wlp2s0 192.168.0.3
13 (C):
14 (C): [I] [20:07] kingdom:mosaic (main) |
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):

View file

@ -1,10 +1,66 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 1644
expression: "format!(\"{:?}\", grid)"
---
00 (C): ➜ mosaic git:(mosaic#130) emacs
01 (C): ➜ mosaic git:(mosaic#130) emacs -nw
02 (C): ➜ mosaic git:(mosaic#130) exit
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):
30 (C):
31 (C):
32 (C):
33 (C):
34 (C):
35 (C):
36 (C):
37 (C):
38 (C):
39 (C):
40 (C):
41 (C):
42 (C):
43 (C):
44 (C):
45 (C):
46 (C):
47 (C):
48 (C):
49 (C):
50 (C):
51 (C):
52 (C):
53 (C):
54 (C):
55 (C):
56 (C):
57 (C):
58 (C):
59 (C):

View file

@ -1,7 +1,7 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 1576
expression: "format!(\"{:?}\", grid)"
---
00 (C):
01 (C): OS: 5.9.14-arch1-1 GNU/Linux
@ -24,4 +24,11 @@ expression: "format!(\"{:?}\", grid)"
18 (W): )$/\\\\e[0;33m\1\\\\e[0m/' | \
19 (C): paste -sd ''\
20 (C): )
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):

View file

@ -1,11 +1,34 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 1275
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/mosaic on main sudo badblocks 11:32:23
02 (C): badblocks (Executable, 33kB) base64 (Executable, 42kB) bash (Executable, 906kB)
03 (C): bandwhich (Executable, 3.0MB) basename (Executable, 38kB) bashbug (Executable, 6.8kB)
04 (C): base32 (Executable, 42kB) basenc (Executable, 50kB) bass
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):

View file

@ -1,11 +1,34 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 1247
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/mosaic on main sudo bandwhich 11:18:26
02 (C): badblocks (Executable, 33kB) base64 (Executable, 42kB) bash (Executable, 906kB)
03 (C): bandwhich (Executable, 3.0MB) basename (Executable, 38kB) bashbug (Executable, 6.8kB)
04 (C): base32 (Executable, 42kB) basenc (Executable, 50kB) bass
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):

View file

@ -1,8 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 853
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/zellij on main
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,7 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 787
expression: "format!(\"{:?}\", grid)"
---
00 (C): [aram@green zellij]$ 🏠 xdef abc
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,8 +1,57 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 2018
expression: "format!(\"{:?}\", grid)"
---
00 (C): 👨y x🔭
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):
30 (C):
31 (C):
32 (C):
33 (C):
34 (C):
35 (C):
36 (C):
37 (C):
38 (C):
39 (C):
40 (C):
41 (C):
42 (C):
43 (C):
44 (C):
45 (C):
46 (C):
47 (C):
48 (C):
49 (C):
50 (C):

View file

@ -1,8 +1,57 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 2044
expression: "format!(\"{:?}\", grid)"
---
00 (C): 👨👨 🔭
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):
30 (C):
31 (C):
32 (C):
33 (C):
34 (C):
35 (C):
36 (C):
37 (C):
38 (C):
39 (C):
40 (C):
41 (C):
42 (C):
43 (C):
44 (C):
45 (C):
46 (C):
47 (C):
48 (C):
49 (C):
50 (C):

View file

@ -1,8 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 963
expression: "format!(\"{:?}\", grid)"
---
00 (C): 12 4
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -0,0 +1,36 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 2273
expression: "format!(\"{:?}\", grid)"
---
00 (C):
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):

View file

@ -0,0 +1,36 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 2267
expression: "format!(\"{:?}\", grid)"
---
00 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
01 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
02 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
03 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
04 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
10 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
11 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
12 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
13 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
14 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
20 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
21 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
22 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
23 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):

View file

@ -0,0 +1,36 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 2338
expression: "format!(\"{:?}\", grid)"
---
00 (C): SixelSixelSix
01 (C): SixelSixelSix
02 (C): SixelSixelSix
03 (C): SixelSixelSix
04 (C): SixelSixelSix
05 (C): line 6
06 (C): line 7
07 (C): line 8
08 (C): line 9
09 (C): line 10
10 (C):
11 (C):
12 (C): SixelSixelSix
13 (C): SixelSixelSix
14 (C): SixelSixelSix
15 (C): SixelSixelSix
16 (C): SixelSixelSix
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):

View file

@ -0,0 +1,36 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 2316
expression: "format!(\"{:?}\", grid)"
---
00 (C): SixelSixelSix
01 (C): SixelSixelSix
02 (C): SixelSixelSix
03 (C): SixelSixelSix
04 (C): SixelSixelSix
05 (C): line 6
06 (C): line 7
07 (C): line 8
08 (C): line 9
09 (C): line 10
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):
21 (C):
22 (C):
23 (C):
24 (C):
25 (C):
26 (C):
27 (C):
28 (C):
29 (C):

View file

@ -1,6 +1,6 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid . pending_messages_to_pty)"
assertion_line: 721
expression: "format!(\"{:?}\", grid.pending_messages_to_pty)"
---
[[27, 91, 63, 54, 99], [27, 91, 56, 59, 53, 49, 59, 57, 55, 116], [27, 91, 63, 54, 99], [27, 91, 48, 110], [27, 91, 49, 59, 49, 82]]
[[27, 91, 63, 54, 52, 59, 52, 99], [27, 91, 56, 59, 53, 49, 59, 57, 55, 116], [27, 91, 63, 54, 99], [27, 91, 48, 110], [27, 91, 49, 59, 49, 82]]

View file

@ -1,8 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 743
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/zellij on main 15:19:10
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -1,10 +1,27 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 765
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/zellij on main bash 15:50:11
02 (C): [aram@green zellij]$
03 (W):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):
20 (C):

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 323
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
01 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
02 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
03 (C): line 5
04 (C):
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
10 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
11 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
12 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
13 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
14 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 364
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): line 1
01 (C): line 2
02 (C): line 3
03 (C): line 4
04 (C): line 5
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 235
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
01 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
02 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
03 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
04 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): line 1
10 (C): line 2
11 (C): line 3
12 (C): line 4
13 (C): line 5
14 (C):
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 156
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
01 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
02 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
03 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
04 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
10 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
11 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
12 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
13 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
14 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C):

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 123
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
01 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
02 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
03 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
04 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
10 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
11 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
12 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
13 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
14 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C):

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 277
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
01 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
02 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
03 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
04 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): line 1
10 (C): line 2
11 (C): line 3
12 (C): line 4
13 (C): line 5
14 (C):
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 195
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): line 30
01 (C):
02 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
03 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
04 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
10 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
11 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
12 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
13 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
14 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 197
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
01 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
02 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
03 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
04 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
10 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
11 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
12 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
13 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
14 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C):

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 193
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): line 27
01 (C): line 28
02 (C): line 29
03 (C): line 30
04 (C):
05 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
06 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
07 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
08 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
09 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
10 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
11 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
12 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
13 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
14 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
15 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
16 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
17 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
18 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix
19 (C): SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/panes/./unit/terminal_pane_tests.rs
assertion_line: 85
expression: "format!(\"{:?}\", terminal_pane.grid)"
---
00 (C): Si
01 (C):
02 (C):
03 (C):
04 (C):
05 (C):
06 (C):
07 (C):
08 (C):
09 (C):
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):

View file

@ -1,16 +1,28 @@
use super::super::TerminalPane;
use crate::panes::sixel::SixelImageStore;
use crate::panes::LinkHandler;
use crate::tab::Pane;
use ::insta::assert_snapshot;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use zellij_utils::{
data::{Palette, Style},
pane_size::PaneGeom,
pane_size::{PaneGeom, SizeInPixels},
};
use std::fmt::Write;
fn read_fixture(fixture_name: &str) -> Vec<u8> {
let mut path_to_file = std::path::PathBuf::new();
path_to_file.push("../src");
path_to_file.push("tests");
path_to_file.push("fixtures");
path_to_file.push(fixture_name);
std::fs::read(path_to_file)
.unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name))
}
#[test]
pub fn scrolling_inside_a_pane() {
let fake_client_id = 1;
@ -20,6 +32,9 @@ pub fn scrolling_inside_a_pane() {
let pid = 1;
let style = Style::default();
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()));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
@ -28,7 +43,9 @@ pub fn scrolling_inside_a_pane() {
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
Rc::new(RefCell::new(None)),
Rc::new(RefCell::new(Palette::default())),
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..30 {
@ -42,3 +59,335 @@ pub fn scrolling_inside_a_pane() {
terminal_pane.clear_scroll();
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}
#[test]
pub fn sixel_image_inside_terminal_pane() {
let mut fake_win_size = PaneGeom::default();
fake_win_size.cols.set_inner(121);
fake_win_size.rows.set_inner(20);
let pid = 1;
let style = Style::default();
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()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
style,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size,
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let sixel_image_bytes = "\u{1b}Pq
#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0
#1~~@@vv@@~~@@~~$
#2??}}GG}}??}}??-
#1!14@
\u{1b}\\";
terminal_pane.handle_pty_bytes(Vec::from(sixel_image_bytes.as_bytes()));
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}
#[test]
pub fn partial_sixel_image_inside_terminal_pane() {
// here we test to make sure we partially render an image that is partially hidden in the
// scrollbuffer
let mut fake_win_size = PaneGeom::default();
fake_win_size.cols.set_inner(121);
fake_win_size.rows.set_inner(20);
let pid = 1;
let style = Style::default();
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()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
style,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size,
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let pane_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_content);
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}
#[test]
pub fn overflowing_sixel_image_inside_terminal_pane() {
// here we test to make sure we properly render an image that overflows both in the width and
// height of the pane
let mut fake_win_size = PaneGeom::default();
fake_win_size.cols.set_inner(50);
fake_win_size.rows.set_inner(20);
let pid = 1;
let style = Style::default();
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()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
style,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size,
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let pane_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_content);
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}
#[test]
pub fn scrolling_through_a_sixel_image() {
let fake_client_id = 1;
let mut fake_win_size = PaneGeom::default();
fake_win_size.cols.set_inner(121);
fake_win_size.rows.set_inner(20);
let pid = 1;
let style = Style::default();
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()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
style,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size,
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..30 {
writeln!(&mut text_to_fill_pane, "\rline {}", i + 1).unwrap();
}
writeln!(&mut text_to_fill_pane, "\r").unwrap();
let pane_sixel_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(text_to_fill_pane.into_bytes());
terminal_pane.handle_pty_bytes(pane_sixel_content);
terminal_pane.scroll_up(10, fake_client_id);
assert_snapshot!(format!("{:?}", terminal_pane.grid));
terminal_pane.scroll_down(3, fake_client_id);
assert_snapshot!(format!("{:?}", terminal_pane.grid));
terminal_pane.clear_scroll();
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}
#[test]
pub fn multiple_sixel_images_in_pane() {
let fake_client_id = 1;
let mut fake_win_size = PaneGeom::default();
fake_win_size.cols.set_inner(121);
fake_win_size.rows.set_inner(20);
let pid = 1;
let style = Style::default();
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()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
style,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size,
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..5 {
writeln!(&mut text_to_fill_pane, "\rline {}", i + 1).unwrap();
}
writeln!(&mut text_to_fill_pane, "\r").unwrap();
let pane_sixel_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_sixel_content.clone()); // one image above text
terminal_pane.handle_pty_bytes(text_to_fill_pane.into_bytes());
terminal_pane.handle_pty_bytes(pane_sixel_content); // one image below text
terminal_pane.scroll_up(20, fake_client_id); // scroll up to see both images
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}
#[test]
pub fn resizing_pane_with_sixel_images() {
// here we test, for example, that sixel images don't wrap with other lines
let fake_client_id = 1;
let mut fake_win_size = PaneGeom::default();
fake_win_size.cols.set_inner(121);
fake_win_size.rows.set_inner(20);
let pid = 1;
let style = Style::default();
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()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
style,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size,
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..5 {
writeln!(&mut text_to_fill_pane, "\rline {}", i + 1).unwrap();
}
writeln!(&mut text_to_fill_pane, "\r").unwrap();
let pane_sixel_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_sixel_content.clone());
terminal_pane.handle_pty_bytes(text_to_fill_pane.into_bytes());
terminal_pane.handle_pty_bytes(pane_sixel_content);
let mut new_win_size = PaneGeom::default();
new_win_size.cols.set_inner(100);
new_win_size.rows.set_inner(20);
terminal_pane.set_geom(new_win_size);
terminal_pane.scroll_up(20, fake_client_id); // scroll up to see both images
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}
#[test]
pub fn changing_character_cell_size_with_sixel_images() {
let fake_client_id = 1;
let mut fake_win_size = PaneGeom::default();
fake_win_size.cols.set_inner(121);
fake_win_size.rows.set_inner(20);
let pid = 1;
let style = Style::default();
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()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
style,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size.clone(),
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..5 {
writeln!(&mut text_to_fill_pane, "\rline {}", i + 1).unwrap();
}
writeln!(&mut text_to_fill_pane, "\r").unwrap();
let pane_sixel_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_sixel_content.clone());
terminal_pane.handle_pty_bytes(text_to_fill_pane.into_bytes());
terminal_pane.handle_pty_bytes(pane_sixel_content);
// here the new_win_size is the same as the old one, we just update the character_cell_size
// which will be picked up upon resize (which is why we're doing set_geom below)
let mut new_win_size = PaneGeom::default();
new_win_size.cols.set_inner(121);
new_win_size.rows.set_inner(20);
*character_cell_size.borrow_mut() = Some(SizeInPixels {
width: 8,
height: 18,
});
terminal_pane.set_geom(new_win_size);
terminal_pane.scroll_up(10, fake_client_id); // scroll up to see both images
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}
#[test]
pub fn keep_working_after_corrupted_sixel_image() {
let mut fake_win_size = PaneGeom::default();
fake_win_size.cols.set_inner(121);
fake_win_size.rows.set_inner(20);
let pid = 1;
let style = Style::default();
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()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
style,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size,
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
); // 0 is the pane index
let sixel_image_bytes = "\u{1b}PI AM CORRUPTED BWAHAHAq
#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0
#1~~@@vv@@~~@@~~$
#2??}}GG}}??}}??-
#1!14@
\u{1b}\\";
terminal_pane.handle_pty_bytes(Vec::from(sixel_image_bytes.as_bytes()));
let mut text_to_fill_pane = String::new();
for i in 0..5 {
writeln!(&mut text_to_fill_pane, "\rline {}", i + 1).unwrap();
}
terminal_pane.handle_pty_bytes(text_to_fill_pane.into_bytes());
assert_snapshot!(format!("{:?}", terminal_pane.grid));
}

View file

@ -28,14 +28,24 @@ fn route_action(
client_id: ClientId,
) -> bool {
let mut should_break = false;
session
.senders
.send_to_plugin(PluginInstruction::Update(
None,
Some(client_id),
Event::InputReceived,
))
.unwrap();
// forward the action to plugins unless it is a mousehold
// this is a bit of a hack around the unfortunate architecture we use with plugins
// this will change as soon as we refactor
match action {
Action::MouseHold(_) => {},
_ => {
session
.senders
.send_to_plugin(PluginInstruction::Update(
None,
Some(client_id),
Event::InputReceived,
))
.unwrap();
},
}
match action {
Action::ToggleTab => {
session
@ -493,6 +503,16 @@ pub(crate) fn route_thread_main(
))
.unwrap();
},
ClientToServerMsg::ColorRegisters(color_registers) => {
rlocked_sessions
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::TerminalColorRegisters(
color_registers,
))
.unwrap();
},
ClientToServerMsg::NewClient(
client_attributes,
cli_args,

View file

@ -1,7 +1,7 @@
//! Things related to [`Screen`]s.
use std::cell::RefCell;
use std::collections::{BTreeMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::os::unix::io::RawFd;
use std::rc::Rc;
use std::str;
@ -15,6 +15,7 @@ use crate::panes::terminal_character::AnsiCode;
use crate::{
output::Output,
panes::sixel::SixelImageStore,
panes::PaneId,
pty::{ClientOrTabIndex, PtyInstruction, VteBytes},
tab::Tab,
@ -95,6 +96,7 @@ pub enum ScreenInstruction {
TerminalPixelDimensions(PixelDimensions),
TerminalBackgroundColor(String),
TerminalForegroundColor(String),
TerminalColorRegisters(Vec<(usize, String)>),
ChangeMode(ModeInfo, ClientId),
LeftClick(Position, ClientId),
RightClick(Position, ClientId),
@ -184,6 +186,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::TerminalForegroundColor(..) => {
ScreenContext::TerminalForegroundColor
},
ScreenInstruction::TerminalColorRegisters(..) => ScreenContext::TerminalColorRegisters,
ScreenInstruction::ChangeMode(..) => ScreenContext::ChangeMode,
ScreenInstruction::ToggleActiveSyncTab(..) => ScreenContext::ToggleActiveSyncTab,
ScreenInstruction::ScrollUpAt(..) => ScreenContext::ScrollUpAt,
@ -247,9 +250,11 @@ pub(crate) struct Screen {
size: Size,
pixel_dimensions: PixelDimensions,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
/// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`]
overlay: OverlayWindow,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
connected_clients: Rc<RefCell<HashSet<ClientId>>>,
/// The indices of this [`Screen`]'s active [`Tab`]s.
active_tab_indices: BTreeMap<ClientId, usize>,
@ -279,12 +284,14 @@ impl Screen {
size: client_attributes.size,
pixel_dimensions: Default::default(),
character_cell_size: Rc::new(RefCell::new(None)),
sixel_image_store: Rc::new(RefCell::new(SixelImageStore::default())),
style: client_attributes.style,
connected_clients: Rc::new(RefCell::new(HashSet::new())),
active_tab_indices: BTreeMap::new(),
tabs: BTreeMap::new(),
overlay: OverlayWindow::default(),
terminal_emulator_colors: Rc::new(RefCell::new(Palette::default())),
terminal_emulator_color_codes: Rc::new(RefCell::new(HashMap::new())),
tab_history: BTreeMap::new(),
mode_info: BTreeMap::new(),
default_mode_info: mode_info,
@ -518,10 +525,19 @@ impl Screen {
self.terminal_emulator_colors.borrow_mut().fg = fg_palette_color;
}
}
pub fn update_terminal_color_registers(&mut self, color_registers: Vec<(usize, String)>) {
let mut terminal_emulator_color_codes = self.terminal_emulator_color_codes.borrow_mut();
for (color_register, color_sequence) in color_registers {
terminal_emulator_color_codes.insert(color_register, color_sequence);
}
}
/// Renders this [`Screen`], which amounts to rendering its active [`Tab`].
pub fn render(&mut self) {
let mut output = Output::default();
let mut output = Output::new(
self.sixel_image_store.clone(),
self.character_cell_size.clone(),
);
let mut tabs_to_close = vec![];
let size = self.size;
let overlay = self.overlay.clone();
@ -599,6 +615,7 @@ impl Screen {
String::new(),
self.size,
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.bus.os_input.as_ref().unwrap().clone(),
self.bus.senders.clone(),
self.max_panes,
@ -610,6 +627,7 @@ impl Screen {
client_id,
self.copy_options.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
);
tab.apply_layout(layout, new_pids, tab_index, client_id);
if self.session_is_mirrored {
@ -793,7 +811,6 @@ impl Screen {
pub fn move_focus_left_or_previous_tab(&mut self, client_id: ClientId) {
if let Some(active_tab) = self.get_active_tab_mut(client_id) {
if !active_tab.move_focus_left(client_id) {
println!("can has true");
self.switch_tab_prev(client_id);
}
} else {
@ -1236,6 +1253,9 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::TerminalForegroundColor(background_color_instruction) => {
screen.update_terminal_foreground_color(background_color_instruction);
},
ScreenInstruction::TerminalColorRegisters(color_registers) => {
screen.update_terminal_color_registers(color_registers);
},
ScreenInstruction::ChangeMode(mode_info, client_id) => {
screen.change_mode(mode_info, client_id);
screen.render();
@ -1264,8 +1284,9 @@ pub(crate) fn screen_thread_main(
screen.render();
},
ScreenInstruction::MouseHold(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_mouse_hold(&point, client_id));
active_tab!(screen, client_id, |tab: &mut Tab| {
tab.handle_mouse_hold(&point, client_id);
});
screen.render();
},
ScreenInstruction::Copy(client_id) => {

View file

@ -17,7 +17,8 @@ use crate::ui::pane_boundaries_frame::FrameParams;
use self::clipboard::ClipboardProvider;
use crate::{
os_input_output::ServerOsApi,
output::{CharacterChunk, Output},
output::{CharacterChunk, Output, SixelImageChunk},
panes::sixel::SixelImageStore,
panes::{FloatingPanes, TiledPanes},
panes::{LinkHandler, PaneId, PluginPane, TerminalPane},
pty::{ClientOrTabIndex, PtyInstruction, VteBytes},
@ -77,6 +78,7 @@ pub(crate) struct Tab {
viewport: Rc<RefCell<Viewport>>, // includes all non-UI panes
display_area: Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout)
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
os_api: Box<dyn ServerOsApi>,
pub senders: ThreadSenders,
synchronize_is_active: bool,
@ -96,6 +98,7 @@ pub(crate) struct Tab {
copy_on_select: bool,
last_mouse_hold_position: Option<Position>,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
@ -135,7 +138,7 @@ pub trait Pane {
fn render(
&mut self,
client_id: Option<ClientId>,
) -> Option<(Vec<CharacterChunk>, Option<String>)>; // TODO: better
) -> Option<(Vec<CharacterChunk>, Option<String>, Vec<SixelImageChunk>)>; // TODO: better
fn render_frame(
&mut self,
client_id: ClientId,
@ -287,6 +290,7 @@ impl Tab {
name: String,
display_area: Size,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
os_api: Box<dyn ServerOsApi>,
senders: ThreadSenders,
max_panes: Option<usize>,
@ -298,6 +302,7 @@ impl Tab {
client_id: ClientId,
copy_options: CopyOptions,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
) -> Self {
let name = if name.is_empty() {
format!("Tab #{}", index + 1)
@ -354,6 +359,7 @@ impl Tab {
viewport,
display_area,
character_cell_size,
sixel_image_store,
synchronize_is_active: false,
os_api,
senders,
@ -371,6 +377,7 @@ impl Tab {
copy_on_select: copy_options.copy_on_select,
last_mouse_hold_position: None,
terminal_emulator_colors,
terminal_emulator_color_codes,
}
}
@ -438,7 +445,9 @@ impl Tab {
layout.pane_name.clone().unwrap_or_default(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
);
new_pane.set_borderless(layout.borderless);
self.tiled_panes
@ -668,7 +677,9 @@ impl Tab {
String::new(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
);
new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame
resize_pty!(new_pane, self.os_api);
@ -691,7 +702,9 @@ impl Tab {
String::new(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
);
self.tiled_panes.insert_pane(pid, Box::new(new_terminal));
self.should_clear_display_before_rendering = true;
@ -717,7 +730,9 @@ impl Tab {
String::new(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
);
let replaced_pane = if self.floating_panes.panes_are_visible() {
self.floating_panes
@ -762,7 +777,9 @@ impl Tab {
String::new(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
);
self.tiled_panes
.split_pane_horizontally(pid, Box::new(new_terminal), client_id);
@ -790,7 +807,9 @@ impl Tab {
String::new(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
);
self.tiled_panes
.split_pane_vertically(pid, Box::new(new_terminal), client_id);
@ -1787,7 +1806,12 @@ impl Tab {
}
}
}
pub fn handle_mouse_hold(&mut self, position_on_screen: &Position, client_id: ClientId) {
pub fn handle_mouse_hold(
&mut self,
position_on_screen: &Position,
client_id: ClientId,
) -> bool {
// return value indicates whether we should trigger a render
// determine if event is repeated to enable smooth scrolling
let is_repeated = if let Some(last_position) = self.last_mouse_hold_position {
position_on_screen == &last_position
@ -1805,7 +1829,8 @@ impl Tab {
.move_pane_with_mouse(*position_on_screen, search_selectable)
{
self.set_force_render();
return;
return !is_repeated; // we don't need to re-render in this case if the pane did not move
// return;
}
let selecting = self.selecting_with_mouse;
@ -1825,10 +1850,13 @@ impl Tab {
let mouse_event = format!("\u{1b}[<32;{:?};{:?}M", col, line);
self.write_to_active_terminal(mouse_event.into_bytes(), client_id);
return true; // we need to re-render in this case so the selection disappears
} else if selecting {
active_pane.update_selection(&relative_position, client_id);
return true; // we need to re-render in this case so the selection is updated
}
}
false // we shouldn't even get here, but might as well not needlessly render if we do
}
pub fn copy_selection(&self, client_id: ClientId) {

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
assertion_line: 1343
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────── SCROLL: 0/7 ┐
01 (C): │ixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixe │
02 (C): │ixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixe │
03 (C): │ixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixe │
04 (C): │ixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixe │
05 (C): │ixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixe │
06 (C): │ixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixe │
07 (C): │ixelSixelSixelSixelSixelSixelSixe┌ Pane #2 ─────────────────────────────────────────────────┐ │
08 (C): │ixelSixelSixelSixelSixelSixelSixe│ │ │
09 (C): │ixelSixelSixelSixelSixelSixelSixe│ │ │
10 (C): │ixelSixelSixelSixelSixelSixelSixe│ │ │
11 (C): │ixelSixelSixelSixelSixelSixelSixe│ │ │
12 (C): │ixelSixelSixelSixelSixelSixelSixe│ │ │
13 (C): │ixelSixelSixelSixelSixelSixelSixe│ │ │
14 (C): │ixelSixelSixelSixelSixelSixelSixe│ │ │
15 (C): │ixelSixelSixelSixelSixelSixelSixe│ │ │
16 (C): │ixelSixelSixelSixelSixelSixelSixe└──────────────────────────────────────────────────────────┘ │
17 (C): │ixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixe │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
assertion_line: 1312
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ │
06 (C): │ │
07 (C): │ ┌ Pane #2 ────────────────────────────────── SCROLL: 0/17 ┐ │
08 (C): │ │SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix│ │
09 (C): │ │SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix│ │
10 (C): │ │SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix│ │
11 (C): │ │SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix│ │
12 (C): │ │SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix│ │
13 (C): │ │SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix│ │
14 (C): │ │SixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSixelSix│ │
15 (C): │ │ │ │
16 (C): │ └──────────────────────────────────────────────────────────┘ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -1,4 +1,5 @@
use super::{Output, Tab};
use crate::panes::sixel::SixelImageStore;
use crate::screen::CopyOptions;
use crate::Arc;
use crate::Mutex;
@ -13,12 +14,11 @@ use std::path::PathBuf;
use zellij_utils::envs::set_session_name;
use zellij_utils::input::layout::LayoutTemplate;
use zellij_utils::ipc::IpcReceiverWithContext;
use zellij_utils::pane_size::Size;
use zellij_utils::pane_size::{Size, SizeInPixels};
use zellij_utils::position::Position;
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::os::unix::io::RawFd;
use std::rc::Rc;
@ -119,12 +119,15 @@ fn create_new_tab(size: Size) -> Tab {
let character_cell_info = Rc::new(RefCell::new(None));
let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let mut tab = Tab::new(
index,
position,
name,
size,
character_cell_info,
sixel_image_store,
os_api,
senders,
max_panes,
@ -136,6 +139,66 @@ fn create_new_tab(size: Size) -> Tab {
client_id,
copy_options,
terminal_emulator_colors,
terminal_emulator_color_codes,
);
tab.apply_layout(
LayoutTemplate::default().try_into().unwrap(),
vec![1],
index,
client_id,
);
tab
}
fn create_new_tab_with_sixel_support(
size: Size,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
) -> Tab {
// this is like the create_new_tab function but includes stuff needed for sixel,
// eg. character_cell_size
set_session_name("test".into());
let index = 0;
let position = 0;
let name = String::new();
let os_api = Box::new(FakeInputOutput {
file_dumps: Arc::new(Mutex::new(HashMap::new())),
});
let senders = ThreadSenders::default().silently_fail_on_send();
let max_panes = None;
let mode_info = ModeInfo::default();
let style = Style::default();
let draw_pane_frames = true;
let client_id = 1;
let session_is_mirrored = true;
let mut connected_clients = HashSet::new();
connected_clients.insert(client_id);
let connected_clients = Rc::new(RefCell::new(connected_clients));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let mut tab = Tab::new(
index,
position,
name,
size,
character_cell_size,
sixel_image_store,
os_api,
senders,
max_panes,
style,
mode_info,
draw_pane_frames,
connected_clients,
session_is_mirrored,
client_id,
copy_options,
terminal_emulator_colors,
terminal_emulator_color_codes,
);
tab.apply_layout(
LayoutTemplate::default().try_into().unwrap(),
@ -162,12 +225,48 @@ use ::insta::assert_snapshot;
use zellij_utils::vte;
fn take_snapshot(ansi_instructions: &str, rows: usize, columns: usize, palette: Palette) -> String {
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut grid = Grid::new(
rows,
columns,
Rc::new(RefCell::new(palette)),
terminal_emulator_color_codes,
Rc::new(RefCell::new(LinkHandler::new())),
Rc::new(RefCell::new(None)),
character_cell_size,
sixel_image_store,
);
let mut vte_parser = vte::Parser::new();
for &byte in ansi_instructions.as_bytes() {
vte_parser.advance(&mut grid, byte);
}
format!("{:?}", grid)
}
fn take_snapshot_with_sixel(
ansi_instructions: &str,
rows: usize,
columns: usize,
palette: Palette,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
) -> String {
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut grid = Grid::new(
rows,
columns,
Rc::new(RefCell::new(palette)),
terminal_emulator_color_codes,
Rc::new(RefCell::new(LinkHandler::new())),
character_cell_size,
sixel_image_store,
);
let mut vte_parser = vte::Parser::new();
for &byte in ansi_instructions.as_bytes() {
@ -183,12 +282,16 @@ fn take_snapshot_and_cursor_position(
palette: Palette,
) -> (String, Option<(usize, usize)>) {
// snapshot, x_coordinates, y_coordinates
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let mut grid = Grid::new(
rows,
columns,
Rc::new(RefCell::new(palette)),
terminal_emulator_color_codes,
Rc::new(RefCell::new(LinkHandler::new())),
Rc::new(RefCell::new(None)),
sixel_image_store,
);
let mut vte_parser = vte::Parser::new();
for &byte in ansi_instructions.as_bytes() {
@ -1233,6 +1336,76 @@ fn save_cursor_position_across_resizes() {
assert_snapshot!(snapshot);
}
#[test]
fn move_floating_pane_with_sixel_image() {
let new_pane_id = PaneId::Terminal(2);
let size = Size {
cols: 121,
rows: 20,
};
let client_id = 1;
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let mut tab = create_new_tab_with_sixel_support(size, sixel_image_store.clone());
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut output = Output::new(sixel_image_store.clone(), character_cell_size);
tab.toggle_floating_panes(client_id, None);
tab.new_pane(new_pane_id, Some(client_id));
let fixture = read_fixture("sixel-image-500px.six");
tab.handle_pty_bytes(2, fixture);
tab.handle_left_click(&Position::new(5, 71), client_id);
tab.handle_mouse_release(&Position::new(7, 75), client_id);
tab.render(&mut output, None);
let snapshot = take_snapshot_with_sixel(
output.serialize().get(&client_id).unwrap(),
size.rows,
size.cols,
Palette::default(),
sixel_image_store,
);
assert_snapshot!(snapshot);
}
#[test]
fn floating_pane_above_sixel_image() {
let new_pane_id = PaneId::Terminal(2);
let size = Size {
cols: 121,
rows: 20,
};
let client_id = 1;
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let mut tab = create_new_tab_with_sixel_support(size, sixel_image_store.clone());
let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels {
width: 8,
height: 21,
})));
let mut output = Output::new(sixel_image_store.clone(), character_cell_size);
tab.toggle_floating_panes(client_id, None);
tab.new_pane(new_pane_id, Some(client_id));
let fixture = read_fixture("sixel-image-500px.six");
tab.handle_pty_bytes(1, fixture);
tab.handle_left_click(&Position::new(5, 71), client_id);
tab.handle_mouse_release(&Position::new(7, 75), client_id);
tab.render(&mut output, None);
let snapshot = take_snapshot_with_sixel(
output.serialize().get(&client_id).unwrap(),
size.rows,
size.cols,
Palette::default(),
sixel_image_store,
);
assert_snapshot!(snapshot);
}
#[test]
fn suppress_tiled_pane() {
let size = Size {

View file

@ -1,4 +1,5 @@
use super::Tab;
use crate::panes::sixel::SixelImageStore;
use crate::screen::CopyOptions;
use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi},
@ -13,7 +14,7 @@ use zellij_utils::ipc::IpcReceiverWithContext;
use zellij_utils::pane_size::{Size, SizeInPixels};
use std::cell::RefCell;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::os::unix::io::RawFd;
use std::rc::Rc;
@ -105,12 +106,15 @@ fn create_new_tab(size: Size) -> Tab {
let connected_clients = Rc::new(RefCell::new(connected_clients));
let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
let copy_options = CopyOptions::default();
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let mut tab = Tab::new(
index,
position,
name,
size,
character_cell_info,
sixel_image_store,
os_api,
senders,
max_panes,
@ -122,6 +126,7 @@ fn create_new_tab(size: Size) -> Tab {
client_id,
copy_options,
terminal_emulator_colors,
terminal_emulator_color_codes,
);
tab.apply_layout(
LayoutTemplate::default().try_into().unwrap(),
@ -152,12 +157,15 @@ fn create_new_tab_with_cell_size(
let connected_clients = Rc::new(RefCell::new(connected_clients));
let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
let copy_options = CopyOptions::default();
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let mut tab = Tab::new(
index,
position,
name,
size,
character_cell_size,
sixel_image_store,
os_api,
senders,
max_panes,
@ -169,6 +177,7 @@ fn create_new_tab_with_cell_size(
client_id,
copy_options,
terminal_emulator_colors,
terminal_emulator_color_codes,
);
tab.apply_layout(
LayoutTemplate::default().try_into().unwrap(),

View file

@ -44,13 +44,19 @@ impl<'a> PaneContentsAndUi<'a> {
&mut self,
clients: impl Iterator<Item = ClientId>,
) {
if let Some((character_chunks, raw_vte_output)) = self.pane.render(None) {
if let Some((character_chunks, raw_vte_output, sixel_image_chunks)) = self.pane.render(None)
{
let clients: Vec<ClientId> = clients.collect();
self.output.add_character_chunks_to_multiple_clients(
character_chunks,
clients.iter().copied(),
self.z_index,
);
self.output.add_sixel_image_chunks_to_multiple_clients(
sixel_image_chunks,
clients.iter().copied(),
self.z_index,
);
if let Some(raw_vte_output) = raw_vte_output {
self.output.add_post_vte_instruction_to_multiple_clients(
clients.iter().copied(),
@ -65,9 +71,16 @@ impl<'a> PaneContentsAndUi<'a> {
}
}
pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) {
if let Some((character_chunks, raw_vte_output)) = self.pane.render(Some(client_id)) {
if let Some((character_chunks, raw_vte_output, sixel_image_chunks)) =
self.pane.render(Some(client_id))
{
self.output
.add_character_chunks_to_client(client_id, character_chunks, self.z_index);
self.output.add_sixel_image_chunks_to_client(
client_id,
sixel_image_chunks,
self.z_index,
);
if let Some(raw_vte_output) = raw_vte_output {
self.output.add_post_vte_instruction_to_client(
client_id,

View file

@ -24,6 +24,10 @@ pub fn set_session_name(v: String) {
set_var(SESSION_NAME_ENV_KEY, v);
}
pub fn set_initial_environment_vars() {
set_var("COLORTERM", "24bit");
}
pub const SOCKET_DIR_ENV_KEY: &str = "ZELLIJ_SOCKET_DIR";
pub fn get_socket_dir() -> Result<String> {
Ok(var(SOCKET_DIR_ENV_KEY)?)

View file

@ -277,6 +277,7 @@ pub enum ScreenContext {
TerminalPixelDimensions,
TerminalBackgroundColor,
TerminalForegroundColor,
TerminalColorRegisters,
ChangeMode,
LeftClick,
RightClick,

View file

@ -78,6 +78,7 @@ pub enum ClientToServerMsg {
TerminalPixelDimensions(PixelDimensions),
BackgroundColor(String),
ForegroundColor(String),
ColorRegisters(Vec<(usize, String)>),
TerminalResize(Size),
NewClient(
ClientAttributes,