bat/
pretty_printer.rs

1use std::io::Read;
2use std::path::Path;
3
4use console::Term;
5
6use crate::{
7    assets::HighlightingAssets,
8    config::{Config, VisibleLines},
9    controller::Controller,
10    error::Result,
11    input,
12    line_range::{HighlightedLineRanges, LineRange, LineRanges},
13    output::OutputHandle,
14    style::StyleComponent,
15    StripAnsiMode, SyntaxMapping, WrappingMode,
16};
17
18#[cfg(feature = "paging")]
19use crate::paging::PagingMode;
20
21#[derive(Default)]
22struct ActiveStyleComponents {
23    header_filename: bool,
24    #[cfg(feature = "git")]
25    vcs_modification_markers: bool,
26    grid: bool,
27    rule: bool,
28    line_numbers: bool,
29    snip: bool,
30}
31
32#[non_exhaustive]
33pub struct Syntax {
34    pub name: String,
35    pub file_extensions: Vec<String>,
36}
37
38pub struct PrettyPrinter<'a> {
39    inputs: Vec<Input<'a>>,
40    config: Config<'a>,
41    assets: HighlightingAssets,
42
43    highlighted_lines: Vec<LineRange>,
44    term_width: Option<usize>,
45    active_style_components: ActiveStyleComponents,
46}
47
48impl<'a> PrettyPrinter<'a> {
49    pub fn new() -> Self {
50        let config = Config {
51            colored_output: true,
52            true_color: true,
53            ..Default::default()
54        };
55
56        PrettyPrinter {
57            inputs: vec![],
58            config,
59            assets: HighlightingAssets::from_binary(),
60
61            highlighted_lines: vec![],
62            term_width: None,
63            active_style_components: ActiveStyleComponents::default(),
64        }
65    }
66
67    /// Add an input which should be pretty-printed
68    pub fn input(&mut self, input: Input<'a>) -> &mut Self {
69        self.inputs.push(input);
70        self
71    }
72
73    /// Adds multiple inputs which should be pretty-printed
74    pub fn inputs(&mut self, inputs: impl IntoIterator<Item = Input<'a>>) -> &mut Self {
75        for input in inputs {
76            self.inputs.push(input);
77        }
78        self
79    }
80
81    /// Add a file which should be pretty-printed
82    pub fn input_file(&mut self, path: impl AsRef<Path>) -> &mut Self {
83        self.input(Input::from_file(path).kind("File"))
84    }
85
86    /// Add multiple files which should be pretty-printed
87    pub fn input_files<I, P>(&mut self, paths: I) -> &mut Self
88    where
89        I: IntoIterator<Item = P>,
90        P: AsRef<Path>,
91    {
92        self.inputs(paths.into_iter().map(Input::from_file))
93    }
94
95    /// Add STDIN as an input
96    pub fn input_stdin(&mut self) -> &mut Self {
97        self.inputs.push(Input::from_stdin());
98        self
99    }
100
101    /// Add a byte string as an input
102    pub fn input_from_bytes(&mut self, content: &'a [u8]) -> &mut Self {
103        self.input_from_reader(content)
104    }
105
106    /// Add a custom reader as an input
107    pub fn input_from_reader<R: Read + 'a>(&mut self, reader: R) -> &mut Self {
108        self.inputs.push(Input::from_reader(reader));
109        self
110    }
111
112    /// Specify the syntax file which should be used (default: auto-detect)
113    pub fn language(&mut self, language: &'a str) -> &mut Self {
114        self.config.language = Some(language);
115        self
116    }
117
118    /// The character width of the terminal (default: autodetect)
119    pub fn term_width(&mut self, width: usize) -> &mut Self {
120        self.term_width = Some(width);
121        self
122    }
123
124    /// The width of tab characters (default: None - do not turn tabs to spaces)
125    pub fn tab_width(&mut self, tab_width: Option<usize>) -> &mut Self {
126        self.config.tab_width = tab_width.unwrap_or(0);
127        self
128    }
129
130    /// Whether or not the output should be colorized (default: true)
131    pub fn colored_output(&mut self, yes: bool) -> &mut Self {
132        self.config.colored_output = yes;
133        self
134    }
135
136    /// Whether or not to output 24bit colors (default: true)
137    pub fn true_color(&mut self, yes: bool) -> &mut Self {
138        self.config.true_color = yes;
139        self
140    }
141
142    /// Whether to show a header with the file name
143    pub fn header(&mut self, yes: bool) -> &mut Self {
144        self.active_style_components.header_filename = yes;
145        self
146    }
147
148    /// Whether to show line numbers
149    pub fn line_numbers(&mut self, yes: bool) -> &mut Self {
150        self.active_style_components.line_numbers = yes;
151        self
152    }
153
154    /// Whether to paint a grid, separating line numbers, git changes and the code
155    pub fn grid(&mut self, yes: bool) -> &mut Self {
156        self.active_style_components.grid = yes;
157        self
158    }
159
160    /// Whether to paint a horizontal rule to delimit files
161    pub fn rule(&mut self, yes: bool) -> &mut Self {
162        self.active_style_components.rule = yes;
163        self
164    }
165
166    /// Whether to show modification markers for VCS changes. This has no effect if
167    /// the `git` feature is not activated.
168    #[cfg(feature = "git")]
169    pub fn vcs_modification_markers(&mut self, yes: bool) -> &mut Self {
170        self.active_style_components.vcs_modification_markers = yes;
171        self
172    }
173
174    /// Whether to print binary content or nonprintable characters (default: no)
175    pub fn show_nonprintable(&mut self, yes: bool) -> &mut Self {
176        self.config.show_nonprintable = yes;
177        self
178    }
179
180    /// Whether to show "snip" markers between visible line ranges (default: no)
181    pub fn snip(&mut self, yes: bool) -> &mut Self {
182        self.active_style_components.snip = yes;
183        self
184    }
185
186    /// Whether to remove ANSI escape sequences from the input (default: never)
187    ///
188    /// If `Auto` is used, escape sequences will only be removed when the input
189    /// is not plain text.
190    pub fn strip_ansi(&mut self, mode: StripAnsiMode) -> &mut Self {
191        self.config.strip_ansi = mode;
192        self
193    }
194
195    /// Text wrapping mode (default: do not wrap)
196    pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self {
197        self.config.wrapping_mode = mode;
198        self
199    }
200
201    /// Whether or not to use ANSI italics (default: off)
202    pub fn use_italics(&mut self, yes: bool) -> &mut Self {
203        self.config.use_italic_text = yes;
204        self
205    }
206
207    /// If and how to use a pager (default: no paging)
208    #[cfg(feature = "paging")]
209    pub fn paging_mode(&mut self, mode: PagingMode) -> &mut Self {
210        self.config.paging_mode = mode;
211        self
212    }
213
214    /// Specify the command to start the pager (default: use "less")
215    #[cfg(feature = "paging")]
216    pub fn pager(&mut self, cmd: &'a str) -> &mut Self {
217        self.config.pager = Some(cmd);
218        self
219    }
220
221    /// Specify the lines that should be printed (default: all)
222    pub fn line_ranges(&mut self, ranges: LineRanges) -> &mut Self {
223        self.config.visible_lines = VisibleLines::Ranges(ranges);
224        self
225    }
226
227    /// Specify a line that should be highlighted (default: none).
228    /// This can be called multiple times to highlight more than one
229    /// line. See also: highlight_range.
230    pub fn highlight(&mut self, line: usize) -> &mut Self {
231        self.highlighted_lines.push(LineRange::new(line, line));
232        self
233    }
234
235    /// Specify a range of lines that should be highlighted (default: none).
236    /// This can be called multiple times to highlight more than one range
237    /// of lines.
238    pub fn highlight_range(&mut self, from: usize, to: usize) -> &mut Self {
239        self.highlighted_lines.push(LineRange::new(from, to));
240        self
241    }
242
243    /// Specify the maximum number of consecutive empty lines to print.
244    pub fn squeeze_empty_lines(&mut self, maximum: Option<usize>) -> &mut Self {
245        self.config.squeeze_lines = maximum;
246        self
247    }
248
249    /// Specify the highlighting theme.
250    /// You can use [`crate::theme::theme`] to pick a theme based on user preferences
251    /// and the terminal's background color.
252    pub fn theme(&mut self, theme: impl AsRef<str>) -> &mut Self {
253        self.config.theme = theme.as_ref().to_owned();
254        self
255    }
256
257    /// Specify custom file extension / file name to syntax mappings
258    pub fn syntax_mapping(&mut self, mapping: SyntaxMapping<'a>) -> &mut Self {
259        self.config.syntax_mapping = mapping;
260        self
261    }
262
263    pub fn themes(&self) -> impl Iterator<Item = &str> {
264        self.assets.themes()
265    }
266
267    pub fn syntaxes(&self) -> impl Iterator<Item = Syntax> + '_ {
268        // We always use assets from the binary, which are guaranteed to always
269        // be valid, so get_syntaxes() can never fail here
270        self.assets
271            .get_syntaxes()
272            .unwrap()
273            .iter()
274            .filter(|s| !s.hidden)
275            .map(|s| Syntax {
276                name: s.name.clone(),
277                file_extensions: s.file_extensions.clone(),
278            })
279    }
280
281    /// Pretty-print all specified inputs. This method will "use" all stored inputs.
282    /// If you want to call 'print' multiple times, you have to call the appropriate
283    /// input_* methods again.
284    pub fn print(&mut self) -> Result<bool> {
285        self.print_with_writer(None::<&mut dyn std::fmt::Write>)
286    }
287
288    /// Pretty-print all specified inputs to a specified writer.
289    pub fn print_with_writer<W: std::fmt::Write>(&mut self, writer: Option<W>) -> Result<bool> {
290        let highlight_lines = std::mem::take(&mut self.highlighted_lines);
291        self.config.highlighted_lines = HighlightedLineRanges(LineRanges::from(highlight_lines));
292        self.config.term_width = self
293            .term_width
294            .unwrap_or_else(|| Term::stdout().size().1 as usize);
295
296        self.config.style_components.clear();
297        if self.active_style_components.grid {
298            self.config.style_components.insert(StyleComponent::Grid);
299        }
300        if self.active_style_components.rule {
301            self.config.style_components.insert(StyleComponent::Rule);
302        }
303        if self.active_style_components.header_filename {
304            self.config
305                .style_components
306                .insert(StyleComponent::HeaderFilename);
307        }
308        if self.active_style_components.line_numbers {
309            self.config
310                .style_components
311                .insert(StyleComponent::LineNumbers);
312        }
313        if self.active_style_components.snip {
314            self.config.style_components.insert(StyleComponent::Snip);
315        }
316        #[cfg(feature = "git")]
317        if self.active_style_components.vcs_modification_markers {
318            self.config.style_components.insert(StyleComponent::Changes);
319        }
320
321        // Collect the inputs to print
322        let inputs = std::mem::take(&mut self.inputs);
323
324        // Run the controller
325        let controller = Controller::new(&self.config, &self.assets);
326
327        // If writer is provided, pass it to the controller, otherwise pass None
328        if let Some(mut w) = writer {
329            controller.run(
330                inputs.into_iter().map(|i| i.into()).collect(),
331                Some(&mut OutputHandle::FmtWrite(&mut w)),
332            )
333        } else {
334            controller.run(inputs.into_iter().map(|i| i.into()).collect(), None)
335        }
336    }
337}
338
339impl Default for PrettyPrinter<'_> {
340    fn default() -> Self {
341        Self::new()
342    }
343}
344
345/// An input source for the pretty printer.
346pub struct Input<'a> {
347    input: input::Input<'a>,
348}
349
350impl<'a> Input<'a> {
351    /// A new input from a reader.
352    pub fn from_reader<R: Read + 'a>(reader: R) -> Self {
353        input::Input::from_reader(Box::new(reader)).into()
354    }
355
356    /// A new input from a file.
357    pub fn from_file(path: impl AsRef<Path>) -> Self {
358        input::Input::ordinary_file(path).into()
359    }
360
361    /// A new input from bytes.
362    pub fn from_bytes(bytes: &'a [u8]) -> Self {
363        Input::from_reader(bytes)
364    }
365
366    /// A new input from STDIN.
367    pub fn from_stdin() -> Self {
368        input::Input::stdin().into()
369    }
370
371    /// The filename of the input.
372    /// This affects syntax detection and changes the default header title.
373    pub fn name(mut self, name: impl AsRef<Path>) -> Self {
374        self.input = self.input.with_name(Some(name));
375        self
376    }
377
378    /// The description for the type of input (e.g. "File")
379    pub fn kind(mut self, kind: impl Into<String>) -> Self {
380        let kind = kind.into();
381        self.input
382            .description_mut()
383            .set_kind(if kind.is_empty() { None } else { Some(kind) });
384        self
385    }
386
387    /// The title for the input (e.g. "Descriptive title")
388    /// This defaults to the file name.
389    pub fn title(mut self, title: impl Into<String>) -> Self {
390        self.input.description_mut().set_title(Some(title.into()));
391        self
392    }
393}
394
395impl<'a> From<input::Input<'a>> for Input<'a> {
396    fn from(input: input::Input<'a>) -> Self {
397        Self { input }
398    }
399}
400
401impl<'a> From<Input<'a>> for input::Input<'a> {
402    fn from(Input { input }: Input<'a>) -> Self {
403        input
404    }
405}