retroglyph_core/color/
sgr.rs1use super::{Color, Style};
14use alloc::string::String;
15use core::fmt::Write as _;
16
17pub fn push_sgr(out: &mut String, style: Style) {
24 let mut params = String::new();
25 if let Some(code) = sgr_color(style.foreground(), false) {
26 let _ = write!(params, "{code}");
27 }
28 if let Some(code) = sgr_color(style.background(), true) {
29 if !params.is_empty() {
30 params.push(';');
31 }
32 let _ = write!(params, "{code}");
33 }
34 if !params.is_empty() {
35 let _ = write!(out, "\x1b[{params}m");
36 }
37}
38
39#[must_use]
45pub fn sgr_color(color: Color, bg: bool) -> Option<String> {
46 match color {
47 Color::Default => None,
48 Color::Ansi(ansi) => {
49 let index = ansi.to_index();
50 let base = match (index < 8, bg) {
51 (true, false) => 30,
52 (true, true) => 40,
53 (false, false) => 90,
54 (false, true) => 100,
55 };
56 Some(alloc::format!("{}", base + index % 8))
57 }
58 Color::Indexed(index) => Some(alloc::format!("{};5;{index}", if bg { 48 } else { 38 })),
59 Color::Rgb { r, g, b } => {
60 Some(alloc::format!("{};2;{r};{g};{b}", if bg { 48 } else { 38 }))
61 }
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::color::AnsiColor;
69 use alloc::borrow::ToOwned as _;
70
71 #[test]
72 fn sgr_color_is_none_for_default() {
73 assert_eq!(sgr_color(Color::Default, false), None);
74 assert_eq!(sgr_color(Color::Default, true), None);
75 }
76
77 #[test]
78 fn sgr_color_standard_ansi_uses_30_40_base() {
79 assert_eq!(
80 sgr_color(Color::Ansi(AnsiColor::Red), false),
81 Some("31".to_owned())
82 );
83 assert_eq!(
84 sgr_color(Color::Ansi(AnsiColor::Red), true),
85 Some("41".to_owned())
86 );
87 }
88
89 #[test]
90 fn sgr_color_bright_ansi_uses_90_100_base() {
91 assert_eq!(
92 sgr_color(Color::Ansi(AnsiColor::BrightRed), false),
93 Some("91".to_owned())
94 );
95 assert_eq!(
96 sgr_color(Color::Ansi(AnsiColor::BrightRed), true),
97 Some("101".to_owned())
98 );
99 }
100
101 #[test]
102 fn sgr_color_indexed_and_rgb() {
103 assert_eq!(
104 sgr_color(Color::Indexed(200), false),
105 Some("38;5;200".to_owned())
106 );
107 assert_eq!(
108 sgr_color(Color::Indexed(200), true),
109 Some("48;5;200".to_owned())
110 );
111 assert_eq!(
112 sgr_color(Color::rgb(1, 2, 3), false),
113 Some("38;2;1;2;3".to_owned())
114 );
115 assert_eq!(
116 sgr_color(Color::rgb(1, 2, 3), true),
117 Some("48;2;1;2;3".to_owned())
118 );
119 }
120
121 #[test]
122 fn push_sgr_emits_nothing_for_a_fully_default_style() {
123 let mut out = String::new();
124 push_sgr(&mut out, Style::default());
125 assert_eq!(out, "");
126 }
127
128 #[test]
129 fn push_sgr_combines_fg_and_bg_into_one_escape() {
130 let mut out = String::new();
131 push_sgr(
132 &mut out,
133 Style::new()
134 .fg(Color::Ansi(AnsiColor::Red))
135 .bg(Color::Ansi(AnsiColor::Blue)),
136 );
137 assert_eq!(out, "\x1b[31;44m");
138 }
139
140 #[test]
141 fn push_sgr_emits_only_the_non_default_channel() {
142 let mut out = String::new();
143 push_sgr(&mut out, Style::new().bg(Color::Ansi(AnsiColor::Blue)));
144 assert_eq!(out, "\x1b[44m");
145 }
146}