1use compact_str::CompactString;
4use unicode_segmentation::UnicodeSegmentation;
5
6use crate::style::Style;
7
8pub(crate) const MAX_CELL_SYMBOL_BYTES: usize = 32;
13
14#[inline]
16pub(crate) fn sanitize_cell_char(ch: char) -> char {
17 let value = ch as u32;
18 if value < 0x20 || value == 0x7f || (0x80..=0x9f).contains(&value) {
19 '\u{FFFD}'
20 } else {
21 ch
22 }
23}
24
25pub(crate) fn normalize_cell_symbol(symbol: &str) -> CompactString {
30 let Some(grapheme) = symbol.graphemes(true).next() else {
31 return CompactString::new("");
32 };
33
34 let mut normalized = CompactString::new("");
35 for ch in grapheme.chars() {
36 let ch = sanitize_cell_char(ch);
37 if normalized.len().saturating_add(ch.len_utf8()) > MAX_CELL_SYMBOL_BYTES {
38 break;
39 }
40 normalized.push(ch);
41 }
42 normalized
43}
44
45const _: () = assert!(
56 std::mem::size_of::<Cell>() <= 64,
57 "Cell exceeds one cache line (64 B). If the size increase is intentional, update this bound and document why."
58);
59
60#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Cell {
68 pub symbol: CompactString,
70 pub style: Style,
72 pub hyperlink: Option<CompactString>,
75}
76
77impl Default for Cell {
78 fn default() -> Self {
79 Self {
80 symbol: CompactString::const_new(" "),
81 style: Style::new(),
82 hyperlink: None,
83 }
84 }
85}
86
87impl Cell {
88 pub fn set_symbol(&mut self, s: &str) -> &mut Self {
94 self.symbol = normalize_cell_symbol(s);
95 self.hyperlink = None;
96 self
97 }
98
99 pub fn set_char(&mut self, ch: char) -> &mut Self {
101 self.symbol.clear();
102 self.symbol.push(sanitize_cell_char(ch));
103 self.hyperlink = None;
104 self
105 }
106
107 #[inline]
112 pub fn is_continuation(&self) -> bool {
113 self.symbol.is_empty()
114 }
115
116 pub(crate) fn set_continuation(&mut self, style: Style) -> &mut Self {
118 self.symbol.clear();
119 self.style = style;
120 self.hyperlink = None;
121 self
122 }
123
124 pub(crate) fn normalized_symbol(&self) -> CompactString {
129 normalize_cell_symbol(&self.symbol)
130 }
131
132 pub fn set_style(&mut self, style: Style) -> &mut Self {
134 self.style = style;
135 self
136 }
137
138 pub fn reset(&mut self) {
140 self.symbol.clear();
141 self.symbol.push(' ');
142 self.style = Style::new();
143 self.hyperlink = None;
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn cell_size_within_cache_line() {
153 let size = std::mem::size_of::<Cell>();
154 assert!(
155 size <= 64,
156 "Cell size = {size}B; exceeds 64B cache-line budget. If intentional, update the const-assert and this test together."
157 );
158 }
159
160 #[test]
161 fn setters_keep_one_safe_grapheme_and_clear_links() {
162 let mut cell = Cell::default();
163 cell.hyperlink = Some(CompactString::new("https://example.com"));
164 cell.set_symbol("π©βπ»tail\x1b");
165
166 assert_eq!(cell.symbol, "π©βπ»");
167 assert!(cell.hyperlink.is_none());
168
169 cell.set_char('\x1b');
170 assert_eq!(cell.symbol, "\u{FFFD}");
171 }
172
173 #[test]
174 fn empty_symbol_is_explicit_continuation_state() {
175 let mut cell = Cell::default();
176 assert!(!cell.is_continuation());
177 cell.set_continuation(Style::new());
178 assert!(cell.is_continuation());
179 }
180
181 #[test]
182 fn normalized_symbol_defends_against_direct_public_mutation() {
183 let mut cell = Cell::default();
184 cell.symbol = CompactString::new("\x1b]52;c;payload");
185 assert_eq!(cell.normalized_symbol(), "\u{FFFD}");
186 }
187}