prettytty/
err.rs

1//! Helper module with this crate's error type.
2//!
3//! Terminal errors complement I/O errors by providing additional information
4//! about error conditions when scanning or parsing terminal input. They
5//! seamlessly convert to and from I/O errors.
6
7use super::cmd::{Format, ResetStyle, SetForeground8};
8
9/// The enumeration of error kinds.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ErrorKind {
12    /// No data is available when reading, most likely due to a timeout.
13    NoData,
14    /// The state machine is in-flight and hence reading raw bytes is not safe.
15    InFlight,
16    /// A malformed UTF-8 character.
17    MalformedUtf8,
18    /// A malformed ANSI escape sequence.
19    MalformedSequence,
20    /// A pathological ANSI escape sequence is longer than a configurable threshold.
21    PathologicalSequence,
22    /// A well-formed ANSI escape sequence starting with the wrong control.
23    BadControl,
24    /// An unexpected but well-formed ANSI escape sequence.
25    BadSequence,
26    /// A token other than a sequence when a sequence is expected.
27    NotASequence,
28    /// An ANSI escape sequence longer than the available internal buffer space.
29    OutOfMemory,
30    /// Too few color components or coordinates.
31    TooFewCoordinates,
32    /// Too many color components or coordinates.
33    TooManyCoordinates,
34    /// Empty color coordinate.
35    EmptyCoordinate,
36    /// Oversized color coordinate.
37    OversizedCoordinate,
38    /// Malformed coordinate.
39    MalformedCoordinate,
40    /// An error reading from the reader providing data.
41    Unreadable,
42}
43
44impl ErrorKind {
45    /// Turn the error kind to an error message.
46    pub fn as_str(&self) -> &'static str {
47        match *self {
48            Self::NoData => "reading terminal input timed out without returning data",
49            Self::InFlight => "token is in-flight, hence access to raw bytes is not safe",
50            Self::MalformedUtf8 => "malformed UTF-8",
51            Self::MalformedSequence => "malformed ANSI escape sequence",
52            Self::PathologicalSequence => "pathologically long ANSI escape sequence",
53            Self::BadControl => "unexpected control for ANSI escape sequence",
54            Self::BadSequence => "unexpected ANSI escape sequence",
55            Self::NotASequence => "token not an ANSI escape sequence",
56            Self::OutOfMemory => "ANSI escape sequence too long for internal buffer",
57            Self::Unreadable => "error reading terminal",
58            Self::TooFewCoordinates => "too few color coordinates",
59            Self::TooManyCoordinates => "too many color coordinates",
60            Self::EmptyCoordinate => "empty color coordinate",
61            Self::OversizedCoordinate => "oversized color coordinate",
62            Self::MalformedCoordinate => "malformed color coordinate",
63        }
64    }
65}
66
67impl From<ErrorKind> for std::io::Error {
68    fn from(value: ErrorKind) -> Self {
69        Error::from(value).into()
70    }
71}
72
73impl From<ErrorKind> for Error {
74    fn from(kind: ErrorKind) -> Self {
75        Self { kind, source: None }
76    }
77}
78
79/// A terminal error.
80#[derive(Debug)]
81pub struct Error {
82    kind: ErrorKind,
83    source: Option<std::io::Error>,
84}
85
86impl Error {
87    /// Create a new unreadable error.
88    #[must_use = "the only reason to invoke method is to access the returned value"]
89    pub fn unreadable(source: std::io::Error) -> Self {
90        Self {
91            kind: ErrorKind::Unreadable,
92            source: Some(source),
93        }
94    }
95
96    /// Get the error kind.
97    pub fn kind(&self) -> ErrorKind {
98        self.kind
99    }
100}
101
102impl core::fmt::Display for Error {
103    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104        f.write_str(self.kind.as_str())
105    }
106}
107
108impl core::error::Error for Error {
109    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
110        if let &Self {
111            kind: ErrorKind::Unreadable,
112            source: Some(ref error),
113        } = self
114        {
115            Some(error)
116        } else {
117            None
118        }
119    }
120}
121
122impl From<std::io::Error> for Error {
123    fn from(value: std::io::Error) -> Self {
124        Self::unreadable(value)
125    }
126}
127
128impl From<Error> for std::io::Error {
129    fn from(value: Error) -> Self {
130        use self::ErrorKind::*;
131
132        match value.kind {
133            MalformedUtf8 | MalformedSequence | PathologicalSequence | BadControl | BadSequence
134            | NotASequence | TooFewCoordinates | TooManyCoordinates | EmptyCoordinate
135            | OversizedCoordinate | MalformedCoordinate => {
136                Self::new(std::io::ErrorKind::InvalidData, value)
137            }
138            NoData => std::io::ErrorKind::TimedOut.into(),
139            InFlight => std::io::ErrorKind::ResourceBusy.into(),
140            OutOfMemory => std::io::ErrorKind::OutOfMemory.into(),
141            Unreadable =>
142            {
143                #[allow(clippy::option_if_let_else)]
144                if let Some(error) = value.source {
145                    error
146                } else {
147                    Self::new(std::io::ErrorKind::Other, value)
148                }
149            }
150        }
151    }
152}
153
154/// Determine whether an operation should be retried.
155///
156/// This function treats both interrupted and timed out operations as retryable.
157pub fn should_retry<T, E>(result: core::result::Result<T, E>) -> bool
158where
159    E: Into<std::io::Error>,
160{
161    if let Err(err) = result {
162        let kind = err.into().kind();
163        kind == std::io::ErrorKind::Interrupted || kind == std::io::ErrorKind::TimedOut
164    } else {
165        false
166    }
167}
168
169/// Report the error, including any sources.
170#[allow(clippy::print_stdout)]
171pub fn report<E: core::error::Error>(error: &E) {
172    println!(
173        "{}{}ERROR: {}{}",
174        Format::Bold,
175        SetForeground8::<1>,
176        error,
177        ResetStyle
178    );
179
180    let mut error: &dyn core::error::Error = error;
181    while let Some(inner) = error.source() {
182        println!("    {}", inner);
183        error = inner;
184    }
185}