Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
@@ -0,0 +1,388 @@
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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
bitflags_serde_legacy::deserialize("Flags", deserializer)
}
}
/// Trait for determining if a reset should be performed.
pub trait ResetDiscriminant<T> {
/// Value based on which equality for the reset will be determined.
fn discriminant(&self) -> T;
}
impl<T: Copy> ResetDiscriminant<T> for T {
fn discriminant(&self) -> T {
*self
}
}
impl ResetDiscriminant<Color> 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<String>,
end_of_prompt: Option<EndOfPromptMarker>,
}
/// 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<Box<CellExtra>>,
}
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<EndOfPromptMarker> {
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<Color> 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<char> for Cell {
fn from(c: char) -> Self {
Cell {
c,
..Default::default()
}
}
}
#[cfg(test)]
#[path = "cell_test.rs"]
mod tests;