use compact_str::CompactString;
use unicode_segmentation::UnicodeSegmentation;
use crate::style::Style;
pub(crate) const MAX_CELL_SYMBOL_BYTES: usize = 32;
#[inline]
pub(crate) fn sanitize_cell_char(ch: char) -> char {
let value = ch as u32;
if value < 0x20 || value == 0x7f || (0x80..=0x9f).contains(&value) {
'\u{FFFD}'
} else {
ch
}
}
pub(crate) fn normalize_cell_symbol(symbol: &str) -> CompactString {
let Some(grapheme) = symbol.graphemes(true).next() else {
return CompactString::new("");
};
let mut normalized = CompactString::new("");
for ch in grapheme.chars() {
let ch = sanitize_cell_char(ch);
if normalized.len().saturating_add(ch.len_utf8()) > MAX_CELL_SYMBOL_BYTES {
break;
}
normalized.push(ch);
}
normalized
}
const _: () = assert!(
std::mem::size_of::<Cell>() <= 64,
"Cell exceeds one cache line (64 B). If the size increase is intentional, update this bound and document why."
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
pub symbol: CompactString,
pub style: Style,
pub hyperlink: Option<CompactString>,
}
impl Default for Cell {
fn default() -> Self {
Self {
symbol: CompactString::const_new(" "),
style: Style::new(),
hyperlink: None,
}
}
}
impl Cell {
pub fn set_symbol(&mut self, s: &str) -> &mut Self {
self.symbol = normalize_cell_symbol(s);
self.hyperlink = None;
self
}
pub fn set_char(&mut self, ch: char) -> &mut Self {
self.symbol.clear();
self.symbol.push(sanitize_cell_char(ch));
self.hyperlink = None;
self
}
#[inline]
pub fn is_continuation(&self) -> bool {
self.symbol.is_empty()
}
pub(crate) fn set_continuation(&mut self, style: Style) -> &mut Self {
self.symbol.clear();
self.style = style;
self.hyperlink = None;
self
}
pub(crate) fn normalized_symbol(&self) -> CompactString {
normalize_cell_symbol(&self.symbol)
}
pub fn set_style(&mut self, style: Style) -> &mut Self {
self.style = style;
self
}
pub fn reset(&mut self) {
self.symbol.clear();
self.symbol.push(' ');
self.style = Style::new();
self.hyperlink = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cell_size_within_cache_line() {
let size = std::mem::size_of::<Cell>();
assert!(
size <= 64,
"Cell size = {size}B; exceeds 64B cache-line budget. If intentional, update the const-assert and this test together."
);
}
#[test]
fn setters_keep_one_safe_grapheme_and_clear_links() {
let mut cell = Cell::default();
cell.hyperlink = Some(CompactString::new("https://example.com"));
cell.set_symbol("π©βπ»tail\x1b");
assert_eq!(cell.symbol, "π©βπ»");
assert!(cell.hyperlink.is_none());
cell.set_char('\x1b');
assert_eq!(cell.symbol, "\u{FFFD}");
}
#[test]
fn empty_symbol_is_explicit_continuation_state() {
let mut cell = Cell::default();
assert!(!cell.is_continuation());
cell.set_continuation(Style::new());
assert!(cell.is_continuation());
}
#[test]
fn normalized_symbol_defends_against_direct_public_mutation() {
let mut cell = Cell::default();
cell.symbol = CompactString::new("\x1b]52;c;payload");
assert_eq!(cell.normalized_symbol(), "\u{FFFD}");
}
}