prettytty/
err.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! Helper module with this crate's error type.
//!
//! Terminal errors complement I/O errors by providing additional information
//! about error conditions when scanning or parsing terminal input. They
//! seamlessly convert to and from I/O errors.

use super::cmd::{Format, ResetStyle, SetForeground8};

/// The enumeration of error kinds.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ErrorKind {
    /// No data is available when reading, most likely due to a timeout.
    NoData,
    /// The state machine is in-flight and hence reading raw bytes is not safe.
    InFlight,
    /// A malformed UTF-8 character.
    MalformedUtf8,
    /// A malformed ANSI escape sequence.
    MalformedSequence,
    /// A pathological ANSI escape sequence is longer than a configurable threshold.
    PathologicalSequence,
    /// A well-formed ANSI escape sequence starting with the wrong control.
    BadControl,
    /// An unexpected but well-formed ANSI escape sequence.
    BadSequence,
    /// A token other than a sequence when a sequence is expected.
    NotASequence,
    /// An ANSI escape sequence longer than the available internal buffer space.
    OutOfMemory,
    /// Too few color components or coordinates.
    TooFewCoordinates,
    /// Too many color components or coordinates.
    TooManyCoordinates,
    /// Empty color coordinate.
    EmptyCoordinate,
    /// Oversized color coordinate.
    OversizedCoordinate,
    /// Malformed coordinate.
    MalformedCoordinate,
    /// An error reading from the reader providing data.
    Unreadable,
}

impl ErrorKind {
    /// Turn the error kind to an error message.
    pub fn as_str(&self) -> &'static str {
        match *self {
            Self::NoData => "reading terminal input timed out without returning data",
            Self::InFlight => "token is in-flight, hence access to raw bytes is not safe",
            Self::MalformedUtf8 => "malformed UTF-8",
            Self::MalformedSequence => "malformed ANSI escape sequence",
            Self::PathologicalSequence => "pathologically long ANSI escape sequence",
            Self::BadControl => "unexpected control for ANSI escape sequence",
            Self::BadSequence => "unexpected ANSI escape sequence",
            Self::NotASequence => "token not an ANSI escape sequence",
            Self::OutOfMemory => "ANSI escape sequence too long for internal buffer",
            Self::Unreadable => "error reading terminal",
            Self::TooFewCoordinates => "too few color coordinates",
            Self::TooManyCoordinates => "too many color coordinates",
            Self::EmptyCoordinate => "empty color coordinate",
            Self::OversizedCoordinate => "oversized color coordinate",
            Self::MalformedCoordinate => "malformed color coordinate",
        }
    }
}

impl From<ErrorKind> for std::io::Error {
    fn from(value: ErrorKind) -> Self {
        Error::from(value).into()
    }
}

impl From<ErrorKind> for Error {
    fn from(kind: ErrorKind) -> Self {
        Self { kind, source: None }
    }
}

/// A terminal error.
#[derive(Debug)]
pub struct Error {
    kind: ErrorKind,
    source: Option<std::io::Error>,
}

impl Error {
    /// Create a new unreadable error.
    #[must_use = "the only reason to invoke method is to access the returned value"]
    pub fn unreadable(source: std::io::Error) -> Self {
        Self {
            kind: ErrorKind::Unreadable,
            source: Some(source),
        }
    }

    /// Get the error kind.
    pub fn kind(&self) -> ErrorKind {
        self.kind
    }
}

impl core::fmt::Display for Error {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.kind.as_str())
    }
}

impl core::error::Error for Error {
    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
        if let &Self {
            kind: ErrorKind::Unreadable,
            source: Some(ref error),
        } = self
        {
            Some(error)
        } else {
            None
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(value: std::io::Error) -> Self {
        Self::unreadable(value)
    }
}

impl From<Error> for std::io::Error {
    fn from(value: Error) -> Self {
        use self::ErrorKind::*;

        match value.kind {
            MalformedUtf8 | MalformedSequence | PathologicalSequence | BadControl | BadSequence
            | NotASequence | TooFewCoordinates | TooManyCoordinates | EmptyCoordinate
            | OversizedCoordinate | MalformedCoordinate => {
                Self::new(std::io::ErrorKind::InvalidData, value)
            }
            NoData => std::io::ErrorKind::TimedOut.into(),
            InFlight => std::io::ErrorKind::ResourceBusy.into(),
            OutOfMemory => std::io::ErrorKind::OutOfMemory.into(),
            Unreadable =>
            {
                #[allow(clippy::option_if_let_else)]
                if let Some(error) = value.source {
                    error
                } else {
                    Self::new(std::io::ErrorKind::Other, value)
                }
            }
        }
    }
}

/// Determine whether an operation should be retried.
///
/// This function treats both interrupted and timed out operations as retryable.
pub fn should_retry<T, E>(result: core::result::Result<T, E>) -> bool
where
    E: Into<std::io::Error>,
{
    if let Err(err) = result {
        let kind = err.into().kind();
        kind == std::io::ErrorKind::Interrupted || kind == std::io::ErrorKind::TimedOut
    } else {
        false
    }
}

/// Report the error, including any sources.
#[allow(clippy::print_stdout)]
pub fn report<E: core::error::Error>(error: &E) {
    println!(
        "{}{}ERROR: {}{}",
        Format::Bold,
        SetForeground8::<1>,
        error,
        ResetStyle
    );

    let mut error: &dyn core::error::Error = error;
    while let Some(inner) = error.source() {
        println!("    {}", inner);
        error = inner;
    }
}