bat/src/controller.rs

250 lines
8.0 KiB
Rust

use std::io::{self, BufRead, Write};
use crate::assets::HighlightingAssets;
use crate::config::{Config, VisibleLines};
#[cfg(feature = "git")]
use crate::diff::{get_git_diff, LineChanges};
use crate::error::*;
use crate::input::{Input, InputReader, OpenedInput};
#[cfg(feature = "git")]
use crate::line_range::LineRange;
use crate::line_range::{LineRanges, RangeCheckResult};
use crate::output::OutputType;
#[cfg(feature = "paging")]
use crate::paging::PagingMode;
use crate::printer::{InteractivePrinter, Printer, SimplePrinter};
use clircle::{Clircle, Identifier};
pub struct Controller<'a> {
config: &'a Config<'a>,
assets: &'a HighlightingAssets,
}
impl<'b> Controller<'b> {
pub fn new<'a>(config: &'a Config, assets: &'a HighlightingAssets) -> Controller<'a> {
Controller { config, assets }
}
pub fn run(&self, inputs: Vec<Input>) -> Result<bool> {
self.run_with_error_handler(inputs, default_error_handler)
}
pub fn run_with_error_handler(
&self,
inputs: Vec<Input>,
handle_error: impl Fn(&Error, &mut dyn Write),
) -> Result<bool> {
let mut output_type;
#[cfg(feature = "paging")]
{
use crate::input::InputKind;
use std::path::Path;
// Do not launch the pager if NONE of the input files exist
let mut paging_mode = self.config.paging_mode;
if self.config.paging_mode != PagingMode::Never {
let call_pager = inputs.iter().any(|input| {
if let InputKind::OrdinaryFile(ref path) = input.kind {
Path::new(path).exists()
} else {
true
}
});
if !call_pager {
paging_mode = PagingMode::Never;
}
}
let wrapping_mode = self.config.wrapping_mode;
output_type = OutputType::from_mode(paging_mode, wrapping_mode, self.config.pager)?;
}
#[cfg(not(feature = "paging"))]
{
output_type = OutputType::stdout();
}
let attached_to_pager = output_type.is_pager();
let stdout_identifier = if cfg!(windows) || attached_to_pager {
None
} else {
clircle::Identifier::stdout()
};
let writer = output_type.handle()?;
let mut no_errors: bool = true;
let stderr = io::stderr();
for (index, input) in inputs.into_iter().enumerate() {
let identifier = stdout_identifier.as_ref();
let is_first = index == 0;
let result = if input.is_stdin() {
self.print_input(input, writer, io::stdin().lock(), identifier, is_first)
} else {
// Use dummy stdin since stdin is actually not used (#1902)
self.print_input(input, writer, io::empty(), identifier, is_first)
};
if let Err(error) = result {
if attached_to_pager {
handle_error(&error, writer);
} else {
handle_error(&error, &mut stderr.lock());
}
no_errors = false;
}
}
Ok(no_errors)
}
fn print_input<R: BufRead>(
&self,
input: Input,
writer: &mut dyn Write,
stdin: R,
stdout_identifier: Option<&Identifier>,
is_first: bool,
) -> Result<()> {
let mut opened_input = input.open(stdin, stdout_identifier)?;
#[cfg(feature = "git")]
let line_changes = if self.config.visible_lines.diff_mode()
|| (!self.config.loop_through && self.config.style_components.changes())
{
match opened_input.kind {
crate::input::OpenedInputKind::OrdinaryFile(ref path) => {
let diff = get_git_diff(path);
// Skip files without Git modifications
if self.config.visible_lines.diff_mode()
&& diff
.as_ref()
.map(|changes| changes.is_empty())
.unwrap_or(false)
{
return Ok(());
}
diff
}
_ if self.config.visible_lines.diff_mode() => {
// Skip non-file inputs in diff mode
return Ok(());
}
_ => None,
}
} else {
None
};
let mut printer: Box<dyn Printer> = if self.config.loop_through {
Box::new(SimplePrinter::new(self.config))
} else {
Box::new(InteractivePrinter::new(
self.config,
self.assets,
&mut opened_input,
#[cfg(feature = "git")]
&line_changes,
)?)
};
self.print_file(
&mut *printer,
writer,
&mut opened_input,
!is_first,
#[cfg(feature = "git")]
&line_changes,
)
}
fn print_file(
&self,
printer: &mut dyn Printer,
writer: &mut dyn Write,
input: &mut OpenedInput,
add_header_padding: bool,
#[cfg(feature = "git")] line_changes: &Option<LineChanges>,
) -> Result<()> {
if !input.reader.first_line.is_empty() || self.config.style_components.header() {
printer.print_header(writer, input, add_header_padding)?;
}
if !input.reader.first_line.is_empty() {
let line_ranges = match self.config.visible_lines {
VisibleLines::Ranges(ref line_ranges) => line_ranges.clone(),
#[cfg(feature = "git")]
VisibleLines::DiffContext(context) => {
let mut line_ranges: Vec<LineRange> = vec![];
if let Some(line_changes) = line_changes {
for &line in line_changes.keys() {
let line = line as usize;
line_ranges
.push(LineRange::new(line.saturating_sub(context), line + context));
}
}
LineRanges::from(line_ranges)
}
};
self.print_file_ranges(printer, writer, &mut input.reader, &line_ranges)?;
}
printer.print_footer(writer, input)?;
Ok(())
}
fn print_file_ranges(
&self,
printer: &mut dyn Printer,
writer: &mut dyn Write,
reader: &mut InputReader,
line_ranges: &LineRanges,
) -> Result<()> {
let mut line_buffer = Vec::new();
let mut line_number: usize = 1;
let mut first_range: bool = true;
let mut mid_range: bool = false;
let style_snip = self.config.style_components.snip();
while reader.read_line(&mut line_buffer)? {
match line_ranges.check(line_number) {
RangeCheckResult::BeforeOrBetweenRanges => {
// Call the printer in case we need to call the syntax highlighter
// for this line. However, set `out_of_range` to `true`.
printer.print_line(true, writer, line_number, &line_buffer)?;
mid_range = false;
}
RangeCheckResult::InRange => {
if style_snip {
if first_range {
first_range = false;
mid_range = true;
} else if !mid_range {
mid_range = true;
printer.print_snip(writer)?;
}
}
printer.print_line(false, writer, line_number, &line_buffer)?;
}
RangeCheckResult::AfterLastRange => {
break;
}
}
line_number += 1;
line_buffer.clear();
}
Ok(())
}
}