Skip to main content

muxr_core/protocol/
pane_render.rs

1use std::sync::Arc;
2
3use compact_str::CompactString;
4use rootcause::prelude::ResultExt;
5use rootcause::report;
6use serde::Deserialize;
7use serde::Serialize;
8
9use super::TerminalSize;
10
11#[derive(rkyv::Archive, Clone, Debug, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
12pub enum RenderUpdate {
13    Baseline(RenderBaseline),
14    Diff(RenderDiff),
15}
16
17#[derive(rkyv::Archive, Clone, Copy, Debug, Eq, PartialEq, rkyv::Serialize)]
18pub(super) enum RenderHyperlinkPresence {
19    Absent,
20    Present,
21}
22
23#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
24pub struct RenderBaseline {
25    cursor: RenderCursor,
26    // Constructors and validated deserialization recompute this routing state so encoding never rescans full frames.
27    #[serde(skip)]
28    hyperlink_presence: RenderHyperlinkPresence,
29    rows: Vec<RenderRowSpan>,
30    seq: u64,
31    size: TerminalSize,
32}
33
34impl RenderBaseline {
35    /// Build a full visible-frame render baseline.
36    ///
37    /// # Errors
38    /// - The sequence is zero.
39    /// - The cursor or any row span is outside the frame size.
40    /// - The baseline does not contain exactly one full-width row for every visible row.
41    pub fn new(
42        seq: u64,
43        size: TerminalSize,
44        cursor: RenderCursor,
45        rows: Vec<RenderRowSpan>,
46    ) -> rootcause::Result<Self> {
47        let baseline = Self {
48            cursor,
49            hyperlink_presence: Self::rows_hyperlink_presence(&rows),
50            rows,
51            seq,
52            size,
53        };
54        baseline.validate()?;
55        Ok(baseline)
56    }
57
58    #[must_use]
59    pub fn into_parts(self) -> (u64, TerminalSize, RenderCursor, Vec<RenderRowSpan>) {
60        (self.seq, self.size, self.cursor, self.rows)
61    }
62
63    #[must_use]
64    pub const fn cursor(&self) -> &RenderCursor {
65        &self.cursor
66    }
67
68    #[must_use]
69    pub fn rows(&self) -> &[RenderRowSpan] {
70        &self.rows
71    }
72
73    #[must_use]
74    pub(super) const fn hyperlink_presence(&self) -> RenderHyperlinkPresence {
75        self.hyperlink_presence
76    }
77
78    #[must_use]
79    pub const fn seq(&self) -> u64 {
80        self.seq
81    }
82
83    #[must_use]
84    pub const fn size(&self) -> &TerminalSize {
85        &self.size
86    }
87
88    fn validate(&self) -> rootcause::Result<()> {
89        if self.seq == 0 {
90            return Err(report!("invalid muxr render baseline").attach("reason=seq must be nonzero"));
91        }
92        self.cursor.validate(self.size.rows(), self.size.cols())?;
93        Self::validate_full_rows(&self.size, &self.rows)
94    }
95
96    fn validate_full_rows(size: &TerminalSize, rows: &[RenderRowSpan]) -> rootcause::Result<()> {
97        if rows.len() != usize::from(size.rows()) {
98            return Err(report!("invalid muxr render baseline")
99                .attach("reason=row count must match frame height")
100                .attach(format!("expected={}", size.rows()))
101                .attach(format!("actual={}", rows.len())));
102        }
103
104        for (expected_row, row) in (0..size.rows()).zip(rows.iter()) {
105            row.validate(size.rows(), size.cols())?;
106            if row.row != expected_row || row.col != 0 || row.width()? != size.cols() {
107                return Err(report!("invalid muxr render baseline")
108                    .attach("reason=baseline rows must be full-width and ordered")
109                    .attach(format!("expected_row={expected_row}"))
110                    .attach(format!("actual_row={}", row.row)));
111            }
112        }
113
114        Ok(())
115    }
116
117    fn rows_hyperlink_presence(rows: &[RenderRowSpan]) -> RenderHyperlinkPresence {
118        if rows
119            .iter()
120            .flat_map(RenderRowSpan::cells)
121            .any(|cell| cell.hyperlink().is_some())
122        {
123            RenderHyperlinkPresence::Present
124        } else {
125            RenderHyperlinkPresence::Absent
126        }
127    }
128}
129
130impl<D> rkyv::Deserialize<RenderBaseline, D> for ArchivedRenderBaseline
131where
132    D: rkyv::rancor::Fallible + ?Sized,
133    D::Error: rkyv::rancor::Source,
134{
135    fn deserialize(&self, deserializer: &mut D) -> Result<RenderBaseline, D::Error> {
136        let cursor = rkyv::Deserialize::<RenderCursor, D>::deserialize(&self.cursor, deserializer)?;
137        let rows = rkyv::Deserialize::<Vec<RenderRowSpan>, D>::deserialize(&self.rows, deserializer)?;
138        let seq = rkyv::Deserialize::<u64, D>::deserialize(&self.seq, deserializer)?;
139        let size = rkyv::Deserialize::<TerminalSize, D>::deserialize(&self.size, deserializer)?;
140        RenderBaseline::new(seq, size, cursor, rows).map_err(super::rkyv_deserialize_error::<D::Error>)
141    }
142}
143
144#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
145pub struct RenderDiff {
146    base_seq: u64,
147    cursor: RenderCursor,
148    // Keep link-free updates on the direct codec without adding a hot-path cell scan.
149    #[serde(skip)]
150    hyperlink_presence: RenderHyperlinkPresence,
151    rows: Vec<RenderRowSpan>,
152    seq: u64,
153    size: TerminalSize,
154}
155
156impl RenderDiff {
157    /// Build a dirty-row render update against a previous sequence.
158    ///
159    /// # Errors
160    /// - The base sequence is zero.
161    /// - The new sequence does not advance the base sequence.
162    /// - The cursor or any dirty row span is outside the frame size.
163    pub fn new(
164        base_seq: u64,
165        seq: u64,
166        size: TerminalSize,
167        cursor: RenderCursor,
168        rows: Vec<RenderRowSpan>,
169    ) -> rootcause::Result<Self> {
170        let diff = Self {
171            base_seq,
172            cursor,
173            hyperlink_presence: RenderBaseline::rows_hyperlink_presence(&rows),
174            rows,
175            seq,
176            size,
177        };
178        diff.validate()?;
179        Ok(diff)
180    }
181
182    #[must_use]
183    pub fn into_parts(self) -> (u64, u64, TerminalSize, RenderCursor, Vec<RenderRowSpan>) {
184        (self.base_seq, self.seq, self.size, self.cursor, self.rows)
185    }
186
187    #[must_use]
188    pub const fn base_seq(&self) -> u64 {
189        self.base_seq
190    }
191
192    #[must_use]
193    pub const fn cursor(&self) -> &RenderCursor {
194        &self.cursor
195    }
196
197    #[must_use]
198    pub fn rows(&self) -> &[RenderRowSpan] {
199        &self.rows
200    }
201
202    #[must_use]
203    pub(super) const fn hyperlink_presence(&self) -> RenderHyperlinkPresence {
204        self.hyperlink_presence
205    }
206
207    #[must_use]
208    pub const fn seq(&self) -> u64 {
209        self.seq
210    }
211
212    #[must_use]
213    pub const fn size(&self) -> &TerminalSize {
214        &self.size
215    }
216
217    fn validate(&self) -> rootcause::Result<()> {
218        if self.base_seq == 0 {
219            return Err(report!("invalid muxr render diff").attach("reason=base_seq must be nonzero"));
220        }
221        if self.seq <= self.base_seq {
222            return Err(report!("invalid muxr render diff")
223                .attach("reason=seq must advance base_seq")
224                .attach(format!("base_seq={}", self.base_seq))
225                .attach(format!("seq={}", self.seq)));
226        }
227        self.cursor.validate(self.size.rows(), self.size.cols())?;
228        for row in &self.rows {
229            row.validate(self.size.rows(), self.size.cols())?;
230        }
231
232        Ok(())
233    }
234}
235
236impl<D> rkyv::Deserialize<RenderDiff, D> for ArchivedRenderDiff
237where
238    D: rkyv::rancor::Fallible + ?Sized,
239    D::Error: rkyv::rancor::Source,
240{
241    fn deserialize(&self, deserializer: &mut D) -> Result<RenderDiff, D::Error> {
242        let base_seq = rkyv::Deserialize::<u64, D>::deserialize(&self.base_seq, deserializer)?;
243        let cursor = rkyv::Deserialize::<RenderCursor, D>::deserialize(&self.cursor, deserializer)?;
244        let rows = rkyv::Deserialize::<Vec<RenderRowSpan>, D>::deserialize(&self.rows, deserializer)?;
245        let seq = rkyv::Deserialize::<u64, D>::deserialize(&self.seq, deserializer)?;
246        let size = rkyv::Deserialize::<TerminalSize, D>::deserialize(&self.size, deserializer)?;
247        RenderDiff::new(base_seq, seq, size, cursor, rows).map_err(super::rkyv_deserialize_error::<D::Error>)
248    }
249}
250
251#[derive(rkyv::Archive, Clone, Debug, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
252pub struct RenderCursor {
253    pub col: u16,
254    pub row: u16,
255    pub shape: RenderCursorShape,
256    pub visibility: RenderCursorVisibility,
257}
258
259impl RenderCursor {
260    fn validate(&self, rows: u16, cols: u16) -> rootcause::Result<()> {
261        if self.visibility != RenderCursorVisibility::Visible {
262            return Ok(());
263        }
264        if self.row >= rows || self.col >= cols {
265            return Err(report!("invalid muxr render cursor")
266                .attach(format!("row={}", self.row))
267                .attach(format!("col={}", self.col))
268                .attach(format!("rows={rows}"))
269                .attach(format!("cols={cols}")));
270        }
271
272        Ok(())
273    }
274}
275
276/// Whether the pane cursor should be rendered.
277#[derive(
278    rkyv::Archive,
279    Clone,
280    Copy,
281    Debug,
282    Default,
283    Deserialize,
284    rkyv::Deserialize,
285    Eq,
286    PartialEq,
287    Serialize,
288    rkyv::Serialize,
289)]
290pub enum RenderCursorVisibility {
291    #[default]
292    Hidden,
293    Visible,
294}
295
296/// Visible cursor shape requested by the pane application.
297#[derive(rkyv::Archive, Clone, Copy, Debug, Default, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
298pub enum RenderCursorShape {
299    /// Terminal default cursor shape (`CSI 0 SP q`).
300    #[default]
301    Default,
302    BlinkingBlock,
303    SteadyBlock,
304    BlinkingUnderline,
305    SteadyUnderline,
306    BlinkingBar,
307    SteadyBar,
308}
309
310impl RenderCursorShape {
311    #[must_use]
312    pub const fn from_csi_param(param: u16) -> Option<Self> {
313        match param {
314            0 => Some(Self::Default),
315            1 => Some(Self::BlinkingBlock),
316            2 => Some(Self::SteadyBlock),
317            3 => Some(Self::BlinkingUnderline),
318            4 => Some(Self::SteadyUnderline),
319            5 => Some(Self::BlinkingBar),
320            6 => Some(Self::SteadyBar),
321            _ => None,
322        }
323    }
324}
325
326#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
327pub struct RenderRowSpan {
328    cells: Vec<RenderCell>,
329    col: u16,
330    row: u16,
331}
332
333impl RenderRowSpan {
334    /// Build a row span with nonempty cells and valid wide-cell pairing.
335    ///
336    /// # Errors
337    /// - The row span has no cells.
338    /// - A wide cell is not followed by exactly one continuation cell.
339    pub fn new(row: u16, col: u16, cells: Vec<RenderCell>) -> rootcause::Result<Self> {
340        let span = Self { cells, col, row };
341        span.validate_cells()?;
342        Ok(span)
343    }
344
345    #[must_use]
346    pub fn cells(&self) -> &[RenderCell] {
347        &self.cells
348    }
349
350    #[must_use]
351    pub const fn col(&self) -> u16 {
352        self.col
353    }
354
355    #[must_use]
356    pub const fn row(&self) -> u16 {
357        self.row
358    }
359
360    /// Return the number of terminal grid cells covered by this row span.
361    ///
362    /// # Errors
363    /// - The cell count does not fit in `u16`.
364    pub fn width(&self) -> rootcause::Result<u16> {
365        let width = self.cells.len();
366        Ok(u16::try_from(width).context("muxr render row span width overflowed")?)
367    }
368
369    fn validate(&self, rows: u16, cols: u16) -> rootcause::Result<()> {
370        self.validate_cells()?;
371        let width = self.width()?;
372        let Some(end_col) = self.col.checked_add(width) else {
373            return Err(report!("invalid muxr render row span").attach("reason=column range overflowed"));
374        };
375        if self.row >= rows || self.col >= cols || end_col > cols {
376            return Err(report!("invalid muxr render row span")
377                .attach(format!("row={}", self.row))
378                .attach(format!("col={}", self.col))
379                .attach(format!("width={width}"))
380                .attach(format!("rows={rows}"))
381                .attach(format!("cols={cols}")));
382        }
383        Ok(())
384    }
385
386    fn validate_cells(&self) -> rootcause::Result<()> {
387        if self.cells.is_empty() {
388            return Err(report!("invalid muxr render row span").attach("reason=cells must not be empty"));
389        }
390        self.validate_wide_cells()
391    }
392
393    fn validate_wide_cells(&self) -> rootcause::Result<()> {
394        let mut wide_cell_index = None;
395
396        for (index, cell) in self.cells.iter().enumerate() {
397            if wide_cell_index.is_some() {
398                match cell.width {
399                    RenderCellWidth::WideContinuation => {
400                        wide_cell_index = None;
401                        continue;
402                    }
403                    RenderCellWidth::Narrow | RenderCellWidth::Wide => {
404                        return Err(self::invalid_wide_cell_sequence(
405                            "wide cell must be followed by a wide continuation",
406                            index,
407                        ));
408                    }
409                }
410            }
411
412            match cell.width {
413                RenderCellWidth::Narrow => {}
414                RenderCellWidth::Wide => {
415                    wide_cell_index = Some(index);
416                }
417                RenderCellWidth::WideContinuation => {
418                    return Err(self::invalid_wide_cell_sequence(
419                        "wide continuation must follow a wide cell",
420                        index,
421                    ));
422                }
423            }
424        }
425
426        if let Some(index) = wide_cell_index {
427            return Err(self::invalid_wide_cell_sequence(
428                "wide cell must be followed by a wide continuation",
429                index,
430            ));
431        }
432
433        Ok(())
434    }
435}
436
437impl<D> rkyv::Deserialize<RenderRowSpan, D> for ArchivedRenderRowSpan
438where
439    D: rkyv::rancor::Fallible + ?Sized,
440    D::Error: rkyv::rancor::Source,
441{
442    fn deserialize(&self, deserializer: &mut D) -> Result<RenderRowSpan, D::Error> {
443        let cells = rkyv::Deserialize::<Vec<RenderCell>, D>::deserialize(&self.cells, deserializer)?;
444        let col = rkyv::Deserialize::<u16, D>::deserialize(&self.col, deserializer)?;
445        let row = rkyv::Deserialize::<u16, D>::deserialize(&self.row, deserializer)?;
446        RenderRowSpan::new(row, col, cells).map_err(super::rkyv_deserialize_error::<D::Error>)
447    }
448}
449
450fn invalid_wide_cell_sequence(reason: &'static str, index: usize) -> rootcause::Report {
451    report!("invalid muxr render row span")
452        .attach("reason=invalid wide-cell sequence")
453        .attach(reason)
454        .attach(format!("cell_index={index}"))
455}
456
457#[derive(rkyv::Archive, Debug, rkyv::Deserialize, Eq, Hash, PartialEq, rkyv::Serialize)]
458struct SharedRenderUri(String);
459
460#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, rkyv::Serialize)]
461pub struct RenderHyperlink {
462    // Domain clones share the resolved URI; direct rkyv archives still encode it as an ordinary owned value.
463    #[rkyv(with = rkyv::with::Unshare)]
464    uri: Arc<SharedRenderUri>,
465}
466
467impl RenderHyperlink {
468    /// Build render hyperlink metadata with a URI safe to emit inside an OSC 8 sequence.
469    ///
470    /// # Errors
471    /// - The URI is empty.
472    /// - The URI contains terminal control characters.
473    pub fn new(uri: impl Into<String>) -> rootcause::Result<Self> {
474        let uri = uri.into();
475        Self::validate_uri(&uri)?;
476        Ok(Self {
477            uri: Arc::new(SharedRenderUri(uri)),
478        })
479    }
480
481    fn validate_uri(uri: &str) -> rootcause::Result<()> {
482        if uri.is_empty() {
483            return Err(report!("invalid muxr render hyperlink").attach("reason=uri must be nonempty"));
484        }
485        if uri.chars().any(char::is_control) {
486            return Err(report!("invalid muxr render hyperlink").attach("reason=uri must not contain control chars"));
487        }
488
489        Ok(())
490    }
491
492    #[must_use]
493    pub fn uri(&self) -> &str {
494        &self.uri.0
495    }
496
497    #[cfg(test)]
498    pub(crate) fn shares_uri_with(&self, other: &Self) -> bool {
499        Arc::ptr_eq(&self.uri, &other.uri)
500    }
501}
502
503impl Serialize for RenderHyperlink {
504    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
505    where
506        S: serde::Serializer,
507    {
508        serializer.serialize_str(self.uri())
509    }
510}
511
512impl<'de> Deserialize<'de> for RenderHyperlink {
513    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
514    where
515        D: serde::Deserializer<'de>,
516    {
517        let uri = String::deserialize(deserializer)?;
518        Self::new(uri).map_err(serde::de::Error::custom)
519    }
520}
521
522impl<D> rkyv::Deserialize<RenderHyperlink, D> for ArchivedRenderHyperlink
523where
524    D: rkyv::rancor::Fallible + ?Sized,
525    D::Error: rkyv::rancor::Source,
526{
527    fn deserialize(&self, deserializer: &mut D) -> Result<RenderHyperlink, D::Error> {
528        let uri = rkyv::Deserialize::<SharedRenderUri, D>::deserialize(&self.uri, deserializer)?;
529        RenderHyperlink::new(uri.0).map_err(super::rkyv_deserialize_error::<D::Error>)
530    }
531}
532
533/// One terminal render cell.
534///
535/// Cell text is stored compactly because most terminal cells are blank or one glyph; callers stay insulated from that
536/// storage choice through text constructors and [`Self::text`].
537#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
538pub struct RenderCell {
539    hyperlink: Option<RenderHyperlink>,
540    style: RenderStyle,
541    text: CompactString,
542    width: RenderCellWidth,
543}
544
545impl RenderCell {
546    #[must_use]
547    pub fn narrow(text: impl AsRef<str>, style: RenderStyle) -> Self {
548        Self::new(CompactString::new(text.as_ref()), style, RenderCellWidth::Narrow)
549    }
550
551    #[must_use]
552    pub fn wide(text: impl AsRef<str>, style: RenderStyle) -> Self {
553        Self::new(CompactString::new(text.as_ref()), style, RenderCellWidth::Wide)
554    }
555
556    #[must_use]
557    pub const fn wide_continuation(style: RenderStyle) -> Self {
558        Self {
559            hyperlink: None,
560            style,
561            text: CompactString::const_new(""),
562            width: RenderCellWidth::WideContinuation,
563        }
564    }
565
566    #[must_use]
567    pub fn with_hyperlink(mut self, hyperlink: RenderHyperlink) -> Self {
568        self.hyperlink = Some(hyperlink);
569        self
570    }
571
572    #[must_use]
573    pub const fn with_style(mut self, style: RenderStyle) -> Self {
574        self.style = style;
575        self
576    }
577
578    /// Attach hyperlink metadata to the render cell.
579    ///
580    /// # Errors
581    /// - The URI is invalid for [`RenderHyperlink`].
582    pub fn with_hyperlink_uri(self, uri: impl Into<String>) -> rootcause::Result<Self> {
583        Ok(self.with_hyperlink(RenderHyperlink::new(uri)?))
584    }
585
586    #[must_use]
587    pub const fn hyperlink(&self) -> Option<&RenderHyperlink> {
588        self.hyperlink.as_ref()
589    }
590
591    #[must_use]
592    pub const fn style(&self) -> RenderStyle {
593        self.style
594    }
595
596    #[must_use]
597    pub fn text(&self) -> &str {
598        &self.text
599    }
600
601    #[must_use]
602    pub const fn width(&self) -> RenderCellWidth {
603        self.width
604    }
605
606    fn with_optional_hyperlink(mut self, hyperlink: Option<RenderHyperlink>) -> Self {
607        self.hyperlink = hyperlink;
608        self
609    }
610
611    const fn new(text: CompactString, style: RenderStyle, width: RenderCellWidth) -> Self {
612        Self {
613            hyperlink: None,
614            style,
615            text,
616            width,
617        }
618    }
619}
620
621impl<D> rkyv::Deserialize<RenderCell, D> for ArchivedRenderCell
622where
623    D: rkyv::rancor::Fallible + ?Sized,
624    D::Error: rkyv::rancor::Source,
625{
626    fn deserialize(&self, deserializer: &mut D) -> Result<RenderCell, D::Error> {
627        let hyperlink = rkyv::Deserialize::<Option<RenderHyperlink>, D>::deserialize(&self.hyperlink, deserializer)?;
628        let style = rkyv::Deserialize::<RenderStyle, D>::deserialize(&self.style, deserializer)?;
629        let text = rkyv::Deserialize::<CompactString, D>::deserialize(&self.text, deserializer)?;
630        let width = rkyv::Deserialize::<RenderCellWidth, D>::deserialize(&self.width, deserializer)?;
631        match width {
632            RenderCellWidth::Narrow => {
633                Ok(RenderCell::new(text, style, RenderCellWidth::Narrow).with_optional_hyperlink(hyperlink))
634            }
635            RenderCellWidth::Wide => {
636                Ok(RenderCell::new(text, style, RenderCellWidth::Wide).with_optional_hyperlink(hyperlink))
637            }
638            RenderCellWidth::WideContinuation => {
639                if !text.is_empty() {
640                    return Err(super::rkyv_deserialize_error::<D::Error>(
641                        "wide continuation cells must not carry text",
642                    ));
643                }
644                Ok(RenderCell::wide_continuation(style).with_optional_hyperlink(hyperlink))
645            }
646        }
647    }
648}
649
650#[derive(rkyv::Archive, Clone, Copy, Debug, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
651pub enum RenderCellWidth {
652    Narrow,
653    Wide,
654    WideContinuation,
655}
656
657#[derive(rkyv::Archive, Clone, Copy, Debug, Default, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
658pub struct RenderStyle {
659    pub attrs: RenderTextStyle,
660    pub bg: RenderColor,
661    pub fg: RenderColor,
662}
663
664#[derive(rkyv::Archive, Clone, Copy, Debug, Default, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
665#[serde(transparent)]
666pub struct RenderTextStyle(u8);
667
668impl RenderTextStyle {
669    const BOLD: u8 = 0b0000_0001;
670    const DIM: u8 = 0b0000_0010;
671    const INVERSE: u8 = 0b0001_0000;
672    const ITALIC: u8 = 0b0000_0100;
673    const UNDERLINE: u8 = 0b0000_1000;
674
675    #[must_use]
676    pub const fn empty() -> Self {
677        Self(0)
678    }
679
680    #[must_use]
681    pub const fn set_bold(self, enabled: bool) -> Self {
682        self.set_flag(Self::BOLD, enabled)
683    }
684
685    #[must_use]
686    pub const fn set_dim(self, enabled: bool) -> Self {
687        self.set_flag(Self::DIM, enabled)
688    }
689
690    #[must_use]
691    pub const fn set_italic(self, enabled: bool) -> Self {
692        self.set_flag(Self::ITALIC, enabled)
693    }
694
695    #[must_use]
696    pub const fn set_underline(self, enabled: bool) -> Self {
697        self.set_flag(Self::UNDERLINE, enabled)
698    }
699
700    #[must_use]
701    pub const fn set_inverse(self, enabled: bool) -> Self {
702        self.set_flag(Self::INVERSE, enabled)
703    }
704
705    #[must_use]
706    pub const fn bold(self) -> bool {
707        self.has_flag(Self::BOLD)
708    }
709
710    #[must_use]
711    pub const fn dim(self) -> bool {
712        self.has_flag(Self::DIM)
713    }
714
715    #[must_use]
716    pub const fn italic(self) -> bool {
717        self.has_flag(Self::ITALIC)
718    }
719
720    #[must_use]
721    pub const fn underline(self) -> bool {
722        self.has_flag(Self::UNDERLINE)
723    }
724
725    #[must_use]
726    pub const fn inverse(self) -> bool {
727        self.has_flag(Self::INVERSE)
728    }
729
730    const fn set_flag(self, flag: u8, enabled: bool) -> Self {
731        if enabled {
732            Self(self.0 | flag)
733        } else {
734            Self(self.0 & !flag)
735        }
736    }
737
738    const fn has_flag(self, flag: u8) -> bool {
739        self.0 & flag != 0
740    }
741}
742
743#[derive(rkyv::Archive, Clone, Copy, Debug, Default, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
744pub enum RenderColor {
745    #[default]
746    Default,
747    /// Xterm 256-color palette index (`38;5;<n>` / `48;5;<n>`), used when terminal-theme-relative colors are enough.
748    Indexed(u8),
749    /// Explicit 24-bit RGB color (`38;2;r;g;b` / `48;2;r;g;b`) for exact colors.
750    Rgb { r: u8, g: u8, b: u8 },
751}
752
753#[cfg(test)]
754pub mod test_helpers {
755    use super::*;
756
757    pub fn raw_render_hyperlink(uri: impl Into<String>) -> RenderHyperlink {
758        RenderHyperlink {
759            uri: Arc::new(SharedRenderUri(uri.into())),
760        }
761    }
762
763    pub fn raw_render_cell(
764        hyperlink: Option<RenderHyperlink>,
765        style: RenderStyle,
766        text: impl AsRef<str>,
767        width: RenderCellWidth,
768    ) -> RenderCell {
769        RenderCell {
770            hyperlink,
771            style,
772            text: CompactString::new(text.as_ref()),
773            width,
774        }
775    }
776
777    pub fn raw_render_diff(
778        base_seq: u64,
779        seq: u64,
780        size: TerminalSize,
781        cursor: RenderCursor,
782        rows: Vec<RenderRowSpan>,
783    ) -> RenderDiff {
784        RenderDiff {
785            base_seq,
786            cursor,
787            hyperlink_presence: RenderBaseline::rows_hyperlink_presence(&rows),
788            rows,
789            seq,
790            size,
791        }
792    }
793
794    pub const fn raw_render_row_span(row: u16, col: u16, cells: Vec<RenderCell>) -> RenderRowSpan {
795        RenderRowSpan { cells, col, row }
796    }
797}
798
799#[cfg(test)]
800mod tests {
801    use rstest::rstest;
802    use test_that::prelude::*;
803
804    use super::*;
805
806    #[rstest]
807    #[case::empty("")]
808    #[case::control_char("https://example.com/\u{1b}")]
809    fn test_render_hyperlink_new_when_uri_is_invalid_returns_error(#[case] uri: &str) {
810        assert_that!(RenderHyperlink::new(uri), err(anything()));
811    }
812
813    #[test]
814    fn test_render_cell_with_hyperlink_uri_when_uri_is_valid_sets_metadata() -> rootcause::Result<()> {
815        let cell = self::render_cell("x").with_hyperlink_uri("https://example.com")?;
816
817        assert_that!(
818            cell.hyperlink().map(RenderHyperlink::uri),
819            eq(Some("https://example.com"))
820        );
821        assert_that!(cell, not(eq(self::render_cell("x"))));
822        Ok(())
823    }
824
825    #[test]
826    fn test_render_cell_with_style_when_cell_has_metadata_preserves_non_style_fields() -> rootcause::Result<()> {
827        let original_style = RenderStyle::default();
828        let updated_style = RenderStyle {
829            attrs: RenderTextStyle::empty().set_dim(true),
830            bg: RenderColor::Indexed(1),
831            fg: RenderColor::Indexed(2),
832        };
833        let cell = RenderCell::wide("字", original_style).with_hyperlink_uri("https://example.com")?;
834
835        let updated = cell.with_style(updated_style);
836
837        assert_that!(updated.style(), eq(updated_style));
838        assert_that!(updated.text(), eq("字"));
839        assert_that!(updated.width(), eq(RenderCellWidth::Wide));
840        assert_that!(
841            updated.hyperlink().map(RenderHyperlink::uri),
842            eq(Some("https://example.com"))
843        );
844        Ok(())
845    }
846
847    #[test]
848    fn test_render_cell_text_when_serialized_preserves_public_string_shape() -> rootcause::Result<()> {
849        let text = "heap-backed render cell text that is longer than compact inline capacity";
850        let cell = RenderCell::narrow(text, RenderStyle::default());
851
852        let serialized = serde_json::to_value(&cell)?;
853
854        assert_that!(serialized["text"], eq(serde_json::json!(text)));
855        Ok(())
856    }
857
858    #[test]
859    fn test_render_cell_rkyv_deserialize_when_text_is_heap_backed_preserves_cell() -> rootcause::Result<()> {
860        let text = "heap-backed render cell text that is longer than compact inline capacity";
861        let cell = RenderCell::wide(text, RenderStyle::default()).with_hyperlink_uri("https://example.com")?;
862        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&cell)?;
863        let archived = rkyv::access::<rkyv::Archived<RenderCell>, rkyv::rancor::Error>(&bytes)?;
864
865        let deserialized = rkyv::deserialize::<RenderCell, rkyv::rancor::Error>(archived)?;
866
867        assert_that!(deserialized, eq(cell));
868        Ok(())
869    }
870
871    #[test]
872    fn test_render_cell_rkyv_deserialize_when_hyperlink_uri_is_invalid_returns_error() -> rootcause::Result<()> {
873        let cell = test_helpers::raw_render_cell(
874            Some(test_helpers::raw_render_hyperlink(String::new())),
875            RenderStyle::default(),
876            "x",
877            RenderCellWidth::Narrow,
878        );
879        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&cell)?;
880        let archived = rkyv::access::<rkyv::Archived<RenderCell>, rkyv::rancor::Error>(&bytes)?;
881
882        assert_that!(
883            rkyv::deserialize::<RenderCell, rkyv::rancor::Error>(archived),
884            err(anything())
885        );
886        Ok(())
887    }
888
889    #[rstest]
890    #[case::zero_seq(0, 80, 24, render_rows(80, 24))]
891    #[case::short_rows(1, 80, 24, render_rows(80, 23))]
892    #[case::out_of_bounds_row(1, 80, 24, vec![raw_render_row_span(24, 0, render_cells(80))])]
893    fn test_render_baseline_new_when_frame_is_invalid_returns_error(
894        #[case] seq: u64,
895        #[case] cols: u16,
896        #[case] rows: u16,
897        #[case] render_rows: Vec<RenderRowSpan>,
898    ) -> rootcause::Result<()> {
899        let size = TerminalSize::new(cols, rows)?;
900        assert_that!(
901            RenderBaseline::new(
902                seq,
903                size,
904                RenderCursor {
905                    row: 0,
906                    col: 0,
907                    shape: RenderCursorShape::Default,
908                    visibility: RenderCursorVisibility::Visible,
909                },
910                render_rows,
911            ),
912            err(anything())
913        );
914        Ok(())
915    }
916
917    #[rstest]
918    #[case::zero_base(0, 2)]
919    #[case::same_seq(1, 1)]
920    #[case::older_seq(2, 1)]
921    fn test_render_diff_new_when_sequence_is_invalid_returns_error(
922        #[case] base_seq: u64,
923        #[case] seq: u64,
924    ) -> rootcause::Result<()> {
925        assert_that!(
926            RenderDiff::new(
927                base_seq,
928                seq,
929                TerminalSize::new(80, 24)?,
930                RenderCursor {
931                    row: 0,
932                    col: 0,
933                    shape: RenderCursorShape::Default,
934                    visibility: RenderCursorVisibility::Visible,
935                },
936                vec![RenderRowSpan::new(0, 0, self::render_cells(1))?],
937            ),
938            err(anything())
939        );
940        Ok(())
941    }
942
943    #[rstest]
944    #[case::empty_cells(raw_render_row_span(0, 0, Vec::new()))]
945    #[case::col_out_of_bounds(raw_render_row_span(0, 80, render_cells(1)))]
946    #[case::span_too_wide(raw_render_row_span(0, 79, render_cells(2)))]
947    fn test_render_diff_new_when_row_span_is_invalid_returns_error(
948        #[case] row: RenderRowSpan,
949    ) -> rootcause::Result<()> {
950        assert_that!(
951            RenderDiff::new(
952                1,
953                2,
954                TerminalSize::new(80, 24)?,
955                RenderCursor {
956                    row: 0,
957                    col: 0,
958                    shape: RenderCursorShape::Default,
959                    visibility: RenderCursorVisibility::Visible,
960                },
961                vec![row],
962            ),
963            err(anything())
964        );
965        Ok(())
966    }
967
968    #[rstest]
969    #[case::wide_without_continuation(raw_render_row_span(
970            0,
971            0,
972            vec![RenderCell::wide("字", RenderStyle::default())]
973        ))]
974    #[case::continuation_without_wide(raw_render_row_span(
975            0,
976            0,
977            vec![RenderCell::wide_continuation(RenderStyle::default())]
978        ))]
979    #[case::wide_followed_by_narrow(raw_render_row_span(
980            0,
981            0,
982            vec![RenderCell::wide("字", RenderStyle::default()), render_cell("x")]
983        ))]
984    #[case::double_continuation(raw_render_row_span(
985            0,
986            0,
987            vec![
988                RenderCell::wide("字", RenderStyle::default()),
989                RenderCell::wide_continuation(RenderStyle::default()),
990                RenderCell::wide_continuation(RenderStyle::default())
991            ]
992        ))]
993    fn test_render_diff_new_when_wide_cell_sequence_is_invalid_returns_error(
994        #[case] row: RenderRowSpan,
995    ) -> rootcause::Result<()> {
996        assert_that!(
997            RenderDiff::new(
998                1,
999                2,
1000                TerminalSize::new(80, 24)?,
1001                RenderCursor {
1002                    row: 0,
1003                    col: 0,
1004                    shape: RenderCursorShape::Default,
1005                    visibility: RenderCursorVisibility::Visible,
1006                },
1007                vec![row],
1008            ),
1009            err(anything())
1010        );
1011        Ok(())
1012    }
1013
1014    #[test]
1015    fn test_render_baseline_new_when_wide_cell_sequence_is_invalid_returns_error() -> rootcause::Result<()> {
1016        let rows = vec![self::raw_render_row_span(
1017            0,
1018            0,
1019            vec![RenderCell::wide("字", RenderStyle::default()), self::render_cell("x")],
1020        )];
1021
1022        assert_that!(
1023            RenderBaseline::new(
1024                1,
1025                TerminalSize::new(2, 1)?,
1026                RenderCursor {
1027                    row: 0,
1028                    col: 0,
1029                    shape: RenderCursorShape::Default,
1030                    visibility: RenderCursorVisibility::Visible,
1031                },
1032                rows,
1033            ),
1034            err(anything())
1035        );
1036        Ok(())
1037    }
1038
1039    #[test]
1040    fn test_render_diff_new_when_wide_cell_has_continuation_returns_diff() -> rootcause::Result<()> {
1041        let row = RenderRowSpan::new(
1042            0,
1043            0,
1044            vec![
1045                RenderCell::wide("字", RenderStyle::default()),
1046                RenderCell::wide_continuation(RenderStyle::default()),
1047            ],
1048        )?;
1049
1050        let diff = RenderDiff::new(
1051            1,
1052            2,
1053            TerminalSize::new(80, 24)?,
1054            RenderCursor {
1055                row: 0,
1056                col: 0,
1057                shape: RenderCursorShape::Default,
1058                visibility: RenderCursorVisibility::Visible,
1059            },
1060            vec![row],
1061        )?;
1062
1063        assert_that!(diff.rows().len(), eq(1));
1064        Ok(())
1065    }
1066
1067    #[test]
1068    fn test_render_row_span_width_when_wide_cell_has_continuation_counts_grid_cells() -> rootcause::Result<()> {
1069        let row = RenderRowSpan::new(
1070            0,
1071            0,
1072            vec![
1073                RenderCell::wide("字", RenderStyle::default()),
1074                RenderCell::wide_continuation(RenderStyle::default()),
1075            ],
1076        )?;
1077
1078        assert_that!(row.width()?, eq(2));
1079        Ok(())
1080    }
1081
1082    fn render_rows(cols: u16, rows: u16) -> Vec<RenderRowSpan> {
1083        (0..rows)
1084            .map(|row| self::raw_render_row_span(row, 0, self::render_cells(cols)))
1085            .collect()
1086    }
1087
1088    fn raw_render_row_span(row: u16, col: u16, cells: Vec<RenderCell>) -> RenderRowSpan {
1089        test_helpers::raw_render_row_span(row, col, cells)
1090    }
1091
1092    fn render_cells(cols: u16) -> Vec<RenderCell> {
1093        (0..cols).map(|_| self::render_cell(" ")).collect()
1094    }
1095
1096    fn render_cell(text: &str) -> RenderCell {
1097        RenderCell::narrow(text, RenderStyle::default())
1098    }
1099}