bat_impl/
controller.rs

1use std::io::{self, BufRead, Write};
2
3use crate::assets::HighlightingAssets;
4use crate::config::{Config, VisibleLines};
5#[cfg(feature = "git")]
6use crate::diff::{get_git_diff, LineChanges};
7use crate::error::*;
8use crate::input::{Input, InputReader, OpenedInput};
9#[cfg(feature = "git")]
10use crate::line_range::LineRange;
11use crate::line_range::{LineRanges, RangeCheckResult};
12use crate::output::OutputType;
13#[cfg(feature = "paging")]
14use crate::paging::PagingMode;
15use crate::printer::{InteractivePrinter, Printer, SimplePrinter};
16
17use clircle::{Clircle, Identifier};
18
19pub struct Controller<'a> {
20    config: &'a Config<'a>,
21    assets: &'a HighlightingAssets,
22}
23
24impl<'b> Controller<'b> {
25    pub fn new<'a>(config: &'a Config, assets: &'a HighlightingAssets) -> Controller<'a> {
26        Controller { config, assets }
27    }
28
29    pub fn run(&self, inputs: Vec<Input>) -> Result<bool> {
30        self.run_with_error_handler(inputs, default_error_handler)
31    }
32
33    pub fn run_with_error_handler(
34        &self,
35        inputs: Vec<Input>,
36        handle_error: impl Fn(&Error, &mut dyn Write),
37    ) -> Result<bool> {
38        let mut output_type;
39
40        #[cfg(feature = "paging")]
41        {
42            use crate::input::InputKind;
43            use std::path::Path;
44
45            // Do not launch the pager if NONE of the input files exist
46            let mut paging_mode = self.config.paging_mode;
47            if self.config.paging_mode != PagingMode::Never {
48                let call_pager = inputs.iter().any(|input| {
49                    if let InputKind::OrdinaryFile(ref path) = input.kind {
50                        Path::new(path).exists()
51                    } else {
52                        true
53                    }
54                });
55                if !call_pager {
56                    paging_mode = PagingMode::Never;
57                }
58            }
59
60            let wrapping_mode = self.config.wrapping_mode;
61
62            output_type = OutputType::from_mode(paging_mode, wrapping_mode, self.config.pager)?;
63        }
64
65        #[cfg(not(feature = "paging"))]
66        {
67            output_type = OutputType::stdout();
68        }
69
70        let attached_to_pager = output_type.is_pager();
71        let stdout_identifier = if cfg!(windows) || attached_to_pager {
72            None
73        } else {
74            clircle::Identifier::stdout()
75        };
76
77        let writer = output_type.handle()?;
78        let mut no_errors: bool = true;
79        let stderr = io::stderr();
80
81        for (index, input) in inputs.into_iter().enumerate() {
82            let identifier = stdout_identifier.as_ref();
83            let is_first = index == 0;
84            let result = if input.is_stdin() {
85                self.print_input(input, writer, io::stdin().lock(), identifier, is_first)
86            } else {
87                // Use dummy stdin since stdin is actually not used (#1902)
88                self.print_input(input, writer, io::empty(), identifier, is_first)
89            };
90            if let Err(error) = result {
91                if attached_to_pager {
92                    handle_error(&error, writer);
93                } else {
94                    handle_error(&error, &mut stderr.lock());
95                }
96                no_errors = false;
97            }
98        }
99
100        Ok(no_errors)
101    }
102
103    fn print_input<R: BufRead>(
104        &self,
105        input: Input,
106        writer: &mut dyn Write,
107        stdin: R,
108        stdout_identifier: Option<&Identifier>,
109        is_first: bool,
110    ) -> Result<()> {
111        let mut opened_input = input.open(stdin, stdout_identifier)?;
112        #[cfg(feature = "git")]
113        let line_changes = if self.config.visible_lines.diff_mode()
114            || (!self.config.loop_through && self.config.style_components.changes())
115        {
116            match opened_input.kind {
117                crate::input::OpenedInputKind::OrdinaryFile(ref path) => {
118                    let diff = get_git_diff(path);
119
120                    // Skip files without Git modifications
121                    if self.config.visible_lines.diff_mode()
122                        && diff
123                            .as_ref()
124                            .map(|changes| changes.is_empty())
125                            .unwrap_or(false)
126                    {
127                        return Ok(());
128                    }
129
130                    diff
131                }
132                _ if self.config.visible_lines.diff_mode() => {
133                    // Skip non-file inputs in diff mode
134                    return Ok(());
135                }
136                _ => None,
137            }
138        } else {
139            None
140        };
141
142        let mut printer: Box<dyn Printer> = if self.config.loop_through {
143            Box::new(SimplePrinter::new(self.config))
144        } else {
145            Box::new(InteractivePrinter::new(
146                self.config,
147                self.assets,
148                &mut opened_input,
149                #[cfg(feature = "git")]
150                &line_changes,
151            )?)
152        };
153
154        self.print_file(
155            &mut *printer,
156            writer,
157            &mut opened_input,
158            !is_first,
159            #[cfg(feature = "git")]
160            &line_changes,
161        )
162    }
163
164    fn print_file(
165        &self,
166        printer: &mut dyn Printer,
167        writer: &mut dyn Write,
168        input: &mut OpenedInput,
169        add_header_padding: bool,
170        #[cfg(feature = "git")] line_changes: &Option<LineChanges>,
171    ) -> Result<()> {
172        if !input.reader.first_line.is_empty() || self.config.style_components.header() {
173            printer.print_header(writer, input, add_header_padding)?;
174        }
175
176        if !input.reader.first_line.is_empty() {
177            let line_ranges = match self.config.visible_lines {
178                VisibleLines::Ranges(ref line_ranges) => line_ranges.clone(),
179                #[cfg(feature = "git")]
180                VisibleLines::DiffContext(context) => {
181                    let mut line_ranges: Vec<LineRange> = vec![];
182
183                    if let Some(line_changes) = line_changes {
184                        for &line in line_changes.keys() {
185                            let line = line as usize;
186                            line_ranges
187                                .push(LineRange::new(line.saturating_sub(context), line + context));
188                        }
189                    }
190
191                    LineRanges::from(line_ranges)
192                }
193            };
194
195            self.print_file_ranges(printer, writer, &mut input.reader, &line_ranges)?;
196        }
197        printer.print_footer(writer, input)?;
198
199        Ok(())
200    }
201
202    fn print_file_ranges(
203        &self,
204        printer: &mut dyn Printer,
205        writer: &mut dyn Write,
206        reader: &mut InputReader,
207        line_ranges: &LineRanges,
208    ) -> Result<()> {
209        let mut line_buffer = Vec::new();
210        let mut line_number: usize = 1;
211
212        let mut first_range: bool = true;
213        let mut mid_range: bool = false;
214
215        let style_snip = self.config.style_components.snip();
216
217        while reader.read_line(&mut line_buffer)? {
218            match line_ranges.check(line_number) {
219                RangeCheckResult::BeforeOrBetweenRanges => {
220                    // Call the printer in case we need to call the syntax highlighter
221                    // for this line. However, set `out_of_range` to `true`.
222                    printer.print_line(true, writer, line_number, &line_buffer)?;
223                    mid_range = false;
224                }
225
226                RangeCheckResult::InRange => {
227                    if style_snip {
228                        if first_range {
229                            first_range = false;
230                            mid_range = true;
231                        } else if !mid_range {
232                            mid_range = true;
233                            printer.print_snip(writer)?;
234                        }
235                    }
236
237                    printer.print_line(false, writer, line_number, &line_buffer)?;
238                }
239                RangeCheckResult::AfterLastRange => {
240                    break;
241                }
242            }
243
244            line_number += 1;
245            line_buffer.clear();
246        }
247        Ok(())
248    }
249}