zellij/zellij-server/src/panes/grid.rs
Aram Drevekenin 820ff85231
feat(ux): reload config options at runtime (#3564)
* change simplified_ui at runtime

* change default_shell at runtime

* change pane_frames (from config) at runtime

* all other options

* some refactoring

* style(fmt): rustfmt
2024-08-23 15:41:48 +02:00

3514 lines
143 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use super::sixel::{PixelRect, SixelGrid, SixelImageStore};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use zellij_utils::data::Style;
use zellij_utils::errors::prelude::*;
use zellij_utils::regex::Regex;
use std::{
cmp::Ordering,
collections::{BTreeSet, VecDeque},
fmt::{self, Debug, Formatter},
str,
};
use zellij_utils::{
consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE},
data::{Palette, PaletteColor},
pane_size::SizeInPixels,
position::Position,
vte,
};
const TABSTOP_WIDTH: usize = 8; // TODO: is this always right?
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, SixelImageChunk};
use crate::panes::alacritty_functions::{parse_number, xparse_color};
use crate::panes::link_handler::LinkHandler;
use crate::panes::search::SearchResult;
use crate::panes::selection::Selection;
use crate::panes::terminal_character::{
AnsiCode, CharsetIndex, Cursor, CursorShape, RcCharacterStyles, StandardCharset,
TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
};
use crate::ui::components::UiComponentParser;
fn get_top_non_canonical_rows(rows: &mut Vec<Row>) -> Vec<Row> {
let mut index_of_last_non_canonical_row = None;
for (i, row) in rows.iter().enumerate() {
if row.is_canonical {
break;
} else {
index_of_last_non_canonical_row = Some(i);
}
}
match index_of_last_non_canonical_row {
Some(index_of_last_non_canonical_row) => {
rows.drain(..=index_of_last_non_canonical_row).collect()
},
None => vec![],
}
}
fn get_lines_above_bottom_canonical_row_and_wraps(rows: &mut VecDeque<Row>) -> Vec<Row> {
let mut index_of_last_non_canonical_row = None;
for (i, row) in rows.iter().enumerate().rev() {
index_of_last_non_canonical_row = Some(i);
if row.is_canonical {
break;
}
}
match index_of_last_non_canonical_row {
Some(index_of_last_non_canonical_row) => {
rows.drain(index_of_last_non_canonical_row..).collect()
},
None => vec![],
}
}
fn get_viewport_bottom_canonical_row_and_wraps(viewport: &mut Vec<Row>) -> Vec<Row> {
let mut index_of_last_non_canonical_row = None;
for (i, row) in viewport.iter().enumerate().rev() {
index_of_last_non_canonical_row = Some(i);
if row.is_canonical {
break;
}
}
match index_of_last_non_canonical_row {
Some(index_of_last_non_canonical_row) => {
viewport.drain(index_of_last_non_canonical_row..).collect()
},
None => vec![],
}
}
fn get_top_canonical_row_and_wraps(rows: &mut Vec<Row>) -> Vec<Row> {
let mut index_of_first_non_canonical_row = None;
let mut end_index_of_first_canonical_line = None;
for (i, row) in rows.iter().enumerate() {
if row.is_canonical && end_index_of_first_canonical_line.is_none() {
index_of_first_non_canonical_row = Some(i);
end_index_of_first_canonical_line = Some(i);
continue;
}
if row.is_canonical && end_index_of_first_canonical_line.is_some() {
break;
}
if index_of_first_non_canonical_row.is_some() {
end_index_of_first_canonical_line = Some(i);
continue;
}
}
match (
index_of_first_non_canonical_row,
end_index_of_first_canonical_line,
) {
(Some(first_index), Some(last_index)) => rows.drain(first_index..=last_index).collect(),
(Some(first_index), None) => rows.drain(first_index..).collect(),
_ => vec![],
}
}
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 {
let mut next_lines: Vec<Row> = vec![];
let mut lines_added_to_viewport: isize = 0;
loop {
if lines_added_to_viewport as usize == count {
break;
}
if next_lines.is_empty() {
match lines_above.pop_back() {
Some(next_line) => {
let mut top_non_canonical_rows_in_dst = get_top_non_canonical_rows(viewport);
lines_added_to_viewport -= top_non_canonical_rows_in_dst.len() as isize;
next_lines.push(next_line);
next_lines.append(&mut top_non_canonical_rows_in_dst);
next_lines =
Row::from_rows(next_lines).split_to_rows_of_length(max_viewport_width);
if next_lines.is_empty() {
// no more lines at lines_above, the line we popped was probably empty
break;
}
},
None => break, // no more rows
}
}
viewport.insert(0, next_lines.pop().unwrap());
lines_added_to_viewport += 1;
}
if !next_lines.is_empty() {
let excess_row = Row::from_rows(next_lines);
bounded_push(lines_above, sixel_grid, excess_row);
}
match usize::try_from(lines_added_to_viewport) {
Ok(n) => n,
_ => 0,
}
}
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 {
let mut transferred_rows_count: isize = 0;
let drained_lines = std::cmp::min(count, viewport.len());
for next_line in viewport.drain(..drained_lines) {
let mut next_lines: Vec<Row> = vec![];
transferred_rows_count +=
calculate_row_display_height(next_line.width(), max_viewport_width) as isize;
if !next_line.is_canonical {
let mut bottom_canonical_row_and_wraps_in_dst =
get_lines_above_bottom_canonical_row_and_wraps(lines_above);
next_lines.append(&mut bottom_canonical_row_and_wraps_in_dst);
}
next_lines.push(next_line);
let dropped_line_width = bounded_push(lines_above, sixel_grid, Row::from_rows(next_lines));
if let Some(width) = dropped_line_width {
transferred_rows_count -=
calculate_row_display_height(width, max_viewport_width) as isize;
}
}
transferred_rows_count
}
fn transfer_rows_from_lines_below_to_viewport(
lines_below: &mut Vec<Row>,
viewport: &mut Vec<Row>,
count: usize,
max_viewport_width: usize,
) {
let mut next_lines: Vec<Row> = vec![];
for _ in 0..count {
let mut lines_pulled_from_viewport = 0;
if next_lines.is_empty() {
if !lines_below.is_empty() {
let mut top_non_canonical_rows_in_lines_below =
get_top_non_canonical_rows(lines_below);
if !top_non_canonical_rows_in_lines_below.is_empty() {
let mut canonical_line = get_viewport_bottom_canonical_row_and_wraps(viewport);
lines_pulled_from_viewport += canonical_line.len();
canonical_line.append(&mut top_non_canonical_rows_in_lines_below);
next_lines =
Row::from_rows(canonical_line).split_to_rows_of_length(max_viewport_width);
} else {
let canonical_row = get_top_canonical_row_and_wraps(lines_below);
next_lines =
Row::from_rows(canonical_row).split_to_rows_of_length(max_viewport_width);
}
} else {
break; // no more rows
}
}
for _ in 0..(lines_pulled_from_viewport + 1) {
if !next_lines.is_empty() {
viewport.push(next_lines.remove(0));
}
}
}
if !next_lines.is_empty() {
let excess_row = Row::from_rows(next_lines);
lines_below.insert(0, excess_row);
}
}
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());
}
}
vec.push_back(value);
dropped_line_width
}
pub fn create_horizontal_tabstops(columns: usize) -> BTreeSet<usize> {
let mut i = TABSTOP_WIDTH;
let mut horizontal_tabstops = BTreeSet::new();
loop {
if i > columns {
break;
}
horizontal_tabstops.insert(i);
i += TABSTOP_WIDTH;
}
horizontal_tabstops
}
fn calculate_row_display_height(row_width: usize, viewport_width: usize) -> usize {
if row_width <= viewport_width {
return 1;
}
(row_width as f64 / viewport_width as f64).ceil() as usize
}
fn subtract_isize_from_usize(u: usize, i: isize) -> usize {
if i.is_negative() {
u - i.abs() as usize
} else {
u + i as usize
}
}
macro_rules! dump_screen {
($lines:expr) => {{
let mut is_first = true;
let mut buf = "".to_owned();
for line in &$lines {
if line.is_canonical && !is_first {
buf.push_str("\n");
}
let s: String = (&line.columns).into_iter().map(|x| x.character).collect();
// Replace the spaces at the end of the line. Sometimes, the lines are
// collected with spaces until the end of the panel.
let re = Regex::new("([^ ])[ ]*$").unwrap();
buf.push_str(&(re.replace(&s, "${1}")));
is_first = false;
}
buf
}};
}
fn utf8_mouse_coordinates(column: usize, line: isize) -> Vec<u8> {
let mut coordinates = vec![];
let mouse_pos_encode = |pos: usize| -> Vec<u8> {
let pos = 32 + pos;
let first = 0xC0 + pos / 64;
let second = 0x80 + (pos & 63);
vec![first as u8, second as u8]
};
if column > 95 {
coordinates.append(&mut mouse_pos_encode(column));
} else {
coordinates.push(32 + column as u8);
}
if line > 95 {
coordinates.append(&mut mouse_pos_encode(line as usize));
} else {
coordinates.push(32 + line as u8);
}
coordinates
}
#[derive(Clone)]
pub struct Grid {
pub(crate) lines_above: VecDeque<Row>,
pub(crate) viewport: Vec<Row>,
pub(crate) lines_below: Vec<Row>,
horizontal_tabstops: BTreeSet<usize>,
alternate_screen_state: Option<AlternateScreenState>,
cursor: Cursor,
cursor_is_hidden: bool,
saved_cursor_position: Option<Cursor>,
// FIXME: change scroll_region to be (usize, usize) - where the top line is always the first
// line of the viewport and the bottom line the last unless it's changed with CSI r and friends
// Having it as an Option causes lots of complexity and issues, and according to DECSTBM, this
// should be the behaviour anyway
scroll_region: Option<(usize, usize)>,
active_charset: CharsetIndex,
preceding_char: Option<TerminalCharacter>,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
pub(crate) 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 lock_renders: 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 new_line_mode: bool, // Automatic newline LNM
pub clear_viewport_before_rendering: bool,
pub width: usize,
pub height: usize,
pub pending_messages_to_pty: Vec<Vec<u8>>,
pub selection: Selection,
pub title: Option<String>,
pub is_scrolled: bool,
pub link_handler: Rc<RefCell<LinkHandler>>,
pub ring_bell: bool,
scrollback_buffer_lines: usize,
pub mouse_mode: MouseMode,
pub mouse_tracking: MouseTracking,
pub focus_event_tracking: bool,
pub search_results: SearchResult,
pub pending_clipboard_update: Option<String>,
ui_component_bytes: Option<Vec<u8>>,
style: Style,
debug: bool,
arrow_fonts: bool,
styled_underlines: bool,
pub supports_kitty_keyboard_protocol: bool, // has the app requested kitty keyboard support?
explicitly_disable_kitty_keyboard_protocol: bool, // has kitty keyboard support been explicitly
// disabled by user config?
}
#[derive(Clone, Debug)]
pub enum MouseMode {
NoEncoding,
Utf8,
Sgr,
}
impl Default for MouseMode {
fn default() -> Self {
MouseMode::NoEncoding
}
}
#[derive(Clone, Debug)]
pub enum MouseTracking {
Off,
Normal,
ButtonEventTracking,
}
impl Default for MouseTracking {
fn default() -> Self {
MouseTracking::Off
}
}
impl Debug for Grid {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut buffer: Vec<Row> = self.viewport.clone();
// pad buffer
for _ in buffer.len()..self.height {
buffer.push(Row::new().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::new_singlewidth(sixel_indication_character(x));
row.add_character_at(fake_sixel_terminal_character, x);
}
}
}
// display terminal characters with stripped styles
for (i, row) in buffer.iter().enumerate() {
let mut cow_row = Cow::Borrowed(row);
self.search_results
.mark_search_results_in_row(&mut cow_row, i);
if row.is_canonical {
writeln!(f, "{:02?} (C): {:?}", i, cow_row)?;
} else {
writeln!(f, "{:02?} (W): {:?}", i, cow_row)?;
}
}
Ok(())
}
}
impl Grid {
pub fn new(
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>>,
style: Style, // TODO: consolidate this with terminal_emulator_colors
debug: bool,
arrow_fonts: bool,
styled_underlines: bool,
explicitly_disable_kitty_keyboard_protocol: bool,
) -> Self {
let sixel_grid = SixelGrid::new(character_cell_size.clone(), sixel_image_store);
// make sure this is initialized as it is used internally
// if it was already initialized (which should happen normally unless this is a test or
// something changed since this comment was written), we get an Error which we ignore
// I don't know why this needs to be a OneCell, but whatevs
let _ = SCROLL_BUFFER_SIZE.set(DEFAULT_SCROLL_BUFFER_SIZE);
Grid {
lines_above: VecDeque::new(),
viewport: vec![Row::new().canonical()],
lines_below: vec![],
horizontal_tabstops: create_horizontal_tabstops(columns),
cursor: Cursor::new(0, 0, styled_underlines),
cursor_is_hidden: false,
saved_cursor_position: None,
scroll_region: None,
preceding_char: None,
width: columns,
height: rows,
should_render: true,
cursor_key_mode: false,
bracketed_paste_mode: false,
erasure_mode: false,
sixel_scrolling: false,
insert_mode: false,
disable_linewrap: false,
new_line_mode: false,
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![],
title: None,
changed_colors: None,
is_scrolled: false,
link_handler,
ring_bell: false,
scrollback_buffer_lines: 0,
mouse_mode: MouseMode::default(),
mouse_tracking: MouseTracking::default(),
focus_event_tracking: false,
character_cell_size,
search_results: Default::default(),
sixel_grid,
pending_clipboard_update: None,
ui_component_bytes: None,
style,
debug,
arrow_fonts,
styled_underlines,
lock_renders: false,
supports_kitty_keyboard_protocol: false,
explicitly_disable_kitty_keyboard_protocol,
}
}
pub fn render_full_viewport(&mut self) {
self.output_buffer.update_all_lines();
}
pub fn update_line_for_rendering(&mut self, line_index: usize) {
self.output_buffer.update_line(line_index);
}
pub fn advance_to_next_tabstop(&mut self, styles: RcCharacterStyles) {
let next_tabstop = self
.horizontal_tabstops
.iter()
.copied()
.find(|&tabstop| tabstop > self.cursor.x && tabstop < self.width);
match next_tabstop {
Some(tabstop) => {
self.cursor.x = tabstop;
},
None => {
self.cursor.x = self.width.saturating_sub(1);
},
}
let mut empty_character = EMPTY_TERMINAL_CHARACTER;
empty_character.styles = styles;
self.pad_current_line_until(self.cursor.x, empty_character);
self.output_buffer.update_line(self.cursor.y);
}
pub fn move_to_previous_tabstop(&mut self) {
let previous_tabstop = self
.horizontal_tabstops
.iter()
.rev()
.copied()
.find(|&tabstop| tabstop < self.cursor.x);
match previous_tabstop {
Some(tabstop) => {
self.cursor.x = tabstop;
},
None => {
self.cursor.x = 0;
},
}
}
pub fn cursor_shape(&self) -> CursorShape {
self.cursor.get_shape()
}
pub fn scrollback_position_and_length(&self) -> (usize, usize) {
// (position, length)
(
self.lines_below.len(),
(self.scrollback_buffer_lines + self.lines_below.len()),
)
}
fn recalculate_scrollback_buffer_count(&self) -> usize {
let mut scrollback_buffer_count = 0;
for row in &self.lines_above {
let row_width = row.width();
// rows in lines_above are unwrapped, so we need to account for that
if row_width > self.width {
scrollback_buffer_count += calculate_row_display_height(row_width, self.width);
} else {
scrollback_buffer_count += 1;
}
}
scrollback_buffer_count
}
fn set_horizontal_tabstop(&mut self) {
self.horizontal_tabstops.insert(self.cursor.x);
}
fn clear_tabstop(&mut self, position: usize) {
self.horizontal_tabstops.remove(&position);
}
fn clear_all_tabstops(&mut self) {
self.horizontal_tabstops.clear();
}
fn save_cursor_position(&mut self) {
self.saved_cursor_position = Some(self.cursor.clone());
}
fn restore_cursor_position(&mut self) {
if let Some(saved_cursor_position) = &self.saved_cursor_position {
self.cursor = saved_cursor_position.clone();
}
}
fn configure_charset(&mut self, charset: StandardCharset, index: CharsetIndex) {
self.cursor.charsets[index] = charset;
}
fn set_active_charset(&mut self, index: CharsetIndex) {
self.active_charset = index;
}
fn cursor_canonical_line_index(&self) -> usize {
let mut cursor_canonical_line_index = 0;
let mut canonical_lines_traversed = 0;
for (i, line) in self.viewport.iter().enumerate() {
if line.is_canonical {
cursor_canonical_line_index = canonical_lines_traversed;
canonical_lines_traversed += 1;
}
if i == self.cursor.y {
break;
}
}
cursor_canonical_line_index
}
// TODO: merge these two functions
fn cursor_index_in_canonical_line(&self) -> usize {
let mut cursor_canonical_line_index = 0;
let mut cursor_index_in_canonical_line = 0;
for (i, line) in self.viewport.iter().enumerate() {
if line.is_canonical {
cursor_canonical_line_index = i;
}
if i == self.cursor.y {
let line_wraps = self.cursor.y.saturating_sub(cursor_canonical_line_index);
cursor_index_in_canonical_line = (line_wraps * self.width) + self.cursor.x;
break;
}
}
cursor_index_in_canonical_line
}
fn saved_cursor_index_in_canonical_line(&self) -> Option<usize> {
if let Some(saved_cursor_position) = self.saved_cursor_position.as_ref() {
let mut cursor_canonical_line_index = 0;
let mut cursor_index_in_canonical_line = 0;
for (i, line) in self.viewport.iter().enumerate() {
if line.is_canonical {
cursor_canonical_line_index = i;
}
if i == saved_cursor_position.y {
let line_wraps = saved_cursor_position.y - cursor_canonical_line_index;
cursor_index_in_canonical_line =
(line_wraps * self.width) + saved_cursor_position.x;
break;
}
}
Some(cursor_index_in_canonical_line)
} else {
None
}
}
fn canonical_line_y_coordinates(&self, canonical_line_index: usize) -> usize {
let mut canonical_lines_traversed = 0;
let mut y_coordinates = 0;
for (i, line) in self.viewport.iter().enumerate() {
if line.is_canonical {
canonical_lines_traversed += 1;
y_coordinates = i;
if canonical_lines_traversed == canonical_line_index + 1 {
break;
}
}
}
y_coordinates
}
pub fn scroll_up_one_line(&mut self) -> bool {
let mut found_something = false;
if !self.lines_above.is_empty() && self.viewport.len() == self.height {
self.is_scrolled = true;
let line_to_push_down = self.viewport.pop().unwrap();
self.lines_below.insert(0, line_to_push_down);
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,
);
self.scrollback_buffer_lines = self
.scrollback_buffer_lines
.saturating_sub(transferred_rows_height);
self.selection.move_down(1);
// Move all search-selections down one line as well
found_something = self
.search_results
.move_down(1, &self.viewport, self.height);
}
self.output_buffer.update_all_lines();
found_something
}
pub fn scroll_down_one_line(&mut self) -> bool {
let mut found_something = false;
if !self.lines_below.is_empty() && self.viewport.len() == self.height {
let mut line_to_push_up = self.viewport.remove(0);
self.scrollback_buffer_lines +=
calculate_row_display_height(line_to_push_up.width(), self.width);
let line_to_push_up = if line_to_push_up.is_canonical {
line_to_push_up
} else {
let mut last_line_above = self.lines_above.pop_back().unwrap();
last_line_above.append(&mut line_to_push_up.columns);
last_line_above
};
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);
self.scrollback_buffer_lines = self
.scrollback_buffer_lines
.saturating_sub(dropped_line_height);
}
transfer_rows_from_lines_below_to_viewport(
&mut self.lines_below,
&mut self.viewport,
1,
self.width,
);
self.selection.move_up(1);
// Move all search-selections up one line as well
found_something =
self.search_results
.move_up(1, &self.viewport, &self.lines_below, self.height);
self.output_buffer.update_all_lines();
}
if self.lines_below.is_empty() {
self.is_scrolled = false;
}
found_something
}
pub fn force_change_size(&mut self, new_rows: usize, new_columns: usize) {
// this is an ugly hack - it's here because sometimes we need to change_size to the
// existing size (eg. when resizing an alternative_grid to the current height/width) and
// the change_size method is a no-op in that case. Should be fixed by making the
// change_size method atomic
let intermediate_rows = if new_rows == self.height {
new_rows + 1
} else {
new_rows
};
let intermediate_columns = if new_columns == self.width {
new_columns + 1
} else {
new_columns
};
self.change_size(intermediate_rows, intermediate_columns);
self.change_size(new_rows, new_columns);
}
pub fn change_size(&mut self, new_rows: usize, new_columns: usize) {
// Do nothing if this pane hasn't been given a proper size yet
if new_columns == 0 || new_rows == 0 {
return;
}
if self.alternate_screen_state.is_some() {
// in alternate screen we do nothing but log the new size, the program in the terminal
// is in control now...
self.height = new_rows;
self.width = new_columns;
return;
}
self.selection.reset();
self.sixel_grid.character_cell_size_possibly_changed();
let cursors = if new_columns != self.width {
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();
let saved_cursor_index_in_canonical_line = self.saved_cursor_index_in_canonical_line();
let mut viewport_canonical_lines = vec![];
for mut row in self.viewport.drain(..) {
if !row.is_canonical
&& viewport_canonical_lines.is_empty()
&& !self.lines_above.is_empty()
{
let mut first_line_above = self.lines_above.pop_back().unwrap();
first_line_above.append(&mut row.columns);
viewport_canonical_lines.push(first_line_above);
cursor_canonical_line_index += 1;
} else if row.is_canonical {
viewport_canonical_lines.push(row);
} else {
match viewport_canonical_lines.last_mut() {
Some(last_line) => {
last_line.append(&mut row.columns);
},
None => {
// the state is corrupted somehow
// this is a bug and I'm not yet sure why it happens
// usually it fixes itself and is a result of some race
// TODO: investigate why this happens and solve it
return;
},
}
}
}
// trim lines after the last empty space that has no following character, because
// terminals don't trim empty lines
for line in &mut viewport_canonical_lines {
let mut trim_at = None;
for (index, character) in line.columns.iter().enumerate() {
if character.character != EMPTY_TERMINAL_CHARACTER.character {
trim_at = None;
} else if trim_at.is_none() {
trim_at = Some(index);
}
}
if let Some(trim_at) = trim_at {
let excess_width_until_trim_at = line.excess_width_until(trim_at);
line.truncate(trim_at + excess_width_until_trim_at);
}
}
let mut new_viewport_rows = vec![];
for mut canonical_line in viewport_canonical_lines {
let mut canonical_line_parts: Vec<Row> = vec![];
if canonical_line.columns.is_empty() {
canonical_line_parts.push(Row::new().canonical());
}
while !canonical_line.columns.is_empty() {
let next_wrap = canonical_line.drain_until(new_columns);
// If the next character is wider than the grid (i.e. there is nothing in
// `next_wrap`, then just abort the resizing
if next_wrap.is_empty() {
break;
}
let row = Row::from_columns(next_wrap);
// if there are no more parts, this row is canonical as long as it originally
// was canonical (it might not have been for example if it's the first row in
// the viewport, and the actual canonical row is above it in the scrollback)
let row = if canonical_line_parts.is_empty() && canonical_line.is_canonical {
row.canonical()
} else {
row
};
canonical_line_parts.push(row);
}
new_viewport_rows.append(&mut canonical_line_parts);
}
self.viewport = new_viewport_rows;
let mut new_cursor_y = self.canonical_line_y_coordinates(cursor_canonical_line_index)
+ (cursor_index_in_canonical_line / new_columns);
let mut saved_cursor_y_coordinates =
self.saved_cursor_position.as_ref().map(|saved_cursor| {
self.canonical_line_y_coordinates(saved_cursor.y)
+ saved_cursor_index_in_canonical_line.as_ref().unwrap() / new_columns
});
// A cursor at EOL has two equivalent positions - end of this line or beginning of
// next. If not already at the beginning of line, bias to EOL so add character logic
// doesn't create spurious canonical lines
let mut new_cursor_x = cursor_index_in_canonical_line % new_columns;
if self.cursor.x != 0 && new_cursor_x == 0 {
new_cursor_y = new_cursor_y.saturating_sub(1);
new_cursor_x = new_columns
}
let saved_cursor_x_coordinates = match (
saved_cursor_index_in_canonical_line.as_ref(),
self.saved_cursor_position.as_mut(),
saved_cursor_y_coordinates.as_mut(),
) {
(
Some(saved_cursor_index_in_canonical_line),
Some(saved_cursor_position),
Some(saved_cursor_y_coordinates),
) => {
let x = saved_cursor_position.x;
let mut new_x = *saved_cursor_index_in_canonical_line % new_columns;
let new_y = saved_cursor_y_coordinates;
if x != 0 && new_x == 0 {
*new_y = new_y.saturating_sub(1);
new_x = new_columns
}
Some(new_x)
},
_ => None,
};
Some((
new_cursor_y,
saved_cursor_y_coordinates,
new_cursor_x,
saved_cursor_x_coordinates,
))
} else if new_rows != self.height {
let saved_cursor_y_coordinates = self
.saved_cursor_position
.as_ref()
.map(|saved_cursor| saved_cursor.y);
let saved_cursor_x_coordinates = self
.saved_cursor_position
.as_ref()
.map(|saved_cursor| saved_cursor.x);
Some((
self.cursor.y,
saved_cursor_y_coordinates,
self.cursor.x,
saved_cursor_x_coordinates,
))
} else {
None
};
if let Some(cursors) = cursors {
// At this point the x coordinates have been calculated, the y coordinates
// will be updated within this block
let (
mut new_cursor_y,
mut saved_cursor_y_coordinates,
new_cursor_x,
saved_cursor_x_coordinates,
) = cursors;
let current_viewport_row_count = self.viewport.len();
match current_viewport_row_count.cmp(&new_rows) {
Ordering::Less => {
let row_count_to_transfer = new_rows - current_viewport_row_count;
transfer_rows_from_lines_above_to_viewport(
&mut self.lines_above,
&mut self.viewport,
&mut self.sixel_grid,
row_count_to_transfer,
new_columns,
);
let rows_pulled = self.viewport.len() - current_viewport_row_count;
new_cursor_y += rows_pulled;
if let Some(saved_cursor_y_coordinates) = saved_cursor_y_coordinates.as_mut() {
*saved_cursor_y_coordinates += rows_pulled;
};
},
Ordering::Greater => {
let row_count_to_transfer = current_viewport_row_count - new_rows;
if row_count_to_transfer > new_cursor_y {
new_cursor_y = 0;
} else {
new_cursor_y -= row_count_to_transfer;
}
if let Some(saved_cursor_y_coordinates) = saved_cursor_y_coordinates.as_mut() {
if row_count_to_transfer > *saved_cursor_y_coordinates {
*saved_cursor_y_coordinates = 0;
} else {
*saved_cursor_y_coordinates -= row_count_to_transfer;
}
}
transfer_rows_from_viewport_to_lines_above(
&mut self.viewport,
&mut self.lines_above,
&mut self.sixel_grid,
row_count_to_transfer,
new_columns,
);
},
Ordering::Equal => {},
}
self.cursor.y = new_cursor_y;
self.cursor.x = new_cursor_x;
if let Some(saved_cursor_position) = self.saved_cursor_position.as_mut() {
match (saved_cursor_x_coordinates, saved_cursor_y_coordinates) {
(Some(saved_cursor_x_coordinates), Some(saved_cursor_y_coordinates)) => {
saved_cursor_position.x = saved_cursor_x_coordinates;
saved_cursor_position.y = saved_cursor_y_coordinates;
},
_ => log::error!(
"invalid state - cannot set saved cursor to {:?} {:?}",
saved_cursor_x_coordinates,
saved_cursor_y_coordinates
),
}
};
}
self.height = new_rows;
self.width = new_columns;
if self.scroll_region.is_some() {
self.set_scroll_region_to_viewport_size();
}
self.scrollback_buffer_lines = self.recalculate_scrollback_buffer_count();
self.search_results.selections.clear();
self.search_viewport();
// If we have thrown out the active element, set it to None
self.search_results.unset_active_selection_if_nonexistent();
self.output_buffer.update_all_lines();
}
pub fn as_character_lines(&self) -> Vec<Vec<TerminalCharacter>> {
// this is only used in the tests
// it's not part of testing the app, but rather is used to interpret the snapshots created
// by it
let mut lines: Vec<Vec<TerminalCharacter>> = self
.viewport
.iter()
.map(|r| {
let excess_width = r.excess_width();
let mut line: Vec<TerminalCharacter> = r.columns.iter().cloned().collect();
// pad line
line.resize(
self.width.saturating_sub(excess_width),
EMPTY_TERMINAL_CHARACTER,
);
line
})
.collect();
let empty_row = vec![EMPTY_TERMINAL_CHARACTER; self.width];
for _ in lines.len()..self.height {
lines.push(empty_row.clone());
}
lines
}
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();
(changed_character_chunks, changed_sixel_image_chunks)
}
pub fn serialize(&self, scrollback_lines_to_serialize: Option<usize>) -> Option<String> {
match scrollback_lines_to_serialize {
Some(scrollback_lines_to_serialize) => {
let first_index = if scrollback_lines_to_serialize == 0 {
0
} else {
self.lines_above
.len()
.saturating_sub(scrollback_lines_to_serialize)
};
let mut to_serialize = vec![];
for line in self.lines_above.iter().skip(first_index) {
to_serialize.push(line.clone());
}
for line in &self.viewport {
to_serialize.push(line.clone())
}
self.output_buffer.serialize(to_serialize.as_slice()).ok()
},
None => self.output_buffer.serialize(&self.viewport).ok(),
}
}
pub fn render(
&mut self,
content_x: usize,
content_y: usize,
style: &Style,
) -> Result<Option<(Vec<CharacterChunk>, Option<String>, Vec<SixelImageChunk>)>> {
if self.lock_renders {
return Ok(None);
}
let mut raw_vte_output = String::new();
let (mut character_chunks, sixel_image_chunks) = self.read_changes(content_x, content_y);
for character_chunk in character_chunks.iter_mut() {
character_chunk.add_changed_colors(self.changed_colors);
if self
.selection
.contains_row(character_chunk.y.saturating_sub(content_y))
{
let background_color = match style.colors.bg {
PaletteColor::Rgb(rgb) => AnsiCode::RgbCode(rgb),
PaletteColor::EightBit(col) => AnsiCode::ColorIndex(col),
};
character_chunk.add_selection_and_colors(
self.selection,
background_color,
None,
content_x,
content_y,
);
} else if !self.search_results.selections.is_empty() {
for res in self.search_results.selections.iter() {
if res.contains_row(character_chunk.y.saturating_sub(content_y)) {
let (select_background_palette, select_foreground_palette) =
if Some(res) == self.search_results.active.as_ref() {
(style.colors.orange, style.colors.black)
} else {
(style.colors.green, style.colors.black)
};
let background_color = match select_background_palette {
PaletteColor::Rgb(rgb) => AnsiCode::RgbCode(rgb),
PaletteColor::EightBit(col) => AnsiCode::ColorIndex(col),
};
let foreground_color = match select_foreground_palette {
PaletteColor::Rgb(rgb) => AnsiCode::RgbCode(rgb),
PaletteColor::EightBit(col) => AnsiCode::ColorIndex(col),
};
character_chunk.add_selection_and_colors(
*res,
background_color,
Some(foreground_color),
content_x,
content_y,
);
}
}
}
}
if self.ring_bell {
let ring_bell = '\u{7}';
raw_vte_output.push(ring_bell);
self.ring_bell = false;
}
return Ok(Some((
character_chunks,
Some(raw_vte_output),
sixel_image_chunks,
)));
}
pub fn cursor_coordinates(&self) -> Option<(usize, usize)> {
if self.cursor_is_hidden {
None
} else {
Some((self.cursor.x, self.cursor.y))
}
}
pub fn is_mid_frame(&self) -> bool {
self.lock_renders
}
/// Clears all buffers with text for a current screen
pub fn clear_screen(&mut self) {
if self.alternate_screen_state.is_some() {
log::warn!("Tried to clear pane with alternate_screen_state");
return;
}
self.reset_terminal_state();
self.mark_for_rerender();
}
/// Dumps all lines above terminal vieport and the viewport itself to a string
pub fn dump_screen(&mut self, full: bool) -> String {
let viewport: String = dump_screen!(self.viewport);
if !full {
return viewport;
}
let mut scrollback: String = dump_screen!(self.lines_above);
if !scrollback.is_empty() {
scrollback.push('\n');
}
scrollback.push_str(&viewport);
scrollback
}
pub fn move_viewport_up(&mut self, count: usize) {
for _ in 0..count {
self.scroll_up_one_line();
}
self.output_buffer.update_all_lines();
}
pub fn move_viewport_down(&mut self, count: usize) {
for _ in 0..count {
self.scroll_down_one_line();
}
self.output_buffer.update_all_lines();
}
pub fn reset_viewport(&mut self) {
let max_lines_to_scroll = *SCROLL_BUFFER_SIZE.get().unwrap() * 2; // while not very elegant, this can prevent minor bugs from becoming showstoppers by sticking the whole app display in an endless loop
let mut lines_scrolled = 0;
let should_clear_output_buffer = self.is_scrolled;
while self.is_scrolled && lines_scrolled < max_lines_to_scroll {
self.scroll_down_one_line();
lines_scrolled += 1;
}
if should_clear_output_buffer {
self.output_buffer.update_all_lines();
}
}
pub fn rotate_scroll_region_up(&mut self, count: usize) {
if let Some((scroll_region_top, scroll_region_bottom)) = self
.scroll_region
.or_else(|| Some((0, self.height.saturating_sub(1))))
{
self.pad_lines_until(scroll_region_bottom, EMPTY_TERMINAL_CHARACTER);
for _ in 0..count {
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
if self.viewport.get(scroll_region_bottom).is_some() {
self.viewport.remove(scroll_region_bottom);
}
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
let columns = VecDeque::from(vec![pad_character; self.width]);
self.viewport
.insert(scroll_region_top, Row::from_columns(columns).canonical());
}
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
}
}
pub fn rotate_scroll_region_down(&mut self, count: usize) {
if let Some((scroll_region_top, scroll_region_bottom)) = self
.scroll_region
.or_else(|| Some((0, self.height.saturating_sub(1))))
{
self.pad_lines_until(scroll_region_bottom, EMPTY_TERMINAL_CHARACTER);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
for _ in 0..count {
self.viewport.remove(scroll_region_top);
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
}
}
pub fn fill_viewport(&mut self, character: TerminalCharacter) {
if self.alternate_screen_state.is_some() {
self.viewport.clear();
} else {
self.transfer_rows_to_lines_above(self.viewport.len())
};
for _ in 0..self.height {
let columns = VecDeque::from(vec![character.clone(); self.width]);
self.viewport.push(Row::from_columns(columns).canonical());
}
self.output_buffer.update_all_lines();
}
pub fn add_canonical_line(&mut self) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
if self.cursor.y == scroll_region_bottom {
// end of scroll region
// when we have a scroll region set and we're at its bottom
// we need to delete its first line, thus shifting all lines in it upwards
// then we add an empty line at its end which will be filled by the application
// controlling the scroll region (presumably filled by whatever comes next in the
// scroll buffer, but that's not something we control)
if scroll_region_top >= self.viewport.len() {
// the state is corrupted
return;
}
if scroll_region_bottom == self.height - 1 && scroll_region_top == 0 {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
self.viewport.remove(0);
}
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
let columns = VecDeque::from(vec![pad_character; self.width]);
self.viewport.push(Row::from_columns(columns).canonical());
self.selection.move_up(1);
} else {
self.viewport.remove(scroll_region_top);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
let columns = VecDeque::from(vec![pad_character; self.width]);
if self.viewport.len() >= scroll_region_bottom {
self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
} else {
self.viewport.push(Row::from_columns(columns).canonical());
}
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
return;
}
}
if self.viewport.len() <= self.cursor.y + 1 {
// FIXME: this should add an empty line with the pad_character
// but for some reason this breaks rendering in various situations
// it needs to be investigated and fixed
let new_row = Row::new().canonical();
self.viewport.push(new_row);
}
if self.cursor.y == self.height.saturating_sub(1) {
if self.scroll_region.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);
}
self.selection.move_up(1);
}
self.output_buffer.update_all_lines();
} else {
self.cursor.y += 1;
self.output_buffer.update_line(self.cursor.y);
}
}
pub fn move_cursor_to_beginning_of_line(&mut self) {
self.cursor.x = 0;
}
pub fn add_character_at_cursor_position(
&mut self,
terminal_character: TerminalCharacter,
should_insert_character: bool,
) {
// this function assumes the current line has enough room for terminal_character (that its
// width has been checked beforehand)
match self.viewport.get_mut(self.cursor.y) {
Some(row) => {
if self.insert_mode || should_insert_character {
row.insert_character_at(terminal_character, self.cursor.x);
if row.width() > self.width {
row.truncate(self.width);
}
} 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 => {
// pad lines until cursor if they do not exist
for _ in self.viewport.len()..self.cursor.y {
self.viewport.push(Row::new().canonical());
}
self.viewport
.push(Row::new().with_character(terminal_character).canonical());
self.output_buffer.update_line(self.cursor.y);
},
}
}
pub fn add_character(&mut self, terminal_character: TerminalCharacter) {
let character_width = terminal_character.width();
// Drop zero-width Unicode/UTF-8 codepoints, like for example Variation Selectors.
// This breaks unicode grapheme segmentation, and is the reason why some characters
// aren't displayed correctly. Refer to this issue for more information:
// https://github.com/zellij-org/zellij/issues/1538
if character_width == 0 {
return;
}
if self.cursor.x + character_width > self.width {
if self.disable_linewrap {
return;
}
self.line_wrap();
}
self.add_character_at_cursor_position(terminal_character, false);
self.move_cursor_forward_until_edge(character_width);
}
pub fn get_character_under_cursor(&self) -> Option<TerminalCharacter> {
let absolute_x_in_line = self.get_absolute_character_index(self.cursor.x, self.cursor.y);
self.viewport
.get(self.cursor.y)
.and_then(|current_line| current_line.columns.get(absolute_x_in_line))
.cloned()
}
pub fn get_absolute_character_index(&self, x: usize, y: usize) -> usize {
self.viewport.get(y).unwrap().absolute_character_index(x)
}
pub fn move_cursor_forward_until_edge(&mut self, count: usize) {
let count_to_move = std::cmp::min(count, self.width.saturating_sub(self.cursor.x));
self.cursor.x += count_to_move;
}
pub fn replace_characters_in_line_after_cursor(&mut self, replace_with: TerminalCharacter) {
if let Some(row) = self.viewport.get_mut(self.cursor.y) {
row.replace_and_pad_end(self.cursor.x, self.width, replace_with);
}
self.output_buffer.update_line(self.cursor.y);
}
pub fn replace_characters_in_line_before_cursor(&mut self, replace_with: TerminalCharacter) {
let row = self.viewport.get_mut(self.cursor.y).unwrap();
row.replace_and_pad_beginning(self.cursor.x, replace_with);
self.output_buffer.update_line(self.cursor.y);
}
pub fn clear_all_after_cursor(&mut self, replace_with: TerminalCharacter) {
if let Some(cursor_row) = self.viewport.get_mut(self.cursor.y) {
cursor_row.truncate(self.cursor.x);
let replace_with_columns = VecDeque::from(vec![replace_with.clone(); self.width]);
self.replace_characters_in_line_after_cursor(replace_with);
for row in self.viewport.iter_mut().skip(self.cursor.y + 1) {
row.replace_columns(replace_with_columns.clone());
}
self.output_buffer.update_all_lines(); // TODO: only update the changed lines
}
}
pub fn clear_all_before_cursor(&mut self, replace_with: TerminalCharacter) {
if self.viewport.get(self.cursor.y).is_some() {
let replace_with_columns = VecDeque::from(vec![replace_with.clone(); self.width]);
self.replace_characters_in_line_before_cursor(replace_with);
for row in self.viewport.iter_mut().take(self.cursor.y) {
row.replace_columns(replace_with_columns.clone());
}
self.output_buffer.update_all_lines(); // TODO: only update the changed lines
}
}
pub fn clear_cursor_line(&mut self) {
if let Some(viewport_line) = self.viewport.get_mut(self.cursor.y) {
viewport_line.truncate(0);
self.output_buffer.update_line(self.cursor.y);
}
}
pub fn clear_all(&mut self, replace_with: TerminalCharacter) {
let replace_with_columns = VecDeque::from(vec![replace_with.clone(); self.width]);
self.replace_characters_in_line_after_cursor(replace_with);
for row in &mut self.viewport {
row.replace_columns(replace_with_columns.clone());
}
self.output_buffer.update_all_lines();
}
fn line_wrap(&mut self) {
self.cursor.x = 0;
if self.cursor.y == self.height.saturating_sub(1) {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
self.viewport.remove(0);
}
let wrapped_row = Row::new();
self.viewport.push(wrapped_row);
self.selection.move_up(1);
self.output_buffer.update_all_lines();
} else {
self.cursor.y += 1;
if self.viewport.len() <= self.cursor.y {
let line_wrapped_row = Row::new();
self.viewport.push(line_wrapped_row);
self.output_buffer.update_line(self.cursor.y);
}
}
}
fn clear_lines_above(&mut self) {
self.lines_above.clear();
self.scrollback_buffer_lines = self.recalculate_scrollback_buffer_count();
}
fn pad_current_line_until(&mut self, position: usize, pad_character: TerminalCharacter) {
if self.viewport.get(self.cursor.y).is_none() {
self.pad_lines_until(self.cursor.y, pad_character.clone());
}
if let Some(current_row) = self.viewport.get_mut(self.cursor.y) {
for _ in current_row.width()..position {
current_row.push(pad_character.clone());
}
self.output_buffer.update_line(self.cursor.y);
}
}
fn pad_lines_until(&mut self, position: usize, pad_character: TerminalCharacter) {
for _ in self.viewport.len()..=position {
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
self.viewport.push(Row::from_columns(columns).canonical());
self.output_buffer.update_line(self.viewport.len() - 1);
}
}
pub fn move_cursor_to(&mut self, x: usize, y: usize, pad_character: TerminalCharacter) {
match self.scroll_region {
Some((scroll_region_top, scroll_region_bottom)) => {
self.cursor.x = std::cmp::min(self.width - 1, x);
let y_offset = if self.erasure_mode {
scroll_region_top
} else {
0
};
if y >= scroll_region_top && y <= scroll_region_bottom {
self.cursor.y = std::cmp::min(scroll_region_bottom, y + y_offset);
} else {
self.cursor.y = std::cmp::min(self.height - 1, y + y_offset);
}
self.pad_lines_until(self.cursor.y, pad_character.clone());
self.pad_current_line_until(self.cursor.x, pad_character);
},
None => {
self.cursor.x = std::cmp::min(self.width - 1, x);
self.cursor.y = std::cmp::min(self.height - 1, y);
self.pad_lines_until(self.cursor.y, pad_character.clone());
self.pad_current_line_until(self.cursor.x, pad_character);
},
}
}
pub fn move_cursor_up(&mut self, count: usize) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
self.cursor.y =
std::cmp::max(self.cursor.y.saturating_sub(count), scroll_region_top);
return;
}
}
self.cursor.y = if self.cursor.y < count {
0
} else {
self.cursor.y - count
};
}
pub fn move_cursor_up_with_scrolling(&mut self, count: usize) {
let (scroll_region_top, scroll_region_bottom) =
self.scroll_region.unwrap_or((0, self.height - 1));
for _ in 0..count {
let current_line_index = self.cursor.y;
if current_line_index == scroll_region_top {
// if we're at the top line, we create a new line and remove the last line that
// would otherwise overflow
if scroll_region_bottom < self.viewport.len() {
self.viewport.remove(scroll_region_bottom);
}
self.viewport
.insert(current_line_index, Row::new().canonical());
} else if current_line_index > scroll_region_top
&& current_line_index <= scroll_region_bottom
{
self.move_cursor_up(count);
}
}
self.output_buffer.update_all_lines();
}
pub fn move_cursor_down_until_edge_of_screen(
&mut self,
count: usize,
pad_character: TerminalCharacter,
) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
self.cursor.y = std::cmp::min(self.cursor.y + count, scroll_region_bottom);
return;
}
}
self.cursor.y = std::cmp::min(self.cursor.y + count, self.height - 1);
self.pad_lines_until(self.cursor.y, pad_character);
}
pub fn move_cursor_back(&mut self, count: usize) {
if self.cursor.x == self.width {
// on the rightmost screen edge, backspace skips one character
self.cursor.x -= 1;
}
if self.cursor.x < count {
self.cursor.x = 0;
} else {
self.cursor.x -= count;
}
}
pub fn hide_cursor(&mut self) {
self.cursor_is_hidden = true;
}
pub fn show_cursor(&mut self) {
self.cursor_is_hidden = false;
}
pub fn set_scroll_region(&mut self, top_line_index: usize, bottom_line_index: Option<usize>) {
let bottom_line_index = bottom_line_index.unwrap_or(self.height.saturating_sub(1));
self.scroll_region = Some((top_line_index, bottom_line_index));
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
self.move_cursor_to(0, 0, pad_character); // DECSTBM moves the cursor to column 1 line 1 of the page
}
pub fn clear_scroll_region(&mut self) {
self.scroll_region = None;
}
pub fn set_scroll_region_to_viewport_size(&mut self) {
self.scroll_region = Some((0, self.height.saturating_sub(1)));
}
pub fn delete_lines_in_scroll_region(
&mut self,
count: usize,
pad_character: TerminalCharacter,
) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
let current_line_index = self.cursor.y;
if current_line_index >= scroll_region_top && current_line_index <= scroll_region_bottom
{
// when deleting lines inside the scroll region, we must make sure it stays the
// same size (and that other lines below it aren't shifted inside it)
// so we delete the current line(s) and add an empty line at the end of the scroll
// region
for _ in 0..count {
self.viewport.remove(current_line_index);
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
if self.viewport.len() > scroll_region_bottom {
self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
} else {
self.viewport.push(Row::from_columns(columns).canonical());
}
}
self.output_buffer.update_all_lines(); // TODO: move accurately
}
}
}
pub fn add_empty_lines_in_scroll_region(
&mut self,
count: usize,
pad_character: TerminalCharacter,
) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
let current_line_index = self.cursor.y;
if current_line_index >= scroll_region_top && current_line_index <= scroll_region_bottom
{
// when adding empty lines inside the scroll region, we must make sure it stays the
// same size and that lines don't "leak" outside of it
// so we add an empty line where the cursor currently is, and delete the last line
// of the scroll region
for _ in 0..count {
if scroll_region_bottom < self.viewport.len() {
self.viewport.remove(scroll_region_bottom);
}
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
self.viewport
.insert(current_line_index, Row::from_columns(columns).canonical());
}
self.output_buffer.update_all_lines(); // TODO: move accurately
}
}
}
pub fn move_cursor_to_column(&mut self, column: usize) {
self.cursor.x = column;
let pad_character = EMPTY_TERMINAL_CHARACTER;
self.pad_current_line_until(self.cursor.x, pad_character);
}
pub fn move_cursor_to_line(&mut self, line: usize, pad_character: TerminalCharacter) {
self.cursor.y = std::cmp::min(self.height - 1, line);
self.pad_lines_until(self.cursor.y, pad_character);
let pad_character = EMPTY_TERMINAL_CHARACTER;
self.pad_current_line_until(self.cursor.x, pad_character);
}
pub fn replace_with_empty_chars(&mut self, count: usize, empty_char_style: RcCharacterStyles) {
let mut empty_character = EMPTY_TERMINAL_CHARACTER;
empty_character.styles = empty_char_style;
let pad_until = std::cmp::min(self.width, self.cursor.x + count);
self.pad_current_line_until(pad_until, empty_character.clone());
if let Some(current_row) = self.viewport.get_mut(self.cursor.y) {
for i in 0..count {
current_row.replace_character_at(empty_character.clone(), self.cursor.x + i);
}
self.output_buffer.update_line(self.cursor.y);
}
}
fn erase_characters(&mut self, count: usize, empty_char_style: RcCharacterStyles) {
let mut empty_character = EMPTY_TERMINAL_CHARACTER;
empty_character.styles = empty_char_style;
if let Some(current_row) = self.viewport.get_mut(self.cursor.y) {
// pad row if needed
if current_row.width_cached() < self.width {
let padding_count = self.width - current_row.width_cached();
let mut columns_padding =
VecDeque::from(vec![EMPTY_TERMINAL_CHARACTER; padding_count]);
current_row.columns.append(&mut columns_padding);
}
for _ in 0..count {
let deleted_character = current_row.delete_and_return_character(self.cursor.x);
let excess_width = deleted_character
.map(|terminal_character| terminal_character.width())
.unwrap_or(0)
.saturating_sub(1);
for _ in 0..excess_width {
current_row.insert_character_at(empty_character.clone(), self.cursor.x);
}
current_row.push(empty_character.clone());
}
self.output_buffer.update_line(self.cursor.y);
}
}
fn add_newline(&mut self) {
self.add_canonical_line();
self.mark_for_rerender();
}
pub fn mark_for_rerender(&mut self) {
self.should_render = true;
}
pub fn reset_terminal_state(&mut self) {
self.lines_above = VecDeque::new();
self.lines_below = vec![];
self.viewport = vec![Row::new().canonical()];
self.alternate_screen_state = None;
self.cursor_key_mode = false;
self.scroll_region = None;
self.clear_viewport_before_rendering = true;
self.cursor = Cursor::new(0, 0, self.styled_underlines);
self.saved_cursor_position = None;
self.active_charset = Default::default();
self.erasure_mode = false;
self.disable_linewrap = false;
self.new_line_mode = false;
self.cursor.change_shape(CursorShape::Initial);
self.output_buffer.update_all_lines();
self.changed_colors = None;
self.scrollback_buffer_lines = 0;
self.search_results = Default::default();
self.sixel_scrolling = false;
self.mouse_mode = MouseMode::NoEncoding;
self.mouse_tracking = MouseTracking::Off;
self.focus_event_tracking = false;
self.cursor_is_hidden = 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);
}
pub fn start_selection(&mut self, start: &Position) {
let old_selection = self.selection;
self.selection.start(*start);
self.update_selected_lines(&old_selection, &self.selection.clone());
self.mark_for_rerender();
}
pub fn update_selection(&mut self, to: &Position) {
let old_selection = self.selection;
self.selection.to(*to);
self.update_selected_lines(&old_selection, &self.selection.clone());
self.mark_for_rerender();
}
pub fn end_selection(&mut self, end: &Position) {
let old_selection = self.selection;
self.selection.end(*end);
self.update_selected_lines(&old_selection, &self.selection.clone());
self.mark_for_rerender();
}
pub fn reset_selection(&mut self) {
let old_selection = self.selection;
self.selection.reset();
self.update_selected_lines(&old_selection, &self.selection.clone());
self.mark_for_rerender();
}
pub fn get_selected_text(&self) -> Option<String> {
if self.selection.is_empty() {
return None;
}
let mut selection: Vec<String> = vec![];
let sorted_selection = self.selection.sorted();
let (start, end) = (sorted_selection.start, sorted_selection.end);
for l in sorted_selection.line_indices() {
let mut line_selection = String::new();
// on the first line of the selection, use the selection start column
// otherwise, start at the beginning of the line
let start_column = if l == start.line.0 { start.column.0 } else { 0 };
// same thing on the last line, but with the selection end column
let end_column = if l == end.line.0 {
end.column.0
} else {
self.width
};
if start_column == end_column {
continue;
}
let empty_row =
Row::from_columns(VecDeque::from(vec![EMPTY_TERMINAL_CHARACTER; self.width]));
// get the row from lines_above, viewport, or lines below depending on index
let row = if l < 0 && self.lines_above.len() > l.abs() as usize {
let offset_from_end = l.abs();
&self.lines_above[self
.lines_above
.len()
.saturating_sub(offset_from_end as usize)]
} else if l >= 0 && (l as usize) < self.viewport.len() {
&self.viewport[l as usize]
} else if (l as usize) < self.height {
// index is in viewport but there is no line
&empty_row
} else if self.lines_below.len() > (l as usize).saturating_sub(self.viewport.len()) {
&self.lines_below[(l as usize) - self.viewport.len()]
} else {
// can't find the line, this probably it's on the pane border
// is on the pane border
continue;
};
let mut terminal_col = 0;
for terminal_character in &row.columns {
if (start_column..end_column).contains(&terminal_col) {
line_selection.push(terminal_character.character);
}
terminal_col += terminal_character.width();
}
if row.is_canonical {
selection.push(line_selection);
} else {
// rejoin wrapped lines if possible
match selection.last_mut() {
Some(previous_line) => previous_line.push_str(&line_selection),
None => selection.push(line_selection),
}
}
}
// TODO: distinguish whitespace that was output explicitly vs implicitly (e.g add_newline)
// for example: echo " " vs empty lines
// for now trim after building the selection to handle whitespace in wrapped lines
let selection: Vec<_> = selection.iter().map(|l| l.trim_end()).collect();
if selection.is_empty() {
None
} else {
Some(selection.join("\n"))
}
}
pub fn absolute_position_in_scrollback(&self) -> usize {
self.lines_above.len() + self.cursor.y
}
fn update_selected_lines(&mut self, old_selection: &Selection, new_selection: &Selection) {
for l in old_selection.diff(new_selection, self.height) {
self.output_buffer.update_line(l as usize);
}
}
fn set_title(&mut self, title: String) {
self.title = Some(title);
}
fn push_current_title_to_stack(&mut self) {
if self.title_stack.len() > MAX_TITLE_STACK_SIZE {
self.title_stack.remove(0);
}
if let Some(title) = &self.title {
self.title_stack.push(title.clone());
}
}
fn pop_title_from_stack(&mut self) {
if let Some(popped_title) = self.title_stack.pop() {
self.title = Some(popped_title);
}
}
fn transfer_rows_to_lines_above(&mut self, count: usize) {
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,
);
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
}
}
}
pub fn mouse_left_click_signal(&self, position: &Position, is_held: bool) -> Option<String> {
let utf8_event = || -> Option<String> {
let button_code = if is_held { b'@' } else { b' ' };
let mut msg: Vec<u8> = vec![27, b'[', b'M', button_code];
msg.append(&mut utf8_mouse_coordinates(
position.column() + 1,
position.line() + 1,
));
Some(String::from_utf8_lossy(&msg).into())
};
let sgr_event = || -> Option<String> {
let button_code = if is_held { 32 } else { 0 };
Some(format!(
"\u{1b}[<{:?};{:?};{:?}M",
button_code,
position.column() + 1,
position.line() + 1
))
};
match (&self.mouse_mode, &self.mouse_tracking) {
(_, MouseTracking::Off) => None,
(MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => {
utf8_event()
},
(MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => {
utf8_event()
},
(MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(),
(MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(),
_ => None,
}
}
pub fn mouse_left_click_release_signal(&self, position: &Position) -> Option<String> {
match (&self.mouse_mode, &self.mouse_tracking) {
(_, MouseTracking::Off) => None,
(MouseMode::NoEncoding | MouseMode::Utf8, _) => {
let mut msg: Vec<u8> = vec![27, b'[', b'M', b'#'];
msg.append(&mut utf8_mouse_coordinates(
position.column() + 1,
position.line() + 1,
));
Some(String::from_utf8_lossy(&msg).into())
},
(MouseMode::Sgr, _) => {
let mouse_event = format!(
"\u{1b}[<0;{:?};{:?}m",
position.column() + 1,
position.line() + 1
);
Some(mouse_event)
},
}
}
pub fn mouse_right_click_signal(&self, position: &Position, is_held: bool) -> Option<String> {
let utf8_event = || -> Option<String> {
let button_code = if is_held { b'B' } else { b'"' };
let mut msg: Vec<u8> = vec![27, b'[', b'M', button_code];
msg.append(&mut utf8_mouse_coordinates(
position.column() + 1,
position.line() + 1,
));
Some(String::from_utf8_lossy(&msg).into())
};
let sgr_event = || -> Option<String> {
let button_code = if is_held { 34 } else { 2 };
Some(format!(
"\u{1b}[<{:?};{:?};{:?}M",
button_code,
position.column() + 1,
position.line() + 1
))
};
match (&self.mouse_mode, &self.mouse_tracking) {
(_, MouseTracking::Off) => None,
(MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => {
utf8_event()
},
(MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => {
utf8_event()
},
(MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(),
(MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(),
_ => None,
}
}
pub fn mouse_right_click_release_signal(&self, position: &Position) -> Option<String> {
match (&self.mouse_mode, &self.mouse_tracking) {
(_, MouseTracking::Off) => None,
(MouseMode::NoEncoding | MouseMode::Utf8, _) => {
let mut msg: Vec<u8> = vec![27, b'[', b'M', b'#'];
msg.append(&mut utf8_mouse_coordinates(
position.column() + 1,
position.line() + 1,
));
Some(String::from_utf8_lossy(&msg).into())
},
(MouseMode::Sgr, _) => {
let mouse_event = format!(
"\u{1b}[<2;{:?};{:?}m",
position.column() + 1,
position.line() + 1
);
Some(mouse_event)
},
}
}
pub fn mouse_middle_click_signal(&self, position: &Position, is_held: bool) -> Option<String> {
let utf8_event = || -> Option<String> {
let button_code = if is_held { b'A' } else { b'!' };
let mut msg: Vec<u8> = vec![27, b'[', b'M', button_code];
msg.append(&mut utf8_mouse_coordinates(
position.column() + 1,
position.line() + 1,
));
Some(String::from_utf8_lossy(&msg).into())
};
let sgr_event = || -> Option<String> {
let button_code = if is_held { 33 } else { 1 };
Some(format!(
"\u{1b}[<{:?};{:?};{:?}M",
button_code,
position.column() + 1,
position.line() + 1
))
};
match (&self.mouse_mode, &self.mouse_tracking) {
(_, MouseTracking::Off) => None,
(MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => {
utf8_event()
},
(MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => {
utf8_event()
},
(MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(),
(MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(),
_ => None,
}
}
pub fn mouse_middle_click_release_signal(&self, position: &Position) -> Option<String> {
match (&self.mouse_mode, &self.mouse_tracking) {
(_, MouseTracking::Off) => None,
(MouseMode::NoEncoding | MouseMode::Utf8, _) => {
let mut msg: Vec<u8> = vec![27, b'[', b'M', b'#'];
msg.append(&mut utf8_mouse_coordinates(
position.column() + 1,
position.line() + 1,
));
Some(String::from_utf8_lossy(&msg).into())
},
(MouseMode::Sgr, _) => {
// TODO: these don't add a +1 because it's done outside, we should change it to
// happen here for consistency
let mouse_event = format!(
"\u{1b}[<1;{:?};{:?}m",
position.column() + 1,
position.line() + 1
);
Some(mouse_event)
},
}
}
pub fn mouse_scroll_up_signal(&self, position: &Position) -> Option<String> {
match (&self.mouse_mode, &self.mouse_tracking) {
(_, MouseTracking::Off) => None,
(MouseMode::NoEncoding | MouseMode::Utf8, _) => {
let mut msg: Vec<u8> = vec![27, b'[', b'M', b'`'];
msg.append(&mut utf8_mouse_coordinates(
position.column() + 1,
position.line() + 1,
));
Some(String::from_utf8_lossy(&msg).into())
},
(MouseMode::Sgr, _) => {
let mouse_event = format!(
"\u{1b}[<64;{:?};{:?}M",
position.column.0 + 1,
position.line.0 + 1
);
Some(mouse_event)
},
}
}
pub fn mouse_scroll_down_signal(&self, position: &Position) -> Option<String> {
match (&self.mouse_mode, &self.mouse_tracking) {
(_, MouseTracking::Off) => None,
(MouseMode::NoEncoding | MouseMode::Utf8, _) => {
let mut msg: Vec<u8> = vec![27, b'[', b'M', b'a'];
msg.append(&mut utf8_mouse_coordinates(
position.column() + 1,
position.line() + 1,
));
Some(String::from_utf8_lossy(&msg).into())
},
(MouseMode::Sgr, _) => {
let mouse_event = format!(
"\u{1b}[<65;{:?};{:?}M",
position.column.0 + 1,
position.line.0 + 1
);
Some(mouse_event)
},
}
}
pub fn is_alternate_mode_active(&self) -> bool {
self.alternate_screen_state.is_some()
}
pub fn focus_event(&self) -> Option<String> {
if self.focus_event_tracking {
Some("\u{1b}[I".into())
} else {
None
}
}
pub fn unfocus_event(&self) -> Option<String> {
if self.focus_event_tracking {
Some("\u{1b}[O".into())
} else {
None
}
}
pub fn delete_viewport_and_scroll(&mut self) {
self.lines_above.clear();
self.viewport.clear();
self.lines_below.clear();
}
pub fn reset_cursor_position(&mut self) {
self.cursor = Cursor::new(0, 0, self.styled_underlines);
}
pub fn lock_renders(&mut self) {
self.lock_renders = true;
}
pub fn unlock_renders(&mut self) {
self.lock_renders = false;
}
pub fn update_theme(&mut self, theme: Palette) {
self.style.colors = theme.clone();
}
pub fn update_arrow_fonts(&mut self, should_support_arrow_fonts: bool) {
self.arrow_fonts = should_support_arrow_fonts;
}
}
impl Perform for Grid {
fn print(&mut self, c: char) {
let c = self.cursor.charsets[self.active_charset].map(c);
let terminal_character =
TerminalCharacter::new_styled(c, self.cursor.pending_styles.clone());
self.set_preceding_character(terminal_character.clone());
self.add_character(terminal_character);
}
fn execute(&mut self, byte: u8) {
match byte {
7 => {
self.ring_bell = true;
},
8 => {
// backspace
self.move_cursor_back(1);
},
9 => {
// tab
self.advance_to_next_tabstop(self.cursor.pending_styles.clone());
},
10 | 11 | 12 => {
// 0a, newline
// 0b, vertical tabulation
// 0c, form feed
self.add_newline();
},
13 => {
// 0d, carriage return
self.move_cursor_to_beginning_of_line();
},
14 => {
self.set_active_charset(CharsetIndex::G1);
},
15 => {
self.set_active_charset(CharsetIndex::G0);
},
_ => {
if self.debug {
log::warn!("Unhandled execute: {:?}", byte);
}
},
}
}
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(),
);
}
} else if c == 'z' {
// UI-component (Zellij internal)
self.ui_component_bytes = Some(vec![]);
}
}
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;
} else if let Some(ui_component_bytes) = self.ui_component_bytes.as_mut() {
ui_component_bytes.push(byte);
}
}
fn unhook(&mut self) {
if self.sixel_grid.is_parsing() {
self.create_sixel_image();
} else if let Some(mut ui_component_bytes) = self.ui_component_bytes.take() {
let component_bytes = ui_component_bytes.drain(..);
let style = self.style.clone();
let arrow_fonts = self.arrow_fonts;
UiComponentParser::new(self, style, arrow_fonts)
.parse(component_bytes.collect())
.non_fatal();
}
self.mark_for_rerender();
}
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
let terminator = if bell_terminated { "\x07" } else { "\x1b\\" };
if params.is_empty() || params[0].is_empty() {
return;
}
match params[0] {
// Set window title.
b"0" | b"2" => {
if params.len() >= 2 {
let title = params[1..]
.iter()
.flat_map(|x| str::from_utf8(x))
.collect::<Vec<&str>>()
.join(";")
.trim()
.to_owned();
self.set_title(title);
}
},
// Set color index.
b"4" => {
for chunk in params[1..].chunks(2) {
let index = chunk.get(0).and_then(|index| parse_number(index));
let color = chunk.get(1).and_then(|color| xparse_color(color));
if let (Some(i), Some(c)) = (index, color) {
if self.changed_colors.is_none() {
self.changed_colors = Some([None; 256]);
}
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());
}
}
}
}
},
// define hyperlink
b"8" => {
if params.len() < 3 {
return;
}
self.cursor.pending_styles.update(|styles| {
styles.link_anchor = self.link_handler.borrow_mut().dispatch_osc8(params)
})
},
// Get/set Foreground (b"10") or background (b"11") colors
b"10" | b"11" => {
if params.len() >= 2 {
if let Some(mut dynamic_code) = parse_number(params[0]) {
for param in &params[1..] {
// currently only getting the color sequence is supported,
// setting still isn't
if param == b"?" {
let saved_terminal_color = if dynamic_code == 10 {
Some(self.terminal_emulator_colors.borrow().fg)
} else if dynamic_code == 11 {
Some(self.terminal_emulator_colors.borrow().bg)
} else {
None
};
let color_response_message = match saved_terminal_color {
Some(PaletteColor::Rgb((r, g, b))) => {
format!(
"\u{1b}]{};rgb:{1:02x}{1:02x}/{2:02x}{2:02x}/{3:02x}{3:02x}{4}",
// dynamic_code, color.r, color.g, color.b, terminator
dynamic_code, r, g, b, terminator
)
},
_ => {
format!(
"\u{1b}]{};rgb:{1:02x}{1:02x}/{2:02x}{2:02x}/{3:02x}{3:02x}{4}",
// dynamic_code, color.r, color.g, color.b, terminator
dynamic_code, 0, 0, 0, terminator
)
},
};
self.pending_messages_to_pty
.push(color_response_message.as_bytes().to_vec());
}
dynamic_code += 1;
}
}
}
},
b"12" => {
// get/set cursor color currently unimplemented
},
// Set cursor style.
b"50" => {
if params.len() >= 2
&& params[1].len() >= 13
&& params[1][0..12] == *b"CursorShape="
{
let shape = match params[1][12] as char {
'0' => Some(CursorShape::Block),
'1' => Some(CursorShape::Beam),
'2' => Some(CursorShape::Underline),
_ => None,
};
if let Some(cursor_shape) = shape {
self.cursor.change_shape(cursor_shape);
}
}
},
// Set clipboard.
b"52" => {
if params.len() < 3 {
return;
}
let _clipboard = params[1].get(0).unwrap_or(&b'c');
match params[2] {
b"?" => {
// TBD: paste from own clipboard - currently unsupported
},
base64 => {
if let Ok(bytes) = base64::decode(base64) {
if let Ok(string) = String::from_utf8(bytes) {
self.pending_clipboard_update = Some(string);
}
};
},
}
},
// Reset color index.
b"104" => {
// Reset all color indexes when no parameters are given.
if params.len() == 1 {
self.changed_colors = None;
return;
}
// Reset color indexes given as parameters.
for param in &params[1..] {
if let Some(index) = parse_number(param) {
if self.changed_colors.is_some() {
self.changed_colors.as_mut().unwrap()[index as usize] = None
}
}
}
// Reset all color indexes when no parameters are given.
if params.len() == 1 {
// TBD - reset all color changes - currently unsupported
return;
}
// Reset color indexes given as parameters.
for param in &params[1..] {
if let Some(_index) = parse_number(param) {
// TBD - reset color index - currently unimplemented
}
}
},
// Reset foreground color.
b"110" => {
// TBD - reset foreground color - currently unimplemented
},
// Reset background color.
b"111" => {
// TBD - reset background color - currently unimplemented
},
// Reset text cursor color.
b"112" => {
// TBD - reset text cursor color - currently unimplemented
},
_ => {
if self.debug {
log::warn!("Unhandled osc: {:?}", params);
}
},
}
}
fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, c: char) {
let mut params_iter = params.iter();
let mut next_param_or = |default: u16| {
params_iter
.next()
.map(|param| param[0])
.filter(|&param| param != 0)
.unwrap_or(default) as usize
};
if c == 'm' {
if intermediates.is_empty() {
self.cursor
.pending_styles
.update(|styles| styles.add_style_from_ansi_params(&mut params_iter))
}
} else if c == 'C' || c == 'a' {
// move cursor forward
let move_by = next_param_or(1);
self.move_cursor_forward_until_edge(move_by);
} else if c == 'K' {
// clear line (0 => right, 1 => left, 2 => all)
if let Some(clear_type) = params_iter.next().map(|param| param[0]) {
let mut char_to_replace = EMPTY_TERMINAL_CHARACTER;
if let Some(background_color) = self.cursor.pending_styles.background {
char_to_replace
.styles
.update(|styles| styles.background = Some(background_color));
}
if clear_type == 0 {
self.replace_characters_in_line_after_cursor(char_to_replace);
} else if clear_type == 1 {
self.replace_characters_in_line_before_cursor(char_to_replace);
} else if clear_type == 2 {
self.clear_cursor_line();
}
};
} else if c == 'J' {
// clear all (0 => below, 1 => above, 2 => all, 3 => saved)
let mut char_to_replace = EMPTY_TERMINAL_CHARACTER;
if let Some(background_color) = self.cursor.pending_styles.background {
char_to_replace
.styles
.update(|styles| styles.background = Some(background_color));
}
if let Some(clear_type) = params_iter.next().map(|param| param[0]) {
if clear_type == 0 {
self.clear_all_after_cursor(char_to_replace);
} else if clear_type == 1 {
self.clear_all_before_cursor(char_to_replace);
} else if clear_type == 2 {
self.set_scroll_region_to_viewport_size();
self.fill_viewport(char_to_replace);
} else if clear_type == 3 {
self.clear_lines_above();
}
};
} else if c == 'H' || c == 'f' {
// goto row/col
// we subtract 1 from the row/column because these are 1 indexed
let row = next_param_or(1).saturating_sub(1);
let col = next_param_or(1).saturating_sub(1);
self.move_cursor_to(col, row, EMPTY_TERMINAL_CHARACTER);
} else if c == 'A' {
// move cursor up until edge of screen
let move_up_count = next_param_or(1);
self.move_cursor_up(move_up_count as usize);
} else if c == 'B' || c == 'e' {
// move cursor down until edge of screen
let move_down_count = next_param_or(1);
let pad_character = EMPTY_TERMINAL_CHARACTER;
self.move_cursor_down_until_edge_of_screen(move_down_count as usize, pad_character);
} else if c == 'D' {
let move_back_count = next_param_or(1);
self.move_cursor_back(move_back_count);
} else if c == 'l' {
let first_intermediate_is_questionmark = match intermediates.get(0) {
Some(b'?') => true,
None => false,
_ => false,
};
if first_intermediate_is_questionmark {
for param in params_iter.map(|param| param[0]) {
match param {
2026 => {
self.unlock_renders();
},
2004 => {
self.bracketed_paste_mode = false;
},
1049 => {
if let Some(mut alternate_screen_state) =
self.alternate_screen_state.take()
{
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,
&mut self.supports_kitty_keyboard_protocol,
);
}
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();
},
25 => {
self.hide_cursor();
self.mark_for_rerender();
},
1 => {
self.cursor_key_mode = false;
},
3 => {
// DECCOLM - only side effects
self.scroll_region = None;
self.clear_all(EMPTY_TERMINAL_CHARACTER);
self.cursor.x = 0;
self.cursor.y = 0;
},
6 => {
self.erasure_mode = false;
},
7 => {
self.disable_linewrap = true;
},
80 => {
self.sixel_scrolling = false;
},
1000 => {
self.mouse_tracking = MouseTracking::Off;
},
1002 => {
self.mouse_tracking = MouseTracking::Off;
},
1003 => {
// TBD: any-even mouse tracking
},
1004 => {
self.focus_event_tracking = false;
},
1005 => {
self.mouse_mode = MouseMode::NoEncoding;
},
1006 => {
self.mouse_mode = MouseMode::NoEncoding;
},
_ => {},
};
}
} else {
for param in params_iter.map(|param| param[0]) {
match param {
4 => {
self.insert_mode = false;
},
20 => {
self.new_line_mode = false;
},
_ => {},
}
}
}
} else if c == 'h' {
let first_intermediate_is_questionmark = match intermediates.get(0) {
Some(b'?') => true,
None => false,
_ => false,
};
if first_intermediate_is_questionmark {
for param in params_iter.map(|param| param[0]) {
match param {
25 => {
self.show_cursor();
self.mark_for_rerender();
},
2026 => {
self.lock_renders();
},
2004 => {
self.bracketed_paste_mode = true;
},
1049 => {
// enter alternate buffer
let current_lines_above =
std::mem::replace(&mut self.lines_above, VecDeque::new());
let current_viewport =
std::mem::replace(&mut self.viewport, vec![Row::new().canonical()]);
let current_cursor = std::mem::replace(
&mut self.cursor,
Cursor::new(0, 0, self.styled_underlines),
);
let current_supports_kitty_keyboard_protocol = std::mem::replace(
&mut self.supports_kitty_keyboard_protocol,
false,
);
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,
current_supports_kitty_keyboard_protocol,
));
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
},
1 => {
self.cursor_key_mode = true;
},
3 => {
// DECCOLM - only side effects
self.scroll_region = None;
self.clear_all(EMPTY_TERMINAL_CHARACTER);
self.cursor.x = 0;
self.cursor.y = 0;
},
6 => {
self.erasure_mode = true;
},
7 => {
self.disable_linewrap = false;
},
80 => {
self.sixel_scrolling = true;
},
1000 => {
self.mouse_tracking = MouseTracking::Normal;
},
1002 => {
self.mouse_tracking = MouseTracking::ButtonEventTracking;
},
1003 => {
// TBD: any-even mouse tracking
},
1004 => {
self.focus_event_tracking = true;
},
1005 => {
self.mouse_mode = MouseMode::Utf8;
},
1006 => {
self.mouse_mode = MouseMode::Sgr;
},
_ => {},
}
}
} else {
for param in params_iter.map(|param| param[0]) {
match param {
4 => {
self.insert_mode = true;
},
20 => {
self.new_line_mode = true;
},
_ => {},
}
}
}
} else if c == 'p' {
let first_intermediate_is_questionmark = match intermediates.get(0) {
Some(b'?') => true,
None => false,
_ => false,
};
if first_intermediate_is_questionmark {
for param in params_iter.map(|param| param[0]) {
match param {
2026 => {
let response = "\u{1b}[2026;2$y";
self.pending_messages_to_pty
.push(response.as_bytes().to_vec());
},
_ => {},
}
}
}
} else if c == 'r' {
if params.len() > 1 {
let top = (next_param_or(1) as usize).saturating_sub(1);
let bottom = params_iter
.next()
.map(|param| param[0] as usize)
.filter(|&param| param != 0)
.map(|bottom| {
std::cmp::min(self.height.saturating_sub(1), bottom.saturating_sub(1))
});
self.set_scroll_region(top, bottom);
if self.erasure_mode {
self.move_cursor_to_line(top, EMPTY_TERMINAL_CHARACTER);
self.move_cursor_to_beginning_of_line();
}
} else {
self.clear_scroll_region();
}
} else if c == 'M' {
// delete lines if currently inside scroll region, or otherwise
// delete lines in the entire viewport
if self.scroll_region.is_none() {
self.set_scroll_region_to_viewport_size();
}
let line_count_to_delete = next_param_or(1);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
self.delete_lines_in_scroll_region(line_count_to_delete, pad_character);
} else if c == 'L' {
// insert blank lines if inside scroll region, or otherwise insert
// blank lines in the entire viewport
if self.scroll_region.is_none() {
self.set_scroll_region_to_viewport_size();
}
let line_count_to_add = next_param_or(1);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
self.add_empty_lines_in_scroll_region(line_count_to_add, pad_character);
} else if c == 'G' || c == '`' {
let column = next_param_or(1).saturating_sub(1);
let column = std::cmp::min(column, self.width.saturating_sub(1));
self.move_cursor_to_column(column);
} else if c == 'g' {
let clear_type = next_param_or(0);
if clear_type == 0 {
self.clear_tabstop(self.cursor.x);
} else if clear_type == 3 {
self.clear_all_tabstops();
}
} else if c == 'd' {
// goto line
let line = next_param_or(1).saturating_sub(1);
let pad_character = EMPTY_TERMINAL_CHARACTER;
self.move_cursor_to_line(line, pad_character);
} else if c == 'P' {
// erase characters
let count = next_param_or(1);
self.erase_characters(count, self.cursor.pending_styles.clone());
} else if c == 'X' {
// erase characters and replace with empty characters of current style
let count = next_param_or(1);
self.replace_with_empty_chars(count, self.cursor.pending_styles.clone());
} else if c == 'T' {
/*
* 124 54 T SD
* Scroll down, new lines inserted at top of screen
* [4T = Scroll down 4, bring previous lines back into view
*/
let line_count = next_param_or(1);
self.rotate_scroll_region_up(line_count as usize);
} else if c == 'S' {
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 = matches!(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' && intermediates == &[b'>'] {
// Zellij only supports the first "progressive enhancement" layer of the kitty keyboard
// protocol
if !self.explicitly_disable_kitty_keyboard_protocol {
self.supports_kitty_keyboard_protocol = true;
}
} else if c == 'u' && intermediates == &[b'<'] {
// Zellij only supports the first "progressive enhancement" layer of the kitty keyboard
// protocol
if !self.explicitly_disable_kitty_keyboard_protocol {
self.supports_kitty_keyboard_protocol = false;
}
} else if c == 'u' && intermediates == &[b'?'] {
// Zellij only supports the first "progressive enhancement" layer of the kitty keyboard
// protocol
let reply = if self.supports_kitty_keyboard_protocol {
"\u{1b}[?1u"
} else {
"\u{1b}[?0u"
};
self.pending_messages_to_pty.push(reply.as_bytes().to_vec());
} else if c == 'u' {
self.restore_cursor_position();
} else if c == '@' {
let count = next_param_or(1);
for _ in 0..count {
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
self.add_character_at_cursor_position(pad_character, true);
}
} else if c == 'b' {
if let Some(c) = self.preceding_char.clone() {
for _ in 0..next_param_or(1) {
self.add_character(c.clone());
}
}
} else if c == 'E' {
// Moves cursor to beginning of the line n (default 1) lines down.
let count = next_param_or(1);
let pad_character = EMPTY_TERMINAL_CHARACTER;
self.move_cursor_down_until_edge_of_screen(count, pad_character);
self.move_cursor_to_beginning_of_line();
} else if c == 'F' {
// Moves cursor to beginning of the line n (default 1) lines up.
let count = next_param_or(1);
self.move_cursor_up(count);
self.move_cursor_to_beginning_of_line();
} else if c == 'I' {
for _ in 0..next_param_or(1) {
self.advance_to_next_tabstop(self.cursor.pending_styles.clone());
}
} else if c == 'q' {
let first_intermediate_is_space = matches!(intermediates.get(0), Some(b' '));
if first_intermediate_is_space {
// DECSCUSR (CSI Ps SP q) -- Set Cursor Style.
let cursor_style_id = next_param_or(0);
let shape = match cursor_style_id {
0 => Some(CursorShape::Initial),
2 => Some(CursorShape::Block),
1 => Some(CursorShape::BlinkingBlock),
3 => Some(CursorShape::BlinkingUnderline),
4 => Some(CursorShape::Underline),
5 => Some(CursorShape::BlinkingBeam),
6 => Some(CursorShape::Beam),
_ => None,
};
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) {
self.move_to_previous_tabstop();
}
} else if c == 'c' {
// identify terminal
// https://vt100.net/docs/vt510-rm/DA1.html
match intermediates.get(0) {
None | Some(0) => {
// primary device attributes - VT220 with sixel
let terminal_capabilities = "\u{1b}[?62;4c";
self.pending_messages_to_pty
.push(terminal_capabilities.as_bytes().to_vec());
},
Some(b'>') => {
// secondary device attributes
let version = version_number(VERSION);
let text = format!("\u{1b}[>0;{};1c", version);
self.pending_messages_to_pty.push(text.as_bytes().to_vec());
},
_ => {},
}
} else if c == 'n' {
// DSR - device status report
// https://vt100.net/docs/vt510-rm/DSR.html
match next_param_or(0) {
5 => {
// report terminal status
let all_good = "\u{1b}[0n";
self.pending_messages_to_pty
.push(all_good.as_bytes().to_vec());
},
6 => {
// CPR - cursor position report
// Note that this is relative to scrolling region.
let offset = match self.scroll_region {
Some((scroll_region_top, _scroll_region_bottom)) => scroll_region_top,
_ => 0,
};
let position_report = format!(
"\u{1b}[{};{}R",
self.cursor.y + 1 - offset,
self.cursor.x + 1
);
self.pending_messages_to_pty
.push(position_report.as_bytes().to_vec());
},
_ => {},
}
} else if c == 'x' {
// DECREQTPARM - Request Terminal Parameters
// https://vt100.net/docs/vt100-ug/chapter3.html#DECREQTPARM
//
// Respond with (same as xterm): Parity NONE, 8 bits,
// xmitspeed 38400, recvspeed 38400. (CLoCk MULtiplier =
// 1, STP option flags = 0)
//
// (xterm used to respond to DECREQTPARM in all modes.
// Now it seems to only do so when explicitly in VT100 mode.)
let query = next_param_or(0);
match query {
0 | 1 => {
let response = format!("\u{1b}[{};1;1;128;128;1;0x", query + 2);
self.pending_messages_to_pty
.push(response.as_bytes().to_vec());
},
_ => {},
}
} else if c == 't' {
match next_param_or(1) as usize {
14 => {
if let Some(character_cell_size) = *self.character_cell_size.borrow() {
let text_area_pixel_size_report = format!(
"\x1b[4;{};{}t",
character_cell_size.height * self.height,
character_cell_size.width * self.width
);
self.pending_messages_to_pty
.push(text_area_pixel_size_report.as_bytes().to_vec());
}
},
16 => {
if let Some(character_cell_size) = *self.character_cell_size.borrow() {
let character_cell_size_report = format!(
"\x1b[6;{};{}t",
character_cell_size.height, character_cell_size.width
);
self.pending_messages_to_pty
.push(character_cell_size_report.as_bytes().to_vec());
}
},
18 => {
// report text area
let text_area_report = format!("\x1b[8;{};{}t", self.height, self.width);
self.pending_messages_to_pty
.push(text_area_report.as_bytes().to_vec());
},
22 => {
self.push_current_title_to_stack();
},
23 => {
self.pop_title_from_stack();
},
_ => {},
}
} else {
if self.debug {
log::warn!("Unhandled csi: {}->{:?}", c, params);
}
}
}
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
match (byte, intermediates.get(0)) {
(b'A', charset_index_symbol) => {
let charset_index: CharsetIndex = match charset_index_symbol {
Some(b'(') => CharsetIndex::G0,
Some(b')') => CharsetIndex::G1,
Some(b'*') => CharsetIndex::G2,
Some(b'+') => CharsetIndex::G3,
_ => {
// invalid, silently do nothing
return;
},
};
self.configure_charset(StandardCharset::UK, charset_index);
},
(b'B', charset_index_symbol) => {
let charset_index: CharsetIndex = match charset_index_symbol {
Some(b'(') => CharsetIndex::G0,
Some(b')') => CharsetIndex::G1,
Some(b'*') => CharsetIndex::G2,
Some(b'+') => CharsetIndex::G3,
_ => {
// invalid, silently do nothing
return;
},
};
self.configure_charset(StandardCharset::Ascii, charset_index);
},
(b'0', charset_index_symbol) => {
let charset_index: CharsetIndex = match charset_index_symbol {
Some(b'(') => CharsetIndex::G0,
Some(b')') => CharsetIndex::G1,
Some(b'*') => CharsetIndex::G2,
Some(b'+') => CharsetIndex::G3,
_ => {
// invalid, silently do nothing
return;
},
};
self.configure_charset(
StandardCharset::SpecialCharacterAndLineDrawing,
charset_index,
);
},
(b'D', None) => {
self.add_newline();
},
(b'E', None) => {
self.add_newline();
self.move_cursor_to_beginning_of_line();
},
(b'M', None) => {
// TODO: if cursor is at the top, it should go down one
self.move_cursor_up_with_scrolling(1);
},
(b'c', None) => {
self.reset_terminal_state();
},
(b'H', None) => {
self.set_horizontal_tabstop();
},
(b'7', None) => {
self.save_cursor_position();
},
(b'Z', None) => {
let terminal_capabilities = "\u{1b}[?6c";
self.pending_messages_to_pty
.push(terminal_capabilities.as_bytes().to_vec());
},
(b'8', None) => {
self.restore_cursor_position();
},
(b'8', Some(b'#')) => {
let mut fill_character = EMPTY_TERMINAL_CHARACTER;
fill_character.character = 'E';
self.fill_viewport(fill_character);
},
_ => {
if self.debug {
log::warn!("Unhandled esc_dispatch: {}->{:?}", byte, intermediates);
}
},
}
}
}
#[derive(Clone)]
pub struct AlternateScreenState {
lines_above: VecDeque<Row>,
viewport: Vec<Row>,
cursor: Cursor,
sixel_grid: SixelGrid,
supports_kitty_keyboard_protocol: bool,
}
impl AlternateScreenState {
pub fn new(
lines_above: VecDeque<Row>,
viewport: Vec<Row>,
cursor: Cursor,
sixel_grid: SixelGrid,
supports_kitty_keyboard_protocol: bool,
) -> Self {
AlternateScreenState {
lines_above,
viewport,
cursor,
sixel_grid,
supports_kitty_keyboard_protocol,
}
}
pub fn apply_contents_to(
&mut self,
lines_above: &mut VecDeque<Row>,
viewport: &mut Vec<Row>,
cursor: &mut Cursor,
sixel_grid: &mut SixelGrid,
supports_kitty_keyboard_protocol: &mut bool,
) {
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);
std::mem::swap(
&mut self.supports_kitty_keyboard_protocol,
supports_kitty_keyboard_protocol,
);
}
}
#[derive(Clone)]
pub struct Row {
pub columns: VecDeque<TerminalCharacter>,
pub is_canonical: bool,
width: Option<usize>,
}
impl Debug for Row {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for character in &self.columns {
write!(f, "{:?}", character)?;
}
Ok(())
}
}
impl Row {
pub fn new() -> Self {
Row {
columns: VecDeque::new(),
is_canonical: false,
width: None,
}
}
pub fn from_columns(columns: VecDeque<TerminalCharacter>) -> Self {
Row {
columns,
is_canonical: false,
width: None,
}
}
pub fn from_rows(mut rows: Vec<Row>) -> Self {
if rows.is_empty() {
Row::new()
} else {
let mut first_row = rows.remove(0);
for row in &mut rows {
first_row.append(&mut row.columns);
}
first_row
}
}
pub fn with_character(mut self, terminal_character: TerminalCharacter) -> Self {
self.columns.push_back(terminal_character);
self.width = None;
self
}
pub fn canonical(mut self) -> Self {
self.is_canonical = true;
self
}
pub fn width_cached(&mut self) -> usize {
if self.width.is_some() {
self.width.unwrap()
} else {
let mut width = 0;
for terminal_character in &self.columns {
width += terminal_character.width();
}
self.width = Some(width);
width
}
}
pub fn width(&self) -> usize {
let mut width = 0;
for terminal_character in &self.columns {
width += terminal_character.width();
}
width
}
pub fn excess_width(&self) -> usize {
let mut acc = 0;
for terminal_character in &self.columns {
if terminal_character.width() > 1 {
acc += terminal_character.width() - 1;
}
}
acc
}
pub fn excess_width_until(&self, x: usize) -> usize {
let mut acc = 0;
for terminal_character in self.columns.iter().take(x) {
if terminal_character.width() > 1 {
acc += terminal_character.width() - 1;
}
}
acc
}
pub fn absolute_character_index(&self, x: usize) -> usize {
// return x's width aware index
let mut absolute_index = x;
for (i, terminal_character) in self.columns.iter().enumerate().take(x) {
if i == absolute_index {
break;
}
if terminal_character.width() > 1 {
absolute_index = absolute_index.saturating_sub(1);
}
}
absolute_index
}
pub fn absolute_character_index_and_position_in_char(&self, x: usize) -> (usize, usize) {
// returns x's width aware index as well as its position inside the wide char (eg. 1 if
// it's in the middle of a 2-char wide character)
let mut accumulated_width = 0;
let mut absolute_index = x;
let mut position_inside_character = 0;
for (i, terminal_character) in self.columns.iter().enumerate() {
accumulated_width += terminal_character.width();
absolute_index = i;
if accumulated_width > x {
let character_start_position = accumulated_width - terminal_character.width();
position_inside_character = x - character_start_position;
break;
}
}
(absolute_index, position_inside_character)
}
pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
match self.width_cached().cmp(&x) {
Ordering::Equal => {
// this is unwrapped because this always happens after self.width_cached()
*self.width.as_mut().unwrap() += terminal_character.width();
// adding the character at the end of the current line
self.columns.push_back(terminal_character);
},
Ordering::Less => {
// adding the character after the end of the current line
// we pad the line up to the character and then add it
let width_offset = self.excess_width_until(x);
self.columns
.resize(x.saturating_sub(width_offset), EMPTY_TERMINAL_CHARACTER);
self.columns.push_back(terminal_character);
self.width = None;
},
Ordering::Greater => {
// adding the character in the middle of the line
// we replace the character at its position
let (absolute_x_index, position_inside_character) =
self.absolute_character_index_and_position_in_char(x);
let character_width = terminal_character.width();
let replaced_character =
std::mem::replace(&mut self.columns[absolute_x_index], terminal_character);
match character_width.cmp(&replaced_character.width()) {
Ordering::Greater => {
// the replaced character is narrower than the current character
// (eg. we added a wide emoji in place of an English character)
// we remove the character after it to make room
let position_to_remove = absolute_x_index + 1;
if let Some(removed) = self.columns.remove(position_to_remove) {
if removed.width() > 1 {
// the character we removed is a wide character itself, so we add
// padding
self.columns
.insert(position_to_remove, EMPTY_TERMINAL_CHARACTER);
}
}
},
Ordering::Less => {
// the replaced character is wider than the current character
// (eg. we added an English character in place of a wide emoji)
// we must make sure to add padding either before the character we added
// or after it, depending on our position inside said removed wide character
// TODO: support characters wider than 2
if position_inside_character > 0 {
self.columns
.insert(absolute_x_index, EMPTY_TERMINAL_CHARACTER);
} else {
self.columns
.insert(absolute_x_index + 1, EMPTY_TERMINAL_CHARACTER);
}
},
_ => {},
}
self.width = None;
},
}
}
pub fn insert_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
let insert_position = self.absolute_character_index(x);
match self.columns.len().cmp(&insert_position) {
Ordering::Equal => self.columns.push_back(terminal_character),
Ordering::Less => {
self.columns
.resize(insert_position, EMPTY_TERMINAL_CHARACTER);
self.columns.push_back(terminal_character);
},
Ordering::Greater => {
self.columns.insert(insert_position, terminal_character);
},
}
self.width = None;
}
pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
let absolute_x_index = self.absolute_character_index(x);
if let Some(character) = self.columns.get_mut(absolute_x_index) {
let terminal_character_width = terminal_character.width();
let character = std::mem::replace(character, terminal_character);
let excess_width = character.width().saturating_sub(terminal_character_width);
for _ in 0..excess_width {
self.columns
.insert(absolute_x_index, EMPTY_TERMINAL_CHARACTER);
}
}
self.width = None;
}
pub fn replace_columns(&mut self, columns: VecDeque<TerminalCharacter>) {
self.columns = columns;
self.width = None;
}
pub fn push(&mut self, terminal_character: TerminalCharacter) {
self.columns.push_back(terminal_character);
self.width = None;
}
pub fn truncate(&mut self, x: usize) {
let width_offset = self.excess_width_until(x);
let truncate_position = x.saturating_sub(width_offset);
if truncate_position < self.columns.len() {
self.columns.truncate(truncate_position);
}
self.width = None;
}
pub fn position_accounting_for_widechars(&self, x: usize) -> usize {
let mut position = x;
for (index, terminal_character) in self.columns.iter().enumerate() {
if index == position {
break;
}
if terminal_character.width() > 1 {
position = position.saturating_sub(terminal_character.width().saturating_sub(1));
}
}
position
}
pub fn replace_and_pad_end(
&mut self,
from: usize,
to: usize,
terminal_character: TerminalCharacter,
) {
let from_position_accounting_for_widechars = self.position_accounting_for_widechars(from);
let to_position_accounting_for_widechars = self.position_accounting_for_widechars(to);
let replacement_length = to_position_accounting_for_widechars
.saturating_sub(from_position_accounting_for_widechars);
let mut replace_with = VecDeque::from(vec![terminal_character; replacement_length]);
self.columns
.truncate(from_position_accounting_for_widechars);
self.columns.append(&mut replace_with);
self.width = None;
}
pub fn append(&mut self, to_append: &mut VecDeque<TerminalCharacter>) {
self.columns.append(to_append);
self.width = None;
}
pub fn drain_until(&mut self, x: usize) -> VecDeque<TerminalCharacter> {
let mut drained_part_len = 0;
let mut split_pos = 0;
for next_character in self.columns.iter() {
// drained_part_len == 0 here is so that if the grid is resized
// to a size of 1, we won't drop wide characters
if drained_part_len + next_character.width() <= x || drained_part_len == 0 {
drained_part_len += next_character.width();
split_pos += 1
} else {
break;
}
}
// Can't use split_off because it doesn't reduce capacity, causing OOM with long lines
let drained_part = self.columns.drain(..split_pos).collect();
self.width = None;
drained_part
}
pub fn replace_and_pad_beginning(&mut self, to: usize, terminal_character: TerminalCharacter) {
let to_position_accounting_for_widechars = self.position_accounting_for_widechars(to);
let width_of_current_character = self
.columns
.get(to_position_accounting_for_widechars)
.map(|character| character.width())
.unwrap_or(1);
let mut replace_with =
VecDeque::from(vec![terminal_character; to + width_of_current_character]);
if to_position_accounting_for_widechars > self.columns.len() {
self.columns.clear();
} else if to_position_accounting_for_widechars >= self.columns.len() {
drop(self.columns.drain(0..to_position_accounting_for_widechars));
} else {
drop(self.columns.drain(0..=to_position_accounting_for_widechars));
}
replace_with.append(&mut self.columns);
self.width = None;
self.columns = replace_with;
}
pub fn len(&self) -> usize {
self.columns.len()
}
pub fn is_empty(&self) -> bool {
self.columns.is_empty()
}
pub fn delete_and_return_character(&mut self, x: usize) -> Option<TerminalCharacter> {
let erase_position = self.absolute_character_index(x);
if erase_position < self.columns.len() {
self.width = None;
self.columns.remove(erase_position)
} else {
None
}
}
pub fn split_to_rows_of_length(&mut self, max_row_length: usize) -> Vec<Row> {
let mut parts: Vec<Row> = vec![];
let mut current_part: VecDeque<TerminalCharacter> = VecDeque::new();
let mut current_part_len = 0;
for character in self.columns.drain(..) {
if current_part_len + character.width() > max_row_length {
parts.push(Row::from_columns(current_part));
current_part = VecDeque::new();
current_part_len = 0;
}
current_part_len += character.width();
current_part.push_back(character);
}
if !current_part.is_empty() {
parts.push(Row::from_columns(current_part))
};
if !parts.is_empty() && self.is_canonical {
if let Some(part) = parts.get_mut(0) {
part.is_canonical = true;
}
}
if parts.is_empty() {
parts.push(self.clone());
}
self.width = None;
parts
}
}
#[cfg(test)]
#[path = "./unit/grid_tests.rs"]
mod grid_tests;