1use std::collections::HashMap;
7use std::fmt::Write;
8
9use crate::buffer::CellChange;
10use crate::cell::Cell;
11use crate::color::{Color, NamedColor};
12use crate::style::Style;
13use crate::terminal::ColorSupport;
14
15pub struct Renderer {
17 color_support: ColorSupport,
18 synchronized_output: bool,
19}
20
21impl Renderer {
22 pub fn new(color_support: ColorSupport, synchronized_output: bool) -> Self {
24 Self {
25 color_support,
26 synchronized_output,
27 }
28 }
29
30 pub fn render(&self, changes: &[CellChange]) -> String {
32 if changes.is_empty() {
33 return String::new();
34 }
35
36 let mut output = String::with_capacity(changes.len() * 16);
37
38 if self.synchronized_output {
40 output.push_str("\x1b[?2026h");
41 }
42
43 let mut last_x: Option<u16> = None;
44 let mut last_y: Option<u16> = None;
45 let mut last_style = Style::default();
46 let mut style_active = false;
47
48 for change in changes {
49 if change.cell.width == 0 {
51 continue;
52 }
53
54 let need_move = !matches!((last_x, last_y), (Some(lx), Some(ly)) if ly == change.y && lx == change.x);
56 if need_move {
57 let _ = write!(output, "\x1b[{};{}H", change.y + 1, change.x + 1);
59 }
60
61 self.write_style_diff(&mut output, &last_style, &change.cell.style, style_active);
63 last_style = change.cell.style.clone();
64 style_active = true;
65
66 output.push_str(&change.cell.grapheme);
68
69 last_x = Some(change.x + u16::from(change.cell.width));
71 last_y = Some(change.y);
72 }
73
74 if style_active && !last_style.is_empty() {
76 output.push_str("\x1b[0m");
77 }
78
79 if self.synchronized_output {
81 output.push_str("\x1b[?2026l");
82 }
83
84 output
85 }
86
87 pub fn render_batched(&self, changes: &[CellChange]) -> String {
94 let batches = batch_changes(changes);
95 if batches.is_empty() {
96 return String::new();
97 }
98
99 let mut output = String::with_capacity(changes.len() * 12);
100
101 if self.synchronized_output {
102 output.push_str("\x1b[?2026h");
103 }
104
105 let mut last_style = Style::default();
106 let mut style_active = false;
107 let mut last_cursor_x: Option<u16> = None;
108 let mut last_cursor_y: Option<u16> = None;
109
110 for batch in &batches {
111 let need_move = !matches!(
113 (last_cursor_x, last_cursor_y),
114 (Some(lx), Some(ly)) if ly == batch.y && lx == batch.x
115 );
116 if need_move {
117 let _ = write!(output, "\x1b[{};{}H", batch.y + 1, batch.x + 1);
118 }
119
120 let mut cursor_x = batch.x;
121 for cell in &batch.cells {
122 self.write_style_diff(&mut output, &last_style, &cell.style, style_active);
123 last_style = cell.style.clone();
124 style_active = true;
125
126 output.push_str(&cell.grapheme);
127 cursor_x += u16::from(cell.width);
128 }
129
130 last_cursor_x = Some(cursor_x);
131 last_cursor_y = Some(batch.y);
132 }
133
134 if style_active && !last_style.is_empty() {
135 output.push_str("\x1b[0m");
136 }
137
138 if self.synchronized_output {
139 output.push_str("\x1b[?2026l");
140 }
141
142 output
143 }
144
145 pub fn render_optimized(&self, changes: &[CellChange]) -> String {
152 if changes.is_empty() {
153 return String::new();
154 }
155
156 let mut output = String::with_capacity(changes.len() * 16);
157
158 output.push_str("\x1b[?25l");
160
161 if self.synchronized_output {
163 output.push_str("\x1b[?2026h");
164 }
165
166 let mut last_x: Option<u16> = None;
167 let mut last_y: Option<u16> = None;
168 let mut last_style = Style::default();
169 let mut style_active = false;
170
171 for change in changes {
172 if change.cell.width == 0 {
174 continue;
175 }
176
177 let need_move = !matches!((last_x, last_y), (Some(lx), Some(ly)) if ly == change.y && lx == change.x);
179 if need_move {
180 let _ = write!(output, "\x1b[{};{}H", change.y + 1, change.x + 1);
181 }
182
183 if !style_active
185 || needs_reset(&last_style, &change.cell.style)
186 || last_style != change.cell.style
187 {
188 if style_active && !last_style.is_empty() {
189 output.push_str("\x1b[0m");
190 }
191 let sgr = build_sgr_sequence(&change.cell.style, self.color_support);
192 output.push_str(&sgr);
193 }
194
195 last_style = change.cell.style.clone();
196 style_active = true;
197
198 output.push_str(&change.cell.grapheme);
199
200 last_x = Some(change.x + u16::from(change.cell.width));
201 last_y = Some(change.y);
202 }
203
204 if style_active && !last_style.is_empty() {
206 output.push_str("\x1b[0m");
207 }
208
209 if self.synchronized_output {
211 output.push_str("\x1b[?2026l");
212 }
213
214 output.push_str("\x1b[?25h");
216
217 output
218 }
219
220 fn write_style_diff(&self, output: &mut String, prev: &Style, next: &Style, active: bool) {
222 if !active || needs_reset(prev, next) {
223 if active && !prev.is_empty() {
225 output.push_str("\x1b[0m");
226 }
227 self.write_full_style(output, next);
229 return;
230 }
231
232 if prev.fg != next.fg {
234 self.write_fg(output, &next.fg);
235 }
236 if prev.bg != next.bg {
237 self.write_bg(output, &next.bg);
238 }
239 if !prev.bold && next.bold {
240 output.push_str("\x1b[1m");
241 }
242 if !prev.dim && next.dim {
243 output.push_str("\x1b[2m");
244 }
245 if !prev.italic && next.italic {
246 output.push_str("\x1b[3m");
247 }
248 if !prev.underline && next.underline {
249 output.push_str("\x1b[4m");
250 }
251 if !prev.reverse && next.reverse {
252 output.push_str("\x1b[7m");
253 }
254 if !prev.strikethrough && next.strikethrough {
255 output.push_str("\x1b[9m");
256 }
257 }
258
259 fn write_full_style(&self, output: &mut String, style: &Style) {
261 self.write_fg(output, &style.fg);
262 self.write_bg(output, &style.bg);
263 if style.bold {
264 output.push_str("\x1b[1m");
265 }
266 if style.dim {
267 output.push_str("\x1b[2m");
268 }
269 if style.italic {
270 output.push_str("\x1b[3m");
271 }
272 if style.underline {
273 output.push_str("\x1b[4m");
274 }
275 if style.reverse {
276 output.push_str("\x1b[7m");
277 }
278 if style.strikethrough {
279 output.push_str("\x1b[9m");
280 }
281 }
282
283 fn write_fg(&self, output: &mut String, color: &Option<Color>) {
285 match color {
286 None => {}
287 Some(c) => {
288 let downgraded = self.downgrade_color(c);
289 write_fg_color(output, &downgraded);
290 }
291 }
292 }
293
294 fn write_bg(&self, output: &mut String, color: &Option<Color>) {
296 match color {
297 None => {}
298 Some(c) => {
299 let downgraded = self.downgrade_color(c);
300 write_bg_color(output, &downgraded);
301 }
302 }
303 }
304
305 fn downgrade_color<'a>(&self, color: &'a Color) -> std::borrow::Cow<'a, Color> {
309 if std::env::var("NO_COLOR").is_ok() {
311 return std::borrow::Cow::Owned(Color::Reset);
312 }
313
314 match self.color_support {
315 ColorSupport::TrueColor => std::borrow::Cow::Borrowed(color),
316 ColorSupport::Extended256 => match color {
317 Color::Rgb { r, g, b } => {
318 std::borrow::Cow::Owned(Color::Indexed(rgb_to_256(*r, *g, *b)))
319 }
320 _ => std::borrow::Cow::Borrowed(color),
321 },
322 ColorSupport::Basic16 => match color {
323 Color::Rgb { r, g, b } => {
324 std::borrow::Cow::Owned(Color::Named(rgb_to_16(*r, *g, *b)))
325 }
326 Color::Indexed(i) => std::borrow::Cow::Owned(Color::Named(index_to_named(*i))),
327 _ => std::borrow::Cow::Borrowed(color),
328 },
329 ColorSupport::NoColor => std::borrow::Cow::Owned(Color::Reset),
330 }
331 }
332}
333
334fn needs_reset(prev: &Style, next: &Style) -> bool {
337 (prev.bold && !next.bold)
338 || (prev.dim && !next.dim)
339 || (prev.italic && !next.italic)
340 || (prev.underline && !next.underline)
341 || (prev.reverse && !next.reverse)
342 || (prev.strikethrough && !next.strikethrough)
343}
344
345pub fn build_sgr_sequence(style: &Style, color_support: ColorSupport) -> String {
351 let mut codes: Vec<String> = Vec::new();
352
353 if style.bold {
355 codes.push("1".to_string());
356 }
357 if style.dim {
358 codes.push("2".to_string());
359 }
360 if style.italic {
361 codes.push("3".to_string());
362 }
363 if style.underline {
364 codes.push("4".to_string());
365 }
366 if style.reverse {
367 codes.push("7".to_string());
368 }
369 if style.strikethrough {
370 codes.push("9".to_string());
371 }
372
373 if let Some(ref fg) = style.fg {
375 let downgraded = downgrade_color_standalone(fg, color_support);
376 codes.extend(fg_color_codes(&downgraded));
377 }
378
379 if let Some(ref bg) = style.bg {
381 let downgraded = downgrade_color_standalone(bg, color_support);
382 codes.extend(bg_color_codes(&downgraded));
383 }
384
385 if codes.is_empty() {
386 return String::new();
387 }
388
389 format!("\x1b[{}m", codes.join(";"))
390}
391
392fn downgrade_color_standalone(color: &Color, support: ColorSupport) -> Color {
396 if std::env::var("NO_COLOR").is_ok() {
398 return Color::Reset;
399 }
400
401 match support {
402 ColorSupport::TrueColor => color.clone(),
403 ColorSupport::Extended256 => match color {
404 Color::Rgb { r, g, b } => Color::Indexed(rgb_to_256(*r, *g, *b)),
405 _ => color.clone(),
406 },
407 ColorSupport::Basic16 => match color {
408 Color::Rgb { r, g, b } => Color::Named(rgb_to_16(*r, *g, *b)),
409 Color::Indexed(i) => Color::Named(index_to_named(*i)),
410 _ => color.clone(),
411 },
412 ColorSupport::NoColor => Color::Reset,
413 }
414}
415
416fn fg_color_codes(color: &Color) -> Vec<String> {
418 match color {
419 Color::Rgb { r, g, b } => vec![
420 "38".to_string(),
421 "2".to_string(),
422 r.to_string(),
423 g.to_string(),
424 b.to_string(),
425 ],
426 Color::Indexed(i) => vec!["38".to_string(), "5".to_string(), i.to_string()],
427 Color::Named(n) => vec![named_fg_code(n).to_string()],
428 Color::Reset => vec!["39".to_string()],
429 }
430}
431
432fn bg_color_codes(color: &Color) -> Vec<String> {
434 match color {
435 Color::Rgb { r, g, b } => vec![
436 "48".to_string(),
437 "2".to_string(),
438 r.to_string(),
439 g.to_string(),
440 b.to_string(),
441 ],
442 Color::Indexed(i) => vec!["48".to_string(), "5".to_string(), i.to_string()],
443 Color::Named(n) => vec![named_bg_code(n).to_string()],
444 Color::Reset => vec!["49".to_string()],
445 }
446}
447
448fn write_fg_color(output: &mut String, color: &Color) {
450 match color {
451 Color::Rgb { r, g, b } => {
452 let _ = write!(output, "\x1b[38;2;{r};{g};{b}m");
453 }
454 Color::Indexed(i) => {
455 let _ = write!(output, "\x1b[38;5;{i}m");
456 }
457 Color::Named(n) => {
458 let _ = write!(output, "\x1b[{}m", named_fg_code(n));
459 }
460 Color::Reset => {
461 output.push_str("\x1b[39m");
462 }
463 }
464}
465
466fn write_bg_color(output: &mut String, color: &Color) {
468 match color {
469 Color::Rgb { r, g, b } => {
470 let _ = write!(output, "\x1b[48;2;{r};{g};{b}m");
471 }
472 Color::Indexed(i) => {
473 let _ = write!(output, "\x1b[48;5;{i}m");
474 }
475 Color::Named(n) => {
476 let _ = write!(output, "\x1b[{}m", named_bg_code(n));
477 }
478 Color::Reset => {
479 output.push_str("\x1b[49m");
480 }
481 }
482}
483
484fn named_fg_code(color: &NamedColor) -> u8 {
486 match color {
487 NamedColor::Black => 30,
488 NamedColor::Red => 31,
489 NamedColor::Green => 32,
490 NamedColor::Yellow => 33,
491 NamedColor::Blue => 34,
492 NamedColor::Magenta => 35,
493 NamedColor::Cyan => 36,
494 NamedColor::White => 37,
495 NamedColor::BrightBlack => 90,
496 NamedColor::BrightRed => 91,
497 NamedColor::BrightGreen => 92,
498 NamedColor::BrightYellow => 93,
499 NamedColor::BrightBlue => 94,
500 NamedColor::BrightMagenta => 95,
501 NamedColor::BrightCyan => 96,
502 NamedColor::BrightWhite => 97,
503 }
504}
505
506fn named_bg_code(color: &NamedColor) -> u8 {
508 match color {
509 NamedColor::Black => 40,
510 NamedColor::Red => 41,
511 NamedColor::Green => 42,
512 NamedColor::Yellow => 43,
513 NamedColor::Blue => 44,
514 NamedColor::Magenta => 45,
515 NamedColor::Cyan => 46,
516 NamedColor::White => 47,
517 NamedColor::BrightBlack => 100,
518 NamedColor::BrightRed => 101,
519 NamedColor::BrightGreen => 102,
520 NamedColor::BrightYellow => 103,
521 NamedColor::BrightBlue => 104,
522 NamedColor::BrightMagenta => 105,
523 NamedColor::BrightCyan => 106,
524 NamedColor::BrightWhite => 107,
525 }
526}
527
528#[derive(Debug)]
533pub struct ColorMapper {
534 cache_256: HashMap<(u8, u8, u8), u8>,
536 cache_16: HashMap<(u8, u8, u8), NamedColor>,
538}
539
540impl Default for ColorMapper {
541 fn default() -> Self {
542 Self::new()
543 }
544}
545
546impl ColorMapper {
547 pub fn new() -> Self {
549 Self {
550 cache_256: HashMap::new(),
551 cache_16: HashMap::new(),
552 }
553 }
554
555 pub fn map_to_256(&mut self, r: u8, g: u8, b: u8) -> u8 {
557 if let Some(&cached) = self.cache_256.get(&(r, g, b)) {
558 return cached;
559 }
560
561 let result = rgb_to_256(r, g, b);
562 self.cache_256.insert((r, g, b), result);
563 result
564 }
565
566 pub fn map_to_16(&mut self, r: u8, g: u8, b: u8) -> NamedColor {
568 if let Some(&cached) = self.cache_16.get(&(r, g, b)) {
569 return cached;
570 }
571
572 let result = rgb_to_16(r, g, b);
573 self.cache_16.insert((r, g, b), result);
574 result
575 }
576
577 pub fn clear_cache(&mut self) {
579 self.cache_256.clear();
580 self.cache_16.clear();
581 }
582}
583
584#[derive(Debug, Clone, Copy)]
586struct Lab {
587 l: f32,
588 a: f32,
589 b: f32,
590}
591
592fn rgb_to_lab(r: u8, g: u8, b: u8) -> Lab {
597 let r_linear = srgb_to_linear(r);
599 let g_linear = srgb_to_linear(g);
600 let b_linear = srgb_to_linear(b);
601
602 let x = r_linear * 0.4124 + g_linear * 0.3576 + b_linear * 0.1805;
604 let y = r_linear * 0.2126 + g_linear * 0.7152 + b_linear * 0.0722;
605 let z = r_linear * 0.0193 + g_linear * 0.1192 + b_linear * 0.9505;
606
607 let x_n = 0.95047;
609 let y_n = 1.0;
610 let z_n = 1.08883;
611
612 let fx = lab_f(x / x_n);
613 let fy = lab_f(y / y_n);
614 let fz = lab_f(z / z_n);
615
616 let l = 116.0 * fy - 16.0;
617 let a = 500.0 * (fx - fy);
618 let b = 200.0 * (fy - fz);
619
620 Lab { l, a, b }
621}
622
623fn srgb_to_linear(c: u8) -> f32 {
625 let c_norm = f32::from(c) / 255.0;
626 if c_norm <= 0.04045 {
627 c_norm / 12.92
628 } else {
629 ((c_norm + 0.055) / 1.055).powf(2.4)
630 }
631}
632
633fn lab_f(t: f32) -> f32 {
635 let delta: f32 = 6.0 / 29.0;
636 if t > delta.powi(3) {
637 t.cbrt()
638 } else {
639 t / (3.0 * delta.powi(2)) + 4.0 / 29.0
640 }
641}
642
643fn lab_distance(lab1: Lab, lab2: Lab) -> f32 {
645 let dl = lab1.l - lab2.l;
646 let da = lab1.a - lab2.a;
647 let db = lab1.b - lab2.b;
648 (dl * dl + da * da + db * db).sqrt()
649}
650
651pub fn rgb_to_256(r: u8, g: u8, b: u8) -> u8 {
659 let source_lab = rgb_to_lab(r, g, b);
660
661 let mut best_idx = 16_u8;
662 let mut best_distance = f32::MAX;
663
664 for i in 0..24_u8 {
666 let gray = 8 + 10 * i;
667 let lab = rgb_to_lab(gray, gray, gray);
668 let dist = lab_distance(source_lab, lab);
669 if dist < best_distance {
670 best_distance = dist;
671 best_idx = 232 + i;
672 }
673 }
674
675 for ri in 0..6_u8 {
677 for gi in 0..6_u8 {
678 for bi in 0..6_u8 {
679 let r_val = if ri == 0 { 0 } else { 55 + 40 * ri };
680 let g_val = if gi == 0 { 0 } else { 55 + 40 * gi };
681 let b_val = if bi == 0 { 0 } else { 55 + 40 * bi };
682 let lab = rgb_to_lab(r_val, g_val, b_val);
683 let dist = lab_distance(source_lab, lab);
684 if dist < best_distance {
685 best_distance = dist;
686 best_idx = 16 + 36 * ri + 6 * gi + bi;
687 }
688 }
689 }
690 }
691
692 let basic_16_rgb = [
694 (0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0), (0, 0, 128), (128, 0, 128), (0, 128, 128), (192, 192, 192), (128, 128, 128), (255, 0, 0), (0, 255, 0), (255, 255, 0), (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255), ];
711
712 for (i, (cr, cg, cb)) in basic_16_rgb.iter().enumerate() {
713 let lab = rgb_to_lab(*cr, *cg, *cb);
714 let dist = lab_distance(source_lab, lab);
715 if dist < best_distance {
716 best_distance = dist;
717 best_idx = i as u8;
718 }
719 }
720
721 best_idx
722}
723
724pub fn rgb_to_16(r: u8, g: u8, b: u8) -> NamedColor {
726 let source_lab = rgb_to_lab(r, g, b);
727
728 let candidates: [(NamedColor, (u8, u8, u8)); 16] = [
729 (NamedColor::Black, (0, 0, 0)),
730 (NamedColor::Red, (128, 0, 0)),
731 (NamedColor::Green, (0, 128, 0)),
732 (NamedColor::Yellow, (128, 128, 0)),
733 (NamedColor::Blue, (0, 0, 128)),
734 (NamedColor::Magenta, (128, 0, 128)),
735 (NamedColor::Cyan, (0, 128, 128)),
736 (NamedColor::White, (192, 192, 192)),
737 (NamedColor::BrightBlack, (128, 128, 128)),
738 (NamedColor::BrightRed, (255, 0, 0)),
739 (NamedColor::BrightGreen, (0, 255, 0)),
740 (NamedColor::BrightYellow, (255, 255, 0)),
741 (NamedColor::BrightBlue, (0, 0, 255)),
742 (NamedColor::BrightMagenta, (255, 0, 255)),
743 (NamedColor::BrightCyan, (0, 255, 255)),
744 (NamedColor::BrightWhite, (255, 255, 255)),
745 ];
746
747 let mut best = NamedColor::White;
748 let mut best_distance = f32::MAX;
749
750 for (name, (cr, cg, cb)) in &candidates {
751 let lab = rgb_to_lab(*cr, *cg, *cb);
752 let dist = lab_distance(source_lab, lab);
753 if dist < best_distance {
754 best_distance = dist;
755 best = *name;
756 }
757 }
758
759 best
760}
761
762#[allow(dead_code)] fn color_cube_index(val: u8) -> u8 {
765 if val < 48 {
766 0
767 } else if val < 115 {
768 1
769 } else {
770 ((u16::from(val) - 35) / 40) as u8
771 }
772}
773
774pub fn rgb_to_named(r: u8, g: u8, b: u8) -> NamedColor {
778 rgb_to_16(r, g, b)
779}
780
781#[derive(Debug, Clone, PartialEq, Eq)]
786pub struct DeltaBatch {
787 pub x: u16,
789 pub y: u16,
791 pub cells: Vec<Cell>,
793}
794
795pub fn batch_changes(changes: &[CellChange]) -> Vec<DeltaBatch> {
802 let mut batches: Vec<DeltaBatch> = Vec::new();
803
804 for change in changes {
805 if change.cell.width == 0 {
807 continue;
808 }
809
810 let can_extend = match batches.last() {
811 Some(batch) => {
812 batch.y == change.y && {
813 let last_cell_x = batch.x;
815 let total_width: u16 = batch.cells.iter().map(|c| u16::from(c.width)).sum();
816 last_cell_x + total_width == change.x
817 }
818 }
819 None => false,
820 };
821
822 if can_extend {
823 match batches.last_mut() {
824 Some(batch) => batch.cells.push(change.cell.clone()),
825 None => unreachable!(),
826 }
827 } else {
828 batches.push(DeltaBatch {
829 x: change.x,
830 y: change.y,
831 cells: vec![change.cell.clone()],
832 });
833 }
834 }
835
836 batches
837}
838
839fn index_to_named(idx: u8) -> NamedColor {
841 match idx {
842 0 => NamedColor::Black,
843 1 => NamedColor::Red,
844 2 => NamedColor::Green,
845 3 => NamedColor::Yellow,
846 4 => NamedColor::Blue,
847 5 => NamedColor::Magenta,
848 6 => NamedColor::Cyan,
849 7 => NamedColor::White,
850 8 => NamedColor::BrightBlack,
851 9 => NamedColor::BrightRed,
852 10 => NamedColor::BrightGreen,
853 11 => NamedColor::BrightYellow,
854 12 => NamedColor::BrightBlue,
855 13 => NamedColor::BrightMagenta,
856 14 => NamedColor::BrightCyan,
857 15 => NamedColor::BrightWhite,
858 16..=231 => {
859 let idx = idx - 16;
861 let b_idx = idx % 6;
862 let g_idx = (idx / 6) % 6;
863 let r_idx = idx / 36;
864 let r = if r_idx == 0 { 0 } else { 55 + 40 * r_idx };
865 let g = if g_idx == 0 { 0 } else { 55 + 40 * g_idx };
866 let b = if b_idx == 0 { 0 } else { 55 + 40 * b_idx };
867 rgb_to_16(r, g, b)
868 }
869 _ => {
870 let gray = 8 + 10 * (idx - 232);
872 rgb_to_16(gray, gray, gray)
873 }
874 }
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880 use crate::buffer::CellChange;
881 use crate::cell::Cell;
882
883 #[test]
884 fn render_empty_changes() {
885 let renderer = Renderer::new(ColorSupport::TrueColor, false);
886 let output = renderer.render(&[]);
887 assert!(output.is_empty());
888 }
889
890 #[test]
891 fn render_cursor_position() {
892 let renderer = Renderer::new(ColorSupport::TrueColor, false);
893 let changes = vec![CellChange {
894 x: 5,
895 y: 3,
896 cell: Cell::new("A", Style::default()),
897 }];
898 let output = renderer.render(&changes);
899 assert!(output.contains("\x1b[4;6H"));
901 assert!(output.contains('A'));
902 }
903
904 #[test]
905 fn render_adjacent_cells_no_redundant_move() {
906 let renderer = Renderer::new(ColorSupport::TrueColor, false);
907 let changes = vec![
908 CellChange {
909 x: 0,
910 y: 0,
911 cell: Cell::new("A", Style::default()),
912 },
913 CellChange {
914 x: 1,
915 y: 0,
916 cell: Cell::new("B", Style::default()),
917 },
918 ];
919 let output = renderer.render(&changes);
920 let move_count = output.matches("\x1b[").count();
922 assert_eq!(move_count, 1, "output: {output:?}");
924 }
925
926 #[test]
927 fn render_fg_truecolor() {
928 let renderer = Renderer::new(ColorSupport::TrueColor, false);
929 let style = Style::new().fg(Color::Rgb {
930 r: 255,
931 g: 128,
932 b: 0,
933 });
934 let changes = vec![CellChange {
935 x: 0,
936 y: 0,
937 cell: Cell::new("X", style),
938 }];
939 let output = renderer.render(&changes);
940 assert!(output.contains("\x1b[38;2;255;128;0m"));
941 }
942
943 #[test]
944 fn render_bg_truecolor() {
945 let renderer = Renderer::new(ColorSupport::TrueColor, false);
946 let style = Style::new().bg(Color::Rgb {
947 r: 0,
948 g: 128,
949 b: 255,
950 });
951 let changes = vec![CellChange {
952 x: 0,
953 y: 0,
954 cell: Cell::new("X", style),
955 }];
956 let output = renderer.render(&changes);
957 assert!(output.contains("\x1b[48;2;0;128;255m"));
958 }
959
960 #[test]
961 fn render_bold_italic() {
962 let renderer = Renderer::new(ColorSupport::TrueColor, false);
963 let style = Style::new().bold(true).italic(true);
964 let changes = vec![CellChange {
965 x: 0,
966 y: 0,
967 cell: Cell::new("X", style),
968 }];
969 let output = renderer.render(&changes);
970 assert!(output.contains("\x1b[1m")); assert!(output.contains("\x1b[3m")); }
973
974 #[test]
975 fn render_named_color() {
976 let renderer = Renderer::new(ColorSupport::TrueColor, false);
977 let style = Style::new().fg(Color::Named(NamedColor::Red));
978 let changes = vec![CellChange {
979 x: 0,
980 y: 0,
981 cell: Cell::new("X", style),
982 }];
983 let output = renderer.render(&changes);
984 assert!(output.contains("\x1b[31m")); }
986
987 #[test]
988 fn render_indexed_color() {
989 let renderer = Renderer::new(ColorSupport::TrueColor, false);
990 let style = Style::new().fg(Color::Indexed(42));
991 let changes = vec![CellChange {
992 x: 0,
993 y: 0,
994 cell: Cell::new("X", style),
995 }];
996 let output = renderer.render(&changes);
997 assert!(output.contains("\x1b[38;5;42m"));
998 }
999
1000 #[test]
1001 fn render_style_reset_at_end() {
1002 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1003 let style = Style::new().bold(true);
1004 let changes = vec![CellChange {
1005 x: 0,
1006 y: 0,
1007 cell: Cell::new("X", style),
1008 }];
1009 let output = renderer.render(&changes);
1010 assert!(output.ends_with("\x1b[0m"));
1011 }
1012
1013 #[test]
1014 fn render_no_reset_for_default_style() {
1015 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1016 let changes = vec![CellChange {
1017 x: 0,
1018 y: 0,
1019 cell: Cell::new("X", Style::default()),
1020 }];
1021 let output = renderer.render(&changes);
1022 assert!(!output.contains("\x1b[0m"));
1023 }
1024
1025 #[test]
1026 fn render_skip_continuation_cells() {
1027 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1028 let changes = vec![
1029 CellChange {
1030 x: 0,
1031 y: 0,
1032 cell: Cell::new("\u{4e16}", Style::default()), },
1034 CellChange {
1035 x: 1,
1036 y: 0,
1037 cell: Cell::continuation(), },
1039 ];
1040 let output = renderer.render(&changes);
1041 assert!(output.contains("\u{4e16}"));
1043 let esc_count = output.matches("\x1b[").count();
1045 assert_eq!(esc_count, 1);
1046 }
1047
1048 #[test]
1049 fn synchronized_output_wrapping() {
1050 let renderer = Renderer::new(ColorSupport::TrueColor, true);
1051 let changes = vec![CellChange {
1052 x: 0,
1053 y: 0,
1054 cell: Cell::new("A", Style::default()),
1055 }];
1056 let output = renderer.render(&changes);
1057 assert!(output.starts_with("\x1b[?2026h"));
1058 assert!(output.ends_with("\x1b[?2026l"));
1059 }
1060
1061 #[test]
1062 fn no_sync_when_disabled() {
1063 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1064 let changes = vec![CellChange {
1065 x: 0,
1066 y: 0,
1067 cell: Cell::new("A", Style::default()),
1068 }];
1069 let output = renderer.render(&changes);
1070 assert!(!output.contains("\x1b[?2026h"));
1071 assert!(!output.contains("\x1b[?2026l"));
1072 }
1073
1074 #[test]
1077 fn truecolor_passthrough() {
1078 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1079 let style = Style::new().fg(Color::Rgb {
1080 r: 100,
1081 g: 200,
1082 b: 50,
1083 });
1084 let changes = vec![CellChange {
1085 x: 0,
1086 y: 0,
1087 cell: Cell::new("X", style),
1088 }];
1089 let output = renderer.render(&changes);
1090 assert!(output.contains("\x1b[38;2;100;200;50m"));
1091 }
1092
1093 #[test]
1094 fn truecolor_to_256() {
1095 let renderer = Renderer::new(ColorSupport::Extended256, false);
1096 let style = Style::new().fg(Color::Rgb { r: 255, g: 0, b: 0 });
1097 let changes = vec![CellChange {
1098 x: 0,
1099 y: 0,
1100 cell: Cell::new("X", style),
1101 }];
1102 let output = renderer.render(&changes);
1103 assert!(output.contains("\x1b[38;5;"));
1105 assert!(!output.contains("\x1b[38;2;"));
1106 }
1107
1108 #[test]
1109 fn truecolor_to_16() {
1110 let renderer = Renderer::new(ColorSupport::Basic16, false);
1111 let style = Style::new().fg(Color::Rgb { r: 255, g: 0, b: 0 });
1112 let changes = vec![CellChange {
1113 x: 0,
1114 y: 0,
1115 cell: Cell::new("X", style),
1116 }];
1117 let output = renderer.render(&changes);
1118 assert!(output.contains("\x1b[91m"));
1120 }
1121
1122 #[test]
1123 fn no_color_strips_all() {
1124 let renderer = Renderer::new(ColorSupport::NoColor, false);
1125 let style = Style::new()
1126 .fg(Color::Rgb { r: 255, g: 0, b: 0 })
1127 .bg(Color::Named(NamedColor::Blue));
1128 let changes = vec![CellChange {
1129 x: 0,
1130 y: 0,
1131 cell: Cell::new("X", style),
1132 }];
1133 let output = renderer.render(&changes);
1134 assert!(output.contains("\x1b[39m")); assert!(output.contains("\x1b[49m")); }
1138
1139 #[test]
1142 fn rgb_to_256_pure_red() {
1143 let idx = rgb_to_256(255, 0, 0);
1144 assert_eq!(idx, 196);
1146 }
1147
1148 #[test]
1149 fn rgb_to_256_grayscale() {
1150 let idx = rgb_to_256(128, 128, 128);
1151 assert_eq!(idx, 244);
1153 }
1154
1155 #[test]
1156 fn rgb_to_256_black() {
1157 let idx = rgb_to_256(0, 0, 0);
1158 assert_eq!(idx, 16); }
1160
1161 #[test]
1162 fn rgb_to_named_pure_red() {
1163 let named = rgb_to_named(255, 0, 0);
1164 assert_eq!(named, NamedColor::BrightRed);
1165 }
1166
1167 #[test]
1168 fn rgb_to_named_pure_black() {
1169 let named = rgb_to_named(0, 0, 0);
1170 assert_eq!(named, NamedColor::Black);
1171 }
1172
1173 #[test]
1174 fn rgb_to_named_pure_white() {
1175 let named = rgb_to_named(255, 255, 255);
1176 assert_eq!(named, NamedColor::BrightWhite);
1177 }
1178
1179 #[test]
1182 fn batch_changes_empty() {
1183 let batches = batch_changes(&[]);
1184 assert!(batches.is_empty());
1185 }
1186
1187 #[test]
1188 fn batch_changes_single_cell() {
1189 let changes = vec![CellChange {
1190 x: 3,
1191 y: 1,
1192 cell: Cell::new("A", Style::default()),
1193 }];
1194 let batches = batch_changes(&changes);
1195 assert_eq!(batches.len(), 1);
1196 assert_eq!(batches[0].x, 3);
1197 assert_eq!(batches[0].y, 1);
1198 assert_eq!(batches[0].cells.len(), 1);
1199 assert_eq!(batches[0].cells[0].grapheme, "A");
1200 }
1201
1202 #[test]
1203 fn batch_changes_consecutive_same_row() {
1204 let changes = vec![
1205 CellChange {
1206 x: 0,
1207 y: 0,
1208 cell: Cell::new("A", Style::default()),
1209 },
1210 CellChange {
1211 x: 1,
1212 y: 0,
1213 cell: Cell::new("B", Style::default()),
1214 },
1215 CellChange {
1216 x: 2,
1217 y: 0,
1218 cell: Cell::new("C", Style::default()),
1219 },
1220 ];
1221 let batches = batch_changes(&changes);
1222 assert_eq!(batches.len(), 1);
1223 assert_eq!(batches[0].cells.len(), 3);
1224 assert_eq!(batches[0].cells[0].grapheme, "A");
1225 assert_eq!(batches[0].cells[1].grapheme, "B");
1226 assert_eq!(batches[0].cells[2].grapheme, "C");
1227 }
1228
1229 #[test]
1230 fn batch_changes_different_rows() {
1231 let changes = vec![
1232 CellChange {
1233 x: 0,
1234 y: 0,
1235 cell: Cell::new("A", Style::default()),
1236 },
1237 CellChange {
1238 x: 0,
1239 y: 1,
1240 cell: Cell::new("B", Style::default()),
1241 },
1242 ];
1243 let batches = batch_changes(&changes);
1244 assert_eq!(batches.len(), 2);
1245 assert_eq!(batches[0].y, 0);
1246 assert_eq!(batches[1].y, 1);
1247 }
1248
1249 #[test]
1250 fn batch_changes_gap_in_column() {
1251 let changes = vec![
1252 CellChange {
1253 x: 0,
1254 y: 0,
1255 cell: Cell::new("A", Style::default()),
1256 },
1257 CellChange {
1258 x: 5,
1259 y: 0,
1260 cell: Cell::new("B", Style::default()),
1261 },
1262 ];
1263 let batches = batch_changes(&changes);
1264 assert_eq!(batches.len(), 2);
1265 assert_eq!(batches[0].x, 0);
1266 assert_eq!(batches[1].x, 5);
1267 }
1268
1269 #[test]
1270 fn batch_changes_skips_continuation_cells() {
1271 let changes = vec![
1272 CellChange {
1273 x: 0,
1274 y: 0,
1275 cell: Cell::new("\u{4e16}", Style::default()), },
1277 CellChange {
1278 x: 1,
1279 y: 0,
1280 cell: Cell::continuation(), },
1282 CellChange {
1283 x: 2,
1284 y: 0,
1285 cell: Cell::new("A", Style::default()),
1286 },
1287 ];
1288 let batches = batch_changes(&changes);
1289 assert_eq!(batches.len(), 1);
1292 assert_eq!(batches[0].cells.len(), 2);
1293 assert_eq!(batches[0].cells[0].grapheme, "\u{4e16}");
1294 assert_eq!(batches[0].cells[1].grapheme, "A");
1295 }
1296
1297 #[test]
1298 fn batch_changes_wide_characters() {
1299 let changes = vec![
1300 CellChange {
1301 x: 0,
1302 y: 0,
1303 cell: Cell::new("\u{4e16}", Style::default()), },
1305 CellChange {
1306 x: 1,
1307 y: 0,
1308 cell: Cell::continuation(),
1309 },
1310 CellChange {
1311 x: 2,
1312 y: 0,
1313 cell: Cell::new("\u{754c}", Style::default()), },
1315 ];
1316 let batches = batch_changes(&changes);
1317 assert_eq!(batches.len(), 1);
1318 assert_eq!(batches[0].cells.len(), 2);
1319 }
1320
1321 #[test]
1322 fn render_batched_empty() {
1323 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1324 let output = renderer.render_batched(&[]);
1325 assert!(output.is_empty());
1326 }
1327
1328 #[test]
1329 fn render_batched_produces_valid_output() {
1330 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1331 let changes = vec![
1332 CellChange {
1333 x: 0,
1334 y: 0,
1335 cell: Cell::new("A", Style::default()),
1336 },
1337 CellChange {
1338 x: 1,
1339 y: 0,
1340 cell: Cell::new("B", Style::default()),
1341 },
1342 ];
1343 let output = renderer.render_batched(&changes);
1344 assert!(output.contains('A'));
1345 assert!(output.contains('B'));
1346 assert!(output.contains("\x1b[1;1H"));
1348 }
1349
1350 #[test]
1351 fn render_batched_no_longer_than_render() {
1352 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1353 let style = Style::new().fg(Color::Named(NamedColor::Red));
1354 let changes = vec![
1355 CellChange {
1356 x: 0,
1357 y: 0,
1358 cell: Cell::new("A", style.clone()),
1359 },
1360 CellChange {
1361 x: 1,
1362 y: 0,
1363 cell: Cell::new("B", style.clone()),
1364 },
1365 CellChange {
1366 x: 2,
1367 y: 0,
1368 cell: Cell::new("C", style),
1369 },
1370 ];
1371 let normal_output = renderer.render(&changes);
1372 let batched_output = renderer.render_batched(&changes);
1373 assert!(batched_output.len() <= normal_output.len());
1375 }
1376
1377 #[test]
1378 fn render_batched_with_styles() {
1379 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1380 let style = Style::new().bold(true).fg(Color::Named(NamedColor::Blue));
1381 let changes = vec![CellChange {
1382 x: 0,
1383 y: 0,
1384 cell: Cell::new("X", style),
1385 }];
1386 let output = renderer.render_batched(&changes);
1387 assert!(output.contains("\x1b[1m")); assert!(output.contains("\x1b[34m")); assert!(output.contains('X'));
1390 assert!(output.ends_with("\x1b[0m")); }
1392
1393 #[test]
1396 fn render_optimized_starts_with_cursor_hide() {
1397 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1398 let changes = vec![CellChange {
1399 x: 0,
1400 y: 0,
1401 cell: Cell::new("A", Style::default()),
1402 }];
1403 let output = renderer.render_optimized(&changes);
1404 assert!(output.starts_with("\x1b[?25l"));
1405 }
1406
1407 #[test]
1408 fn render_optimized_ends_with_cursor_show() {
1409 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1410 let changes = vec![CellChange {
1411 x: 0,
1412 y: 0,
1413 cell: Cell::new("A", Style::default()),
1414 }];
1415 let output = renderer.render_optimized(&changes);
1416 assert!(output.ends_with("\x1b[?25h"));
1417 }
1418
1419 #[test]
1420 fn render_optimized_sync_before_cursor_show() {
1421 let renderer = Renderer::new(ColorSupport::TrueColor, true);
1422 let changes = vec![CellChange {
1423 x: 0,
1424 y: 0,
1425 cell: Cell::new("A", Style::default()),
1426 }];
1427 let output = renderer.render_optimized(&changes);
1428 assert!(output.starts_with("\x1b[?25l\x1b[?2026h"));
1430 assert!(output.ends_with("\x1b[?2026l\x1b[?25h"));
1431 }
1432
1433 #[test]
1434 fn build_sgr_combined_bold_italic_red() {
1435 let style = Style::new()
1436 .bold(true)
1437 .italic(true)
1438 .fg(Color::Named(NamedColor::Red));
1439 let sgr = build_sgr_sequence(&style, ColorSupport::TrueColor);
1440 assert!(sgr.starts_with("\x1b["));
1442 assert!(sgr.ends_with('m'));
1443 assert!(sgr.contains("1;"));
1445 assert!(sgr.contains(";3;"));
1446 assert!(sgr.contains("31"));
1447 let esc_count = sgr.matches("\x1b[").count();
1449 assert_eq!(esc_count, 1);
1450 }
1451
1452 #[test]
1453 fn build_sgr_default_style_is_empty() {
1454 let style = Style::default();
1455 let sgr = build_sgr_sequence(&style, ColorSupport::TrueColor);
1456 assert!(sgr.is_empty());
1457 }
1458
1459 #[test]
1460 fn render_optimized_contains_correct_content() {
1461 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1462 let style = Style::new().bold(true).fg(Color::Named(NamedColor::Green));
1463 let changes = vec![CellChange {
1464 x: 0,
1465 y: 0,
1466 cell: Cell::new("Z", style),
1467 }];
1468 let output = renderer.render_optimized(&changes);
1469 assert!(output.contains('Z'));
1471 assert!(output.contains("1;"));
1473 assert!(output.contains("32"));
1474 assert!(output.contains("\x1b[0m"));
1476 }
1477
1478 #[test]
1479 fn render_optimized_empty_is_empty() {
1480 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1481 let output = renderer.render_optimized(&[]);
1482 assert!(output.is_empty());
1483 }
1484
1485 #[test]
1486 fn build_sgr_truecolor_rgb() {
1487 let style = Style::new().fg(Color::Rgb {
1488 r: 100,
1489 g: 200,
1490 b: 50,
1491 });
1492 let sgr = build_sgr_sequence(&style, ColorSupport::TrueColor);
1493 assert_eq!(sgr, "\x1b[38;2;100;200;50m");
1494 }
1495
1496 #[test]
1497 fn build_sgr_fg_and_bg() {
1498 let style = Style::new()
1499 .fg(Color::Named(NamedColor::Red))
1500 .bg(Color::Named(NamedColor::Blue));
1501 let sgr = build_sgr_sequence(&style, ColorSupport::TrueColor);
1502 let esc_count = sgr.matches("\x1b[").count();
1504 assert_eq!(esc_count, 1);
1505 assert!(sgr.contains("31")); assert!(sgr.contains("44")); }
1508
1509 #[test]
1512 fn color_mapper_caches_256_mappings() {
1513 let mut mapper = ColorMapper::new();
1514 let idx1 = mapper.map_to_256(255, 0, 0);
1515 let idx2 = mapper.map_to_256(255, 0, 0);
1516 assert_eq!(idx1, idx2);
1517 assert_eq!(mapper.cache_256.len(), 1);
1519 }
1520
1521 #[test]
1522 fn color_mapper_caches_16_mappings() {
1523 let mut mapper = ColorMapper::new();
1524 let name1 = mapper.map_to_16(255, 0, 0);
1525 let name2 = mapper.map_to_16(255, 0, 0);
1526 assert_eq!(name1, name2);
1527 assert_eq!(name1, NamedColor::BrightRed);
1528 assert_eq!(mapper.cache_16.len(), 1);
1530 }
1531
1532 #[test]
1533 fn color_mapper_clear_cache() {
1534 let mut mapper = ColorMapper::new();
1535 mapper.map_to_256(255, 0, 0);
1536 mapper.map_to_16(255, 0, 0);
1537 assert_eq!(mapper.cache_256.len(), 1);
1538 assert_eq!(mapper.cache_16.len(), 1);
1539 mapper.clear_cache();
1540 assert_eq!(mapper.cache_256.len(), 0);
1541 assert_eq!(mapper.cache_16.len(), 0);
1542 }
1543
1544 #[test]
1545 fn rgb_to_lab_black() {
1546 let lab = rgb_to_lab(0, 0, 0);
1547 assert!(lab.l < 1.0);
1549 }
1550
1551 #[test]
1552 fn rgb_to_lab_white() {
1553 let lab = rgb_to_lab(255, 255, 255);
1554 assert!(lab.l > 99.0);
1556 }
1557
1558 #[test]
1559 fn lab_distance_same_color() {
1560 let lab1 = rgb_to_lab(128, 128, 128);
1561 let lab2 = rgb_to_lab(128, 128, 128);
1562 let dist = lab_distance(lab1, lab2);
1563 assert!(dist < 0.001);
1564 }
1565
1566 #[test]
1567 fn lab_distance_different_colors() {
1568 let lab1 = rgb_to_lab(255, 0, 0); let lab2 = rgb_to_lab(0, 0, 255); let dist = lab_distance(lab1, lab2);
1571 assert!(dist > 100.0);
1573 }
1574
1575 #[test]
1576 fn rgb_to_16_pure_colors() {
1577 assert_eq!(rgb_to_16(255, 0, 0), NamedColor::BrightRed);
1578 assert_eq!(rgb_to_16(0, 255, 0), NamedColor::BrightGreen);
1579 assert_eq!(rgb_to_16(0, 0, 255), NamedColor::BrightBlue);
1580 assert_eq!(rgb_to_16(255, 255, 0), NamedColor::BrightYellow);
1581 assert_eq!(rgb_to_16(255, 0, 255), NamedColor::BrightMagenta);
1582 assert_eq!(rgb_to_16(0, 255, 255), NamedColor::BrightCyan);
1583 assert_eq!(rgb_to_16(0, 0, 0), NamedColor::Black);
1584 assert_eq!(rgb_to_16(255, 255, 255), NamedColor::BrightWhite);
1585 }
1586
1587 #[test]
1588 fn rgb_to_16_dark_colors() {
1589 assert_eq!(rgb_to_16(128, 0, 0), NamedColor::Red);
1590 assert_eq!(rgb_to_16(0, 128, 0), NamedColor::Green);
1591 assert_eq!(rgb_to_16(0, 0, 128), NamedColor::Blue);
1592 }
1593
1594 #[test]
1595 fn rgb_to_256_with_lab_pure_red() {
1596 let idx = rgb_to_256(255, 0, 0);
1597 assert!((9..=231).contains(&idx)); }
1601
1602 #[test]
1603 fn rgb_to_256_with_lab_grayscale() {
1604 let idx = rgb_to_256(128, 128, 128);
1605 assert!(idx >= 8); }
1608
1609 #[test]
1610 fn rgb_to_256_with_lab_pure_black() {
1611 let idx = rgb_to_256(0, 0, 0);
1612 assert!(idx <= 16);
1614 }
1615
1616 #[test]
1617 fn rgb_to_256_with_lab_pure_white() {
1618 let idx = rgb_to_256(255, 255, 255);
1619 assert!(idx == 15 || idx >= 231);
1621 }
1622
1623 #[test]
1624 fn no_color_environment_variable() {
1625 unsafe {
1627 std::env::set_var("NO_COLOR", "1");
1628 }
1629
1630 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1631 let style = Style::new().fg(Color::Rgb { r: 255, g: 0, b: 0 });
1632 let changes = vec![CellChange {
1633 x: 0,
1634 y: 0,
1635 cell: Cell::new("X", style),
1636 }];
1637 let output = renderer.render(&changes);
1638
1639 assert!(output.contains("\x1b[39m")); assert!(!output.contains("\x1b[38;2;")); unsafe {
1645 std::env::remove_var("NO_COLOR");
1646 }
1647 }
1648
1649 #[test]
1650 fn no_color_strips_all_colors() {
1651 unsafe {
1653 std::env::set_var("NO_COLOR", "1");
1654 }
1655
1656 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1657 let style = Style::new()
1658 .fg(Color::Rgb { r: 255, g: 0, b: 0 })
1659 .bg(Color::Named(NamedColor::Blue));
1660 let changes = vec![CellChange {
1661 x: 0,
1662 y: 0,
1663 cell: Cell::new("X", style),
1664 }];
1665 let output = renderer.render(&changes);
1666
1667 assert!(output.contains("\x1b[39m")); assert!(output.contains("\x1b[49m")); unsafe {
1673 std::env::remove_var("NO_COLOR");
1674 }
1675 }
1676
1677 #[test]
1678 fn no_color_overrides_color_support() {
1679 unsafe {
1681 std::env::set_var("NO_COLOR", "1");
1682 }
1683
1684 let renderer = Renderer::new(ColorSupport::TrueColor, false);
1686 let style = Style::new().fg(Color::Rgb {
1687 r: 100,
1688 g: 200,
1689 b: 50,
1690 });
1691 let changes = vec![CellChange {
1692 x: 0,
1693 y: 0,
1694 cell: Cell::new("X", style),
1695 }];
1696 let output = renderer.render(&changes);
1697
1698 assert!(output.contains("\x1b[39m"));
1699 assert!(!output.contains("\x1b[38;2;"));
1700
1701 unsafe {
1703 std::env::remove_var("NO_COLOR");
1704 }
1705 }
1706
1707 #[test]
1708 fn downgrade_color_standalone_no_color() {
1709 unsafe {
1711 std::env::set_var("NO_COLOR", "1");
1712 }
1713
1714 let color = Color::Rgb { r: 255, g: 0, b: 0 };
1715 let result = downgrade_color_standalone(&color, ColorSupport::TrueColor);
1716 assert_eq!(result, Color::Reset);
1717
1718 unsafe {
1720 std::env::remove_var("NO_COLOR");
1721 }
1722 }
1723
1724 #[test]
1725 fn build_sgr_with_no_color() {
1726 unsafe {
1728 std::env::set_var("NO_COLOR", "1");
1729 }
1730
1731 let style = Style::new()
1732 .bold(true)
1733 .fg(Color::Rgb { r: 255, g: 0, b: 0 });
1734 let sgr = build_sgr_sequence(&style, ColorSupport::TrueColor);
1735
1736 assert!(sgr.contains('1'));
1738 assert!(sgr.contains("39")); unsafe {
1742 std::env::remove_var("NO_COLOR");
1743 }
1744 }
1745
1746 #[test]
1747 fn rgb_to_16_perceptual_accuracy() {
1748 let c1 = rgb_to_16(255, 0, 0);
1751 let c2 = rgb_to_16(255, 10, 10);
1752 let c3 = rgb_to_16(250, 0, 5);
1753 assert_eq!(c1, NamedColor::BrightRed);
1755 assert_eq!(c2, NamedColor::BrightRed);
1756 assert_eq!(c3, NamedColor::BrightRed);
1757 }
1758
1759 #[test]
1760 fn rgb_to_256_perceptual_better_than_euclidean() {
1761 let idx = rgb_to_256(128, 255, 128);
1764 let is_greenish = if idx <= 15 {
1766 matches!(
1767 idx,
1768 2 | 10 )
1770 } else if (16..=231).contains(&idx) {
1771 let idx = idx - 16;
1773 let g_idx = (idx / 6) % 6;
1774 g_idx >= 4 } else {
1776 false };
1778 assert!(is_greenish, "idx={idx} should be greenish");
1779 }
1780}