on sponsor help sign in
bat/src/printer.rs
698 lines 25 kB history raw
1use std::io::Write;
2use std::vec::Vec;
4use ansi_term::Colour::{Fixed, Green, Red, Yellow};
5use ansi_term::Style;
7use bytesize::ByteSize;
9use console::AnsiCodeIterator;
11use syntect::easy::HighlightLines;
12use syntect::highlighting::Color;
13use syntect::highlighting::Theme;
14use syntect::parsing::SyntaxSet;
16use content_inspector::ContentType;
18use encoding::all::{UTF_16BE, UTF_16LE};
19use encoding::{DecoderTrap, Encoding};
21use unicode_width::UnicodeWidthChar;
23use crate::assets::{HighlightingAssets, SyntaxReferenceInSet};
24use crate::config::Config;
25#[cfg(feature = "git")]
26use crate::decorations::LineChangesDecoration;
27use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration};
28#[cfg(feature = "git")]
29use crate::diff::LineChanges;
30use crate::error::*;
31use crate::input::OpenedInput;
32use crate::line_range::RangeCheckResult;
33use crate::preprocessor::{expand_tabs, replace_nonprintable};
34use crate::style::StyleComponent;
35use crate::terminal::{as_terminal_escaped, to_ansi_color};
36use crate::vscreen::AnsiStyle;
37use crate::wrapping::WrappingMode;
39pub(crate) trait Printer {
40 fn print_header(
41 &mut self,
42 handle: &mut dyn Write,
43 input: &OpenedInput,
44 add_header_padding: bool,
45 ) -> Result<()>;
46 fn print_footer(&mut self, handle: &mut dyn Write, input: &OpenedInput) -> Result<()>;
48 fn print_snip(&mut self, handle: &mut dyn Write) -> Result<()>;
50 fn print_line(
51 &mut self,
52 out_of_range: bool,
53 handle: &mut dyn Write,
54 line_number: usize,
55 line_buffer: &[u8],
56 ) -> Result<()>;
59pub struct SimplePrinter<'a> {
60 config: &'a Config<'a>,
63impl<'a> SimplePrinter<'a> {
64 pub fn new(config: &'a Config) -> Self {
65 SimplePrinter { config }
66 }
69impl<'a> Printer for SimplePrinter<'a> {
70 fn print_header(
71 &mut self,
72 _handle: &mut dyn Write,
73 _input: &OpenedInput,
74 _add_header_padding: bool,
75 ) -> Result<()> {
76 Ok(())
77 }
79 fn print_footer(&mut self, _handle: &mut dyn Write, _input: &OpenedInput) -> Result<()> {
80 Ok(())
81 }
83 fn print_snip(&mut self, _handle: &mut dyn Write) -> Result<()> {
84 Ok(())
85 }
87 fn print_line(
88 &mut self,
89 out_of_range: bool,
90 handle: &mut dyn Write,
91 _line_number: usize,
92 line_buffer: &[u8],
93 ) -> Result<()> {
94 if !out_of_range {
95 if self.config.show_nonprintable {
96 let line = replace_nonprintable(line_buffer, self.config.tab_width);
97 write!(handle, "{}", line)?;
98 } else {
99 handle.write_all(line_buffer)?
100 };
101 }
102 Ok(())
103 }
106struct HighlighterFromSet<'a> {
107 highlighter: HighlightLines<'a>,
108 syntax_set: &'a SyntaxSet,
111impl<'a> HighlighterFromSet<'a> {
112 fn new(syntax_in_set: SyntaxReferenceInSet<'a>, theme: &'a Theme) -> Self {
113 Self {
114 highlighter: HighlightLines::new(syntax_in_set.syntax, theme),
115 syntax_set: syntax_in_set.syntax_set,
116 }
117 }
120pub(crate) struct InteractivePrinter<'a> {
121 colors: Colors,
122 config: &'a Config<'a>,
123 decorations: Vec<Box<dyn Decoration>>,
124 panel_width: usize,
125 ansi_style: AnsiStyle,
126 content_type: Option<ContentType>,
127 #[cfg(feature = "git")]
128 pub line_changes: &'a Option<LineChanges>,
129 highlighter_from_set: Option<HighlighterFromSet<'a>>,
130 background_color_highlight: Option<Color>,
133impl<'a> InteractivePrinter<'a> {
134 pub(crate) fn new(
135 config: &'a Config,
136 assets: &'a HighlightingAssets,
137 input: &mut OpenedInput,
138 #[cfg(feature = "git")] line_changes: &'a Option<LineChanges>,
139 ) -> Result<Self> {
140 let theme = assets.get_theme(&config.theme);
142 let background_color_highlight = theme.settings.line_highlight;
144 let colors = if config.colored_output {
145 Colors::colored(theme, config.true_color)
146 } else {
147 Colors::plain()
148 };
150 // Create decorations.
151 let mut decorations: Vec<Box<dyn Decoration>> = Vec::new();
153 if config.style_components.numbers() {
154 decorations.push(Box::new(LineNumberDecoration::new(&colors)));
155 }
157 #[cfg(feature = "git")]
158 {
159 if config.style_components.changes() {
160 decorations.push(Box::new(LineChangesDecoration::new(&colors)));
161 }
162 }
164 let mut panel_width: usize =
165 decorations.len() + decorations.iter().fold(0, |a, x| a + x.width());
167 // The grid border decoration isn't added until after the panel_width calculation, since the
168 // print_horizontal_line, print_header, and print_footer functions all assume the panel
169 // width is without the grid border.
170 if config.style_components.grid() && !decorations.is_empty() {
171 decorations.push(Box::new(GridBorderDecoration::new(&colors)));
172 }
174 // Disable the panel if the terminal is too small (i.e. can't fit 5 characters with the
175 // panel showing).
176 if config.term_width
177 < (decorations.len() + decorations.iter().fold(0, |a, x| a + x.width())) + 5
178 {
179 decorations.clear();
180 panel_width = 0;
181 }
183 let highlighter_from_set = if input
184 .reader
185 .content_type
186 .map_or(false, |c| c.is_binary() && !config.show_nonprintable)
187 {
188 None
189 } else {
190 // Determine the type of syntax for highlighting
191 let syntax_in_set =
192 match assets.get_syntax(config.language, input, &config.syntax_mapping) {
193 Ok(syntax_in_set) => syntax_in_set,
194 Err(Error::UndetectedSyntax(_)) => assets
195 .find_syntax_by_name("Plain Text")?
196 .expect("A plain text syntax is available"),
197 Err(e) => return Err(e),
198 };
200 Some(HighlighterFromSet::new(syntax_in_set, theme))
201 };
203 Ok(InteractivePrinter {
204 panel_width,
205 colors,
206 config,
207 decorations,
208 content_type: input.reader.content_type,
209 ansi_style: AnsiStyle::new(),
210 #[cfg(feature = "git")]
211 line_changes,
212 highlighter_from_set,
213 background_color_highlight,
214 })
215 }
217 fn print_horizontal_line_term(&mut self, handle: &mut dyn Write, style: Style) -> Result<()> {
218 writeln!(
219 handle,
220 "{}",
221 style.paint("─".repeat(self.config.term_width))
222 )?;
223 Ok(())
224 }
226 fn print_horizontal_line(&mut self, handle: &mut dyn Write, grid_char: char) -> Result<()> {
227 if self.panel_width == 0 {
228 self.print_horizontal_line_term(handle, self.colors.grid)?;
229 } else {
230 let hline = "─".repeat(self.config.term_width - (self.panel_width + 1));
231 let hline = format!("{}{}{}", "─".repeat(self.panel_width), grid_char, hline);
232 writeln!(handle, "{}", self.colors.grid.paint(hline))?;
233 }
235 Ok(())
236 }
238 fn create_fake_panel(&self, text: &str) -> String {
239 if self.panel_width == 0 {
240 return "".to_string();
241 }
243 let text_truncated: String = text.chars().take(self.panel_width - 1).collect();
244 let text_filled: String = format!(
245 "{}{}",
246 text_truncated,
247 " ".repeat(self.panel_width - 1 - text_truncated.len())
248 );
249 if self.config.style_components.grid() {
250 format!("{} │ ", text_filled)
251 } else {
252 text_filled
253 }
254 }
256 fn print_header_component_indent(&mut self, handle: &mut dyn Write) -> std::io::Result<()> {
257 if self.config.style_components.grid() {
258 write!(
259 handle,
260 "{}{}",
261 " ".repeat(self.panel_width),
262 self.colors
263 .grid
264 .paint(if self.panel_width > 0 { "│ " } else { "" }),
265 )
266 } else {
267 write!(handle, "{}", " ".repeat(self.panel_width))
268 }
269 }
271 fn preprocess(&self, text: &str, cursor: &mut usize) -> String {
272 if self.config.tab_width > 0 {
273 return expand_tabs(text, self.config.tab_width, cursor);
274 }
276 *cursor += text.len();
277 text.to_string()
278 }
281impl<'a> Printer for InteractivePrinter<'a> {
282 fn print_header(
283 &mut self,
284 handle: &mut dyn Write,
285 input: &OpenedInput,
286 add_header_padding: bool,
287 ) -> Result<()> {
288 if add_header_padding && self.config.style_components.rule() {
289 self.print_horizontal_line_term(handle, self.colors.rule)?;
290 }
292 if !self.config.style_components.header() {
293 if Some(ContentType::BINARY) == self.content_type && !self.config.show_nonprintable {
294 writeln!(
295 handle,
296 "{}: Binary content from {} will not be printed to the terminal \
297 (but will be present if the output of 'bat' is piped). You can use 'bat -A' \
298 to show the binary file contents.",
299 Yellow.paint("[bat warning]"),
300 input.description.summary(),
301 )?;
302 } else if self.config.style_components.grid() {
303 self.print_horizontal_line(handle, '┬')?;
304 }
305 return Ok(());
306 }
308 let mode = match self.content_type {
309 Some(ContentType::BINARY) => " <BINARY>",
310 Some(ContentType::UTF_16LE) => " <UTF-16LE>",
311 Some(ContentType::UTF_16BE) => " <UTF-16BE>",
312 None => " <EMPTY>",
313 _ => "",
314 };
316 let description = &input.description;
317 let metadata = &input.metadata;
319 // We use this iterator to have a deterministic order for
320 // header components. HashSet has arbitrary order, but Vec is ordered.
321 let header_components: Vec<StyleComponent> = [
322 (
323 StyleComponent::HeaderFilename,
324 self.config.style_components.header_filename(),
325 ),
326 (
327 StyleComponent::HeaderFilesize,
328 self.config.style_components.header_filesize(),
329 ),
330 ]
331 .iter()
332 .filter(|(_, is_enabled)| *is_enabled)
333 .map(|(component, _)| *component)
334 .collect();
336 // Print the cornering grid before the first header component
337 if self.config.style_components.grid() {
338 self.print_horizontal_line(handle, '┬')?;
339 } else {
340 // Only pad space between files, if we haven't already drawn a horizontal rule
341 if add_header_padding && !self.config.style_components.rule() {
342 writeln!(handle)?;
343 }
344 }
346 header_components.iter().try_for_each(|component| {
347 self.print_header_component_indent(handle)?;
349 match component {
350 StyleComponent::HeaderFilename => writeln!(
351 handle,
352 "{}{}{}",
353 description
354 .kind()
355 .map(|kind| format!("{}: ", kind))
356 .unwrap_or_else(|| "".into()),
357 self.colors.header_value.paint(description.title()),
358 mode
359 ),
361 StyleComponent::HeaderFilesize => {
362 let bsize = metadata
363 .size
364 .map(|s| format!("{}", ByteSize(s)))
365 .unwrap_or_else(|| "-".into());
366 writeln!(handle, "Size: {}", self.colors.header_value.paint(bsize))
367 }
368 _ => Ok(()),
369 }
370 })?;
372 if self.config.style_components.grid() {
373 if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {
374 self.print_horizontal_line(handle, '┼')?;
375 } else {
376 self.print_horizontal_line(handle, '┴')?;
377 }
378 }
380 Ok(())
381 }
383 fn print_footer(&mut self, handle: &mut dyn Write, _input: &OpenedInput) -> Result<()> {
384 if self.config.style_components.grid()
385 && (self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable)
386 {
387 self.print_horizontal_line(handle, '┴')
388 } else {
389 Ok(())
390 }
391 }
393 fn print_snip(&mut self, handle: &mut dyn Write) -> Result<()> {
394 let panel = self.create_fake_panel(" ...");
395 let panel_count = panel.chars().count();
397 let title = "8<";
398 let title_count = title.chars().count();
400 let snip_left = "─ ".repeat((self.config.term_width - panel_count - (title_count / 2)) / 4);
401 let snip_left_count = snip_left.chars().count(); // Can't use .len() with Unicode.
403 let snip_right =
404 " ─".repeat((self.config.term_width - panel_count - snip_left_count - title_count) / 2);
406 writeln!(
407 handle,
408 "{}",
409 self.colors
410 .grid
411 .paint(format!("{}{}{}{}", panel, snip_left, title, snip_right))
412 )?;
414 Ok(())
415 }
417 fn print_line(
418 &mut self,
419 out_of_range: bool,
420 handle: &mut dyn Write,
421 line_number: usize,
422 line_buffer: &[u8],
423 ) -> Result<()> {
424 let line = if self.config.show_nonprintable {
425 replace_nonprintable(line_buffer, self.config.tab_width)
426 } else {
427 match self.content_type {
428 Some(ContentType::BINARY) | None => {
429 return Ok(());
430 }
431 Some(ContentType::UTF_16LE) => UTF_16LE
432 .decode(line_buffer, DecoderTrap::Replace)
433 .map_err(|_| "Invalid UTF-16LE")?,
434 Some(ContentType::UTF_16BE) => UTF_16BE
435 .decode(line_buffer, DecoderTrap::Replace)
436 .map_err(|_| "Invalid UTF-16BE")?,
437 _ => String::from_utf8_lossy(line_buffer).to_string(),
438 }
439 };
441 let regions = {
442 let highlighter_from_set = match self.highlighter_from_set {
443 Some(ref mut highlighter_from_set) => highlighter_from_set,
444 _ => {
445 return Ok(());
446 }
447 };
448 highlighter_from_set
449 .highlighter
450 .highlight(&line, highlighter_from_set.syntax_set)
451 };
453 if out_of_range {
454 return Ok(());
455 }
457 let mut cursor: usize = 0;
458 let mut cursor_max: usize = self.config.term_width;
459 let mut cursor_total: usize = 0;
460 let mut panel_wrap: Option<String> = None;
462 // Line highlighting
463 let highlight_this_line =
464 self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
466 let background_color = self
467 .background_color_highlight
468 .filter(|_| highlight_this_line);
470 // Line decorations.
471 if self.panel_width > 0 {
472 let decorations = self
473 .decorations
474 .iter()
475 .map(|d| d.generate(line_number, false, self));
477 for deco in decorations {
478 write!(handle, "{} ", deco.text)?;
479 cursor_max -= deco.width + 1;
480 }
481 }
483 // Line contents.
484 if matches!(self.config.wrapping_mode, WrappingMode::NoWrapping(_)) {
485 let true_color = self.config.true_color;
486 let colored_output = self.config.colored_output;
487 let italics = self.config.use_italic_text;
489 for &(style, region) in &regions {
490 let ansi_iterator = AnsiCodeIterator::new(region);
491 for chunk in ansi_iterator {
492 match chunk {
493 // ANSI escape passthrough.
494 (ansi, true) => {
495 self.ansi_style.update(ansi);
496 write!(handle, "{}", ansi)?;
497 }
499 // Regular text.
500 (text, false) => {
501 let text = &*self.preprocess(text, &mut cursor_total);
502 let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
504 write!(
505 handle,
506 "{}",
507 as_terminal_escaped(
508 style,
509 &format!("{}{}", self.ansi_style, text_trimmed),
510 true_color,
511 colored_output,
512 italics,
513 background_color
514 )
515 )?;
517 if text.len() != text_trimmed.len() {
518 if let Some(background_color) = background_color {
519 let ansi_style = Style {
520 background: to_ansi_color(background_color, true_color),
521 ..Default::default()
522 };
524 let width = if cursor_total <= cursor_max {
525 cursor_max - cursor_total + 1
526 } else {
527 0
528 };
529 write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
530 }
531 write!(handle, "{}", &text[text_trimmed.len()..])?;
532 }
533 }
534 }
535 }
536 }
538 if !self.config.style_components.plain() && line.bytes().next_back() != Some(b'\n') {
539 writeln!(handle)?;
540 }
541 } else {
542 for &(style, region) in &regions {
543 let ansi_iterator = AnsiCodeIterator::new(region);
544 for chunk in ansi_iterator {
545 match chunk {
546 // ANSI escape passthrough.
547 (ansi, true) => {
548 self.ansi_style.update(ansi);
549 write!(handle, "{}", ansi)?;
550 }
552 // Regular text.
553 (text, false) => {
554 let text = self.preprocess(
555 text.trim_end_matches(|c| c == '\r' || c == '\n'),
556 &mut cursor_total,
557 );
559 let mut max_width = cursor_max - cursor;
561 // line buffer (avoid calling write! for every character)
562 let mut line_buf = String::with_capacity(max_width * 4);
564 // Displayed width of line_buf
565 let mut current_width = 0;
567 for c in text.chars() {
568 // calculate the displayed width for next character
569 let cw = c.width().unwrap_or(0);
570 current_width += cw;
572 // if next character cannot be printed on this line,
573 // flush the buffer.
574 if current_width > max_width {
575 // Generate wrap padding if not already generated.
576 if panel_wrap.is_none() {
577 panel_wrap = if self.panel_width > 0 {
578 Some(format!(
579 "{} ",
580 self.decorations
581 .iter()
582 .map(|d| d
583 .generate(line_number, true, self)
584 .text)
585 .collect::<Vec<String>>()
586 .join(" ")
587 ))
588 } else {
589 Some("".to_string())
590 }
591 }
593 // It wraps.
594 write!(
595 handle,
596 "{}\n{}",
597 as_terminal_escaped(
598 style,
599 &*format!("{}{}", self.ansi_style, line_buf),
600 self.config.true_color,
601 self.config.colored_output,
602 self.config.use_italic_text,
603 background_color
604 ),
605 panel_wrap.clone().unwrap()
606 )?;
608 cursor = 0;
609 max_width = cursor_max;
611 line_buf.clear();
612 current_width = cw;
613 }
615 line_buf.push(c);
616 }
618 // flush the buffer
619 cursor += current_width;
620 write!(
621 handle,
622 "{}",
623 as_terminal_escaped(
624 style,
625 &*format!("{}{}", self.ansi_style, line_buf),
626 self.config.true_color,
627 self.config.colored_output,
628 self.config.use_italic_text,
629 background_color
630 )
631 )?;
632 }
633 }
634 }
635 }
637 if let Some(background_color) = background_color {
638 let ansi_style = Style {
639 background: to_ansi_color(background_color, self.config.true_color),
640 ..Default::default()
641 };
643 write!(
644 handle,
645 "{}",
646 ansi_style.paint(" ".repeat(cursor_max - cursor))
647 )?;
648 }
649 writeln!(handle)?;
650 }
652 Ok(())
653 }
656const DEFAULT_GUTTER_COLOR: u8 = 238;
658#[derive(Debug, Default)]
659pub struct Colors {
660 pub grid: Style,
661 pub rule: Style,
662 pub header_value: Style,
663 pub git_added: Style,
664 pub git_removed: Style,
665 pub git_modified: Style,
666 pub line_number: Style,
669impl Colors {
670 fn plain() -> Self {
671 Colors::default()
672 }
674 fn colored(theme: &Theme, true_color: bool) -> Self {
675 let gutter_style = Style {
676 foreground: match theme.settings.gutter_foreground {
677 // If the theme provides a gutter foreground color, use it.
678 // Note: It might be the special value #00000001, in which case
679 // to_ansi_color returns None and we use an empty Style
680 // (resulting in the terminal's default foreground color).
681 Some(c) => to_ansi_color(c, true_color),
682 // Otherwise, use a specific fallback color.
683 None => Some(Fixed(DEFAULT_GUTTER_COLOR)),
684 },
685 ..Style::default()
686 };
688 Colors {
689 grid: gutter_style,
690 rule: gutter_style,
691 header_value: Style::new().bold(),
692 git_added: Green.normal(),
693 git_removed: Red.normal(),
694 git_modified: Yellow.normal(),
695 line_number: gutter_style,
696 }
697 }