Skip to main content

justerm_core/
cell.rs

1//! The cell — one character position in the grid (see CONTEXT.md "Cell").
2
3use crate::color::Color;
4
5bitflags::bitflags! {
6    /// Per-cell flags: the standard SGR attributes plus layout markers.
7    ///
8    /// The high bits are intentionally left free so underline-style + underline
9    /// colour and an OSC 8 hyperlink id can be added later without a format
10    /// change (see `docs/architecture.md` "Cell").
11    #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
12    pub struct CellFlags: u16 {
13        // --- standard SGR attributes ---
14        const BOLD          = 1 << 0;
15        const DIM           = 1 << 1;
16        const ITALIC        = 1 << 2;
17        const UNDERLINE     = 1 << 3;
18        const BLINK         = 1 << 4;
19        const INVERSE       = 1 << 5;
20        const HIDDEN        = 1 << 6;
21        const STRIKETHROUGH = 1 << 7;
22
23        // --- layout markers (not SGR): a width-2 glyph occupies two cells ---
24        /// The first cell of a width-2 glyph; holds the actual character.
25        const WIDE_CHAR        = 1 << 8;
26        /// The trailing cell of a width-2 glyph. A distinct marker, *not* a
27        /// plain blank — overwrite, erase, selection, and cursor positioning all
28        /// depend on knowing this column belongs to the wide char to its left.
29        const WIDE_CHAR_SPACER = 1 << 9;
30        /// A row that soft-wrapped (auto-wrap) into the next — distinguishing it from a hard
31        /// CR/LF line-end so reflow (#7) can merge and re-split logical lines.
32        ///
33        /// **Wire-only.** The live grid holds this on the `Row` (`Grid::is_row_wrapped`); it used
34        /// to live here, where every whole-cell write and clear destroyed it and ordinary typing
35        /// in the last column silently split the logical line (#538). The wire has no per-row
36        /// slot, so it is derived back onto a span's last cell at encode time — which is why the
37        /// storage could move without a format change. On a cell read from the live grid this bit
38        /// is never set.
39        const WRAPLINE = 1 << 10;
40        // bits 11..=13 are the underline STYLE (#829) — a 3-bit field, not flags; read and
41        // written through `underline_style` / `set_underline_style`, never with `insert`.
42        // bits 14..=15 reserved (hyperlink id).
43    }
44}
45
46/// How a cell's underline is drawn — `SGR 4 : Ps` (#829).
47///
48/// **This is the storage, and `None` is a member of it.** There is no second boolean saying
49/// whether the cell is underlined: [`CellFlags::UNDERLINE`] survives as a *derived* view bit so
50/// existing consumers keep working, and the style is its only writer. So the two cannot disagree —
51/// which is not a stylistic preference but the defect three of the four references demonstrably
52/// pay for. alacritty's display layer inserts a plain `UNDERLINE` over a cell that already carries
53/// a curl and its renderer draws both rects; xterm.js has two readers that resolve the same
54/// conflict in opposite directions, pinned by a test; xterm leaves both bits set after
55/// `CSI 4m; CSI 21m` and lets each consumer pick a resolution. ghostty is the one where it is not
56/// representable — its `underline` is an `enum(u3)` with `none` among the members and there is no
57/// `underline: bool` — and that is the shape here.
58///
59/// Stored in the **content** word (bits 26..=28), not in a row side map: the packed cell is not
60/// full, so [the side-map
61/// invariant](https://github.com/kihyun1998/justerm/blob/master/docs/map/invariant/row-keyed-side-maps.md)
62/// does not apply, and packing is what makes the style ride along for free everywhere a cell
63/// moves — reflow, scroll, and the blank a wrapping wide glyph leaves behind. **One path needed an
64/// explicit carry and is worth naming rather than counting in**: `promote_cluster_to_wide` /
65/// `relocate_cluster_wide` *synthesise* the pair's spacer from the pen rather than moving it, so
66/// they take the style from the lead the same way they already take its extended attrs (ADR-0025
67/// D4). A refuting pass found that one; the other three are free.
68#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
69#[repr(u8)]
70pub enum UnderlineStyle {
71    /// Not underlined. The absence is a member rather than a separate flag.
72    #[default]
73    None = 0,
74    /// `SGR 4` or `4:1` — one straight line.
75    Single = 1,
76    /// Two straight lines — `4:2`, and also the legacy `SGR 21`, which is the **only** value with
77    /// a second spelling (#830). Both land on this field, so `SGR 24` clears either of them.
78    ///
79    /// The legacy form is not unanimous in the prior art and the spec is what settles it: `vte`
80    /// reads `21` as *cancel bold*, so an application meaning "stop bold" gets a double underline
81    /// here and keeps its bold. `SGR 22` is the arm that cancels bold.
82    Double = 2,
83    /// `4:3` — a curl. The mark #829 carries end to end.
84    Curly = 3,
85    /// `4:4` — a dotted line (#830). Drawn with a whole number of dots per cell, so the pattern
86    /// does not restart at a cell boundary.
87    Dotted = 4,
88    /// `4:5` — a dashed line (#830). One period per cell, with the dash split across the boundary
89    /// so adjacent cells' dashes join.
90    Dashed = 5,
91}
92
93impl UnderlineStyle {
94    /// The style for a raw 3-bit field, normalising anything outside the enum to
95    /// [`Single`](Self::Single) — the packing has eight representable values and six meanings, and
96    /// a total function here is what keeps `Cell`'s bit pattern canonical.
97    fn from_bits(v: u32) -> Self {
98        match v {
99            0 => Self::None,
100            2 => Self::Double,
101            3 => Self::Curly,
102            4 => Self::Dotted,
103            5 => Self::Dashed,
104            _ => Self::Single,
105        }
106    }
107}
108
109/// The underline style's field in the **view** (`CellFlags`), bits 11..=13.
110const CF_USTYLE_SHIFT: u32 = 11;
111const CF_USTYLE_MASK: u32 = 0b111 << CF_USTYLE_SHIFT;
112
113impl CellFlags {
114    /// The underline style this flag set carries.
115    pub fn underline_style(self) -> UnderlineStyle {
116        UnderlineStyle::from_bits((self.bits() as u32 & CF_USTYLE_MASK) >> CF_USTYLE_SHIFT)
117    }
118
119    /// Set the underline style, and [`UNDERLINE`](Self::UNDERLINE) with it.
120    ///
121    /// The single writer of both: setting a style arms the derived flag and clearing it disarms
122    /// the flag, so no caller can produce a set that disagrees with itself.
123    pub fn set_underline_style(&mut self, style: UnderlineStyle) {
124        let bits = (self.bits() as u32 & !CF_USTYLE_MASK) | ((style as u32) << CF_USTYLE_SHIFT);
125        *self = CellFlags::from_bits_retain(bits as u16);
126        self.set(CellFlags::UNDERLINE, style != UnderlineStyle::None);
127    }
128}
129
130// --- packed bit layout (#44) ----------------------------------------------
131//
132// Three 32-bit words mirroring xterm.js's `BufferLine` cell (verified against
133// `xtermjs/xterm.js@master` `src/common/buffer/Constants.ts`). The fg/bg colour
134// words are byte-identical to xterm's `Attributes` + `FgFlags`/`BgFlags`; the
135// content word keeps justerm's explicit layout-marker flags where xterm stores a
136// 2-bit `wcwidth` value (justerm's model is flag-based — the spacer markers are load-bearing for
137// overwrite/selection/reflow; WRAPLINE is wire-only, the live flag is on the `Row`, #538).
138//
139//   content u32: codepoint(21) | COMBINED(1) | WIDE | SPACER | WRAP | reserved
140//   fg/bg   u32: colour value(24) | colour mode(2) | flags(6)
141//
142// COMBINED_PRESENT (content) and LINK_PRESENT (bg, xterm's HAS_EXTENDED slot) are
143// both live: combining clusters (#45) and OSC 8 hyperlink indices (#46) live in
144// per-row, column-keyed maps, and these bits gate every read of them. The cell is
145// now pure packed words — three u32, no `Option` field (the epic's 12 B target).
146
147const CODEPOINT_MASK: u32 = 0x001F_FFFF; // bits 0..21
148const C_COMBINED: u32 = 1 << 21; // a combining cluster lives in the row's map at this column (#45)
149const C_WIDE: u32 = 1 << 22;
150const C_SPACER: u32 = 1 << 23;
151const C_WRAP: u32 = 1 << 24;
152// The vacated column left when a width-2 glyph wraps off the right edge (#113):
153// a blank that holds no character but isn't a hard line-end. Unlike C_SPACER it
154// has *no wide lead to its left*, so the overwrite/erase repair paths (which key
155// off C_SPACER) must not treat it as one — it's a separate marker the text
156// extractors skip. Engine-internal: it stays in the content word and never
157// reaches `flags()` / the wire (a frame-mode consumer gets the already-correct
158// text, and the cell renders as the blank it is).
159const C_LEADING_SPACER: u32 = 1 << 25;
160const CONTENT_MARKER_MASK: u32 = C_WIDE | C_SPACER | C_WRAP;
161// The underline style (#829) — a 3-bit field, bits 26..=28 of the content word. It is deliberately
162// *outside* `CONTENT_MARKER_MASK`: that mask means "this column means something even though its
163// codepoint is a space", and a decoration does not, exactly as a colour does not (see
164// `Cell::is_blank`). Bits 29..=31 stay free.
165const C_USTYLE_SHIFT: u32 = 26;
166const C_USTYLE_MASK: u32 = 0b111 << C_USTYLE_SHIFT;
167
168const COLOR_VALUE_MASK: u32 = 0x00FF_FFFF; // bits 0..24
169const COLOR_MODE_SHIFT: u32 = 24; // bits 24..26
170const CM_DEFAULT: u32 = 0;
171const CM_INDEXED: u32 = 1;
172const CM_RGB: u32 = 2;
173
174// fg flags, bits 26..32 — xterm FgFlags order (HIDDEN == xterm INVISIBLE).
175const FG_INVERSE: u32 = 1 << 26;
176const FG_BOLD: u32 = 1 << 27;
177const FG_UNDERLINE: u32 = 1 << 28;
178const FG_BLINK: u32 = 1 << 29;
179const FG_HIDDEN: u32 = 1 << 30;
180const FG_STRIKE: u32 = 1 << 31;
181const FG_FLAG_MASK: u32 = FG_INVERSE | FG_BOLD | FG_UNDERLINE | FG_BLINK | FG_HIDDEN | FG_STRIKE;
182
183// bg flags, bits 26..28 — xterm BgFlags order.
184const BG_ITALIC: u32 = 1 << 26;
185const BG_DIM: u32 = 1 << 27;
186// LINK_PRESENT: an OSC 8 hyperlink URI lives in the row's link map at this
187// column (#46; the URI itself rather than a pool index since #628). Reuses xterm's `BgFlags.HAS_EXTENDED = 0x10000000` (bit 28)
188// exactly — in xterm this is a *shared* "extended attrs present" gate (link +
189// underline colour). justerm keeps the two concerns in *separate* per-row maps
190// (as combining and links are separate, #520/ADR-none), so it gates each with
191// its own bit rather than xterm's one shared object.
192const BG_LINK: u32 = 1 << 28;
193// UCOLOR_PRESENT (#520): a non-default underline colour (SGR 58) lives in the
194// row's ucolor map at this column. Its own presence bit, gating a separate map —
195// the 12-byte cell (three packed words) has no room for a fourth colour, so the
196// colour rides a side map exactly as the hyperlink does. **The underline style did NOT land
197// here** — this comment used to send the next reader to bits 30,31 for it, and two bits cannot
198// hold six values; it went to the CONTENT word at bits 26..=28 (#829). Bits 30,31 stay free for a
199// second extended attr.
200const BG_UCOLOR: u32 = 1 << 29;
201const BG_FLAG_MASK: u32 = BG_ITALIC | BG_DIM;
202
203/// Pack a colour reference into the low 26 bits of a colour word (mode + value);
204/// the high 6 bits are left for the SGR flags.
205fn pack_color(c: Color) -> u32 {
206    match c {
207        Color::Default => CM_DEFAULT << COLOR_MODE_SHIFT,
208        Color::Indexed(i) => (CM_INDEXED << COLOR_MODE_SHIFT) | i as u32,
209        Color::Rgb(r, g, b) => {
210            (CM_RGB << COLOR_MODE_SHIFT) | (r as u32) << 16 | (g as u32) << 8 | b as u32
211        }
212    }
213}
214
215/// Inverse of [`pack_color`] — reads only the mode + value bits, ignoring the
216/// flag bits that share the word.
217fn unpack_color(w: u32) -> Color {
218    match (w >> COLOR_MODE_SHIFT) & 0b11 {
219        CM_INDEXED => Color::Indexed((w & 0xFF) as u8),
220        CM_RGB => Color::Rgb((w >> 16) as u8, (w >> 8) as u8, w as u8),
221        _ => Color::Default, // CM_DEFAULT (and the unused mode 3) resolve to Default
222    }
223}
224
225/// Scatter a `CellFlags` bit set (as a `u32`) into the three words' flag-bit
226/// positions: `(content_markers, fg_flags, bg_flags)`. Branchless — each group is
227/// masked and shifted in one step. The `CellFlags` bit values are frozen by the
228/// wire format (`serialize` encodes `flags().bits()`), so the source positions are
229/// fixed; see the shift comments. One place for store / insert / remove to share.
230#[inline]
231fn flag_words(f: u32) -> (u32, u32, u32) {
232    // The underline style owns the underline (#829). A caller that set only `UNDERLINE` means a
233    // single one, so it is normalised here rather than stored as a styleless underline — which is
234    // both the single-owner rule and what keeps the packing canonical, the property `Cell`'s
235    // derived `Eq` is a bitwise compare because of.
236    let mut style = UnderlineStyle::from_bits((f & CF_USTYLE_MASK) >> CF_USTYLE_SHIFT);
237    if style == UnderlineStyle::None && f & 0x0008 != 0 {
238        style = UnderlineStyle::Single;
239    }
240    let content = ((f & 0x0700) << 14)              // WIDE/SPACER/WRAP bits 8,9,10 -> 22,23,24
241        | ((style as u32) << C_USTYLE_SHIFT); // underline style -> 26,27,28
242    let fg = ((f & 0x0001) << 27)                   // BOLD     bit 0  -> 27
243        | ((f & 0x0020) << 21)                      // INVERSE  bit 5  -> 26
244        | ((f & 0x0010) << 25)                      // BLINK    bit 4  -> 29
245        | ((f & 0x00C0) << 24); // HIDDEN/STRIKE bits 6,7 -> 30,31
246    // `FG_UNDERLINE` is deliberately **not written** (#829). The style in the content word is the
247    // single owner, and a mutation proved a derived copy here would be write-only: nothing reads
248    // it — `flags()` derives `UNDERLINE` from the style, and the wire carries
249    // `encode_color(cell.fg())`, a tagged colour, so the fg word's flag bits never leave the
250    // process. A duplicate nobody reads is still a duplicate: it is named like every authoritative
251    // flag beside it, so the next reader uses it, which is exactly how xterm.js ended up with two
252    // readers resolving the same question in opposite directions. The bit stays reserved for its
253    // xterm-mirror position and stays zero, cleared with the rest by `FG_FLAG_MASK`.
254    let bg = ((f & 0x0004) << 24)     // ITALIC bit 2 -> 26
255        | ((f & 0x0002) << 26); // DIM    bit 1 -> 27
256    (content, fg, bg)
257}
258
259/// One character position: a base glyph, fg/bg colour references, and flags.
260/// Combining marks (#45) and an OSC 8 hyperlink (#46) attach via per-row maps,
261/// signalled by the `COMBINED_PRESENT` / `LINK_PRESENT` bits — the cell itself is
262/// three packed words, no `Option` field. All access is through the accessor seam
263/// (#44); construct with [`Cell::from_parts`] or [`Cell::default`].
264///
265/// `Eq` is a derived bitwise compare, which is exact because the packing is
266/// canonical — every logical cell maps to one bit pattern (unused bits stay 0).
267#[derive(Clone, Copy, PartialEq, Eq)]
268pub struct Cell {
269    content: u32,
270    fg: u32,
271    bg: u32,
272}
273
274impl Default for Cell {
275    fn default() -> Self {
276        // The packed form of a blank cell: ' ' (U+0020) in the codepoint field,
277        // every other word zero (Default colours, no flags, no combining/link
278        // bits). Built directly rather than through `from_parts` so scroll/erase
279        // blanking — which constructs defaults by the rowful — stays a cheap copy.
280        Cell {
281            content: ' ' as u32,
282            fg: 0,
283            bg: 0,
284        }
285    }
286}
287
288impl core::fmt::Debug for Cell {
289    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
290        f.debug_struct("Cell")
291            .field("c", &self.c())
292            .field("fg", &self.fg())
293            .field("bg", &self.bg())
294            .field("flags", &self.flags())
295            .field("combined", &self.is_combined())
296            .field("linked", &self.is_linked())
297            // Every cell-state bit that lives OUTSIDE `flags()` — the three presence
298            // bits and the engine-internal leading-spacer marker. `flags()` above
299            // covers the rest; these do not appear in it, so omitting one makes two
300            // unequal cells print as two identical lines. Not hypothetical: #531 read
301            // exactly that way in a failing `assert_eq!(frame, …)` (the ucolor bit was
302            // missing here), and `leading_spacer` reproduced it a second time in the
303            // same session. A bit added to this struct is added here too.
304            .field("ucolored", &self.is_ucolored())
305            .field("leading_spacer", &self.is_leading_spacer())
306            .finish()
307    }
308}
309
310impl Cell {
311    /// Assemble a cell from its logical parts. The single construction seam —
312    /// `Pen::cell` and the wire decoder funnel through here, so the bit-packing
313    /// lives in exactly one place (#44).
314    pub fn from_parts(c: char, fg: Color, bg: Color, flags: CellFlags) -> Self {
315        let mut cell = Cell {
316            content: c as u32, // a `char` is <= U+10FFFF, so it fits the 21-bit field
317            fg: pack_color(fg),
318            bg: pack_color(bg),
319        };
320        cell.store_flags(flags);
321        cell
322    }
323
324    /// Replace the flag bits across the three words from `flags`, preserving the
325    /// codepoint, colours, and the dormant presence bits. The inverse is
326    /// [`Cell::flags`].
327    fn store_flags(&mut self, flags: CellFlags) {
328        let (content, fg, bg) = flag_words(flags.bits() as u32);
329        self.content = (self.content & !(CONTENT_MARKER_MASK | C_USTYLE_MASK)) | content;
330        self.fg = (self.fg & !FG_FLAG_MASK) | fg;
331        self.bg = (self.bg & !BG_FLAG_MASK) | bg;
332    }
333
334    /// The base code point.
335    pub fn c(&self) -> char {
336        char::from_u32(self.content & CODEPOINT_MASK)
337            .expect("codepoint bits always hold a valid char")
338    }
339
340    /// Does this cell hold no **content** — no glyph and no layout marker?
341    ///
342    /// A blank the app never wrote and one it erased to a coloured background are both blank: the
343    /// background is not content. But a wide-char spacer, a leading-spacer wrap artefact, or a
344    /// combining-cluster carrier all *mean* something at their column even though their base
345    /// code point is a space — they are not blank. Used by reflow to find where a hard-ended
346    /// line ends (mirrors xterm.js `getTrimmedLength` / alacritty `line_length`, which likewise
347    /// test content, not the background); it says nothing about a cell's colour.
348    pub fn is_blank(&self) -> bool {
349        // Space codepoint, and none of the content-marker bits set. `content` holds the codepoint
350        // plus the COMBINED / WIDE / SPACER / WRAP / LEADING_SPACER markers, so a single check on
351        // the whole word covers every "means something here" case at once.
352        self.content & (CODEPOINT_MASK | CONTENT_MARKER_MASK | C_COMBINED | C_LEADING_SPACER)
353            == ' ' as u32
354    }
355
356    /// How this cell's underline is drawn (#829). [`UnderlineStyle::None`] means not underlined —
357    /// there is no separate boolean to consult, and [`CellFlags::UNDERLINE`] is derived from this.
358    pub fn underline_style(&self) -> UnderlineStyle {
359        UnderlineStyle::from_bits((self.content & C_USTYLE_MASK) >> C_USTYLE_SHIFT)
360    }
361
362    /// The foreground colour reference.
363    pub fn fg(&self) -> Color {
364        unpack_color(self.fg)
365    }
366
367    /// The background colour reference.
368    pub fn bg(&self) -> Color {
369        unpack_color(self.bg)
370    }
371
372    /// The cell's flags (SGR attributes + layout markers), reassembled from the
373    /// three words — the branchless inverse of `Cell::store_flags`.
374    pub fn flags(&self) -> CellFlags {
375        // `UNDERLINE` is *derived* from the style rather than read from `FG_UNDERLINE` (#829), so a
376        // reader can never be handed a set where the flag and the style disagree.
377        let style = (self.content & C_USTYLE_MASK) >> C_USTYLE_SHIFT;
378        let bits = ((self.content & CONTENT_MARKER_MASK) >> 14)         // 22,23,24 -> 8,9,10
379            | (style << CF_USTYLE_SHIFT)                               // 26,27,28 -> 11,12,13
380            | if style == 0 { 0 } else { 0x0008 }                      // UNDERLINE, derived
381            | ((self.fg & FG_BOLD) >> 27)                              // 27 -> 0
382            | ((self.fg & FG_INVERSE) >> 21)                           // 26 -> 5
383            | ((self.fg & FG_BLINK) >> 25)                             // 29 -> 4
384            | ((self.fg & (FG_HIDDEN | FG_STRIKE)) >> 24)              // 30,31 -> 6,7
385            | ((self.bg & BG_ITALIC) >> 24)                           // 26 -> 2
386            | ((self.bg & BG_DIM) >> 26); // 27 -> 1
387        CellFlags::from_bits_retain(bits as u16)
388    }
389
390    /// Does this column carry combining marks? When true, the cluster lives in
391    /// the row's combining map at this column (#45) — a flag-gated cache: never
392    /// read the map without first checking this bit.
393    pub fn is_combined(&self) -> bool {
394        self.content & C_COMBINED != 0
395    }
396
397    /// Does this column carry an OSC 8 hyperlink? When true, the URI lives in the
398    /// row's link map at this column (#46; the URI itself rather than an index into a
399    /// buffer-wide pool since #628) — flag-gated like combining: never read the link
400    /// map without first checking this bit.
401    pub fn is_linked(&self) -> bool {
402        self.bg & BG_LINK != 0
403    }
404
405    /// Does this column carry a non-default underline colour (SGR 58, #520)? When
406    /// true, the `Color` reference lives in the row's ucolor map at this column —
407    /// flag-gated exactly like the hyperlink: never read the ucolor map without
408    /// first checking this bit.
409    pub fn is_ucolored(&self) -> bool {
410        self.bg & BG_UCOLOR != 0
411    }
412
413    /// Overwrite the base code point, preserving the layout markers.
414    pub fn set_c(&mut self, c: char) {
415        self.content = (self.content & !CODEPOINT_MASK) | c as u32;
416    }
417
418    /// Overwrite the background colour (the BCE erase fill, #16), preserving the
419    /// bg-word flag bits.
420    pub fn set_bg(&mut self, bg: Color) {
421        self.bg = pack_color(bg) | (self.bg & !(COLOR_VALUE_MASK | (0b11 << COLOR_MODE_SHIFT)));
422    }
423
424    /// Mark (or unmark) this column as carrying combining marks in the row map.
425    pub fn set_combined(&mut self, on: bool) {
426        if on {
427            self.content |= C_COMBINED;
428        } else {
429            self.content &= !C_COMBINED;
430        }
431    }
432
433    /// Mark (or unmark) this column as carrying an OSC 8 hyperlink in the row map.
434    pub fn set_linked(&mut self, on: bool) {
435        if on {
436            self.bg |= BG_LINK;
437        } else {
438            self.bg &= !BG_LINK;
439        }
440    }
441
442    /// Mark (or unmark) this column as carrying a non-default underline colour in
443    /// the row's ucolor map (#520). Mirror of [`Cell::set_linked`].
444    pub fn set_ucolored(&mut self, on: bool) {
445        if on {
446            self.bg |= BG_UCOLOR;
447        } else {
448            self.bg &= !BG_UCOLOR;
449        }
450    }
451
452    /// Add the given flags (leaving the others set). Sets the word bits directly —
453    /// no round-trip through `flags()`/`store_flags`.
454    ///
455    /// **The underline is a field, not a bit, so it is *replaced* rather than OR-ed (#829).**
456    /// Bit-OR is the right operation for every other member and the wrong one for a 3-bit value:
457    /// OR-ing `Dotted` (4) into a `Single` (1) cell yields `Dashed` (5), a style neither the
458    /// caller nor the parser asked for, and a bit pattern no canonical cell has — which would
459    /// break the property this type's derived `Eq` is a bitwise compare because of. A completeness
460    /// pass found this; nothing in this repository reached it, but `Cell` is published.
461    pub fn insert_flags(&mut self, flags: CellFlags) {
462        let (content, fg, bg) = flag_words(flags.bits() as u32);
463        self.content |= content & !C_USTYLE_MASK;
464        self.fg |= fg;
465        self.bg |= bg;
466        // `flag_words` already normalised a bare `UNDERLINE` to `Single`, so "names the underline
467        // at all" is exactly "the normalised style is not `None`".
468        let named = UnderlineStyle::from_bits((content & C_USTYLE_MASK) >> C_USTYLE_SHIFT);
469        if named != UnderlineStyle::None {
470            self.set_underline_style(named);
471        }
472    }
473
474    /// Clear the given flags (leaving the others as they are).
475    ///
476    /// **Naming the underline clears the whole field (#829)**, whichever way it was named — the
477    /// `UNDERLINE` flag or a style value. Masking the bits off instead would turn one style into
478    /// another (clearing `UNDERLINE`, which normalises to `Single` = `0b001`, subtracts a bit from
479    /// `Curly` = `0b011` and leaves `Double`), so a method documented as clearing a flag would
480    /// return a cell that is still underlined, in a style no input can produce.
481    pub fn remove_flags(&mut self, flags: CellFlags) {
482        let (content, fg, bg) = flag_words(flags.bits() as u32);
483        self.content &= !(content & !C_USTYLE_MASK);
484        self.fg &= !fg;
485        self.bg &= !bg;
486        let named = UnderlineStyle::from_bits((content & C_USTYLE_MASK) >> C_USTYLE_SHIFT);
487        if named != UnderlineStyle::None {
488            self.set_underline_style(UnderlineStyle::None);
489        }
490    }
491
492    /// Set the underline style on this cell, arming or disarming the derived `UNDERLINE` view bit
493    /// with it. The one writer of the field on a built cell.
494    pub fn set_underline_style(&mut self, style: UnderlineStyle) {
495        self.content = (self.content & !C_USTYLE_MASK) | ((style as u32) << C_USTYLE_SHIFT);
496    }
497
498    /// Reset to a blank **default** cell — default background included.
499    ///
500    /// That is rarely what a terminal operation wants on its own: a blank the engine creates
501    /// carries the current background (BCE for an erase, and the same for a structural repair,
502    /// #530). Callers pair this with `set_bg`; `Term::free_cell` and the erase paths are the
503    /// places that do. Using it bare leaves an uncoloured notch in a coloured run.
504    pub fn reset(&mut self) {
505        *self = Cell::default();
506    }
507
508    /// Is this the lead cell of a width-2 glyph? Direct content-bit query — the
509    /// hot overwrite/erase/reflow paths use this instead of reconstructing the
510    /// full `flags()` to test one marker.
511    pub fn is_wide(&self) -> bool {
512        self.content & C_WIDE != 0
513    }
514
515    /// Is this the trailing spacer cell of a width-2 glyph?
516    pub fn is_wide_spacer(&self) -> bool {
517        self.content & C_SPACER != 0
518    }
519
520    /// Is this the blank column vacated when a wide glyph wrapped off the right
521    /// edge (#113)? It holds no character; unlike a trailing spacer it has no
522    /// wide lead to its left, so only the *text* extractors skip it.
523    pub fn is_leading_spacer(&self) -> bool {
524        self.content & C_LEADING_SPACER != 0
525    }
526
527    /// Does this column hold no text — either half of a wide glyph's trailing
528    /// spacer or a wide-wrap leading spacer? Used by the text extractors (search,
529    /// selection text, logical lines) to skip non-character columns.
530    pub fn is_spacer(&self) -> bool {
531        self.content & (C_SPACER | C_LEADING_SPACER) != 0
532    }
533
534    /// Drop the leading-spacer marker, leaving the cell otherwise untouched.
535    ///
536    /// The marker claims two things at once, and it has to go when **either** stops holding, or
537    /// the text extractors keep skipping a column that is now a real blank: the row still
538    /// soft-wraps (`Term::end_wrap` owns that half — #538, #540), and the continuation still
539    /// begins with the wide lead that could not fit (`Term::repair_wrap_artefact_above` owns that
540    /// one — #534). Clearing is deliberately one-way: nothing here re-arms the marker, because a
541    /// wide glyph typed at column 0 of the next row did not *wrap* from anywhere.
542    pub fn clear_leading_spacer(&mut self) {
543        self.content &= !C_LEADING_SPACER;
544    }
545
546    /// Mark this column as the leading spacer of a wrapped wide glyph.
547    ///
548    /// **Records** that the column is blank; it does not make it so. The caller must have
549    /// written the blank first — this only ORs a marker onto whatever cell is there. Setting
550    /// it over a live glyph leaves a cell the text extractors skip while a renderer still
551    /// draws it, which is exactly the defect #528 fixed (`Term::vacate_for_wrap` is the one
552    /// place that establishes the precondition; reflow is the other set site, #533).
553    pub fn set_leading_spacer(&mut self) {
554        self.content |= C_LEADING_SPACER;
555    }
556
557    /// Does this **wire** cell end a soft-wrapped row? See `CellFlags::WRAPLINE` — on the live
558    /// grid this is always false and `Grid::is_row_wrapped` is the question to ask (#538).
559    pub fn is_wrapline(&self) -> bool {
560        self.content & C_WRAP != 0
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::{Cell, CellFlags, UnderlineStyle};
567    use crate::color::Color;
568
569    /// Size pin: slice C moves `link` out of the cell into the row's link map (a
570    /// cell now signals a hyperlink with only the `LINK_PRESENT` bg bit), so `Cell`
571    /// is **12 bytes** — three packed `u32` words, matching xterm.js's `BufferLine`
572    /// cell. This is the epic's target (#43): combining and link both ride per-row
573    /// maps, the cell is pure packed words. Flood throughput is
574    /// memory-bandwidth-bound, so this size is touched on every print/scroll-blank.
575    /// [#42, #46]
576    #[test]
577    fn cell_is_12_bytes() {
578        assert_eq!(std::mem::size_of::<Cell>(), 12);
579    }
580
581    /// The packing must be lossless: every colour reference read back equal in
582    /// both the fg and bg word, including the tag-distinguished trio that must not
583    /// collapse (`Default` / `Indexed(0)` / `Rgb(0,0,0)`).
584    #[test]
585    fn every_colour_round_trips_in_both_words() {
586        let colours = [
587            Color::Default,
588            Color::Indexed(0),
589            Color::Indexed(255),
590            Color::Rgb(0, 0, 0),
591            Color::Rgb(255, 128, 1),
592        ];
593        for &fg in &colours {
594            for &bg in &colours {
595                let cell = Cell::from_parts('x', fg, bg, CellFlags::empty());
596                assert_eq!(cell.fg(), fg, "fg {fg:?} / bg {bg:?}");
597                assert_eq!(cell.bg(), bg, "fg {fg:?} / bg {bg:?}");
598            }
599        }
600    }
601
602    /// Every flag bit — SGR attributes (split across the fg/bg words) and the
603    /// layout markers (in the content word) — round-trips, alone and combined.
604    #[test]
605    fn every_flag_round_trips() {
606        // `UNDERLINE` is the one flag that deliberately does **not** round-trip as given (#829).
607        // The underline style is the single owner and `None` is one of its members, so a cell
608        // built with the flag alone is a cell with a *single* underline and reads back carrying
609        // that style — "underlined with no style" is not a state this model has. That is what
610        // keeps the packing canonical, which this type's derived `Eq` is a bitwise compare
611        // because of, and it is asserted here rather than accommodated so the normalisation
612        // cannot be mistaken for a leak.
613        let mut normalised_underline = CellFlags::UNDERLINE;
614        normalised_underline.set_underline_style(UnderlineStyle::Single);
615
616        let all = CellFlags::all();
617        for bit in all.iter() {
618            let cell = Cell::from_parts('x', Color::Default, Color::Default, bit);
619            let expected = if bit == CellFlags::UNDERLINE {
620                normalised_underline
621            } else {
622                bit
623            };
624            assert_eq!(cell.flags(), expected, "single {bit:?}");
625        }
626        let cell = Cell::from_parts('x', Color::Default, Color::Default, all);
627        assert_eq!(
628            cell.flags(),
629            all.union(normalised_underline),
630            "all flags at once",
631        );
632    }
633
634    /// The codepoint occupies 21 bits — the full Unicode range, up to the
635    /// maximum scalar value, survives alongside flags set in the same word.
636    #[test]
637    fn codepoint_round_trips_to_the_unicode_max() {
638        for c in ['a', ' ', '한', '🦀', '\u{10FFFF}'] {
639            let cell = Cell::from_parts(c, Color::Default, Color::Default, CellFlags::WIDE_CHAR);
640            assert_eq!(cell.c(), c, "codepoint {c:?}");
641            assert!(cell.flags().contains(CellFlags::WIDE_CHAR));
642        }
643    }
644
645    /// The combining-presence bit (content word) and link-presence bit (bg word)
646    /// are independent of each other, of the codepoint/markers, of the colours, and
647    /// of the SGR flags — toggling one must disturb none of the others.
648    #[test]
649    fn combined_and_linked_bits_are_independent() {
650        let mut cell = Cell::from_parts(
651            'e',
652            Color::Indexed(3),
653            Color::Rgb(1, 2, 3),
654            CellFlags::WIDE_CHAR | CellFlags::DIM,
655        );
656        assert!(!cell.is_combined());
657        assert!(!cell.is_linked());
658
659        cell.set_combined(true);
660        cell.set_linked(true);
661        assert!(cell.is_combined() && cell.is_linked());
662        // Everything else survives both bits being set.
663        assert_eq!(cell.c(), 'e');
664        assert_eq!(cell.fg(), Color::Indexed(3));
665        assert_eq!(
666            cell.bg(),
667            Color::Rgb(1, 2, 3),
668            "link bit shares the bg word"
669        );
670        assert!(cell.flags().contains(CellFlags::WIDE_CHAR | CellFlags::DIM));
671
672        cell.set_linked(false);
673        assert!(cell.is_combined() && !cell.is_linked());
674        cell.set_combined(false);
675        assert!(!cell.is_combined() && !cell.is_linked());
676        assert_eq!(
677            cell.bg(),
678            Color::Rgb(1, 2, 3),
679            "bg colour intact after clearing"
680        );
681
682        let spacer = Cell::from_parts(
683            ' ',
684            Color::Default,
685            Color::Default,
686            CellFlags::WIDE_CHAR_SPACER,
687        );
688        assert!(spacer.is_wide_spacer());
689        assert!(!Cell::default().is_wide_spacer());
690    }
691
692    /// The underline-colour presence bit (#520) is its own bg-word bit, independent
693    /// of the link bit that shares the word and of the bg colour value — toggling
694    /// it disturbs neither, and it does NOT grow the cell (the 12-byte pin above
695    /// still holds because the colour rides a side map, not the cell).
696    #[test]
697    fn the_ucolor_presence_bit_is_independent_of_the_link_bit_and_bg_colour() {
698        let mut cell = Cell::from_parts('u', Color::Default, Color::Rgb(1, 2, 3), CellFlags::DIM);
699        assert!(!cell.is_ucolored());
700        assert!(!cell.is_linked());
701
702        cell.set_ucolored(true);
703        cell.set_linked(true);
704        assert!(cell.is_ucolored() && cell.is_linked());
705        // The bg colour and the DIM flag (both in the bg word) survive both bits.
706        assert_eq!(cell.bg(), Color::Rgb(1, 2, 3));
707        assert!(cell.flags().contains(CellFlags::DIM));
708
709        // Clearing one leaves the other and the colour intact.
710        cell.set_linked(false);
711        assert!(cell.is_ucolored() && !cell.is_linked());
712        cell.set_ucolored(false);
713        assert!(!cell.is_ucolored());
714        assert_eq!(
715            cell.bg(),
716            Color::Rgb(1, 2, 3),
717            "bg colour intact after clearing"
718        );
719    }
720}