grep_printer/
color.rs

1use termcolor::{Color, ColorSpec, ParseColorError};
2
3/// Returns a default set of color specifications.
4///
5/// This may change over time, but the color choices are meant to be fairly
6/// conservative that work across terminal themes.
7///
8/// Additional color specifications can be added to the list returned. More
9/// recently added specifications override previously added specifications.
10pub fn default_color_specs() -> Vec<UserColorSpec> {
11    vec![
12        #[cfg(unix)]
13        "path:fg:magenta".parse().unwrap(),
14        #[cfg(windows)]
15        "path:fg:cyan".parse().unwrap(),
16        "line:fg:green".parse().unwrap(),
17        "match:fg:red".parse().unwrap(),
18        "match:style:bold".parse().unwrap(),
19    ]
20}
21
22/// An error that can occur when parsing color specifications.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub enum ColorError {
25    /// This occurs when an unrecognized output type is used.
26    UnrecognizedOutType(String),
27    /// This occurs when an unrecognized spec type is used.
28    UnrecognizedSpecType(String),
29    /// This occurs when an unrecognized color name is used.
30    UnrecognizedColor(String, String),
31    /// This occurs when an unrecognized style attribute is used.
32    UnrecognizedStyle(String),
33    /// This occurs when the format of a color specification is invalid.
34    InvalidFormat(String),
35}
36
37impl std::error::Error for ColorError {}
38
39impl ColorError {
40    fn from_parse_error(err: ParseColorError) -> ColorError {
41        ColorError::UnrecognizedColor(
42            err.invalid().to_string(),
43            err.to_string(),
44        )
45    }
46}
47
48impl std::fmt::Display for ColorError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match *self {
51            ColorError::UnrecognizedOutType(ref name) => write!(
52                f,
53                "unrecognized output type '{}'. Choose from: \
54                 path, line, column, match, highlight.",
55                name,
56            ),
57            ColorError::UnrecognizedSpecType(ref name) => write!(
58                f,
59                "unrecognized spec type '{}'. Choose from: \
60                 fg, bg, style, none.",
61                name,
62            ),
63            ColorError::UnrecognizedColor(_, ref msg) => write!(f, "{}", msg),
64            ColorError::UnrecognizedStyle(ref name) => write!(
65                f,
66                "unrecognized style attribute '{}'. Choose from: \
67                 nobold, bold, nointense, intense, nounderline, \
68                 underline, noitalic, italic.",
69                name,
70            ),
71            ColorError::InvalidFormat(ref original) => write!(
72                f,
73                "invalid color spec format: '{}'. Valid format is \
74                 '(path|line|column|match|highlight):(fg|bg|style):(value)'.",
75                original,
76            ),
77        }
78    }
79}
80
81/// A merged set of color specifications.
82///
83/// This set of color specifications represents the various color types that
84/// are supported by the printers in this crate. A set of color specifications
85/// can be created from a sequence of
86/// [`UserColorSpec`]s.
87#[derive(Clone, Debug, Default, Eq, PartialEq)]
88pub struct ColorSpecs {
89    path: ColorSpec,
90    line: ColorSpec,
91    column: ColorSpec,
92    matched: ColorSpec,
93    highlight: ColorSpec,
94}
95
96/// A single color specification provided by the user.
97///
98/// ## Format
99///
100/// The format of a `Spec` is a triple: `{type}:{attribute}:{value}`. Each
101/// component is defined as follows:
102///
103/// * `{type}` can be one of `path`, `line`, `column`, `match` or `highlight`.
104/// * `{attribute}` can be one of `fg`, `bg` or `style`. `{attribute}` may also
105///   be the special value `none`, in which case, `{value}` can be omitted.
106/// * `{value}` is either a color name (for `fg`/`bg`) or a style instruction.
107///
108/// `{type}` controls which part of the output should be styled.
109///
110/// When `{attribute}` is `none`, then this should cause any existing style
111/// settings to be cleared for the specified `type`.
112///
113/// `{value}` should be a color when `{attribute}` is `fg` or `bg`, or it
114/// should be a style instruction when `{attribute}` is `style`. When
115/// `{attribute}` is `none`, `{value}` must be omitted.
116///
117/// Valid colors are `black`, `blue`, `green`, `red`, `cyan`, `magenta`,
118/// `yellow`, `white`. Extended colors can also be specified, and are formatted
119/// as `x` (for 256-bit colors) or `x,x,x` (for 24-bit true color), where
120/// `x` is a number between 0 and 255 inclusive. `x` may be given as a normal
121/// decimal number of a hexadecimal number, where the latter is prefixed by
122/// `0x`.
123///
124/// Valid style instructions are `nobold`, `bold`, `intense`, `nointense`,
125/// `underline`, `nounderline`, `italic`, `noitalic`.
126///
127/// ## Example
128///
129/// The standard way to build a `UserColorSpec` is to parse it from a string.
130/// Once multiple `UserColorSpec`s have been constructed, they can be provided
131/// to the standard printer where they will automatically be applied to the
132/// output.
133///
134/// A `UserColorSpec` can also be converted to a `termcolor::ColorSpec`:
135///
136/// ```rust
137/// # fn main() {
138/// use termcolor::{Color, ColorSpec};
139/// use grep_printer::UserColorSpec;
140///
141/// let user_spec1: UserColorSpec = "path:fg:blue".parse().unwrap();
142/// let user_spec2: UserColorSpec = "match:bg:0xff,0x7f,0x00".parse().unwrap();
143///
144/// let spec1 = user_spec1.to_color_spec();
145/// let spec2 = user_spec2.to_color_spec();
146///
147/// assert_eq!(spec1.fg(), Some(&Color::Blue));
148/// assert_eq!(spec2.bg(), Some(&Color::Rgb(0xFF, 0x7F, 0x00)));
149/// # }
150/// ```
151#[derive(Clone, Debug, Eq, PartialEq)]
152pub struct UserColorSpec {
153    ty: OutType,
154    value: SpecValue,
155}
156
157impl UserColorSpec {
158    /// Convert this user provided color specification to a specification that
159    /// can be used with `termcolor`. This drops the type of this specification
160    /// (where the type indicates where the color is applied in the standard
161    /// printer, e.g., to the file path or the line numbers, etc.).
162    pub fn to_color_spec(&self) -> ColorSpec {
163        let mut spec = ColorSpec::default();
164        self.value.merge_into(&mut spec);
165        spec
166    }
167}
168
169/// The actual value given by the specification.
170#[derive(Clone, Debug, Eq, PartialEq)]
171enum SpecValue {
172    None,
173    Fg(Color),
174    Bg(Color),
175    Style(Style),
176}
177
178/// The set of configurable portions of ripgrep's output.
179#[derive(Clone, Debug, Eq, PartialEq)]
180enum OutType {
181    Path,
182    Line,
183    Column,
184    Match,
185    Highlight,
186}
187
188/// The specification type.
189#[derive(Clone, Debug, Eq, PartialEq)]
190enum SpecType {
191    Fg,
192    Bg,
193    Style,
194    None,
195}
196
197/// The set of available styles for use in the terminal.
198#[derive(Clone, Debug, Eq, PartialEq)]
199enum Style {
200    Bold,
201    NoBold,
202    Intense,
203    NoIntense,
204    Underline,
205    NoUnderline,
206    Italic,
207    NoItalic,
208}
209
210impl ColorSpecs {
211    /// Create color specifications from a list of user supplied
212    /// specifications.
213    pub fn new(specs: &[UserColorSpec]) -> ColorSpecs {
214        let mut merged = ColorSpecs::default();
215        for spec in specs {
216            match spec.ty {
217                OutType::Path => spec.merge_into(&mut merged.path),
218                OutType::Line => spec.merge_into(&mut merged.line),
219                OutType::Column => spec.merge_into(&mut merged.column),
220                OutType::Match => spec.merge_into(&mut merged.matched),
221                OutType::Highlight => spec.merge_into(&mut merged.highlight),
222            }
223        }
224        merged
225    }
226
227    /// Create a default set of specifications that have color.
228    ///
229    /// This is distinct from `ColorSpecs`'s `Default` implementation in that
230    /// this provides a set of default color choices, where as the `Default`
231    /// implementation provides no color choices.
232    pub fn default_with_color() -> ColorSpecs {
233        ColorSpecs::new(&default_color_specs())
234    }
235
236    /// Return the color specification for coloring file paths.
237    pub fn path(&self) -> &ColorSpec {
238        &self.path
239    }
240
241    /// Return the color specification for coloring line numbers.
242    pub fn line(&self) -> &ColorSpec {
243        &self.line
244    }
245
246    /// Return the color specification for coloring column numbers.
247    pub fn column(&self) -> &ColorSpec {
248        &self.column
249    }
250
251    /// Return the color specification for coloring matched text.
252    pub fn matched(&self) -> &ColorSpec {
253        &self.matched
254    }
255
256    /// Return the color specification for coloring entire line if there is a
257    /// matched text.
258    pub fn highlight(&self) -> &ColorSpec {
259        &self.highlight
260    }
261}
262
263impl UserColorSpec {
264    /// Merge this spec into the given color specification.
265    fn merge_into(&self, cspec: &mut ColorSpec) {
266        self.value.merge_into(cspec);
267    }
268}
269
270impl SpecValue {
271    /// Merge this spec value into the given color specification.
272    fn merge_into(&self, cspec: &mut ColorSpec) {
273        match *self {
274            SpecValue::None => cspec.clear(),
275            SpecValue::Fg(ref color) => {
276                cspec.set_fg(Some(color.clone()));
277            }
278            SpecValue::Bg(ref color) => {
279                cspec.set_bg(Some(color.clone()));
280            }
281            SpecValue::Style(ref style) => match *style {
282                Style::Bold => {
283                    cspec.set_bold(true);
284                }
285                Style::NoBold => {
286                    cspec.set_bold(false);
287                }
288                Style::Intense => {
289                    cspec.set_intense(true);
290                }
291                Style::NoIntense => {
292                    cspec.set_intense(false);
293                }
294                Style::Underline => {
295                    cspec.set_underline(true);
296                }
297                Style::NoUnderline => {
298                    cspec.set_underline(false);
299                }
300                Style::Italic => {
301                    cspec.set_italic(true);
302                }
303                Style::NoItalic => {
304                    cspec.set_italic(false);
305                }
306            },
307        }
308    }
309}
310
311impl std::str::FromStr for UserColorSpec {
312    type Err = ColorError;
313
314    fn from_str(s: &str) -> Result<UserColorSpec, ColorError> {
315        let pieces: Vec<&str> = s.split(':').collect();
316        if pieces.len() <= 1 || pieces.len() > 3 {
317            return Err(ColorError::InvalidFormat(s.to_string()));
318        }
319        let otype: OutType = pieces[0].parse()?;
320        match pieces[1].parse()? {
321            SpecType::None => {
322                Ok(UserColorSpec { ty: otype, value: SpecValue::None })
323            }
324            SpecType::Style => {
325                if pieces.len() < 3 {
326                    return Err(ColorError::InvalidFormat(s.to_string()));
327                }
328                let style: Style = pieces[2].parse()?;
329                Ok(UserColorSpec { ty: otype, value: SpecValue::Style(style) })
330            }
331            SpecType::Fg => {
332                if pieces.len() < 3 {
333                    return Err(ColorError::InvalidFormat(s.to_string()));
334                }
335                let color: Color =
336                    pieces[2].parse().map_err(ColorError::from_parse_error)?;
337                Ok(UserColorSpec { ty: otype, value: SpecValue::Fg(color) })
338            }
339            SpecType::Bg => {
340                if pieces.len() < 3 {
341                    return Err(ColorError::InvalidFormat(s.to_string()));
342                }
343                let color: Color =
344                    pieces[2].parse().map_err(ColorError::from_parse_error)?;
345                Ok(UserColorSpec { ty: otype, value: SpecValue::Bg(color) })
346            }
347        }
348    }
349}
350
351impl std::str::FromStr for OutType {
352    type Err = ColorError;
353
354    fn from_str(s: &str) -> Result<OutType, ColorError> {
355        match &*s.to_lowercase() {
356            "path" => Ok(OutType::Path),
357            "line" => Ok(OutType::Line),
358            "column" => Ok(OutType::Column),
359            "match" => Ok(OutType::Match),
360            "highlight" => Ok(OutType::Highlight),
361            _ => Err(ColorError::UnrecognizedOutType(s.to_string())),
362        }
363    }
364}
365
366impl std::str::FromStr for SpecType {
367    type Err = ColorError;
368
369    fn from_str(s: &str) -> Result<SpecType, ColorError> {
370        match &*s.to_lowercase() {
371            "fg" => Ok(SpecType::Fg),
372            "bg" => Ok(SpecType::Bg),
373            "style" => Ok(SpecType::Style),
374            "none" => Ok(SpecType::None),
375            _ => Err(ColorError::UnrecognizedSpecType(s.to_string())),
376        }
377    }
378}
379
380impl std::str::FromStr for Style {
381    type Err = ColorError;
382
383    fn from_str(s: &str) -> Result<Style, ColorError> {
384        match &*s.to_lowercase() {
385            "bold" => Ok(Style::Bold),
386            "nobold" => Ok(Style::NoBold),
387            "intense" => Ok(Style::Intense),
388            "nointense" => Ok(Style::NoIntense),
389            "underline" => Ok(Style::Underline),
390            "nounderline" => Ok(Style::NoUnderline),
391            "italic" => Ok(Style::Italic),
392            "noitalic" => Ok(Style::NoItalic),
393            _ => Err(ColorError::UnrecognizedStyle(s.to_string())),
394        }
395    }
396}