// The code in this file is adapted from the alacritty_terminal crate under the // Apache license; see: crates/warp_terminal/src/model/LICENSE-ALACRITTY. use std::boxed::Box; use bitflags::bitflags; use serde::{Deserialize, Serialize}; use crate::model::ansi::{Color, NamedColor}; use crate::model::char_or_str::CharOrStr; use crate::model::grid::row::Row; /// The character set as the content in new, not-yet-set cells. This can be /// used to disambiguate between a cell which has never had any content and one /// which has had a space character written into it. pub const DEFAULT_CHAR: char = '\0'; pub const DEFAULT_CHAR_BYTE: u8 = b'\0'; pub const DEFAULT_CHAR_STR: &str = "\0"; /// Maximum byte length of a single cell's accumulated grapheme cluster /// (the base character plus any zero-width characters attached to it). /// /// This is chosen to be: /// /// 1. Well above the size of any legitimate grapheme cluster. Unicode's /// Stream-Safe Text Format (UAX #15) restricts runs of non-starters to /// at most 30 codepoints, which caps out around 120 bytes in UTF-8; the /// longest standardized emoji ZWJ sequences (e.g. multi-person family /// emoji with skin-tone modifiers) fit well below 100 bytes. /// 2. Well below the per-chunk size used by flat scrollback storage, so /// that pushing a cell's grapheme into scrollback can never produce an /// oversized grapheme that would violate the chunk-size invariant. pub const MAX_GRAPHEME_BYTES: usize = 256; /// Soft threshold for warning about an unusually large accumulated /// grapheme cluster on a single cell. See [`Cell::push_zerowidth`]. const WARN_GRAPHEME_BYTES: usize = 128; bitflags! { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Flags: u16 { const INVERSE = 0b0000_0000_0000_0001; const BOLD = 0b0000_0000_0000_0010; const ITALIC = 0b0000_0000_0000_0100; const BOLD_ITALIC = 0b0000_0000_0000_0110; const UNDERLINE = 0b0000_0000_0000_1000; const WRAPLINE = 0b0000_0000_0001_0000; const WIDE_CHAR = 0b0000_0000_0010_0000; const WIDE_CHAR_SPACER = 0b0000_0000_0100_0000; const DIM = 0b0000_0000_1000_0000; const DIM_BOLD = 0b0000_0000_1000_0010; const HIDDEN = 0b0000_0001_0000_0000; const STRIKEOUT = 0b0000_0010_0000_0000; const LEADING_WIDE_CHAR_SPACER = 0b0000_0100_0000_0000; const DOUBLE_UNDERLINE = 0b0000_1000_0000_0000; /// Set on cells which are the locations of cursor points and should be /// tracked through grid resizes. const HAS_CURSOR = 0b0001_0000_0000_0000; /// Equivalent to the union of all of the following: Flags::UNDERLINE, /// Flags::STRIKEOUT, Flags::DOUBLE_UNDERLINE. const CELL_DECORATIONS = 0b0000_1010_0000_1000; } } // Use the legacy serialization strategy for bitflags. The 2.XX version of bitflags has a different // serialization strategy, which would require us to update our ref tests. impl serde::Serialize for Flags { fn serialize(&self, serializer: S) -> Result { bitflags_serde_legacy::serialize(self, "Flags", serializer) } } // Use the legacy serialization strategy for bitflags. The 2.XX version of bitflags has a different // serialization strategy, which would require us to update our ref tests. impl<'de> serde::Deserialize<'de> for Flags { fn deserialize>(deserializer: D) -> Result { bitflags_serde_legacy::deserialize("Flags", deserializer) } } /// Trait for determining if a reset should be performed. pub trait ResetDiscriminant { /// Value based on which equality for the reset will be determined. fn discriminant(&self) -> T; } impl ResetDiscriminant for T { fn discriminant(&self) -> T { *self } } impl ResetDiscriminant for Cell { fn discriminant(&self) -> Color { self.bg } } /// Struct used simply as a "marker" for indicating whether a cell is at the end of the prompt. #[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Eq, PartialEq)] pub(super) struct EndOfPromptMarker { /// Defined in the case of EndOfPromptMarker being at the end of a line. Indicates whether /// the prompt has a trailing newline (that isn't covered in the marker, which is inclusive of /// printable characters only). pub has_extra_trailing_newline: bool, } /// Dynamically allocated cell content. /// /// This storage is reserved for cell attributes which are rarely set. This allows reducing the /// allocation required ahead of time for every cell, with some additional overhead when the extra /// storage is actually required. #[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] struct CellExtra { /// Zerowidth characters stored in this cell WITH the base character at the start. This helps /// optimize reads on this data structure (we don't need to allocate a new string to join the /// base character and zerowidth characters). cell_with_zero_width: Option, end_of_prompt: Option, } /// Content and attributes of a single cell in the terminal grid. /// NOTE: Many cells are allocated per grid, so this should be as memory compact as possible. Fields /// that may be optional, or set for only a few cells, should go into the `CellExtra` instead. /// /// Additional memory usage note: Due to holding a pointer (Box), this struct has an alignment of /// 8 bytes. This means that the total size taken up by the struct in memory will be an even /// multiple of 8; if the data is not an even multiple then padding will be added to reach an even /// value. Currently, this holds exactly 24 bytes, so it is tightly packed and does not need any /// padding: /// /// * c: 4 bytes (equivalent to a u32) /// * fg: 5 bytes (the data contains 4 bytes plus a discriminator for the enum variant. Since it /// has an alignment of 1, the extra space required by the discriminator is only 1 /// byte. Altering the data could change the alignment, which could then result in /// more padding and the total size of `Color` increasing) /// * bg: 5 bytes (Same as fg) /// * flags: 2 bytes (stored as a u16) /// * extra: 8 bytes (pointer with null representing None) /// /// Increasing any of these values by even 1 byte will cause `Cell` to ultimately take up 32 bytes /// instead of 24, an increase of 33%. #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct Cell { pub c: char, pub fg: Color, pub bg: Color, pub flags: Flags, extra: Option>, } impl Default for Cell { #[inline] fn default() -> Cell { Cell { c: DEFAULT_CHAR, bg: Color::Named(NamedColor::Background), fg: Color::Named(NamedColor::Foreground), flags: Flags::empty(), extra: None, } } } impl Cell { /// Cell's character followed by all zerowidth characters stored in this cell. This /// is only Some if the cell has zerowidth characters. #[inline] fn content_with_zerowidth(&self) -> Option<&str> { self.extra .as_ref() .and_then(|extra| extra.cell_with_zero_width.as_deref()) } /// Returns the content of the cell that should be used for display /// purposes, e.g.: rendering or stringification. #[inline] pub fn content_for_display(&self) -> CharOrStr<'_> { match self.raw_content() { CharOrStr::Char(DEFAULT_CHAR) => CharOrStr::Char(' '), content => content, } } /// Returns the raw cell content. /// /// This may include non-printable marker characters. /// /// TODO(visibility): This should be changed to `pub(super)` when possible. pub fn raw_content(&self) -> CharOrStr<'_> { match self.content_with_zerowidth() { Some(content_with_zerowidth) => CharOrStr::Str(content_with_zerowidth), None => CharOrStr::Char(self.c), } } /// Write a new zerowidth character to this cell. /// /// Accumulated zero-width content is capped at [`MAX_GRAPHEME_BYTES`] /// so that adversarial or buggy input streams cannot produce a single /// grapheme cluster larger than the scrollback chunk size. See /// [`MAX_GRAPHEME_BYTES`] for details. /// /// If `log_long_grapheme_warnings` is true, a [`log::warn!`] is /// emitted on the push that first takes this cell's accumulated /// grapheme across [`WARN_GRAPHEME_BYTES`]. Callers that are /// replaying already-validated grapheme content (e.g. materializing /// a row from flat scrollback storage, where the stored content was /// already capped on the way in) should pass `false` to suppress /// that redundant warning. #[inline] pub fn push_zerowidth(&mut self, c: char, log_long_grapheme_warnings: bool) { // If we're adding a zero-width character to this cell, but it has not // had any content set yet, set the content to a space. This preserves // its visual appearance, but clearly marks the cell as having been // modified from its default "empty" state. if self.c == DEFAULT_CHAR { self.c = ' '; } let extra = self.extra.get_or_insert_with(Box::default); match &mut extra.cell_with_zero_width { Some(zerowidth) => { let old_len = zerowidth.len(); let new_len = old_len + c.len_utf8(); if new_len > MAX_GRAPHEME_BYTES { // The accumulated grapheme cluster would exceed our // per-cell cap, which is in turn well below the // scrollback chunk size. Silently drop additional // zero-width characters: logging every dropped // character would produce a flood of spam for // pathological streams. return; } zerowidth.push(c); // Log exactly once, on the push that first takes this cell // across the soft threshold. This surfaces unusually-large // graphemes in logs without producing per-character spam. if log_long_grapheme_warnings && old_len < WARN_GRAPHEME_BYTES && new_len >= WARN_GRAPHEME_BYTES { log::warn!( "cell grapheme has accumulated {new_len} bytes of zero-width content (base char {:?}); further zero-width pushes beyond {MAX_GRAPHEME_BYTES} bytes will be dropped", self.c, ); } } None => { // First zero-width push seeds the string with the base // character. The base character is always a single `char`, // so it cannot by itself exceed the cap. extra.cell_with_zero_width = Some(format!("{}{}", self.c, c)); } } } /// Returns whether cell is the end of prompt content (contains `EndOfPromptMarker`). #[inline] pub fn is_end_of_prompt(&self) -> bool { self.end_of_prompt_marker().is_some() } /// Returns information about the end-of-prompt marker in this cell, if any. pub(super) fn end_of_prompt_marker(&self) -> Option { self.extra.as_ref()?.end_of_prompt } /// Mark cell as the end of prompt content. #[inline] pub fn mark_end_of_prompt(&mut self, has_extra_trailing_newline: bool) { self.extra .get_or_insert_with(Default::default) .end_of_prompt = Some(EndOfPromptMarker { has_extra_trailing_newline, }); } /// Free all dynamically allocated cell storage. Preserves EndOfPromptMarker if present. #[inline] pub fn drop_extra(&mut self) { if let Some(extra) = self.extra.take() { if let Some(end_of_prompt_marker) = extra.end_of_prompt { // If we had a end of prompt marker, we preserve it (re-insert into extras). self.mark_end_of_prompt(end_of_prompt_marker.has_extra_trailing_newline); } // If `end_of_prompt` is None, `extra` is dropped here and not put back. } } } impl Cell { #[inline] // TODO(visibility): This should be changed to `pub(crate)` when possible. pub fn is_empty(&self) -> bool { // TODO(vorporeal): can this be a simple equality check vs. Cell::default()? self.c == DEFAULT_CHAR && self.bg == Color::Named(NamedColor::Background) && self.fg == Color::Named(NamedColor::Foreground) && !self.flags.intersects( Flags::INVERSE | Flags::UNDERLINE | Flags::DOUBLE_UNDERLINE | Flags::STRIKEOUT | Flags::WRAPLINE | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER | Flags::HAS_CURSOR, ) } /// Returns whether or not rendering the cell would produce anything visible. pub fn is_visible(&self) -> bool { !self.is_empty() && !self.c.is_ascii_whitespace() } #[inline] #[allow(dead_code)] // TODO(visibility): This should be changed to `pub(crate)` when possible. pub fn flags(&self) -> &Flags { &self.flags } #[inline] #[allow(dead_code)] // TODO(visibility): This should be changed to `pub(crate)` when possible. pub fn flags_mut(&mut self) -> &mut Flags { &mut self.flags } #[inline] pub(crate) fn reset(&mut self, template: &Self) { *self = Cell { bg: template.bg, ..Cell::default() }; } } impl From for Cell { #[inline] fn from(color: Color) -> Self { Self { bg: color, ..Cell::default() } } } /// Get the length of occupied cells in a line. pub trait LineLength { /// Calculate the occupied line length. fn line_length(&self) -> usize; } impl LineLength for Row { fn line_length(&self) -> usize { // If the row has no cells, then the line length is 0, by definition. if self.len() == 0 { return 0; } let mut length = 0; if self[self.len() - 1].flags.contains(Flags::WRAPLINE) { return self.len(); } for (index, cell) in self[..].iter().rev().enumerate() { if cell.c != DEFAULT_CHAR { length = self.len() - index; break; } } length } } #[cfg(any(test, feature = "test-util"))] impl From for Cell { fn from(c: char) -> Self { Cell { c, ..Default::default() } } } #[cfg(test)] #[path = "cell_tests.rs"] mod tests;