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 #[serde(skip)]
28 hyperlink_presence: RenderHyperlinkPresence,
29 rows: Vec<RenderRowSpan>,
30 seq: u64,
31 size: TerminalSize,
32}
33
34impl RenderBaseline {
35 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 #[serde(skip)]
150 hyperlink_presence: RenderHyperlinkPresence,
151 rows: Vec<RenderRowSpan>,
152 seq: u64,
153 size: TerminalSize,
154}
155
156impl RenderDiff {
157 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#[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#[derive(rkyv::Archive, Clone, Copy, Debug, Default, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
298pub enum RenderCursorShape {
299 #[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 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 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 #[rkyv(with = rkyv::with::Unshare)]
464 uri: Arc<SharedRenderUri>,
465}
466
467impl RenderHyperlink {
468 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#[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 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 Indexed(u8),
749 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}