term_transcript/
utils.rs

1//! Misc utils.
2
3use std::{borrow::Cow, fmt::Write as WriteStr, io, str};
4
5#[cfg(any(feature = "svg", feature = "test"))]
6pub(crate) use self::rgb_color::IndexOrRgb;
7#[cfg(any(feature = "svg", feature = "test"))]
8pub use self::rgb_color::RgbColor;
9#[cfg(feature = "svg")]
10pub use self::rgb_color::RgbColorParseError;
11
12/// Adapter for `dyn fmt::Write` that implements `io::Write`.
13pub(crate) struct WriteAdapter<'a> {
14    inner: &'a mut dyn WriteStr,
15}
16
17impl<'a> WriteAdapter<'a> {
18    pub fn new(output: &'a mut dyn WriteStr) -> Self {
19        Self { inner: output }
20    }
21}
22
23impl io::Write for WriteAdapter<'_> {
24    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
25        let segment =
26            str::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
27        self.inner.write_str(segment).map_err(io::Error::other)?;
28        Ok(buf.len())
29    }
30
31    fn flush(&mut self) -> io::Result<()> {
32        Ok(())
33    }
34}
35
36pub(crate) fn normalize_newlines(s: &str) -> Cow<'_, str> {
37    if s.contains("\r\n") {
38        Cow::Owned(s.replace("\r\n", "\n"))
39    } else {
40        Cow::Borrowed(s)
41    }
42}
43
44#[cfg(not(windows))]
45pub(crate) fn is_recoverable_kill_error(err: &io::Error) -> bool {
46    matches!(err.kind(), io::ErrorKind::InvalidInput)
47}
48
49// As per `TerminateProcess` docs (`TerminateProcess` is used by `Child::kill()`),
50// the call will result in ERROR_ACCESS_DENIED if the process has already terminated.
51//
52// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess
53#[cfg(windows)]
54pub(crate) fn is_recoverable_kill_error(err: &io::Error) -> bool {
55    matches!(
56        err.kind(),
57        io::ErrorKind::InvalidInput | io::ErrorKind::PermissionDenied
58    )
59}
60
61#[cfg(any(feature = "svg", feature = "test"))]
62mod rgb_color {
63    use std::{error::Error as StdError, fmt, num::ParseIntError, str::FromStr};
64
65    /// RGB color with 8-bit channels.
66    ///
67    /// A color [can be parsed](FromStr) from a hex string like `#fed` or `#de382b`.
68    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69    pub struct RgbColor(pub u8, pub u8, pub u8);
70
71    impl fmt::LowerHex for RgbColor {
72        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73            write!(formatter, "#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
74        }
75    }
76
77    /// Errors that can occur when [parsing](FromStr) an [`RgbColor`] from a string.
78    #[derive(Debug)]
79    #[non_exhaustive]
80    pub enum RgbColorParseError {
81        /// Color string contains non-ASCII chars.
82        NotAscii,
83        /// The color does not have a `#` prefix.
84        NoHashPrefix,
85        /// The color has incorrect string length (not 1 or 2 chars per color channel).
86        /// The byte length of the string (including 1 char for the `#` prefix)
87        /// is provided within this variant.
88        IncorrectLen(usize),
89        /// Error parsing color channel value.
90        IncorrectDigit(ParseIntError),
91    }
92
93    impl fmt::Display for RgbColorParseError {
94        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95            match self {
96                Self::NotAscii => formatter.write_str("color string contains non-ASCII chars"),
97                Self::NoHashPrefix => formatter.write_str("missing '#' prefix"),
98                Self::IncorrectLen(len) => write!(
99                    formatter,
100                    "unexpected byte length {len} of color string, expected 4 or 7"
101                ),
102                Self::IncorrectDigit(err) => write!(formatter, "error parsing hex digit: {err}"),
103            }
104        }
105    }
106
107    impl StdError for RgbColorParseError {
108        fn source(&self) -> Option<&(dyn StdError + 'static)> {
109            match self {
110                Self::IncorrectDigit(err) => Some(err),
111                _ => None,
112            }
113        }
114    }
115
116    impl FromStr for RgbColor {
117        type Err = RgbColorParseError;
118
119        fn from_str(s: &str) -> Result<Self, Self::Err> {
120            if s.is_empty() || s.as_bytes()[0] != b'#' {
121                Err(RgbColorParseError::NoHashPrefix)
122            } else if s.len() == 4 {
123                if !s.is_ascii() {
124                    return Err(RgbColorParseError::NotAscii);
125                }
126
127                let r =
128                    u8::from_str_radix(&s[1..2], 16).map_err(RgbColorParseError::IncorrectDigit)?;
129                let g =
130                    u8::from_str_radix(&s[2..3], 16).map_err(RgbColorParseError::IncorrectDigit)?;
131                let b =
132                    u8::from_str_radix(&s[3..], 16).map_err(RgbColorParseError::IncorrectDigit)?;
133                Ok(Self(r * 17, g * 17, b * 17))
134            } else if s.len() == 7 {
135                if !s.is_ascii() {
136                    return Err(RgbColorParseError::NotAscii);
137                }
138
139                let r =
140                    u8::from_str_radix(&s[1..3], 16).map_err(RgbColorParseError::IncorrectDigit)?;
141                let g =
142                    u8::from_str_radix(&s[3..5], 16).map_err(RgbColorParseError::IncorrectDigit)?;
143                let b =
144                    u8::from_str_radix(&s[5..], 16).map_err(RgbColorParseError::IncorrectDigit)?;
145                Ok(Self(r, g, b))
146            } else {
147                Err(RgbColorParseError::IncorrectLen(s.len()))
148            }
149        }
150    }
151
152    #[derive(Debug, Clone, Copy, PartialEq)]
153    #[cfg_attr(feature = "svg", derive(serde::Serialize))]
154    #[cfg_attr(feature = "svg", serde(untagged))]
155    pub(crate) enum IndexOrRgb {
156        Index(u8),
157        Rgb(RgbColor),
158    }
159
160    impl IndexOrRgb {
161        #[cfg(feature = "svg")]
162        #[allow(clippy::match_wildcard_for_single_variants)]
163        // ^-- `Color` is an old-school non-exhaustive enum
164        pub(crate) fn new(color: termcolor::Color) -> std::io::Result<Self> {
165            use termcolor::Color;
166
167            Ok(match color {
168                Color::Black => Self::index(0),
169                Color::Red => Self::index(1),
170                Color::Green => Self::index(2),
171                Color::Yellow => Self::index(3),
172                Color::Blue => Self::index(4),
173                Color::Magenta => Self::index(5),
174                Color::Cyan => Self::index(6),
175                Color::White => Self::index(7),
176                Color::Ansi256(idx) => Self::indexed_color(idx),
177                Color::Rgb(r, g, b) => Self::Rgb(RgbColor(r, g, b)),
178                _ => return Err(std::io::Error::other("Unsupported color")),
179            })
180        }
181
182        fn index(value: u8) -> Self {
183            debug_assert!(value < 16);
184            Self::Index(value)
185        }
186
187        pub(crate) fn indexed_color(index: u8) -> Self {
188            match index {
189                0..=15 => Self::index(index),
190
191                16..=231 => {
192                    let index = index - 16;
193                    let r = Self::color_cube_color(index / 36);
194                    let g = Self::color_cube_color((index / 6) % 6);
195                    let b = Self::color_cube_color(index % 6);
196                    Self::Rgb(RgbColor(r, g, b))
197                }
198
199                _ => {
200                    let gray = 10 * (index - 232) + 8;
201                    Self::Rgb(RgbColor(gray, gray, gray))
202                }
203            }
204        }
205
206        fn color_cube_color(index: u8) -> u8 {
207            match index {
208                0 => 0,
209                1 => 0x5f,
210                2 => 0x87,
211                3 => 0xaf,
212                4 => 0xd7,
213                5 => 0xff,
214                _ => unreachable!(),
215            }
216        }
217    }
218}
219
220#[cfg(all(test, any(feature = "svg", feature = "test")))]
221mod tests {
222    use assert_matches::assert_matches;
223
224    use super::*;
225
226    #[test]
227    fn parsing_color() {
228        let RgbColor(r, g, b) = "#fed".parse().unwrap();
229        assert_eq!((r, g, b), (0xff, 0xee, 0xdd));
230        let RgbColor(r, g, b) = "#c0ffee".parse().unwrap();
231        assert_eq!((r, g, b), (0xc0, 0xff, 0xee));
232    }
233
234    #[test]
235    fn errors_parsing_color() {
236        let err = "123".parse::<RgbColor>().unwrap_err();
237        assert_matches!(err, RgbColorParseError::NoHashPrefix);
238        let err = "#12".parse::<RgbColor>().unwrap_err();
239        assert_matches!(err, RgbColorParseError::IncorrectLen(3));
240        let err = "#тэг".parse::<RgbColor>().unwrap_err();
241        assert_matches!(err, RgbColorParseError::NotAscii);
242        let err = "#coffee".parse::<RgbColor>().unwrap_err();
243        assert_matches!(err, RgbColorParseError::IncorrectDigit(_));
244    }
245}