fix(terminal): improve reflow performance and general memory usage by reducing size of TerminalCharacter (#3043)

* refactor: Simplify transfer_rows_from_viewport_to_lines_above

next_lines is always consolidated to a single Row, which immediately
gets removed - we can remove some dead code as a result

* perf: Batch remove rows from the viewport for performance

Given a 1MB line catted into the terminal, a toggle-fullscreen +
toggle-fullscreen + close-pane + `run true` goes from ~9s to ~3s

* perf: Optimize Row::drain_until by splitting chars in one step

Given a 10MB line catted into the terminal, a toggle-fullscreen +
toggle-fullscreen + close-pane + `run true` goes from ~23s to ~20s

* refactor: Simplify `if let` into a `.map`

* refactor: There are only new saved coordinates when there were old ones

* refactor: Unify viewport transfer: use common variable names

* fix: Use same saved cursor logic in height resize as width

See #2182 for original introduction that only added it in one branch,
this fixes an issue where the saved cursor was incorrectly reset when
the real cursor was

* fix: Correct saved+real cursor calculations when reflowing long lines

* fix: Don't create canonical lines if cursor ends on EOL after resize

Previously if a 20 character line were split into two 10 character
lines, the cursor would be placed on the line after the two lines.
New characters would then be treated as a new canonical line. This
commit fixes this by biasing cursors to the end of the previous line.

* fix: for cursor index calculation in lines that are already wrapped

* chore: test for real/saved cursor position being handled separately

* chore: Apply cargo format

* refactor: Unify viewport transfer: transfer + cursor update together

* perf: Reduce size of TerminalCharacter from 72 to 60 bytes

With a 10MB single line catted into a fresh terminal, VmRSS goes from
964092 kB to 874020 kB (as reported by /proc/<pid>/status) before/after
this patch

Given a 10MB line catted into the terminal, a toggle-fullscreen +
toggle-fullscreen + close-pane + `run true` goes from ~15s to ~12.5s

* fix(build): Don't unconditionally rebuild zellij-utils

* refactor: Remove Copy impl on TerminalCharacter

* perf: Rc styles to reduce TerminalCharacter from 60 to 24 bytes

With a 10MB single line catted into a fresh terminal, VmRSS goes from
845156 kB to 478396 kB (as reported by /proc/<pid>/status) before/after
this patch

Given a 10MB line catted into the terminal, a toggle-fullscreen +
toggle-fullscreen + close-pane + `run true` goes from ~12.5s to ~7s

* perf: Remove RcCharacterStyles::Default, allow enum niche optimisation

This reduces TerminalCharacter from 24 to 16 bytes

With a 10MB single line catted into a fresh terminal, VmRSS goes from
478396 kB to 398108 kB (as reported by /proc/<pid>/status) before/after
this patch

Given a 10MB line catted into the terminal, a toggle-fullscreen +
toggle-fullscreen + close-pane + `run true` goes from ~7s to ~4.75s

* docs: link anchor omission from reset_all is deliberate

reset_all is only used from ansi params, and ansi params don't control
link anchor

* fix: Remove no-op on variable that gets immediately dropped

* refactor: Simplify replace_character_at logic

The original condition checked absolute_x_index was in bounds, then
used the index to manipulate it. This is equivalent to getting a
ref to the character at that position and manipulating directly

* chore: Run xtask format

* chore(repo): update issue templates

* Bump rust version to 1.75.0 (#3039)

* rust-toolchain: Bump toolchain version to 1.69.0

which, compared to the previous 1.67.0, has the following impacts on
`zellij`:

- [Turn off debuginfo for build deps][2]: Increases build time (on my
  machine) from ~230 s in 1.67.0 to ~250 s now, *which is unexpected*

This version also changes [handling of the `default-features` flag][3]
when specifying dependencies in `Cargo.toml`. If a dependent crate
requires `default-features = true` on a crate that is required as
`default-features = false` further up the dependency tree, the `true`
setting "wins". We only specify `default-features = false` for three
crates total:

- `names`: This is used only by us
- `surf`: This is used only by us
- `vte`: This is also required by `strip-ansi-escapes`, but that has
  `default-features = false` as well

How this affects our transitive dependencies is unknown at this point.

[2]: https://github.com/rust-lang/cargo/pull/11252/
[3]: https://github.com/rust-lang/cargo/pull/11409/

* rust-toolchain: Bump toolchain version to 1.70.0

which, compared to the previous 1.69.0, as the following impacts on
`zellij`:

1. [Enable sparse registry checkout for crates.io by default][1]

This drastically increases the time to first build on a fresh rust
installation/a rust installation with a clean cargo registry cache.
Previously it took about 75s to populate the deps/cache (with `cargo
fetch --locked` and ~100 MBit/s network), whereas now the same process
takes ~10 s.

2. [The `OnceCell` type is now part of std][2]

In theory, this would allow us to cut a dependency from `zellij-utils`,
but the `once_cell` crate is pulled in by another 16 deps, so there's no
point in attempting it right now.

Build times and binary sizes are unaffected by this change compared to
the previous 1.69.0 toolchain.

[1]: https://github.com/rust-lang/cargo/pull/11791/
[2]: https://doc.rust-lang.org/stable/std/cell/struct.OnceCell.html

* rust-toolchain: Bump toolchain version to 1.75.0

which, compared to the previous 1.70.0, has the following impacts on
`zellij`:

1. [cross-crate inlining][8]

This should increase application performance, as functions can now be
inlined across crates.

2. [`async fn` in traits][9]

This would allow us to drop the `async_trait` dependency, but it is
currently still required by 3 other dependencies.

Build time in debug mode (on my own PC) is cut down from 256s to 189s
(for a clean build). Build time in release mode is cut down from 473s to
391s (for a clean build). Binary sizes only change minimally (825 MB ->
807 MB in debug, 29 MB -> 30 MB in release).

[8]: https://github.com/rust-lang/rust/pull/116505
[9]: https://github.com/rust-lang/rust/pull/115822/

* chore: Apply rustfmt.

* CHANGELOG: Add PR #3039.

* feat(plugins): introduce 'pipes', allowing users to pipe data to and control plugins from the command line (#3066)

* prototype - working with message from the cli

* prototype - pipe from the CLI to plugins

* prototype - pipe from the CLI to plugins and back again

* prototype - working with better cli interface

* prototype - working after removing unused stuff

* prototype - working with launching plugin if it is not launched, also fixed event ordering

* refactor: change message to cli-message

* prototype - allow plugins to send messages to each other

* fix: allow cli messages to send plugin parameters (and implement backpressure)

* fix: use input_pipe_id to identify cli pipes instead of their message name

* fix: come cleanups and add skip_cache parameter

* fix: pipe/client-server communication robustness

* fix: leaking messages between plugins while loading

* feat: allow plugins to specify how a new plugin instance is launched when sending messages

* fix: add permissions

* refactor: adjust cli api

* fix: improve cli plugin loading error messages

* docs: cli pipe

* fix: take plugin configuration into account when messaging between plugins

* refactor: pipe message protobuf interface

* refactor: update(event) -> pipe

* refactor - rename CliMessage to CliPipe

* fix: add is_private to pipes and change some naming

* refactor - cli client

* refactor: various cleanups

* style(fmt): rustfmt

* fix(pipes): backpressure across multiple plugins

* style: some cleanups

* style(fmt): rustfmt

* style: fix merge conflict mistake

* style(wording): clarify pipe permission

* docs(changelog): introduce pipes

* xtask: Disable pusing during publish (#3040)

* xtask: Add `--no-push` flag to `publish`

which can be used when simulating releases to work without a writable
git fork of the zellij code.

* xtask: Fix borrow issues

* xtask/pipe: Require lockfile in publish

to avoid errors from invalid dependency versions.

* CHANGELOG: Add PR #3040.

* fix(terminal): some real/saved cursor bugs during resize (#3032)

* refactor: Simplify transfer_rows_from_viewport_to_lines_above

next_lines is always consolidated to a single Row, which immediately
gets removed - we can remove some dead code as a result

* perf: Batch remove rows from the viewport for performance

Given a 1MB line catted into the terminal, a toggle-fullscreen +
toggle-fullscreen + close-pane + `run true` goes from ~9s to ~3s

* perf: Optimize Row::drain_until by splitting chars in one step

Given a 10MB line catted into the terminal, a toggle-fullscreen +
toggle-fullscreen + close-pane + `run true` goes from ~23s to ~20s

* refactor: Simplify `if let` into a `.map`

* refactor: There are only new saved coordinates when there were old ones

* refactor: Unify viewport transfer: use common variable names

* fix: Use same saved cursor logic in height resize as width

See #2182 for original introduction that only added it in one branch,
this fixes an issue where the saved cursor was incorrectly reset when
the real cursor was

* fix: Correct saved+real cursor calculations when reflowing long lines

* fix: Don't create canonical lines if cursor ends on EOL after resize

Previously if a 20 character line were split into two 10 character
lines, the cursor would be placed on the line after the two lines.
New characters would then be treated as a new canonical line. This
commit fixes this by biasing cursors to the end of the previous line.

* fix: for cursor index calculation in lines that are already wrapped

* chore: test for real/saved cursor position being handled separately

* chore: Apply cargo format

* chore(repo): update issue templates

* Bump rust version to 1.75.0 (#3039)

* rust-toolchain: Bump toolchain version to 1.69.0

which, compared to the previous 1.67.0, has the following impacts on
`zellij`:

- [Turn off debuginfo for build deps][2]: Increases build time (on my
  machine) from ~230 s in 1.67.0 to ~250 s now, *which is unexpected*

This version also changes [handling of the `default-features` flag][3]
when specifying dependencies in `Cargo.toml`. If a dependent crate
requires `default-features = true` on a crate that is required as
`default-features = false` further up the dependency tree, the `true`
setting "wins". We only specify `default-features = false` for three
crates total:

- `names`: This is used only by us
- `surf`: This is used only by us
- `vte`: This is also required by `strip-ansi-escapes`, but that has
  `default-features = false` as well

How this affects our transitive dependencies is unknown at this point.

[2]: https://github.com/rust-lang/cargo/pull/11252/
[3]: https://github.com/rust-lang/cargo/pull/11409/

* rust-toolchain: Bump toolchain version to 1.70.0

which, compared to the previous 1.69.0, as the following impacts on
`zellij`:

1. [Enable sparse registry checkout for crates.io by default][1]

This drastically increases the time to first build on a fresh rust
installation/a rust installation with a clean cargo registry cache.
Previously it took about 75s to populate the deps/cache (with `cargo
fetch --locked` and ~100 MBit/s network), whereas now the same process
takes ~10 s.

2. [The `OnceCell` type is now part of std][2]

In theory, this would allow us to cut a dependency from `zellij-utils`,
but the `once_cell` crate is pulled in by another 16 deps, so there's no
point in attempting it right now.

Build times and binary sizes are unaffected by this change compared to
the previous 1.69.0 toolchain.

[1]: https://github.com/rust-lang/cargo/pull/11791/
[2]: https://doc.rust-lang.org/stable/std/cell/struct.OnceCell.html

* rust-toolchain: Bump toolchain version to 1.75.0

which, compared to the previous 1.70.0, has the following impacts on
`zellij`:

1. [cross-crate inlining][8]

This should increase application performance, as functions can now be
inlined across crates.

2. [`async fn` in traits][9]

This would allow us to drop the `async_trait` dependency, but it is
currently still required by 3 other dependencies.

Build time in debug mode (on my own PC) is cut down from 256s to 189s
(for a clean build). Build time in release mode is cut down from 473s to
391s (for a clean build). Binary sizes only change minimally (825 MB ->
807 MB in debug, 29 MB -> 30 MB in release).

[8]: https://github.com/rust-lang/rust/pull/116505
[9]: https://github.com/rust-lang/rust/pull/115822/

* chore: Apply rustfmt.

* CHANGELOG: Add PR #3039.

* feat(plugins): introduce 'pipes', allowing users to pipe data to and control plugins from the command line (#3066)

* prototype - working with message from the cli

* prototype - pipe from the CLI to plugins

* prototype - pipe from the CLI to plugins and back again

* prototype - working with better cli interface

* prototype - working after removing unused stuff

* prototype - working with launching plugin if it is not launched, also fixed event ordering

* refactor: change message to cli-message

* prototype - allow plugins to send messages to each other

* fix: allow cli messages to send plugin parameters (and implement backpressure)

* fix: use input_pipe_id to identify cli pipes instead of their message name

* fix: come cleanups and add skip_cache parameter

* fix: pipe/client-server communication robustness

* fix: leaking messages between plugins while loading

* feat: allow plugins to specify how a new plugin instance is launched when sending messages

* fix: add permissions

* refactor: adjust cli api

* fix: improve cli plugin loading error messages

* docs: cli pipe

* fix: take plugin configuration into account when messaging between plugins

* refactor: pipe message protobuf interface

* refactor: update(event) -> pipe

* refactor - rename CliMessage to CliPipe

* fix: add is_private to pipes and change some naming

* refactor - cli client

* refactor: various cleanups

* style(fmt): rustfmt

* fix(pipes): backpressure across multiple plugins

* style: some cleanups

* style(fmt): rustfmt

* style: fix merge conflict mistake

* style(wording): clarify pipe permission

* docs(changelog): introduce pipes

* fix: add some robustness and future proofing

* fix e2e tests

---------

Co-authored-by: Aram Drevekenin <aram@poor.dev>
Co-authored-by: har7an <99636919+har7an@users.noreply.github.com>

* fix integer overflow again (oops)

---------

Co-authored-by: Aram Drevekenin <aram@poor.dev>
Co-authored-by: har7an <99636919+har7an@users.noreply.github.com>
This commit is contained in:
Aidan Hobson Sayers 2024-01-22 14:28:57 +00:00 committed by GitHub
parent 96b70e688e
commit 8fd7a67898
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 298 additions and 238 deletions

View file

@ -52,23 +52,39 @@ pub fn build(sh: &Shell, flags: flags::Build) -> anyhow::Result<()> {
std::fs::create_dir_all(&prost_asset_dir).unwrap(); std::fs::create_dir_all(&prost_asset_dir).unwrap();
let mut prost = prost_build::Config::new(); 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.out_dir(prost_asset_dir);
prost.include_file("generated_plugin_api.rs"); prost.include_file("generated_plugin_api.rs");
let mut proto_files = vec![]; let mut proto_files = vec![];
for entry in std::fs::read_dir(&protobuf_source_dir).unwrap() { for entry in std::fs::read_dir(&protobuf_source_dir).unwrap() {
let entry_path = entry.unwrap().path(); let entry_path = entry.unwrap().path();
if entry_path.is_file() { if entry_path.is_file() {
if let Some(extension) = entry_path.extension() { if !entry_path
if extension == "proto" { .extension()
proto_files.push(entry_path.display().to_string()) .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,
} }
} }
} }
if needs_regeneration {
prost prost
.compile_protos(&proto_files, &[protobuf_source_dir]) .compile_protos(&proto_files, &[protobuf_source_dir])
.unwrap(); .unwrap();
} }
}
let _pd = sh.push_dir(Path::new(crate_name)); let _pd = sh.push_dir(Path::new(crate_name));
// Tell the user where we are now // Tell the user where we are now

View file

@ -6,7 +6,7 @@ use crate::panes::Row;
use crate::{ use crate::{
panes::sixel::SixelImageStore, panes::sixel::SixelImageStore,
panes::terminal_character::{AnsiCode, CharacterStyles}, panes::terminal_character::{AnsiCode, CharacterStyles},
panes::{LinkHandler, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}, panes::{LinkHandler, TerminalCharacter, DEFAULT_STYLES, EMPTY_TERMINAL_CHARACTER},
ClientId, ClientId,
}; };
use std::cell::RefCell; use std::cell::RefCell;
@ -91,14 +91,13 @@ fn serialize_chunks_with_newlines(
let link_handler = link_handler.map(|l_h| l_h.borrow()); let link_handler = link_handler.map(|l_h| l_h.borrow());
for character_chunk in character_chunks { for character_chunk in character_chunks {
let chunk_changed_colors = character_chunk.changed_colors(); let chunk_changed_colors = character_chunk.changed_colors();
let mut character_styles = let mut character_styles = DEFAULT_STYLES.enable_styled_underlines(styled_underlines);
CharacterStyles::new().enable_styled_underlines(styled_underlines);
vte_output.push_str("\n\r"); vte_output.push_str("\n\r");
let mut chunk_width = character_chunk.x; let mut chunk_width = character_chunk.x;
for t_character in character_chunk.terminal_characters.iter() { for t_character in character_chunk.terminal_characters.iter() {
let current_character_styles = adjust_styles_for_possible_selection( let current_character_styles = adjust_styles_for_possible_selection(
character_chunk.selection_and_colors(), character_chunk.selection_and_colors(),
t_character.styles, *t_character.styles,
character_chunk.y, character_chunk.y,
chunk_width, chunk_width,
); );
@ -110,10 +109,9 @@ fn serialize_chunks_with_newlines(
&mut vte_output, &mut vte_output,
) )
.with_context(err_context)?; .with_context(err_context)?;
chunk_width += t_character.width; chunk_width += t_character.width();
vte_output.push(t_character.character); vte_output.push(t_character.character);
} }
character_styles.clear();
} }
Ok(vte_output) Ok(vte_output)
} }
@ -131,15 +129,14 @@ fn serialize_chunks(
let link_handler = link_handler.map(|l_h| l_h.borrow()); let link_handler = link_handler.map(|l_h| l_h.borrow());
for character_chunk in character_chunks { for character_chunk in character_chunks {
let chunk_changed_colors = character_chunk.changed_colors(); let chunk_changed_colors = character_chunk.changed_colors();
let mut character_styles = let mut character_styles = DEFAULT_STYLES.enable_styled_underlines(styled_underlines);
CharacterStyles::new().enable_styled_underlines(styled_underlines);
vte_goto_instruction(character_chunk.x, character_chunk.y, &mut vte_output) vte_goto_instruction(character_chunk.x, character_chunk.y, &mut vte_output)
.with_context(err_context)?; .with_context(err_context)?;
let mut chunk_width = character_chunk.x; let mut chunk_width = character_chunk.x;
for t_character in character_chunk.terminal_characters.iter() { for t_character in character_chunk.terminal_characters.iter() {
let current_character_styles = adjust_styles_for_possible_selection( let current_character_styles = adjust_styles_for_possible_selection(
character_chunk.selection_and_colors(), character_chunk.selection_and_colors(),
t_character.styles, *t_character.styles,
character_chunk.y, character_chunk.y,
chunk_width, chunk_width,
); );
@ -151,10 +148,9 @@ fn serialize_chunks(
&mut vte_output, &mut vte_output,
) )
.with_context(err_context)?; .with_context(err_context)?;
chunk_width += t_character.width; chunk_width += t_character.width();
vte_output.push(t_character.character); vte_output.push(t_character.character);
} }
character_styles.clear();
} }
if let Some(sixel_image_store) = sixel_image_store { if let Some(sixel_image_store) = sixel_image_store {
if let Some(sixel_chunks) = sixel_chunks { 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_left_end_by = 0;
let mut pad_right_start_by = 0; let mut pad_right_start_by = 0;
for (absolute_index, t_character) in terminal_characters.iter().enumerate() { 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 && absolute_middle_start_index.is_none() {
if current_x > middle_start { if current_x > middle_start {
pad_left_end_by = current_x - middle_start; pad_left_end_by = current_x - middle_start;
@ -802,7 +798,7 @@ impl CharacterChunk {
pub fn width(&self) -> usize { pub fn width(&self) -> usize {
let mut width = 0; let mut width = 0;
for t_character in &self.terminal_characters { for t_character in &self.terminal_characters {
width += t_character.width width += t_character.width()
} }
width width
} }
@ -814,14 +810,14 @@ impl CharacterChunk {
break; break;
} }
let next_character = self.terminal_characters.remove(0); // TODO: consider copying self.terminal_characters into a VecDeque to make this process faster? 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.push_back(next_character);
drained_part_len += next_character.width;
} else { } else {
if drained_part_len == x { if drained_part_len == x {
self.terminal_characters.insert(0, next_character); // put it back self.terminal_characters.insert(0, next_character); // put it back
} else if next_character.width > 1 { } else if next_character.width() > 1 {
for _ in 1..next_character.width { for _ in 1..next_character.width() {
self.terminal_characters.insert(0, EMPTY_TERMINAL_CHARACTER); self.terminal_characters.insert(0, EMPTY_TERMINAL_CHARACTER);
drained_part.push_back(EMPTY_TERMINAL_CHARACTER); drained_part.push_back(EMPTY_TERMINAL_CHARACTER);
} }
@ -963,7 +959,7 @@ impl OutputBuffer {
row: &Row, row: &Row,
viewport_width: usize, viewport_width: usize,
) -> Vec<TerminalCharacter> { ) -> Vec<TerminalCharacter> {
let mut terminal_characters: Vec<TerminalCharacter> = row.columns.iter().copied().collect(); let mut terminal_characters: Vec<TerminalCharacter> = row.columns.iter().cloned().collect();
// pad row // pad row
let row_width = row.width(); let row_width = row.width();
if row_width < viewport_width { if row_width < viewport_width {

View file

@ -3,7 +3,6 @@ use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use unicode_width::UnicodeWidthChar;
use zellij_utils::data::Style; use zellij_utils::data::Style;
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::regex::Regex; use zellij_utils::regex::Regex;
@ -35,7 +34,7 @@ use crate::panes::link_handler::LinkHandler;
use crate::panes::search::SearchResult; use crate::panes::search::SearchResult;
use crate::panes::selection::Selection; use crate::panes::selection::Selection;
use crate::panes::terminal_character::{ use crate::panes::terminal_character::{
AnsiCode, CharacterStyles, CharsetIndex, Cursor, CursorShape, StandardCharset, AnsiCode, CharsetIndex, Cursor, CursorShape, RcCharacterStyles, StandardCharset,
TerminalCharacter, EMPTY_TERMINAL_CHARACTER, TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
}; };
use crate::ui::components::UiComponentParser; use crate::ui::components::UiComponentParser;
@ -416,11 +415,8 @@ impl Debug for Grid {
for y in image_top_edge..image_bottom_edge { for y in image_top_edge..image_bottom_edge {
let row = buffer.get_mut(y).unwrap(); let row = buffer.get_mut(y).unwrap();
for x in image_left_edge..image_right_edge { for x in image_left_edge..image_right_edge {
let fake_sixel_terminal_character = TerminalCharacter { let fake_sixel_terminal_character =
character: sixel_indication_character(x), TerminalCharacter::new_singlewidth(sixel_indication_character(x));
width: 1,
styles: Default::default(),
};
row.add_character_at(fake_sixel_terminal_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) { pub fn update_line_for_rendering(&mut self, line_index: usize) {
self.output_buffer.update_line(line_index); 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 let next_tabstop = self
.horizontal_tabstops .horizontal_tabstops
.iter() .iter()
@ -769,7 +765,7 @@ impl Grid {
} }
self.selection.reset(); self.selection.reset();
self.sixel_grid.character_cell_size_possibly_changed(); 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); self.horizontal_tabstops = create_horizontal_tabstops(new_columns);
let mut cursor_canonical_line_index = self.cursor_canonical_line_index(); let mut cursor_canonical_line_index = self.cursor_canonical_line_index();
let cursor_index_in_canonical_line = self.cursor_index_in_canonical_line(); let cursor_index_in_canonical_line = self.cursor_index_in_canonical_line();
@ -885,78 +881,42 @@ impl Grid {
}, },
_ => None, _ => None,
}; };
Some((
let current_viewport_row_count = self.viewport.len(); new_cursor_y,
match current_viewport_row_count.cmp(&self.height) { saved_cursor_y_coordinates,
Ordering::Less => { new_cursor_x,
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_x_coordinates,
saved_cursor_y_coordinates ))
), } else if new_rows != self.height {
} let saved_cursor_y_coordinates = self
};
}
if new_rows != self.height {
let mut new_cursor_y = self.cursor.y;
let mut saved_cursor_y_coordinates = self
.saved_cursor_position .saved_cursor_position
.as_ref() .as_ref()
.map(|saved_cursor| saved_cursor.y); .map(|saved_cursor| saved_cursor.y);
let new_cursor_x = self.cursor.x;
let saved_cursor_x_coordinates = self let saved_cursor_x_coordinates = self
.saved_cursor_position .saved_cursor_position
.as_ref() .as_ref()
.map(|saved_cursor| saved_cursor.x); .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(); let current_viewport_row_count = self.viewport.len();
match current_viewport_row_count.cmp(&new_rows) { match current_viewport_row_count.cmp(&new_rows) {
Ordering::Less => { Ordering::Less => {
@ -1035,7 +995,7 @@ impl Grid {
.iter() .iter()
.map(|r| { .map(|r| {
let excess_width = r.excess_width(); let excess_width = r.excess_width();
let mut line: Vec<TerminalCharacter> = r.columns.iter().copied().collect(); let mut line: Vec<TerminalCharacter> = r.columns.iter().cloned().collect();
// pad line // pad line
line.resize( line.resize(
self.width.saturating_sub(excess_width), self.width.saturating_sub(excess_width),
@ -1234,7 +1194,7 @@ impl Grid {
self.viewport.remove(scroll_region_bottom); self.viewport.remove(scroll_region_bottom);
} }
let mut pad_character = 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();
let columns = VecDeque::from(vec![pad_character; self.width]); let columns = VecDeque::from(vec![pad_character; self.width]);
self.viewport self.viewport
.insert(scroll_region_top, Row::from_columns(columns).canonical()); .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); self.pad_lines_until(scroll_region_bottom, EMPTY_TERMINAL_CHARACTER);
let mut pad_character = 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 { for _ in 0..count {
self.viewport.remove(scroll_region_top); 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 self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical()); .insert(scroll_region_bottom, Row::from_columns(columns).canonical());
} }
@ -1268,7 +1228,7 @@ impl Grid {
}; };
for _ in 0..self.height { 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.viewport.push(Row::from_columns(columns).canonical());
} }
self.output_buffer.update_all_lines(); self.output_buffer.update_all_lines();
@ -1294,14 +1254,14 @@ impl Grid {
} }
let mut pad_character = 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();
let columns = VecDeque::from(vec![pad_character; self.width]); let columns = VecDeque::from(vec![pad_character; self.width]);
self.viewport.push(Row::from_columns(columns).canonical()); self.viewport.push(Row::from_columns(columns).canonical());
self.selection.move_up(1); self.selection.move_up(1);
} else { } else {
self.viewport.remove(scroll_region_top); self.viewport.remove(scroll_region_top);
let mut pad_character = 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();
let columns = VecDeque::from(vec![pad_character; self.width]); let columns = VecDeque::from(vec![pad_character; self.width]);
if self.viewport.len() >= scroll_region_bottom { if self.viewport.len() >= scroll_region_bottom {
self.viewport self.viewport
@ -1393,7 +1353,7 @@ impl Grid {
} }
} }
pub fn add_character(&mut self, terminal_character: TerminalCharacter) { 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. // Drop zero-width Unicode/UTF-8 codepoints, like for example Variation Selectors.
// This breaks unicode grapheme segmentation, and is the reason why some characters // This breaks unicode grapheme segmentation, and is the reason why some characters
// aren't displayed correctly. Refer to this issue for more information: // aren't displayed correctly. Refer to this issue for more information:
@ -1415,7 +1375,7 @@ impl Grid {
self.viewport self.viewport
.get(self.cursor.y) .get(self.cursor.y)
.and_then(|current_line| current_line.columns.get(absolute_x_in_line)) .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 { pub fn get_absolute_character_index(&self, x: usize, y: usize) -> usize {
self.viewport.get(y).unwrap().absolute_character_index(x) 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) { pub fn clear_all_after_cursor(&mut self, replace_with: TerminalCharacter) {
if let Some(cursor_row) = self.viewport.get_mut(self.cursor.y) { if let Some(cursor_row) = self.viewport.get_mut(self.cursor.y) {
cursor_row.truncate(self.cursor.x); 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); self.replace_characters_in_line_after_cursor(replace_with);
for row in self.viewport.iter_mut().skip(self.cursor.y + 1) { for row in self.viewport.iter_mut().skip(self.cursor.y + 1) {
row.replace_columns(replace_with_columns.clone()); row.replace_columns(replace_with_columns.clone());
@ -1448,8 +1408,8 @@ impl Grid {
} }
pub fn clear_all_before_cursor(&mut self, replace_with: TerminalCharacter) { pub fn clear_all_before_cursor(&mut self, replace_with: TerminalCharacter) {
if self.viewport.get(self.cursor.y).is_some() { 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); 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) { for row in self.viewport.iter_mut().take(self.cursor.y) {
row.replace_columns(replace_with_columns.clone()); row.replace_columns(replace_with_columns.clone());
} }
@ -1463,7 +1423,7 @@ impl Grid {
} }
} }
pub fn clear_all(&mut self, replace_with: TerminalCharacter) { 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); self.replace_characters_in_line_after_cursor(replace_with);
for row in &mut self.viewport { for row in &mut self.viewport {
row.replace_columns(replace_with_columns.clone()); 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) { fn pad_current_line_until(&mut self, position: usize, pad_character: TerminalCharacter) {
if self.viewport.get(self.cursor.y).is_none() { 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) { if let Some(current_row) = self.viewport.get_mut(self.cursor.y) {
for _ in current_row.width()..position { 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); self.output_buffer.update_line(self.cursor.y);
} }
} }
fn pad_lines_until(&mut self, position: usize, pad_character: TerminalCharacter) { fn pad_lines_until(&mut self, position: usize, pad_character: TerminalCharacter) {
for _ in self.viewport.len()..=position { 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.viewport.push(Row::from_columns(columns).canonical());
self.output_buffer.update_line(self.viewport.len() - 1); self.output_buffer.update_line(self.viewport.len() - 1);
} }
@ -1528,13 +1488,13 @@ impl Grid {
} else { } else {
self.cursor.y = std::cmp::min(self.height - 1, y + y_offset); 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); self.pad_current_line_until(self.cursor.x, pad_character);
}, },
None => { None => {
self.cursor.x = std::cmp::min(self.width - 1, x); self.cursor.x = std::cmp::min(self.width - 1, x);
self.cursor.y = std::cmp::min(self.height - 1, y); 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); 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); let bottom_line_index = bottom_line_index.unwrap_or(self.height);
self.scroll_region = Some((top_line_index, bottom_line_index)); self.scroll_region = Some((top_line_index, bottom_line_index));
let mut pad_character = 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();
self.move_cursor_to(0, 0, pad_character); // DECSTBM moves the cursor to column 1 line 1 of the page 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) { pub fn clear_scroll_region(&mut self) {
@ -1634,7 +1594,7 @@ impl Grid {
// region // region
for _ in 0..count { for _ in 0..count {
self.viewport.remove(current_line_index); 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 { if self.viewport.len() > scroll_region_bottom {
self.viewport self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical()); .insert(scroll_region_bottom, Row::from_columns(columns).canonical());
@ -1663,7 +1623,7 @@ impl Grid {
if scroll_region_bottom < self.viewport.len() { if scroll_region_bottom < self.viewport.len() {
self.viewport.remove(scroll_region_bottom); 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 self.viewport
.insert(current_line_index, Row::from_columns(columns).canonical()); .insert(current_line_index, Row::from_columns(columns).canonical());
} }
@ -1682,19 +1642,19 @@ impl Grid {
let pad_character = EMPTY_TERMINAL_CHARACTER; let pad_character = EMPTY_TERMINAL_CHARACTER;
self.pad_current_line_until(self.cursor.x, pad_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; let mut empty_character = EMPTY_TERMINAL_CHARACTER;
empty_character.styles = empty_char_style; empty_character.styles = empty_char_style;
let pad_until = std::cmp::min(self.width, self.cursor.x + count); 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) { if let Some(current_row) = self.viewport.get_mut(self.cursor.y) {
for i in 0..count { 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); 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; let mut empty_character = EMPTY_TERMINAL_CHARACTER;
empty_character.styles = empty_char_style; empty_character.styles = empty_char_style;
if let Some(current_row) = self.viewport.get_mut(self.cursor.y) { if let Some(current_row) = self.viewport.get_mut(self.cursor.y) {
@ -1708,13 +1668,13 @@ impl Grid {
for _ in 0..count { for _ in 0..count {
let deleted_character = current_row.delete_and_return_character(self.cursor.x); let deleted_character = current_row.delete_and_return_character(self.cursor.x);
let excess_width = deleted_character let excess_width = deleted_character
.map(|terminal_character| terminal_character.width) .map(|terminal_character| terminal_character.width())
.unwrap_or(0) .unwrap_or(0)
.saturating_sub(1); .saturating_sub(1);
for _ in 0..excess_width { 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); self.output_buffer.update_line(self.cursor.y);
} }
@ -1839,7 +1799,7 @@ impl Grid {
line_selection.push(terminal_character.character); line_selection.push(terminal_character.character);
} }
terminal_col += terminal_character.width; terminal_col += terminal_character.width();
} }
if row.is_canonical { if row.is_canonical {
@ -2192,14 +2152,9 @@ impl Perform for Grid {
fn print(&mut self, c: char) { fn print(&mut self, c: char) {
let c = self.cursor.charsets[self.active_charset].map(c); let c = self.cursor.charsets[self.active_charset].map(c);
// apparently, building TerminalCharacter like this without a "new" method let terminal_character =
// is a little faster TerminalCharacter::new_styled(c, self.cursor.pending_styles.clone());
let terminal_character = TerminalCharacter { self.set_preceding_character(terminal_character.clone());
character: c,
width: c.width().unwrap_or(0),
styles: self.cursor.pending_styles,
};
self.set_preceding_character(terminal_character);
self.add_character(terminal_character); self.add_character(terminal_character);
} }
@ -2214,7 +2169,7 @@ impl Perform for Grid {
}, },
9 => { 9 => {
// tab // tab
self.advance_to_next_tabstop(self.cursor.pending_styles); self.advance_to_next_tabstop(self.cursor.pending_styles.clone());
}, },
10 | 11 | 12 => { 10 | 11 | 12 => {
// 0a, newline // 0a, newline
@ -2342,8 +2297,9 @@ impl Perform for Grid {
if params.len() < 3 { if params.len() < 3 {
return; return;
} }
self.cursor.pending_styles.link_anchor = self.cursor.pending_styles.update(|styles| {
self.link_handler.borrow_mut().dispatch_osc8(params); styles.link_anchor = self.link_handler.borrow_mut().dispatch_osc8(params)
})
}, },
// Get/set Foreground (b"10") or background (b"11") colors // Get/set Foreground (b"10") or background (b"11") colors
@ -2496,7 +2452,7 @@ impl Perform for Grid {
if intermediates.is_empty() { if intermediates.is_empty() {
self.cursor self.cursor
.pending_styles .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' { } else if c == 'C' || c == 'a' {
// move cursor forward // move cursor forward
@ -2507,7 +2463,9 @@ impl Perform for Grid {
if let Some(clear_type) = params_iter.next().map(|param| param[0]) { if let Some(clear_type) = params_iter.next().map(|param| param[0]) {
let mut char_to_replace = EMPTY_TERMINAL_CHARACTER; let mut char_to_replace = EMPTY_TERMINAL_CHARACTER;
if let Some(background_color) = self.cursor.pending_styles.background { 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 { if clear_type == 0 {
self.replace_characters_in_line_after_cursor(char_to_replace); 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) // clear all (0 => below, 1 => above, 2 => all, 3 => saved)
let mut char_to_replace = EMPTY_TERMINAL_CHARACTER; let mut char_to_replace = EMPTY_TERMINAL_CHARACTER;
if let Some(background_color) = self.cursor.pending_styles.background { 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 let Some(clear_type) = params_iter.next().map(|param| param[0]) {
if clear_type == 0 { if clear_type == 0 {
@ -2783,13 +2743,13 @@ impl Perform for Grid {
// delete lines if currently inside scroll region // delete lines if currently inside scroll region
let line_count_to_delete = next_param_or(1); let line_count_to_delete = next_param_or(1);
let mut pad_character = 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();
self.delete_lines_in_scroll_region(line_count_to_delete, pad_character); self.delete_lines_in_scroll_region(line_count_to_delete, pad_character);
} else if c == 'L' { } else if c == 'L' {
// insert blank lines if inside scroll region // insert blank lines if inside scroll region
let line_count_to_add = next_param_or(1); let line_count_to_add = next_param_or(1);
let mut pad_character = 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();
self.add_empty_lines_in_scroll_region(line_count_to_add, pad_character); self.add_empty_lines_in_scroll_region(line_count_to_add, pad_character);
} else if c == 'G' || c == '`' { } else if c == 'G' || c == '`' {
let column = next_param_or(1).saturating_sub(1); let column = next_param_or(1).saturating_sub(1);
@ -2810,11 +2770,11 @@ impl Perform for Grid {
} else if c == 'P' { } else if c == 'P' {
// erase characters // erase characters
let count = next_param_or(1); 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' { } else if c == 'X' {
// erase characters and replace with empty characters of current style // erase characters and replace with empty characters of current style
let count = next_param_or(1); 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' { } else if c == 'T' {
/* /*
* 124 54 T SD * 124 54 T SD
@ -2871,13 +2831,13 @@ impl Perform for Grid {
let count = next_param_or(1); let count = next_param_or(1);
for _ in 0..count { for _ in 0..count {
let mut pad_character = 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();
self.add_character_at_cursor_position(pad_character, true); self.add_character_at_cursor_position(pad_character, true);
} }
} else if c == 'b' { } 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) { for _ in 0..next_param_or(1) {
self.add_character(c); self.add_character(c.clone());
} }
} }
} else if c == 'E' { } else if c == 'E' {
@ -2893,7 +2853,7 @@ impl Perform for Grid {
self.move_cursor_to_beginning_of_line(); self.move_cursor_to_beginning_of_line();
} else if c == 'I' { } else if c == 'I' {
for _ in 0..next_param_or(1) { 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' { } else if c == 'q' {
let first_intermediate_is_space = matches!(intermediates.get(0), Some(b' ')); let first_intermediate_is_space = matches!(intermediates.get(0), Some(b' '));
@ -3210,7 +3170,7 @@ impl Row {
} else { } else {
let mut width = 0; let mut width = 0;
for terminal_character in &self.columns { for terminal_character in &self.columns {
width += terminal_character.width; width += terminal_character.width();
} }
self.width = Some(width); self.width = Some(width);
width width
@ -3219,15 +3179,15 @@ impl Row {
pub fn width(&self) -> usize { pub fn width(&self) -> usize {
let mut width = 0; let mut width = 0;
for terminal_character in &self.columns { for terminal_character in &self.columns {
width += terminal_character.width; width += terminal_character.width();
} }
width width
} }
pub fn excess_width(&self) -> usize { pub fn excess_width(&self) -> usize {
let mut acc = 0; let mut acc = 0;
for terminal_character in &self.columns { for terminal_character in &self.columns {
if terminal_character.width > 1 { if terminal_character.width() > 1 {
acc += terminal_character.width - 1; acc += terminal_character.width() - 1;
} }
} }
acc acc
@ -3235,8 +3195,8 @@ impl Row {
pub fn excess_width_until(&self, x: usize) -> usize { pub fn excess_width_until(&self, x: usize) -> usize {
let mut acc = 0; let mut acc = 0;
for terminal_character in self.columns.iter().take(x) { for terminal_character in self.columns.iter().take(x) {
if terminal_character.width > 1 { if terminal_character.width() > 1 {
acc += terminal_character.width - 1; acc += terminal_character.width() - 1;
} }
} }
acc acc
@ -3248,7 +3208,7 @@ impl Row {
if i == absolute_index { if i == absolute_index {
break; break;
} }
if terminal_character.width > 1 { if terminal_character.width() > 1 {
absolute_index = absolute_index.saturating_sub(1); absolute_index = absolute_index.saturating_sub(1);
} }
} }
@ -3261,10 +3221,10 @@ impl Row {
let mut absolute_index = x; let mut absolute_index = x;
let mut position_inside_character = 0; let mut position_inside_character = 0;
for (i, terminal_character) in self.columns.iter().enumerate() { for (i, terminal_character) in self.columns.iter().enumerate() {
accumulated_width += terminal_character.width; accumulated_width += terminal_character.width();
absolute_index = i; absolute_index = i;
if accumulated_width > x { 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; position_inside_character = x - character_start_position;
break; break;
} }
@ -3274,10 +3234,10 @@ impl Row {
pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
match self.width_cached().cmp(&x) { match self.width_cached().cmp(&x) {
Ordering::Equal => { 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 // adding the character at the end of the current line
self.columns.push_back(terminal_character); 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 => { Ordering::Less => {
// adding the character after the end of the current line // adding the character after the end of the current line
@ -3293,17 +3253,17 @@ impl Row {
// we replace the character at its position // we replace the character at its position
let (absolute_x_index, position_inside_character) = let (absolute_x_index, position_inside_character) =
self.absolute_character_index_and_position_in_char(x); self.absolute_character_index_and_position_in_char(x);
let character_width = terminal_character.width; let character_width = terminal_character.width();
let replaced_character = let replaced_character =
std::mem::replace(&mut self.columns[absolute_x_index], terminal_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 => { Ordering::Greater => {
// the replaced character is narrower than the current character // the replaced character is narrower than the current character
// (eg. we added a wide emoji in place of an English character) // (eg. we added a wide emoji in place of an English character)
// we remove the character after it to make room // we remove the character after it to make room
let position_to_remove = absolute_x_index + 1; let position_to_remove = absolute_x_index + 1;
if let Some(removed) = self.columns.remove(position_to_remove) { 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 // the character we removed is a wide character itself, so we add
// padding // padding
self.columns self.columns
@ -3348,17 +3308,15 @@ impl Row {
} }
pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
let absolute_x_index = self.absolute_character_index(x); let absolute_x_index = self.absolute_character_index(x);
if absolute_x_index < self.columns.len() { if let Some(character) = self.columns.get_mut(absolute_x_index) {
self.columns.push_back(terminal_character); let terminal_character_width = terminal_character.width();
// this is much more performant than remove/insert let character = std::mem::replace(character, terminal_character);
if let Some(character) = self.columns.swap_remove_back(absolute_x_index) { let excess_width = character.width().saturating_sub(terminal_character_width);
let excess_width = character.width.saturating_sub(terminal_character.width);
for _ in 0..excess_width { for _ in 0..excess_width {
self.columns self.columns
.insert(absolute_x_index, EMPTY_TERMINAL_CHARACTER); .insert(absolute_x_index, EMPTY_TERMINAL_CHARACTER);
} }
} }
}
self.width = None; self.width = None;
} }
pub fn replace_columns(&mut self, columns: VecDeque<TerminalCharacter>) { pub fn replace_columns(&mut self, columns: VecDeque<TerminalCharacter>) {
@ -3383,8 +3341,8 @@ impl Row {
if index == position { if index == position {
break; break;
} }
if terminal_character.width > 1 { if terminal_character.width() > 1 {
position = position.saturating_sub(terminal_character.width.saturating_sub(1)); position = position.saturating_sub(terminal_character.width().saturating_sub(1));
} }
} }
position position
@ -3415,8 +3373,8 @@ impl Row {
for next_character in self.columns.iter() { for next_character in self.columns.iter() {
// drained_part_len == 0 here is so that if the grid is resized // drained_part_len == 0 here is so that if the grid is resized
// to a size of 1, we won't drop wide characters // to a size of 1, we won't drop wide characters
if drained_part_len + next_character.width <= x || drained_part_len == 0 { if drained_part_len + next_character.width() <= x || drained_part_len == 0 {
drained_part_len += next_character.width; drained_part_len += next_character.width();
split_pos += 1 split_pos += 1
} else { } else {
break; break;
@ -3432,7 +3390,7 @@ impl Row {
let width_of_current_character = self let width_of_current_character = self
.columns .columns
.get(to_position_accounting_for_widechars) .get(to_position_accounting_for_widechars)
.map(|character| character.width) .map(|character| character.width())
.unwrap_or(1); .unwrap_or(1);
let mut replace_with = let mut replace_with =
VecDeque::from(vec![terminal_character; to + width_of_current_character]); VecDeque::from(vec![terminal_character; to + width_of_current_character]);
@ -3467,13 +3425,13 @@ impl Row {
let mut current_part: VecDeque<TerminalCharacter> = VecDeque::new(); let mut current_part: VecDeque<TerminalCharacter> = VecDeque::new();
let mut current_part_len = 0; let mut current_part_len = 0;
for character in self.columns.drain(..) { 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)); parts.push(Row::from_columns(current_part));
current_part = VecDeque::new(); current_part = VecDeque::new();
current_part_len = 0; current_part_len = 0;
} }
current_part_len += character.width();
current_part.push_back(character); current_part.push_back(character);
current_part_len += character.width;
} }
if !current_part.is_empty() { if !current_part.is_empty() {
parts.push(Row::from_columns(current_part)) parts.push(Row::from_columns(current_part))

View file

@ -1,6 +1,7 @@
use std::convert::From; use std::convert::From;
use std::fmt::{self, Debug, Display, Formatter}; use std::fmt::{self, Debug, Display, Formatter};
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use std::rc::Rc;
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -15,7 +16,7 @@ use crate::panes::alacritty_functions::parse_sgr_color;
pub const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter { pub const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter {
character: ' ', character: ' ',
width: 1, width: 1,
styles: RESET_STYLES, styles: RcCharacterStyles::Reset,
}; };
pub const RESET_STYLES: CharacterStyles = CharacterStyles { pub const RESET_STYLES: CharacterStyles = CharacterStyles {
@ -35,6 +36,30 @@ pub const RESET_STYLES: CharacterStyles = CharacterStyles {
styled_underlines_enabled: false, 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)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AnsiCode { pub enum AnsiCode {
On, 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<CharacterStyles>),
}
const _: [(); 8] = [(); std::mem::size_of::<RcCharacterStyles>()];
impl From<CharacterStyles> 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 struct CharacterStyles {
pub foreground: Option<AnsiCode>, pub foreground: Option<AnsiCode>,
pub background: Option<AnsiCode>, pub background: Option<AnsiCode>,
@ -166,9 +246,6 @@ impl PartialEq for CharacterStyles {
} }
impl CharacterStyles { impl CharacterStyles {
pub fn new() -> Self {
Self::default()
}
pub fn foreground(mut self, foreground_code: Option<AnsiCode>) -> Self { pub fn foreground(mut self, foreground_code: Option<AnsiCode>) -> Self {
self.foreground = foreground_code; self.foreground = foreground_code;
self self
@ -255,8 +332,7 @@ impl CharacterStyles {
} }
// create diff from all changed styles // create diff from all changed styles
let mut diff = let mut diff = DEFAULT_STYLES.enable_styled_underlines(self.styled_underlines_enabled);
CharacterStyles::new().enable_styled_underlines(self.styled_underlines_enabled);
if self.foreground != new_styles.foreground { if self.foreground != new_styles.foreground {
diff.foreground = new_styles.foreground; diff.foreground = new_styles.foreground;
@ -315,7 +391,7 @@ impl CharacterStyles {
} }
Some(diff) Some(diff)
} }
pub fn reset_all(&mut self) { fn reset_ansi(&mut self) {
self.foreground = Some(AnsiCode::Reset); self.foreground = Some(AnsiCode::Reset);
self.background = Some(AnsiCode::Reset); self.background = Some(AnsiCode::Reset);
self.underline_color = Some(AnsiCode::Reset); self.underline_color = Some(AnsiCode::Reset);
@ -328,11 +404,12 @@ impl CharacterStyles {
self.reverse = Some(AnsiCode::Reset); self.reverse = Some(AnsiCode::Reset);
self.hidden = Some(AnsiCode::Reset); self.hidden = Some(AnsiCode::Reset);
self.strike = 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) { pub fn add_style_from_ansi_params(&mut self, params: &mut ParamsIter) {
while let Some(param) = params.next() { while let Some(param) = params.next() {
match param { match param {
[] | [0] => self.reset_all(), [] | [0] => self.reset_ansi(),
[1] => *self = self.bold(Some(AnsiCode::On)), [1] => *self = self.bold(Some(AnsiCode::On)),
[2] => *self = self.dim(Some(AnsiCode::On)), [2] => *self = self.dim(Some(AnsiCode::On)),
[3] => *self = self.italic(Some(AnsiCode::On)), [3] => *self = self.italic(Some(AnsiCode::On)),
@ -813,7 +890,7 @@ impl CursorShape {
pub struct Cursor { pub struct Cursor {
pub x: usize, pub x: usize,
pub y: usize, pub y: usize,
pub pending_styles: CharacterStyles, pub pending_styles: RcCharacterStyles,
pub charsets: Charsets, pub charsets: Charsets,
shape: CursorShape, shape: CursorShape,
} }
@ -823,7 +900,9 @@ impl Cursor {
Cursor { Cursor {
x, x,
y, y,
pending_styles: RESET_STYLES.enable_styled_underlines(styled_underlines), pending_styles: RESET_STYLES
.enable_styled_underlines(styled_underlines)
.into(),
charsets: Default::default(), charsets: Default::default(),
shape: CursorShape::Initial, shape: CursorShape::Initial,
} }
@ -836,21 +915,48 @@ impl Cursor {
} }
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, PartialEq)]
pub struct TerminalCharacter { pub struct TerminalCharacter {
pub character: char, pub character: char,
pub styles: CharacterStyles, pub styles: RcCharacterStyles,
pub width: usize, 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::<TerminalCharacter>()];
impl TerminalCharacter { impl TerminalCharacter {
#[inline]
pub fn new(character: char) -> Self { pub fn new(character: char) -> Self {
Self::new_styled(character, Default::default())
}
#[inline]
pub fn new_styled(character: char, styles: RcCharacterStyles) -> Self {
TerminalCharacter { TerminalCharacter {
character, character,
styles: CharacterStyles::default(), styles,
width: character.width().unwrap_or(0), 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,
width: 1,
}
}
pub fn width(&self) -> usize {
self.width as usize
}
} }
impl ::std::fmt::Debug for TerminalCharacter { impl ::std::fmt::Debug for TerminalCharacter {

View file

@ -437,8 +437,10 @@ impl Pane for TerminalPane {
.grid .grid
.get_character_under_cursor() .get_character_under_cursor()
.unwrap_or(EMPTY_TERMINAL_CHARACTER); .unwrap_or(EMPTY_TERMINAL_CHARACTER);
character_under_cursor.styles.background = Some(cursor_color.into()); character_under_cursor.styles.update(|styles| {
character_under_cursor.styles.foreground = Some(text_color.into()); 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) // 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)); self.fake_cursor_locations.insert((cursor_y, cursor_x));
let mut fake_cursor = format!( let mut fake_cursor = format!(

View file

@ -63,12 +63,12 @@ impl BoundarySymbol {
self.boundary_type self.boundary_type
) )
})?; })?;
TerminalCharacter { TerminalCharacter::new_singlewidth_styled(
character, character,
width: 1, RESET_STYLES
styles: RESET_STYLES .foreground(self.color.map(|palette_color| palette_color.into()))
.foreground(self.color.map(|palette_color| palette_color.into())), .into(),
} )
}; };
Ok(tc) Ok(tc)
} }

View file

@ -1,5 +1,5 @@
use crate::output::CharacterChunk; 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::ui::boundaries::boundary_type;
use crate::ClientId; use crate::ClientId;
use zellij_utils::data::{client_id_to_colors, PaletteColor, Style}; 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<PaletteColor>) -> Vec<TerminalCharacter> { fn foreground_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
let mut colored_string = Vec::new(); let mut colored_string = Vec::new();
for character in characters.chars() { for character in characters.chars() {
let styles = match color { let mut styles = RcCharacterStyles::reset();
styles.update(|styles| {
styles.bold = Some(AnsiCode::On);
match color {
Some(palette_color) => { Some(palette_color) => {
let mut styles = CharacterStyles::new(); styles.foreground = Some(AnsiCode::from(palette_color));
styles.reset_all();
styles
.foreground(Some(AnsiCode::from(palette_color)))
.bold(Some(AnsiCode::On))
}, },
None => { None => {},
let mut styles = CharacterStyles::new(); }
styles.reset_all(); });
styles.bold(Some(AnsiCode::On)) let terminal_character = TerminalCharacter::new_styled(character, styles);
},
};
let terminal_character = TerminalCharacter {
character,
styles,
width: character.width().unwrap_or(0),
};
colored_string.push(terminal_character); colored_string.push(terminal_character);
} }
colored_string colored_string
@ -38,25 +30,15 @@ fn foreground_color(characters: &str, color: Option<PaletteColor>) -> Vec<Termin
fn background_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> { fn background_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
let mut colored_string = Vec::new(); let mut colored_string = Vec::new();
for character in characters.chars() { for character in characters.chars() {
let styles = match color { let mut styles = RcCharacterStyles::reset();
styles.update(|styles| match color {
Some(palette_color) => { Some(palette_color) => {
let mut styles = CharacterStyles::new(); styles.background = Some(AnsiCode::from(palette_color));
styles.reset_all(); styles.bold(Some(AnsiCode::On));
styles
.background(Some(AnsiCode::from(palette_color)))
.bold(Some(AnsiCode::On))
}, },
None => { None => {},
let mut styles = CharacterStyles::new(); });
styles.reset_all(); let terminal_character = TerminalCharacter::new_styled(character, styles);
styles
},
};
let terminal_character = TerminalCharacter {
character,
styles,
width: character.width().unwrap_or(0),
};
colored_string.push(terminal_character); colored_string.push(terminal_character);
} }
colored_string colored_string