1use bitflags::bitflags;
9use std::collections::HashMap;
10use unicode_width::UnicodeWidthChar;
11
12bitflags! {
13 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17 pub struct SgrFlags: u16 {
18 const BOLD = 1 << 0;
19 const DIM = 1 << 1;
20 const ITALIC = 1 << 2;
21 const UNDERLINE = 1 << 3;
22 const BLINK = 1 << 4;
23 const INVERSE = 1 << 5;
24 const HIDDEN = 1 << 6;
25 const STRIKETHROUGH = 1 << 7;
26 const DOUBLE_UNDERLINE = 1 << 8;
27 const CURLY_UNDERLINE = 1 << 9;
28 const OVERLINE = 1 << 10;
29 }
30}
31
32bitflags! {
33 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35 pub struct CellFlags: u8 {
36 const WIDE_CHAR = 1 << 0;
38 const WIDE_CONTINUATION = 1 << 1;
41 const HAS_COMBINING = 1 << 2;
43 }
44}
45
46pub const MAX_COMBINING: usize = 2;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
57pub enum Color {
58 #[default]
60 Default,
61 Named(u8),
63 Indexed(u8),
65 Rgb(u8, u8, u8),
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
71pub struct SgrAttrs {
72 pub flags: SgrFlags,
73 pub fg: Color,
74 pub bg: Color,
75 pub underline_color: Option<Color>,
77}
78
79impl SgrAttrs {
80 pub fn reset(&mut self) {
82 *self = Self::default();
83 }
84
85 pub fn apply_sgr_params(&mut self, params: &[u16]) {
91 let mut i = 0usize;
92 let params = if params.is_empty() {
93 &[0u16][..]
94 } else {
95 params
96 };
97
98 while i < params.len() {
99 let p = params[i];
100 match p {
101 0 => self.reset(),
102
103 1 => self.flags.insert(SgrFlags::BOLD),
104 2 => self.flags.insert(SgrFlags::DIM),
105 3 => self.flags.insert(SgrFlags::ITALIC),
106 4 => self.flags.insert(SgrFlags::UNDERLINE),
107 5 => self.flags.insert(SgrFlags::BLINK),
108 7 => self.flags.insert(SgrFlags::INVERSE),
109 8 => self.flags.insert(SgrFlags::HIDDEN),
110 9 => self.flags.insert(SgrFlags::STRIKETHROUGH),
111 21 => self.flags.insert(SgrFlags::DOUBLE_UNDERLINE),
112 53 => self.flags.insert(SgrFlags::OVERLINE),
113
114 22 => self.flags.remove(SgrFlags::BOLD | SgrFlags::DIM),
115 23 => self.flags.remove(SgrFlags::ITALIC),
116 24 => self.flags.remove(
117 SgrFlags::UNDERLINE | SgrFlags::DOUBLE_UNDERLINE | SgrFlags::CURLY_UNDERLINE,
118 ),
119 25 => self.flags.remove(SgrFlags::BLINK),
120 27 => self.flags.remove(SgrFlags::INVERSE),
121 28 => self.flags.remove(SgrFlags::HIDDEN),
122 29 => self.flags.remove(SgrFlags::STRIKETHROUGH),
123 55 => self.flags.remove(SgrFlags::OVERLINE),
124
125 30..=37 => self.fg = Color::Named((p - 30) as u8),
126 39 => self.fg = Color::Default,
127 40..=47 => self.bg = Color::Named((p - 40) as u8),
128 49 => self.bg = Color::Default,
129 90..=97 => self.fg = Color::Named(((p - 90) as u8).saturating_add(8)),
130 100..=107 => self.bg = Color::Named(((p - 100) as u8).saturating_add(8)),
131
132 38 => {
134 if let Some((c, consumed)) = parse_extended_color(params, i) {
135 self.fg = c;
136 i = i.saturating_add(consumed);
137 continue;
138 }
139 }
140 48 => {
141 if let Some((c, consumed)) = parse_extended_color(params, i) {
142 self.bg = c;
143 i = i.saturating_add(consumed);
144 continue;
145 }
146 }
147 58 => {
148 if let Some((c, consumed)) = parse_extended_color(params, i) {
149 self.underline_color = Some(c);
150 i = i.saturating_add(consumed);
151 continue;
152 }
153 }
154 59 => self.underline_color = None,
155
156 _ => {}
157 }
158 i = i.saturating_add(1);
159 }
160
161 fn parse_extended_color(params: &[u16], start: usize) -> Option<(Color, usize)> {
162 let mode = *params.get(start + 1)?;
167 match mode {
168 5 => {
169 let idx = *params.get(start + 2)?;
170 Some((Color::Indexed(idx.min(255) as u8), 3))
171 }
172 2 => {
173 let r = *params.get(start + 2)?;
174 let g = *params.get(start + 3)?;
175 let b = *params.get(start + 4)?;
176 Some((
177 Color::Rgb(r.min(255) as u8, g.min(255) as u8, b.min(255) as u8),
178 5,
179 ))
180 }
181 _ => None,
182 }
183 }
184 }
185}
186
187pub type HyperlinkId = u16;
192
193#[derive(Debug, Clone)]
200pub struct HyperlinkRegistry {
201 slots: Vec<Option<HyperlinkSlot>>,
203 lookup: HashMap<String, HyperlinkId>,
205 free_list: Vec<HyperlinkId>,
207}
208
209#[derive(Debug, Clone)]
210struct HyperlinkSlot {
211 uri: String,
212 ref_count: u32,
213}
214
215impl HyperlinkRegistry {
216 pub fn new() -> Self {
218 Self {
219 slots: vec![None],
220 lookup: HashMap::new(),
221 free_list: Vec::new(),
222 }
223 }
224
225 pub fn intern(&mut self, uri: &str) -> HyperlinkId {
229 if uri.is_empty() {
230 return 0;
231 }
232 if let Some(&id) = self.lookup.get(uri) {
233 return id;
234 }
235
236 let id = if let Some(id) = self.free_list.pop() {
237 id
238 } else {
239 let next = self.slots.len();
240 if next > HyperlinkId::MAX as usize {
241 return 0;
242 }
243 let id = next as HyperlinkId;
244 self.slots.push(None);
245 id
246 };
247
248 if id == 0 {
249 return 0;
250 }
251 let idx = id as usize;
252 if idx >= self.slots.len() {
253 return 0;
254 }
255
256 self.slots[idx] = Some(HyperlinkSlot {
257 uri: uri.to_string(),
258 ref_count: 0,
259 });
260 self.lookup.insert(uri.to_string(), id);
261 id
262 }
263
264 pub fn acquire(&mut self, uri: &str) -> HyperlinkId {
266 let id = self.intern(uri);
267 self.acquire_id(id);
268 id
269 }
270
271 pub fn acquire_id(&mut self, id: HyperlinkId) {
275 if id == 0 {
276 return;
277 }
278 let Some(slot) = self.slots.get_mut(id as usize) else {
279 return;
280 };
281 let Some(slot) = slot.as_mut() else {
282 return;
283 };
284 slot.ref_count = slot.ref_count.saturating_add(1);
285 }
286
287 pub fn release_id(&mut self, id: HyperlinkId) {
291 if id == 0 {
292 return;
293 }
294 let Some(entry) = self.slots.get_mut(id as usize) else {
295 return;
296 };
297
298 let should_remove = match entry.as_mut() {
299 Some(slot) if slot.ref_count > 0 => {
300 slot.ref_count -= 1;
301 slot.ref_count == 0
302 }
303 _ => false,
304 };
305
306 if should_remove && let Some(removed) = entry.take() {
307 self.lookup.remove(&removed.uri);
308 self.free_list.push(id);
309 }
310 }
311
312 pub fn release_cells(&mut self, cells: &[Cell]) {
316 for cell in cells {
317 self.release_id(cell.hyperlink);
318 }
319 }
320
321 pub fn get(&self, id: HyperlinkId) -> Option<&str> {
323 self.slots
324 .get(id as usize)
325 .and_then(|slot| slot.as_ref())
326 .map(|slot| slot.uri.as_str())
327 }
328
329 pub fn clear(&mut self) {
331 self.slots.clear();
332 self.slots.push(None);
333 self.lookup.clear();
334 self.free_list.clear();
335 }
336
337 pub fn len(&self) -> usize {
339 self.slots.iter().filter(|slot| slot.is_some()).count()
340 }
341
342 #[inline]
344 pub fn is_empty(&self) -> bool {
345 self.len() == 0
346 }
347
348 pub fn contains(&self, id: HyperlinkId) -> bool {
350 self.get(id).is_some()
351 }
352}
353
354impl Default for HyperlinkRegistry {
355 fn default() -> Self {
356 Self::new()
357 }
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub struct Cell {
363 content: char,
365 width: u8,
367 pub flags: CellFlags,
369 pub attrs: SgrAttrs,
371 pub hyperlink: HyperlinkId,
373 combining: [char; MAX_COMBINING],
375 combining_len: u8,
377}
378
379impl Default for Cell {
380 fn default() -> Self {
381 Self {
382 content: ' ',
383 width: 1,
384 flags: CellFlags::empty(),
385 attrs: SgrAttrs::default(),
386 hyperlink: 0,
387 combining: ['\0'; MAX_COMBINING],
388 combining_len: 0,
389 }
390 }
391}
392
393impl Cell {
394 pub fn new(ch: char) -> Self {
396 Self {
397 content: ch,
398 width: 1,
399 flags: CellFlags::empty(),
400 attrs: SgrAttrs::default(),
401 hyperlink: 0,
402 combining: ['\0'; MAX_COMBINING],
403 combining_len: 0,
404 }
405 }
406
407 pub fn with_attrs(ch: char, width: u8, attrs: SgrAttrs) -> Self {
409 Self {
410 content: ch,
411 width,
412 flags: CellFlags::empty(),
413 attrs,
414 hyperlink: 0,
415 combining: ['\0'; MAX_COMBINING],
416 combining_len: 0,
417 }
418 }
419
420 pub fn wide(ch: char, attrs: SgrAttrs) -> (Self, Self) {
425 let leading = Self {
426 content: ch,
427 width: 2,
428 flags: CellFlags::WIDE_CHAR,
429 attrs,
430 hyperlink: 0,
431 combining: ['\0'; MAX_COMBINING],
432 combining_len: 0,
433 };
434 let continuation = Self {
435 content: ' ',
436 width: 0,
437 flags: CellFlags::WIDE_CONTINUATION,
438 attrs,
439 hyperlink: 0,
440 combining: ['\0'; MAX_COMBINING],
441 combining_len: 0,
442 };
443 (leading, continuation)
444 }
445
446 #[inline]
448 pub fn content(&self) -> char {
449 self.content
450 }
451
452 #[inline]
454 pub fn width(&self) -> u8 {
455 self.width
456 }
457
458 #[inline]
460 pub fn is_wide(&self) -> bool {
461 self.flags.contains(CellFlags::WIDE_CHAR)
462 }
463
464 #[inline]
466 pub fn is_wide_continuation(&self) -> bool {
467 self.flags.contains(CellFlags::WIDE_CONTINUATION)
468 }
469
470 pub fn set_content(&mut self, ch: char, width: u8) {
472 self.content = ch;
473 self.width = width;
474 self.flags
476 .remove(CellFlags::WIDE_CHAR | CellFlags::WIDE_CONTINUATION | CellFlags::HAS_COMBINING);
477 self.combining_len = 0;
478 }
479
480 pub fn erase(&mut self, bg: Color) {
485 self.content = ' ';
486 self.width = 1;
487 self.flags = CellFlags::empty();
488 self.attrs = SgrAttrs {
489 bg,
490 ..SgrAttrs::default()
491 };
492 self.hyperlink = 0;
493 self.combining_len = 0;
494 }
495
496 pub fn clear(&mut self) {
498 *self = Self::default();
499 }
500
501 pub fn push_combining(&mut self, mark: char) -> bool {
508 let len = self.combining_len as usize;
509 if len >= MAX_COMBINING {
510 return false;
511 }
512 self.combining[len] = mark;
513 self.combining_len += 1;
514 self.flags.insert(CellFlags::HAS_COMBINING);
515 true
516 }
517
518 #[inline]
520 pub fn combining_marks(&self) -> &[char] {
521 &self.combining[..self.combining_len as usize]
523 }
524
525 #[inline]
527 pub fn has_combining(&self) -> bool {
528 self.combining_len > 0
529 }
530
531 pub fn display_width(ch: char) -> u8 {
540 let width = UnicodeWidthChar::width(ch).unwrap_or(0);
541 width.min(2) as u8
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 use crate::grid::Grid;
550 use crate::scrollback::Scrollback;
551
552 #[test]
553 fn default_cell_is_space() {
554 let cell = Cell::default();
555 assert_eq!(cell.content(), ' ');
556 assert_eq!(cell.width(), 1);
557 assert_eq!(cell.attrs, SgrAttrs::default());
558 assert!(!cell.is_wide());
559 assert!(!cell.is_wide_continuation());
560 assert_eq!(cell.hyperlink, 0);
561 }
562
563 #[test]
564 fn cell_new_has_default_attrs() {
565 let cell = Cell::new('A');
566 assert_eq!(cell.content(), 'A');
567 assert_eq!(cell.attrs.flags, SgrFlags::empty());
568 assert_eq!(cell.attrs.fg, Color::Default);
569 assert_eq!(cell.attrs.bg, Color::Default);
570 }
571
572 #[test]
573 fn cell_erase_clears_content_and_attrs() {
574 let mut cell = Cell::with_attrs(
575 'X',
576 1,
577 SgrAttrs {
578 flags: SgrFlags::BOLD | SgrFlags::ITALIC,
579 fg: Color::Named(1),
580 bg: Color::Named(4),
581 underline_color: None,
582 },
583 );
584 cell.hyperlink = 42;
585 cell.erase(Color::Named(2));
586 assert_eq!(cell.content(), ' ');
587 assert_eq!(cell.attrs.flags, SgrFlags::empty());
588 assert_eq!(cell.attrs.fg, Color::Default);
589 assert_eq!(cell.attrs.bg, Color::Named(2));
590 assert_eq!(cell.hyperlink, 0);
591 }
592
593 #[test]
594 fn wide_char_pair() {
595 let attrs = SgrAttrs {
596 flags: SgrFlags::BOLD,
597 ..SgrAttrs::default()
598 };
599 let (lead, cont) = Cell::wide('\u{4E2D}', attrs); assert!(lead.is_wide());
601 assert!(!lead.is_wide_continuation());
602 assert_eq!(lead.width(), 2);
603 assert_eq!(lead.content(), '中');
604
605 assert!(!cont.is_wide());
606 assert!(cont.is_wide_continuation());
607 assert_eq!(cont.width(), 0);
608 }
609
610 #[test]
611 fn set_content_clears_wide_flags() {
612 let (mut lead, _) = Cell::wide('中', SgrAttrs::default());
613 assert!(lead.is_wide());
614 lead.set_content('A', 1);
615 assert!(!lead.is_wide());
616 assert!(!lead.is_wide_continuation());
617 }
618
619 #[test]
620 fn erase_clears_wide_flags() {
621 let (mut lead, _) = Cell::wide('中', SgrAttrs::default());
622 lead.erase(Color::Default);
623 assert!(!lead.is_wide());
624 }
625
626 #[test]
627 fn display_width_unicode_cases() {
628 assert_eq!(Cell::display_width('a'), 1);
629 assert_eq!(Cell::display_width('中'), 2);
630 assert_eq!(Cell::display_width('\u{1F680}'), 2); assert_eq!(Cell::display_width('\u{0301}'), 0); assert_eq!(Cell::display_width('\u{200D}'), 0); assert_eq!(Cell::display_width('\u{FE0F}'), 0); }
635
636 #[test]
637 fn sgr_attrs_reset() {
638 let mut attrs = SgrAttrs {
639 flags: SgrFlags::BOLD,
640 fg: Color::Rgb(255, 0, 0),
641 bg: Color::Indexed(42),
642 underline_color: Some(Color::Named(3)),
643 };
644 attrs.reset();
645 assert_eq!(attrs, SgrAttrs::default());
646 }
647
648 #[test]
649 fn sgr_attrs_apply_params_basic_colors_and_reset() {
650 let mut attrs = SgrAttrs::default();
651 attrs.apply_sgr_params(&[31]);
652 assert_eq!(attrs.fg, Color::Named(1));
653 attrs.apply_sgr_params(&[44]);
654 assert_eq!(attrs.bg, Color::Named(4));
655 attrs.apply_sgr_params(&[0]);
656 assert_eq!(attrs, SgrAttrs::default());
657 }
658
659 #[test]
660 fn sgr_attrs_apply_params_extended_colors() {
661 let mut attrs = SgrAttrs::default();
662 attrs.apply_sgr_params(&[38, 5, 200]);
663 assert_eq!(attrs.fg, Color::Indexed(200));
664 attrs.apply_sgr_params(&[48, 2, 1, 2, 3]);
665 assert_eq!(attrs.bg, Color::Rgb(1, 2, 3));
666 attrs.apply_sgr_params(&[39, 49]);
667 assert_eq!(attrs.fg, Color::Default);
668 assert_eq!(attrs.bg, Color::Default);
669 }
670
671 #[test]
672 fn sgr_attrs_apply_params_empty_means_reset() {
673 let mut attrs = SgrAttrs {
674 flags: SgrFlags::BOLD,
675 fg: Color::Named(2),
676 bg: Color::Named(3),
677 underline_color: Some(Color::Indexed(7)),
678 };
679 attrs.apply_sgr_params(&[]);
680 assert_eq!(attrs, SgrAttrs::default());
681 }
682
683 #[test]
684 fn color_default() {
685 assert_eq!(Color::default(), Color::Default);
686 }
687
688 #[test]
689 fn cell_clear_resets_everything() {
690 let mut cell = Cell::with_attrs(
691 'Z',
692 2,
693 SgrAttrs {
694 flags: SgrFlags::BOLD | SgrFlags::UNDERLINE,
695 fg: Color::Rgb(1, 2, 3),
696 bg: Color::Named(5),
697 underline_color: Some(Color::Indexed(100)),
698 },
699 );
700 cell.hyperlink = 99;
701 cell.flags = CellFlags::WIDE_CHAR;
702 cell.clear();
703 assert_eq!(cell, Cell::default());
704 }
705
706 #[test]
709 fn hyperlink_registry_intern_and_get() {
710 let mut reg = HyperlinkRegistry::new();
711 let id = reg.intern("https://example.com");
712 assert_ne!(id, 0);
713 assert_eq!(reg.get(id), Some("https://example.com"));
714 }
715
716 #[test]
717 fn hyperlink_registry_dedup_and_id_reuse_on_release() {
718 let mut reg = HyperlinkRegistry::new();
719 let id1 = reg.intern("https://one.test");
720 let id2 = reg.intern("https://one.test");
721 assert_eq!(id1, id2);
722
723 reg.acquire_id(id1);
725 reg.acquire_id(id1);
726 reg.release_id(id1);
727 reg.release_id(id1);
728 assert_eq!(reg.get(id1), None);
729
730 let reused = reg.intern("https://two.test");
732 assert_eq!(reused, id1);
733 assert_eq!(reg.get(reused), Some("https://two.test"));
734 }
735
736 #[test]
737 fn hyperlink_registry_overlap_and_reset() {
738 let mut reg = HyperlinkRegistry::new();
739 let id_a = reg.acquire("https://a.test");
740 let id_b = reg.acquire("https://b.test");
741
742 let mut c0 = Cell::new('x');
744 c0.hyperlink = id_a;
745 let mut c1 = Cell::new('y');
746 c1.hyperlink = id_b;
747
748 assert_eq!(reg.get(c0.hyperlink), Some("https://a.test"));
749 assert_eq!(reg.get(c1.hyperlink), Some("https://b.test"));
750
751 reg.release_id(c0.hyperlink);
753 c0.hyperlink = 0;
754 assert_eq!(reg.get(c0.hyperlink), None);
755 }
756
757 #[test]
758 fn click_mapping_via_grid_helper() {
759 let mut reg = HyperlinkRegistry::new();
760 let id = reg.acquire("https://click.test");
761 let mut grid = Grid::new(3, 1);
762 let cell = grid.cell_mut(0, 1).unwrap();
763 *cell = Cell::new('C');
764 cell.hyperlink = id;
765
766 assert_eq!(
767 grid.hyperlink_uri_at(0, 1, ®),
768 Some("https://click.test")
769 );
770 assert_eq!(grid.hyperlink_uri_at(0, 0, ®), None);
771 assert_eq!(grid.hyperlink_uri_at(9, 9, ®), None);
772 }
773
774 #[test]
777 fn sgr_dim_flag() {
778 let mut a = SgrAttrs::default();
779 a.apply_sgr_params(&[2]);
780 assert!(a.flags.contains(SgrFlags::DIM));
781 a.apply_sgr_params(&[22]); assert!(!a.flags.contains(SgrFlags::DIM));
783 }
784
785 #[test]
786 fn sgr_underline_and_variants() {
787 let mut a = SgrAttrs::default();
788 a.apply_sgr_params(&[4]);
789 assert!(a.flags.contains(SgrFlags::UNDERLINE));
790 a.apply_sgr_params(&[24]); assert!(!a.flags.contains(SgrFlags::UNDERLINE));
792
793 a.apply_sgr_params(&[21]);
794 assert!(a.flags.contains(SgrFlags::DOUBLE_UNDERLINE));
795 a.apply_sgr_params(&[24]);
796 assert!(!a.flags.contains(SgrFlags::DOUBLE_UNDERLINE));
797 }
798
799 #[test]
800 fn sgr_blink_inverse_hidden_strikethrough() {
801 let mut a = SgrAttrs::default();
802
803 a.apply_sgr_params(&[5]);
804 assert!(a.flags.contains(SgrFlags::BLINK));
805 a.apply_sgr_params(&[25]);
806 assert!(!a.flags.contains(SgrFlags::BLINK));
807
808 a.apply_sgr_params(&[7]);
809 assert!(a.flags.contains(SgrFlags::INVERSE));
810 a.apply_sgr_params(&[27]);
811 assert!(!a.flags.contains(SgrFlags::INVERSE));
812
813 a.apply_sgr_params(&[8]);
814 assert!(a.flags.contains(SgrFlags::HIDDEN));
815 a.apply_sgr_params(&[28]);
816 assert!(!a.flags.contains(SgrFlags::HIDDEN));
817
818 a.apply_sgr_params(&[9]);
819 assert!(a.flags.contains(SgrFlags::STRIKETHROUGH));
820 a.apply_sgr_params(&[29]);
821 assert!(!a.flags.contains(SgrFlags::STRIKETHROUGH));
822 }
823
824 #[test]
825 fn sgr_overline() {
826 let mut a = SgrAttrs::default();
827 a.apply_sgr_params(&[53]);
828 assert!(a.flags.contains(SgrFlags::OVERLINE));
829 a.apply_sgr_params(&[55]);
830 assert!(!a.flags.contains(SgrFlags::OVERLINE));
831 }
832
833 #[test]
834 fn sgr_bold_dim_remove_clears_both() {
835 let mut a = SgrAttrs::default();
836 a.apply_sgr_params(&[1, 2]); assert!(a.flags.contains(SgrFlags::BOLD));
838 assert!(a.flags.contains(SgrFlags::DIM));
839 a.apply_sgr_params(&[22]); assert!(!a.flags.contains(SgrFlags::BOLD));
841 assert!(!a.flags.contains(SgrFlags::DIM));
842 }
843
844 #[test]
845 fn sgr_bright_fg_colors() {
846 let mut a = SgrAttrs::default();
847 a.apply_sgr_params(&[90]);
848 assert_eq!(a.fg, Color::Named(8)); a.apply_sgr_params(&[97]);
850 assert_eq!(a.fg, Color::Named(15)); }
852
853 #[test]
854 fn sgr_bright_bg_colors() {
855 let mut a = SgrAttrs::default();
856 a.apply_sgr_params(&[100]);
857 assert_eq!(a.bg, Color::Named(8));
858 a.apply_sgr_params(&[107]);
859 assert_eq!(a.bg, Color::Named(15));
860 }
861
862 #[test]
863 fn sgr_underline_color_set_and_reset() {
864 let mut a = SgrAttrs::default();
865 a.apply_sgr_params(&[58, 5, 42]);
866 assert_eq!(a.underline_color, Some(Color::Indexed(42)));
867 a.apply_sgr_params(&[59]);
868 assert_eq!(a.underline_color, None);
869
870 a.apply_sgr_params(&[58, 2, 10, 20, 30]);
871 assert_eq!(a.underline_color, Some(Color::Rgb(10, 20, 30)));
872 }
873
874 #[test]
875 fn sgr_multiple_params_in_one_call() {
876 let mut a = SgrAttrs::default();
877 a.apply_sgr_params(&[1, 3, 31, 42]);
878 assert!(a.flags.contains(SgrFlags::BOLD));
879 assert!(a.flags.contains(SgrFlags::ITALIC));
880 assert_eq!(a.fg, Color::Named(1)); assert_eq!(a.bg, Color::Named(2)); }
883
884 #[test]
885 fn sgr_unknown_param_ignored() {
886 let mut a = SgrAttrs::default();
887 a.apply_sgr_params(&[999]);
888 assert_eq!(a, SgrAttrs::default());
889 }
890
891 #[test]
892 fn sgr_indexed_fg_clamps_to_255() {
893 let mut a = SgrAttrs::default();
894 a.apply_sgr_params(&[38, 5, 300]);
895 assert_eq!(a.fg, Color::Indexed(255));
896 }
897
898 #[test]
899 fn sgr_rgb_clamps_to_255() {
900 let mut a = SgrAttrs::default();
901 a.apply_sgr_params(&[38, 2, 999, 500, 300]);
902 assert_eq!(a.fg, Color::Rgb(255, 255, 255));
903 }
904
905 #[test]
906 fn sgr_extended_color_with_bad_mode_ignored() {
907 let mut a = SgrAttrs::default();
908 a.apply_sgr_params(&[38, 99, 100]);
910 assert_eq!(a.fg, Color::Default);
911 }
912
913 #[test]
914 fn sgr_truncated_extended_color_ignored() {
915 let mut a = SgrAttrs::default();
916 a.apply_sgr_params(&[38]);
918 assert_eq!(a.fg, Color::Default);
919 a.apply_sgr_params(&[38, 5]);
921 assert_eq!(a.fg, Color::Default);
922 a.apply_sgr_params(&[38, 2, 10]);
924 assert_eq!(a.fg, Color::Default);
925 }
926
927 #[test]
930 fn hyperlink_intern_empty_returns_zero() {
931 let mut reg = HyperlinkRegistry::new();
932 assert_eq!(reg.intern(""), 0);
933 }
934
935 #[test]
936 fn hyperlink_get_zero_returns_none() {
937 let reg = HyperlinkRegistry::new();
938 assert_eq!(reg.get(0), None);
939 }
940
941 #[test]
942 fn hyperlink_get_invalid_id_returns_none() {
943 let reg = HyperlinkRegistry::new();
944 assert_eq!(reg.get(999), None);
945 }
946
947 #[test]
948 fn hyperlink_contains() {
949 let mut reg = HyperlinkRegistry::new();
950 let id = reg.intern("https://test.com");
951 assert!(reg.contains(id));
952 assert!(!reg.contains(0));
953 assert!(!reg.contains(999));
954 }
955
956 #[test]
957 fn hyperlink_len_and_is_empty() {
958 let mut reg = HyperlinkRegistry::new();
959 assert!(reg.is_empty());
960 assert_eq!(reg.len(), 0);
961
962 let id = reg.intern("https://a.test");
963 assert!(!reg.is_empty());
964 assert_eq!(reg.len(), 1);
965
966 reg.intern("https://b.test");
967 assert_eq!(reg.len(), 2);
968
969 reg.intern("https://a.test");
971 assert_eq!(reg.len(), 2);
972
973 reg.acquire_id(id);
975 reg.release_id(id);
976 assert_eq!(reg.len(), 1);
977 }
978
979 #[test]
980 fn hyperlink_clear_resets() {
981 let mut reg = HyperlinkRegistry::new();
982 let id = reg.intern("https://x.test");
983 reg.clear();
984 assert!(reg.is_empty());
985 assert_eq!(reg.get(id), None);
986 }
987
988 #[test]
989 fn hyperlink_release_id_zero_is_noop() {
990 let mut reg = HyperlinkRegistry::new();
991 reg.release_id(0); assert!(reg.is_empty());
993 }
994
995 #[test]
996 fn hyperlink_release_invalid_id_is_noop() {
997 let mut reg = HyperlinkRegistry::new();
998 reg.release_id(500); }
1000
1001 #[test]
1002 fn hyperlink_acquire_id_zero_is_noop() {
1003 let mut reg = HyperlinkRegistry::new();
1004 reg.acquire_id(0); assert!(reg.is_empty());
1006 }
1007
1008 #[test]
1011 fn cell_with_attrs_preserves_values() {
1012 let attrs = SgrAttrs {
1013 flags: SgrFlags::ITALIC | SgrFlags::UNDERLINE,
1014 fg: Color::Rgb(10, 20, 30),
1015 bg: Color::Indexed(42),
1016 underline_color: Some(Color::Named(3)),
1017 };
1018 let cell = Cell::with_attrs('Q', 2, attrs);
1019 assert_eq!(cell.content(), 'Q');
1020 assert_eq!(cell.width(), 2);
1021 assert_eq!(cell.attrs, attrs);
1022 assert_eq!(cell.hyperlink, 0);
1023 assert!(!cell.is_wide());
1024 }
1025
1026 #[test]
1027 fn clear_on_scrollback_eviction() {
1028 let mut reg = HyperlinkRegistry::new();
1029 let mut sb = Scrollback::new(1);
1030
1031 let mut row_a = vec![Cell::new('a'), Cell::new('a'), Cell::new('a')];
1033 let id_a = reg.intern("https://a.test");
1034 for cell in &mut row_a {
1035 reg.acquire_id(id_a);
1036 cell.hyperlink = id_a;
1037 }
1038 assert_eq!(reg.get(id_a), Some("https://a.test"));
1039
1040 let _ = sb.push_row(&row_a, false);
1042 let row_b = vec![Cell::new('b')];
1043 let evicted = sb.push_row(&row_b, false).expect("capacity=1 must evict");
1044 reg.release_cells(&evicted.cells);
1045
1046 assert_eq!(reg.get(id_a), None);
1048 }
1049
1050 #[test]
1053 fn push_combining_stores_mark() {
1054 let mut cell = Cell::new('e');
1055 assert!(!cell.has_combining());
1056 assert!(cell.combining_marks().is_empty());
1057
1058 assert!(cell.push_combining('\u{0301}')); assert!(cell.has_combining());
1060 assert_eq!(cell.combining_marks(), &['\u{0301}']);
1061 assert!(cell.flags.contains(CellFlags::HAS_COMBINING));
1062 }
1063
1064 #[test]
1065 fn push_combining_multiple_marks() {
1066 let mut cell = Cell::new('a');
1067 assert!(cell.push_combining('\u{0300}')); assert!(cell.push_combining('\u{0301}')); assert_eq!(cell.combining_marks(), &['\u{0300}', '\u{0301}']);
1070 }
1071
1072 #[test]
1073 fn push_combining_overflow_returns_false() {
1074 let mut cell = Cell::new('a');
1075 let marks = ['\u{0300}', '\u{0301}', '\u{0302}', '\u{0303}'];
1076 for mark in &marks[..MAX_COMBINING] {
1077 assert!(cell.push_combining(*mark));
1078 }
1079 assert!(!cell.push_combining(marks[MAX_COMBINING]));
1081 assert_eq!(cell.combining_marks().len(), MAX_COMBINING);
1082 }
1083
1084 #[test]
1085 fn set_content_clears_combining() {
1086 let mut cell = Cell::new('e');
1087 cell.push_combining('\u{0301}');
1088 assert!(cell.has_combining());
1089
1090 cell.set_content('x', 1);
1091 assert!(!cell.has_combining());
1092 assert!(cell.combining_marks().is_empty());
1093 assert!(!cell.flags.contains(CellFlags::HAS_COMBINING));
1094 }
1095
1096 #[test]
1097 fn erase_clears_combining() {
1098 let mut cell = Cell::new('e');
1099 cell.push_combining('\u{0301}');
1100 cell.erase(Color::Default);
1101 assert!(!cell.has_combining());
1102 assert!(cell.combining_marks().is_empty());
1103 }
1104
1105 #[test]
1106 fn clear_clears_combining() {
1107 let mut cell = Cell::new('e');
1108 cell.push_combining('\u{0301}');
1109 cell.clear();
1110 assert!(!cell.has_combining());
1111 assert_eq!(cell, Cell::default());
1112 }
1113
1114 #[test]
1115 fn combining_on_wide_char() {
1116 let (mut lead, _) = Cell::wide('中', SgrAttrs::default());
1117 assert!(lead.push_combining('\u{0300}'));
1118 assert_eq!(lead.combining_marks(), &['\u{0300}']);
1119 assert_eq!(lead.content(), '中');
1120 assert!(lead.is_wide());
1121 }
1122
1123 #[test]
1124 fn default_cell_has_no_combining() {
1125 let cell = Cell::default();
1126 assert!(!cell.has_combining());
1127 assert!(cell.combining_marks().is_empty());
1128 }
1129}