diff --git a/xtask/src/build.rs b/xtask/src/build.rs index 4babad0c3f..67547a1c24 100644 --- a/xtask/src/build.rs +++ b/xtask/src/build.rs @@ -52,22 +52,38 @@ pub fn build(sh: &Shell, flags: flags::Build) -> anyhow::Result<()> { std::fs::create_dir_all(&prost_asset_dir).unwrap(); let mut prost = prost_build::Config::new(); + let last_generated = prost_asset_dir + .join("generated_plugin_api.rs") + .metadata() + .and_then(|m| m.modified()); + let mut needs_regeneration = false; prost.out_dir(prost_asset_dir); prost.include_file("generated_plugin_api.rs"); let mut proto_files = vec![]; for entry in std::fs::read_dir(&protobuf_source_dir).unwrap() { let entry_path = entry.unwrap().path(); if entry_path.is_file() { - if let Some(extension) = entry_path.extension() { - if extension == "proto" { - proto_files.push(entry_path.display().to_string()) - } + if !entry_path + .extension() + .map(|e| e == "proto") + .unwrap_or(false) + { + continue; + } + proto_files.push(entry_path.display().to_string()); + let modified = entry_path.metadata().and_then(|m| m.modified()); + needs_regeneration |= match (&last_generated, modified) { + (Ok(last_generated), Ok(modified)) => modified >= *last_generated, + // Couldn't read some metadata, assume needs update + _ => true, } } } - prost - .compile_protos(&proto_files, &[protobuf_source_dir]) - .unwrap(); + if needs_regeneration { + prost + .compile_protos(&proto_files, &[protobuf_source_dir]) + .unwrap(); + } } let _pd = sh.push_dir(Path::new(crate_name)); diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs index 3f99b4013d..7ad7a38bc8 100644 --- a/zellij-server/src/output/mod.rs +++ b/zellij-server/src/output/mod.rs @@ -6,7 +6,7 @@ use crate::panes::Row; use crate::{ panes::sixel::SixelImageStore, panes::terminal_character::{AnsiCode, CharacterStyles}, - panes::{LinkHandler, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}, + panes::{LinkHandler, TerminalCharacter, DEFAULT_STYLES, EMPTY_TERMINAL_CHARACTER}, ClientId, }; use std::cell::RefCell; @@ -91,14 +91,13 @@ fn serialize_chunks_with_newlines( let link_handler = link_handler.map(|l_h| l_h.borrow()); for character_chunk in character_chunks { let chunk_changed_colors = character_chunk.changed_colors(); - let mut character_styles = - CharacterStyles::new().enable_styled_underlines(styled_underlines); + let mut character_styles = DEFAULT_STYLES.enable_styled_underlines(styled_underlines); vte_output.push_str("\n\r"); let mut chunk_width = character_chunk.x; for t_character in character_chunk.terminal_characters.iter() { let current_character_styles = adjust_styles_for_possible_selection( character_chunk.selection_and_colors(), - t_character.styles, + *t_character.styles, character_chunk.y, chunk_width, ); @@ -110,10 +109,9 @@ fn serialize_chunks_with_newlines( &mut vte_output, ) .with_context(err_context)?; - chunk_width += t_character.width; + chunk_width += t_character.width(); vte_output.push(t_character.character); } - character_styles.clear(); } Ok(vte_output) } @@ -131,15 +129,14 @@ fn serialize_chunks( let link_handler = link_handler.map(|l_h| l_h.borrow()); for character_chunk in character_chunks { let chunk_changed_colors = character_chunk.changed_colors(); - let mut character_styles = - CharacterStyles::new().enable_styled_underlines(styled_underlines); + let mut character_styles = DEFAULT_STYLES.enable_styled_underlines(styled_underlines); vte_goto_instruction(character_chunk.x, character_chunk.y, &mut vte_output) .with_context(err_context)?; let mut chunk_width = character_chunk.x; for t_character in character_chunk.terminal_characters.iter() { let current_character_styles = adjust_styles_for_possible_selection( character_chunk.selection_and_colors(), - t_character.styles, + *t_character.styles, character_chunk.y, chunk_width, ); @@ -151,10 +148,9 @@ fn serialize_chunks( &mut vte_output, ) .with_context(err_context)?; - chunk_width += t_character.width; + chunk_width += t_character.width(); vte_output.push(t_character.character); } - character_styles.clear(); } if let Some(sixel_image_store) = sixel_image_store { if let Some(sixel_chunks) = sixel_chunks { @@ -215,7 +211,7 @@ fn adjust_middle_segment_for_wide_chars( let mut pad_left_end_by = 0; let mut pad_right_start_by = 0; for (absolute_index, t_character) in terminal_characters.iter().enumerate() { - current_x += t_character.width; + current_x += t_character.width(); if current_x >= middle_start && absolute_middle_start_index.is_none() { if current_x > middle_start { pad_left_end_by = current_x - middle_start; @@ -802,7 +798,7 @@ impl CharacterChunk { pub fn width(&self) -> usize { let mut width = 0; for t_character in &self.terminal_characters { - width += t_character.width + width += t_character.width() } width } @@ -814,14 +810,14 @@ impl CharacterChunk { break; } let next_character = self.terminal_characters.remove(0); // TODO: consider copying self.terminal_characters into a VecDeque to make this process faster? - if drained_part_len + next_character.width <= x { + if drained_part_len + next_character.width() <= x { + drained_part_len += next_character.width(); drained_part.push_back(next_character); - drained_part_len += next_character.width; } else { if drained_part_len == x { self.terminal_characters.insert(0, next_character); // put it back - } else if next_character.width > 1 { - for _ in 1..next_character.width { + } else if next_character.width() > 1 { + for _ in 1..next_character.width() { self.terminal_characters.insert(0, EMPTY_TERMINAL_CHARACTER); drained_part.push_back(EMPTY_TERMINAL_CHARACTER); } @@ -963,7 +959,7 @@ impl OutputBuffer { row: &Row, viewport_width: usize, ) -> Vec { - let mut terminal_characters: Vec = row.columns.iter().copied().collect(); + let mut terminal_characters: Vec = row.columns.iter().cloned().collect(); // pad row let row_width = row.width(); if row_width < viewport_width { diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index f74e2c07ba..3109cb10f4 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -3,7 +3,6 @@ use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use unicode_width::UnicodeWidthChar; use zellij_utils::data::Style; use zellij_utils::errors::prelude::*; use zellij_utils::regex::Regex; @@ -35,7 +34,7 @@ use crate::panes::link_handler::LinkHandler; use crate::panes::search::SearchResult; use crate::panes::selection::Selection; use crate::panes::terminal_character::{ - AnsiCode, CharacterStyles, CharsetIndex, Cursor, CursorShape, StandardCharset, + AnsiCode, CharsetIndex, Cursor, CursorShape, RcCharacterStyles, StandardCharset, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, }; use crate::ui::components::UiComponentParser; @@ -416,11 +415,8 @@ impl Debug for Grid { for y in image_top_edge..image_bottom_edge { let row = buffer.get_mut(y).unwrap(); for x in image_left_edge..image_right_edge { - let fake_sixel_terminal_character = TerminalCharacter { - character: sixel_indication_character(x), - width: 1, - styles: Default::default(), - }; + let fake_sixel_terminal_character = + TerminalCharacter::new_singlewidth(sixel_indication_character(x)); row.add_character_at(fake_sixel_terminal_character, x); } } @@ -517,7 +513,7 @@ impl Grid { 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: CharacterStyles) { + pub fn advance_to_next_tabstop(&mut self, styles: RcCharacterStyles) { let next_tabstop = self .horizontal_tabstops .iter() @@ -769,7 +765,7 @@ impl Grid { } self.selection.reset(); self.sixel_grid.character_cell_size_possibly_changed(); - if new_columns != self.width { + 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(); @@ -885,78 +881,42 @@ impl Grid { }, _ => None, }; - - let current_viewport_row_count = self.viewport.len(); - match current_viewport_row_count.cmp(&self.height) { - Ordering::Less => { - let row_count_to_transfer = self.height - 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 - self.height; - 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 - ), - } - }; - } - if new_rows != self.height { - let mut new_cursor_y = self.cursor.y; - let mut saved_cursor_y_coordinates = self + 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 new_cursor_x = self.cursor.x; 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 => { @@ -1035,7 +995,7 @@ impl Grid { .iter() .map(|r| { let excess_width = r.excess_width(); - let mut line: Vec = r.columns.iter().copied().collect(); + let mut line: Vec = r.columns.iter().cloned().collect(); // pad line line.resize( self.width.saturating_sub(excess_width), @@ -1234,7 +1194,7 @@ impl Grid { self.viewport.remove(scroll_region_bottom); } let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.cursor.pending_styles; + 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()); @@ -1250,10 +1210,10 @@ impl Grid { { self.pad_lines_until(scroll_region_bottom, EMPTY_TERMINAL_CHARACTER); let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.cursor.pending_styles; + 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; self.width]); + let columns = VecDeque::from(vec![pad_character.clone(); self.width]); self.viewport .insert(scroll_region_bottom, Row::from_columns(columns).canonical()); } @@ -1268,7 +1228,7 @@ impl Grid { }; for _ in 0..self.height { - let columns = VecDeque::from(vec![character; self.width]); + let columns = VecDeque::from(vec![character.clone(); self.width]); self.viewport.push(Row::from_columns(columns).canonical()); } self.output_buffer.update_all_lines(); @@ -1294,14 +1254,14 @@ impl Grid { } let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.cursor.pending_styles; + 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; + 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 @@ -1393,7 +1353,7 @@ impl Grid { } } pub fn add_character(&mut self, terminal_character: TerminalCharacter) { - let character_width = terminal_character.width; + 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: @@ -1415,7 +1375,7 @@ impl Grid { self.viewport .get(self.cursor.y) .and_then(|current_line| current_line.columns.get(absolute_x_in_line)) - .copied() + .cloned() } pub fn get_absolute_character_index(&self, x: usize, y: usize) -> usize { self.viewport.get(y).unwrap().absolute_character_index(x) @@ -1438,7 +1398,7 @@ impl Grid { 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; self.width]); + 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()); @@ -1448,8 +1408,8 @@ impl Grid { } 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); - let replace_with_columns = VecDeque::from(vec![replace_with; self.width]); for row in self.viewport.iter_mut().take(self.cursor.y) { row.replace_columns(replace_with_columns.clone()); } @@ -1463,7 +1423,7 @@ impl Grid { } } pub fn clear_all(&mut self, replace_with: TerminalCharacter) { - let replace_with_columns = VecDeque::from(vec![replace_with; self.width]); + 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()); @@ -1498,18 +1458,18 @@ impl Grid { 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); + 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); + 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; self.width]); + 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); } @@ -1528,13 +1488,13 @@ impl Grid { } else { self.cursor.y = std::cmp::min(self.height - 1, y + y_offset); } - self.pad_lines_until(self.cursor.y, pad_character); + 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); + self.pad_lines_until(self.cursor.y, pad_character.clone()); self.pad_current_line_until(self.cursor.x, pad_character); }, } @@ -1610,7 +1570,7 @@ impl Grid { let bottom_line_index = bottom_line_index.unwrap_or(self.height); self.scroll_region = Some((top_line_index, bottom_line_index)); let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.cursor.pending_styles; + 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) { @@ -1634,7 +1594,7 @@ impl Grid { // region for _ in 0..count { self.viewport.remove(current_line_index); - let columns = VecDeque::from(vec![pad_character; self.width]); + 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()); @@ -1663,7 +1623,7 @@ impl Grid { if scroll_region_bottom < self.viewport.len() { self.viewport.remove(scroll_region_bottom); } - let columns = VecDeque::from(vec![pad_character; self.width]); + let columns = VecDeque::from(vec![pad_character.clone(); self.width]); self.viewport .insert(current_line_index, Row::from_columns(columns).canonical()); } @@ -1682,19 +1642,19 @@ impl Grid { 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: CharacterStyles) { + 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); + 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, self.cursor.x + i); + 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: CharacterStyles) { + 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) { @@ -1708,13 +1668,13 @@ impl Grid { 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) + .map(|terminal_character| terminal_character.width()) .unwrap_or(0) .saturating_sub(1); for _ in 0..excess_width { - current_row.insert_character_at(empty_character, self.cursor.x); + current_row.insert_character_at(empty_character.clone(), self.cursor.x); } - current_row.push(empty_character); + current_row.push(empty_character.clone()); } self.output_buffer.update_line(self.cursor.y); } @@ -1839,7 +1799,7 @@ impl Grid { line_selection.push(terminal_character.character); } - terminal_col += terminal_character.width; + terminal_col += terminal_character.width(); } if row.is_canonical { @@ -2192,14 +2152,9 @@ impl Perform for Grid { fn print(&mut self, c: char) { let c = self.cursor.charsets[self.active_charset].map(c); - // apparently, building TerminalCharacter like this without a "new" method - // is a little faster - let terminal_character = TerminalCharacter { - character: c, - width: c.width().unwrap_or(0), - styles: self.cursor.pending_styles, - }; - self.set_preceding_character(terminal_character); + let terminal_character = + TerminalCharacter::new_styled(c, self.cursor.pending_styles.clone()); + self.set_preceding_character(terminal_character.clone()); self.add_character(terminal_character); } @@ -2214,7 +2169,7 @@ impl Perform for Grid { }, 9 => { // tab - self.advance_to_next_tabstop(self.cursor.pending_styles); + self.advance_to_next_tabstop(self.cursor.pending_styles.clone()); }, 10 | 11 | 12 => { // 0a, newline @@ -2342,8 +2297,9 @@ impl Perform for Grid { if params.len() < 3 { return; } - self.cursor.pending_styles.link_anchor = - self.link_handler.borrow_mut().dispatch_osc8(params); + 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 @@ -2496,7 +2452,7 @@ impl Perform for Grid { if intermediates.is_empty() { self.cursor .pending_styles - .add_style_from_ansi_params(&mut params_iter); + .update(|styles| styles.add_style_from_ansi_params(&mut params_iter)) } } else if c == 'C' || c == 'a' { // move cursor forward @@ -2507,7 +2463,9 @@ impl Perform for Grid { 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.background = Some(background_color); + 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); @@ -2521,7 +2479,9 @@ impl Perform for Grid { // 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.background = Some(background_color); + 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 { @@ -2783,13 +2743,13 @@ impl Perform for Grid { // delete lines if currently inside scroll region let line_count_to_delete = next_param_or(1); let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.cursor.pending_styles; + 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 let line_count_to_add = next_param_or(1); let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.cursor.pending_styles; + 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); @@ -2810,11 +2770,11 @@ impl Perform for Grid { } else if c == 'P' { // erase characters let count = next_param_or(1); - self.erase_characters(count, self.cursor.pending_styles); + 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); + self.replace_with_empty_chars(count, self.cursor.pending_styles.clone()); } else if c == 'T' { /* * 124 54 T SD @@ -2871,13 +2831,13 @@ impl Perform for Grid { let count = next_param_or(1); for _ in 0..count { let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.cursor.pending_styles; + 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 { + if let Some(c) = self.preceding_char.clone() { for _ in 0..next_param_or(1) { - self.add_character(c); + self.add_character(c.clone()); } } } else if c == 'E' { @@ -2893,7 +2853,7 @@ impl Perform for Grid { 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); + 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' ')); @@ -3210,7 +3170,7 @@ impl Row { } else { let mut width = 0; for terminal_character in &self.columns { - width += terminal_character.width; + width += terminal_character.width(); } self.width = Some(width); width @@ -3219,15 +3179,15 @@ impl Row { pub fn width(&self) -> usize { let mut width = 0; for terminal_character in &self.columns { - width += terminal_character.width; + 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; + if terminal_character.width() > 1 { + acc += terminal_character.width() - 1; } } acc @@ -3235,8 +3195,8 @@ impl Row { 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; + if terminal_character.width() > 1 { + acc += terminal_character.width() - 1; } } acc @@ -3248,7 +3208,7 @@ impl Row { if i == absolute_index { break; } - if terminal_character.width > 1 { + if terminal_character.width() > 1 { absolute_index = absolute_index.saturating_sub(1); } } @@ -3261,10 +3221,10 @@ impl Row { 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; + accumulated_width += terminal_character.width(); absolute_index = i; if accumulated_width > x { - let character_start_position = accumulated_width - terminal_character.width; + let character_start_position = accumulated_width - terminal_character.width(); position_inside_character = x - character_start_position; break; } @@ -3274,10 +3234,10 @@ impl Row { 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); - // this is unwrapped because this always happens after self.width_cached() - *self.width.as_mut().unwrap() += terminal_character.width; }, Ordering::Less => { // adding the character after the end of the current line @@ -3293,17 +3253,17 @@ impl Row { // 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 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) { + 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 { + if removed.width() > 1 { // the character we removed is a wide character itself, so we add // padding self.columns @@ -3348,15 +3308,13 @@ impl Row { } pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { let absolute_x_index = self.absolute_character_index(x); - if absolute_x_index < self.columns.len() { - self.columns.push_back(terminal_character); - // this is much more performant than remove/insert - if let Some(character) = self.columns.swap_remove_back(absolute_x_index) { - let excess_width = character.width.saturating_sub(terminal_character.width); - for _ in 0..excess_width { - self.columns - .insert(absolute_x_index, EMPTY_TERMINAL_CHARACTER); - } + 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; @@ -3383,8 +3341,8 @@ impl Row { if index == position { break; } - if terminal_character.width > 1 { - position = position.saturating_sub(terminal_character.width.saturating_sub(1)); + if terminal_character.width() > 1 { + position = position.saturating_sub(terminal_character.width().saturating_sub(1)); } } position @@ -3415,8 +3373,8 @@ impl Row { 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; + if drained_part_len + next_character.width() <= x || drained_part_len == 0 { + drained_part_len += next_character.width(); split_pos += 1 } else { break; @@ -3432,7 +3390,7 @@ impl Row { let width_of_current_character = self .columns .get(to_position_accounting_for_widechars) - .map(|character| character.width) + .map(|character| character.width()) .unwrap_or(1); let mut replace_with = VecDeque::from(vec![terminal_character; to + width_of_current_character]); @@ -3467,13 +3425,13 @@ impl Row { let mut current_part: VecDeque = VecDeque::new(); let mut current_part_len = 0; for character in self.columns.drain(..) { - if current_part_len + character.width > max_row_length { + 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); - current_part_len += character.width; } if !current_part.is_empty() { parts.push(Row::from_columns(current_part)) diff --git a/zellij-server/src/panes/terminal_character.rs b/zellij-server/src/panes/terminal_character.rs index 50f3ec8e8d..2bb1326496 100644 --- a/zellij-server/src/panes/terminal_character.rs +++ b/zellij-server/src/panes/terminal_character.rs @@ -1,6 +1,7 @@ use std::convert::From; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::{Index, IndexMut}; +use std::rc::Rc; use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthStr; @@ -15,7 +16,7 @@ use crate::panes::alacritty_functions::parse_sgr_color; pub const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter { character: ' ', width: 1, - styles: RESET_STYLES, + styles: RcCharacterStyles::Reset, }; pub const RESET_STYLES: CharacterStyles = CharacterStyles { @@ -35,6 +36,30 @@ pub const RESET_STYLES: CharacterStyles = CharacterStyles { styled_underlines_enabled: false, }; +// Prefer to use RcCharacterStyles::default() where it makes sense +// as it will reduce memory usage +pub const DEFAULT_STYLES: CharacterStyles = CharacterStyles { + foreground: None, + background: None, + underline_color: None, + strike: None, + hidden: None, + reverse: None, + slow_blink: None, + fast_blink: None, + underline: None, + bold: None, + dim: None, + italic: None, + link_anchor: None, + styled_underlines_enabled: false, +}; + +thread_local! { + static RC_DEFAULT_STYLES: RcCharacterStyles = + RcCharacterStyles::Rc(Rc::new(DEFAULT_STYLES)); +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AnsiCode { On, @@ -129,7 +154,62 @@ impl NamedColor { } } -#[derive(Clone, Copy, Debug, Default)] +// This enum carefully only has two variants so +// enum niche optimisations can keep it to 8 bytes +#[derive(Clone, Debug, PartialEq)] +pub enum RcCharacterStyles { + Reset, + Rc(Rc), +} +const _: [(); 8] = [(); std::mem::size_of::()]; + +impl From for RcCharacterStyles { + fn from(styles: CharacterStyles) -> Self { + if styles == RESET_STYLES { + RcCharacterStyles::Reset + } else { + RcCharacterStyles::Rc(Rc::new(styles)) + } + } +} + +impl Default for RcCharacterStyles { + fn default() -> Self { + RC_DEFAULT_STYLES.with(|s| s.clone()) + } +} + +impl std::ops::Deref for RcCharacterStyles { + type Target = CharacterStyles; + + fn deref(&self) -> &Self::Target { + match self { + RcCharacterStyles::Reset => &RESET_STYLES, + RcCharacterStyles::Rc(styles) => &*styles, + } + } +} + +impl Display for RcCharacterStyles { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let styles: &CharacterStyles = &*self; + Display::fmt(&styles, f) + } +} + +impl RcCharacterStyles { + pub fn reset() -> Self { + Self::Reset + } + + pub fn update(&mut self, f: impl FnOnce(&mut CharacterStyles)) { + let mut styles: CharacterStyles = **self; + f(&mut styles); + *self = styles.into(); + } +} + +#[derive(Clone, Copy, Debug)] pub struct CharacterStyles { pub foreground: Option, pub background: Option, @@ -166,9 +246,6 @@ impl PartialEq for CharacterStyles { } impl CharacterStyles { - pub fn new() -> Self { - Self::default() - } pub fn foreground(mut self, foreground_code: Option) -> Self { self.foreground = foreground_code; self @@ -255,8 +332,7 @@ impl CharacterStyles { } // create diff from all changed styles - let mut diff = - CharacterStyles::new().enable_styled_underlines(self.styled_underlines_enabled); + let mut diff = DEFAULT_STYLES.enable_styled_underlines(self.styled_underlines_enabled); if self.foreground != new_styles.foreground { diff.foreground = new_styles.foreground; @@ -315,7 +391,7 @@ impl CharacterStyles { } Some(diff) } - pub fn reset_all(&mut self) { + fn reset_ansi(&mut self) { self.foreground = Some(AnsiCode::Reset); self.background = Some(AnsiCode::Reset); self.underline_color = Some(AnsiCode::Reset); @@ -328,11 +404,12 @@ impl CharacterStyles { self.reverse = Some(AnsiCode::Reset); self.hidden = Some(AnsiCode::Reset); self.strike = Some(AnsiCode::Reset); + // Deliberately don't end link anchor } pub fn add_style_from_ansi_params(&mut self, params: &mut ParamsIter) { while let Some(param) = params.next() { match param { - [] | [0] => self.reset_all(), + [] | [0] => self.reset_ansi(), [1] => *self = self.bold(Some(AnsiCode::On)), [2] => *self = self.dim(Some(AnsiCode::On)), [3] => *self = self.italic(Some(AnsiCode::On)), @@ -813,7 +890,7 @@ impl CursorShape { pub struct Cursor { pub x: usize, pub y: usize, - pub pending_styles: CharacterStyles, + pub pending_styles: RcCharacterStyles, pub charsets: Charsets, shape: CursorShape, } @@ -823,7 +900,9 @@ impl Cursor { Cursor { x, y, - pending_styles: RESET_STYLES.enable_styled_underlines(styled_underlines), + pending_styles: RESET_STYLES + .enable_styled_underlines(styled_underlines) + .into(), charsets: Default::default(), shape: CursorShape::Initial, } @@ -836,21 +915,48 @@ impl Cursor { } } -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, PartialEq)] pub struct TerminalCharacter { pub character: char, - pub styles: CharacterStyles, - pub width: usize, + pub styles: RcCharacterStyles, + width: u8, } +// This size has significant memory and CPU implications for long lines, +// be careful about allowing it to grow +const _: [(); 16] = [(); std::mem::size_of::()]; impl TerminalCharacter { + #[inline] pub fn new(character: char) -> Self { + Self::new_styled(character, Default::default()) + } + + #[inline] + pub fn new_styled(character: char, styles: RcCharacterStyles) -> Self { + TerminalCharacter { + character, + styles, + width: character.width().unwrap_or(0) as u8, + } + } + + #[inline] + pub fn new_singlewidth(character: char) -> Self { + Self::new_singlewidth_styled(character, Default::default()) + } + + #[inline] + pub fn new_singlewidth_styled(character: char, styles: RcCharacterStyles) -> Self { TerminalCharacter { character, - styles: CharacterStyles::default(), - width: character.width().unwrap_or(0), + styles, + width: 1, } } + + pub fn width(&self) -> usize { + self.width as usize + } } impl ::std::fmt::Debug for TerminalCharacter { diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index bff78b981d..ab36664f67 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -437,8 +437,10 @@ impl Pane for TerminalPane { .grid .get_character_under_cursor() .unwrap_or(EMPTY_TERMINAL_CHARACTER); - character_under_cursor.styles.background = Some(cursor_color.into()); - character_under_cursor.styles.foreground = Some(text_color.into()); + character_under_cursor.styles.update(|styles| { + styles.background = Some(cursor_color.into()); + styles.foreground = Some(text_color.into()); + }); // we keep track of these so that we can clear them up later (see render function) self.fake_cursor_locations.insert((cursor_y, cursor_x)); let mut fake_cursor = format!( diff --git a/zellij-server/src/ui/boundaries.rs b/zellij-server/src/ui/boundaries.rs index 29b614c102..0df0e4eeae 100644 --- a/zellij-server/src/ui/boundaries.rs +++ b/zellij-server/src/ui/boundaries.rs @@ -63,12 +63,12 @@ impl BoundarySymbol { self.boundary_type ) })?; - TerminalCharacter { + TerminalCharacter::new_singlewidth_styled( character, - width: 1, - styles: RESET_STYLES - .foreground(self.color.map(|palette_color| palette_color.into())), - } + RESET_STYLES + .foreground(self.color.map(|palette_color| palette_color.into())) + .into(), + ) }; Ok(tc) } diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index 745786de5f..57cff1ce42 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -1,5 +1,5 @@ use crate::output::CharacterChunk; -use crate::panes::{AnsiCode, CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}; +use crate::panes::{AnsiCode, RcCharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}; use crate::ui::boundaries::boundary_type; use crate::ClientId; use zellij_utils::data::{client_id_to_colors, PaletteColor, Style}; @@ -11,25 +11,17 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; fn foreground_color(characters: &str, color: Option) -> Vec { let mut colored_string = Vec::new(); for character in characters.chars() { - let styles = match color { - Some(palette_color) => { - let mut styles = CharacterStyles::new(); - styles.reset_all(); - styles - .foreground(Some(AnsiCode::from(palette_color))) - .bold(Some(AnsiCode::On)) - }, - None => { - let mut styles = CharacterStyles::new(); - styles.reset_all(); - styles.bold(Some(AnsiCode::On)) - }, - }; - let terminal_character = TerminalCharacter { - character, - styles, - width: character.width().unwrap_or(0), - }; + let mut styles = RcCharacterStyles::reset(); + styles.update(|styles| { + styles.bold = Some(AnsiCode::On); + match color { + Some(palette_color) => { + styles.foreground = Some(AnsiCode::from(palette_color)); + }, + None => {}, + } + }); + let terminal_character = TerminalCharacter::new_styled(character, styles); colored_string.push(terminal_character); } colored_string @@ -38,25 +30,15 @@ fn foreground_color(characters: &str, color: Option) -> Vec) -> Vec { let mut colored_string = Vec::new(); for character in characters.chars() { - let styles = match color { + let mut styles = RcCharacterStyles::reset(); + styles.update(|styles| match color { Some(palette_color) => { - let mut styles = CharacterStyles::new(); - styles.reset_all(); - styles - .background(Some(AnsiCode::from(palette_color))) - .bold(Some(AnsiCode::On)) - }, - None => { - let mut styles = CharacterStyles::new(); - styles.reset_all(); - styles + styles.background = Some(AnsiCode::from(palette_color)); + styles.bold(Some(AnsiCode::On)); }, - }; - let terminal_character = TerminalCharacter { - character, - styles, - width: character.width().unwrap_or(0), - }; + None => {}, + }); + let terminal_character = TerminalCharacter::new_styled(character, styles); colored_string.push(terminal_character); } colored_string