Skip to main content

tectonic/status/
termcolor.rs

1// src/status/termcolor.rs -- 'termcolor' based status backend
2// Copyright 2017 the Tectonic Project
3// Licensed under the MIT License.
4
5// TODO: make this module a feature that can be disabled if the user doesn't want to
6//! Status backend that emits colorized errors to the terminal.
7
8use std::fmt::Arguments;
9use std::io::Write;
10use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
11
12use tectonic_errors::Error;
13
14use super::{ChatterLevel, MessageKind, StatusBackend};
15
16/// Status backend based on `termcolor` that emits compile errors and note with terminal colors.
17pub struct TermcolorStatusBackend {
18    chatter: ChatterLevel,
19    always_stderr: bool,
20    stdout: StandardStream,
21    stderr: StandardStream,
22    note_spec: ColorSpec,
23    highlight_spec: ColorSpec,
24    warning_spec: ColorSpec,
25    error_spec: ColorSpec,
26}
27
28impl TermcolorStatusBackend {
29    /// Create a new instance of this backend with default colorization.
30    pub fn new(chatter: ChatterLevel) -> TermcolorStatusBackend {
31        let mut note_spec = ColorSpec::new();
32        note_spec.set_fg(Some(Color::Green)).set_bold(true);
33
34        let mut highlight_spec = ColorSpec::new();
35        highlight_spec.set_bold(true);
36
37        let mut warning_spec = ColorSpec::new();
38        warning_spec.set_fg(Some(Color::Yellow)).set_bold(true);
39
40        let mut error_spec = ColorSpec::new();
41        error_spec.set_fg(Some(Color::Red)).set_bold(true);
42
43        TermcolorStatusBackend {
44            chatter,
45            always_stderr: false,
46            stdout: StandardStream::stdout(ColorChoice::Auto),
47            stderr: StandardStream::stderr(ColorChoice::Auto),
48            note_spec,
49            highlight_spec,
50            warning_spec,
51            error_spec,
52        }
53    }
54
55    /// Set whether non-error messages such as notes should be sent to stderr or stdout.
56    pub fn always_stderr(&mut self, setting: bool) -> &mut Self {
57        self.always_stderr = setting;
58        self
59    }
60
61    fn styled<F>(&mut self, kind: MessageKind, f: F)
62    where
63        F: FnOnce(&mut StandardStream),
64    {
65        if kind == MessageKind::Note && self.chatter <= ChatterLevel::Minimal {
66            return;
67        }
68
69        let (spec, stream) = match kind {
70            MessageKind::Note => {
71                if self.always_stderr {
72                    (&self.note_spec, &mut self.stderr)
73                } else {
74                    (&self.note_spec, &mut self.stdout)
75                }
76            }
77            MessageKind::Warning => (&self.warning_spec, &mut self.stderr),
78            MessageKind::Error => (&self.error_spec, &mut self.stderr),
79        };
80
81        stream.set_color(spec).expect("failed to set color");
82        f(stream);
83        stream.reset().expect("failed to clear color");
84    }
85
86    fn with_stream<F>(&mut self, kind: MessageKind, f: F)
87    where
88        F: FnOnce(&mut StandardStream),
89    {
90        if kind == MessageKind::Note && self.chatter <= ChatterLevel::Minimal {
91            return;
92        }
93
94        let stream = match kind {
95            MessageKind::Note => {
96                if self.always_stderr {
97                    &mut self.stderr
98                } else {
99                    &mut self.stdout
100                }
101            }
102            MessageKind::Warning => &mut self.stderr,
103            MessageKind::Error => &mut self.stderr,
104        };
105
106        f(stream);
107    }
108
109    fn generic_message(&mut self, kind: MessageKind, prefix: Option<&str>, args: Arguments) {
110        let text = match prefix {
111            Some(s) => s,
112            None => match kind {
113                MessageKind::Note => "note:",
114                MessageKind::Warning => "warning:",
115                MessageKind::Error => "error:",
116            },
117        };
118
119        self.styled(kind, |s| {
120            write!(s, "{text}").expect("failed to write to standard stream");
121        });
122        self.with_stream(kind, |s| {
123            writeln!(s, " {args}").expect("failed to write to standard stream");
124        });
125    }
126
127    // Helpers for the CLI program that aren't needed by the internal bits,
128    // so we put them here to minimize the cross-section of the StatusBackend
129    // trait.
130
131    /// Write the result of `fmt_args!` as a colorized note.
132    pub fn note_styled(&mut self, args: Arguments) {
133        if self.chatter > ChatterLevel::Minimal {
134            if self.always_stderr {
135                writeln!(self.stderr, "{args}").expect("write to stderr failed");
136            } else {
137                writeln!(self.stdout, "{args}").expect("write to stdout failed");
138            }
139        }
140    }
141
142    /// Write the results of `fmt_args!` as a colorized error.
143    pub fn error_styled(&mut self, args: Arguments) {
144        self.styled(MessageKind::Error, |s| {
145            writeln!(s, "{args}").expect("write to stderr failed");
146        });
147    }
148
149    /// Write an [`Error`] to output directly.
150    pub fn bare_error(&mut self, err: &Error) {
151        let mut prefix = "error:";
152
153        for item in err.chain() {
154            self.generic_message(MessageKind::Error, Some(prefix), format_args!("{item}"));
155            prefix = "caused by:";
156        }
157    }
158}
159
160/// Show formatted text to the user, styled as an error message.
161///
162/// On the console, this will normally cause the printed text to show up in
163/// bright red.
164#[macro_export]
165macro_rules! tt_error_styled {
166    ($dest:expr, $( $fmt_args:expr ),*) => {
167        $dest.error_styled(format_args!($( $fmt_args ),*))
168    };
169}
170
171impl StatusBackend for TermcolorStatusBackend {
172    fn report(&mut self, kind: MessageKind, args: Arguments, err: Option<&Error>) {
173        self.generic_message(kind, None, args);
174
175        if let Some(e) = err {
176            for item in e.chain() {
177                self.generic_message(kind, Some("caused by:"), format_args!("{item}"));
178            }
179        }
180    }
181
182    fn report_error(&mut self, err: &Error) {
183        let mut first = true;
184        let kind = MessageKind::Error;
185
186        for item in err.chain() {
187            if first {
188                self.generic_message(kind, None, format_args!("{item}"));
189                first = false;
190            } else {
191                self.generic_message(kind, Some("caused by:"), format_args!("{item}"));
192            }
193        }
194    }
195
196    fn note_highlighted(&mut self, before: &str, highlighted: &str, after: &str) {
197        if self.chatter > ChatterLevel::Minimal {
198            let stream = if self.always_stderr {
199                &mut self.stderr
200            } else {
201                &mut self.stdout
202            };
203
204            write!(stream, "{before}").expect("write failed");
205            stream
206                .set_color(&self.highlight_spec)
207                .expect("write failed");
208            write!(stream, "{highlighted}").expect("write failed");
209            stream.reset().expect("write failed");
210            writeln!(stream, "{after}").expect("write failed");
211        }
212    }
213
214    fn dump_error_logs(&mut self, output: &[u8]) {
215        tt_error_styled!(
216            self,
217            "==============================================================================="
218        );
219
220        self.stderr
221            .write_all(output)
222            .expect("write to stderr failed");
223
224        tt_error_styled!(
225            self,
226            "==============================================================================="
227        );
228    }
229}