on 448bfaf help sign in
bat/src/printer.rs
707 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 if highlight_this_line && self.config.theme == "ansi" {
467 self.ansi_style.update("^[4m");
468 }
470 let background_color = self
471 .background_color_highlight
472 .filter(|_| highlight_this_line);
474 // Line decorations.
475 if self.panel_width > 0 {
476 let decorations = self
477 .decorations
478 .iter()
479 .map(|d| d.generate(line_number, false, self));
481 for deco in decorations {
482 write!(handle, "{} ", deco.text)?;
483 cursor_max -= deco.width + 1;
484 }
485 }
487 // Line contents.
488 if matches!(self.config.wrapping_mode, WrappingMode::NoWrapping(_)) {
489 let true_color = self.config.true_color;
490 let colored_output = self.config.colored_output;
491 let italics = self.config.use_italic_text;
493 for &(style, region) in &regions {
494 let ansi_iterator = AnsiCodeIterator::new(region);
495 for chunk in ansi_iterator {
496 match chunk {
497 // ANSI escape passthrough.
498 (ansi, true) => {
499 self.ansi_style.update(ansi);
500 write!(handle, "{}", ansi)?;
501 }
503 // Regular text.
504 (text, false) => {
505 let text = &*self.preprocess(text, &mut cursor_total);
506 let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
508 write!(
509 handle,
510 "{}",
511 as_terminal_escaped(
512 style,
513 &format!("{}{}", self.ansi_style, text_trimmed),
514 true_color,
515 colored_output,
516 italics,
517 background_color
518 )
519 )?;
521 if text.len() != text_trimmed.len() {
522 if let Some(background_color) = background_color {
523 let ansi_style = Style {
524 background: to_ansi_color(background_color, true_color),
525 ..Default::default()
526 };
528 let width = if cursor_total <= cursor_max {
529 cursor_max - cursor_total + 1
530 } else {
531 0
532 };
533 write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
534 }
535 write!(handle, "{}", &text[text_trimmed.len()..])?;
536 }
537 }
538 }
539 }
540 }
542 if !self.config.style_components.plain() && line.bytes().next_back() != Some(b'\n') {
543 writeln!(handle)?;
544 }
545 } else {
546 for &(style, region) in &regions {
547 let ansi_iterator = AnsiCodeIterator::new(region);
548 for chunk in ansi_iterator {
549 match chunk {
550 // ANSI escape passthrough.
551 (ansi, true) => {
552 self.ansi_style.update(ansi);
553 write!(handle, "{}", ansi)?;
554 }
556 // Regular text.
557 (text, false) => {
558 let text = self.preprocess(
559 text.trim_end_matches(|c| c == '\r' || c == '\n'),
560 &mut cursor_total,
561 );
563 let mut max_width = cursor_max - cursor;
565 // line buffer (avoid calling write! for every character)
566 let mut line_buf = String::with_capacity(max_width * 4);
568 // Displayed width of line_buf
569 let mut current_width = 0;
571 for c in text.chars() {
572 // calculate the displayed width for next character
573 let cw = c.width().unwrap_or(0);
574 current_width += cw;
576 // if next character cannot be printed on this line,
577 // flush the buffer.
578 if current_width > max_width {
579 // Generate wrap padding if not already generated.
580 if panel_wrap.is_none() {
581 panel_wrap = if self.panel_width > 0 {
582 Some(format!(
583 "{} ",
584 self.decorations
585 .iter()
586 .map(|d| d
587 .generate(line_number, true, self)
588 .text)
589 .collect::<Vec<String>>()
590 .join(" ")
591 ))
592 } else {
593 Some("".to_string())
594 }
595 }
597 // It wraps.
598 write!(
599 handle,
600 "{}\n{}",
601 as_terminal_escaped(
602 style,
603 &*format!("{}{}", self.ansi_style, line_buf),
604 self.config.true_color,
605 self.config.colored_output,
606 self.config.use_italic_text,
607 background_color
608 ),
609 panel_wrap.clone().unwrap()
610 )?;
612 cursor = 0;
613 max_width = cursor_max;
615 line_buf.clear();
616 current_width = cw;
617 }
619 line_buf.push(c);
620 }
622 // flush the buffer
623 cursor += current_width;
624 write!(
625 handle,
626 "{}",
627 as_terminal_escaped(
628 style,
629 &*format!("{}{}", self.ansi_style, line_buf),
630 self.config.true_color,
631 self.config.colored_output,
632 self.config.use_italic_text,
633 background_color
634 )
635 )?;
636 }
637 }
638 }
639 }
641 if let Some(background_color) = background_color {
642 let ansi_style = Style {
643 background: to_ansi_color(background_color, self.config.true_color),
644 ..Default::default()
645 };
647 write!(
648 handle,
649 "{}",
650 ansi_style.paint(" ".repeat(cursor_max - cursor))
651 )?;
652 }
653 writeln!(handle)?;
654 }
656 if highlight_this_line && self.config.theme == "ansi" {
657 self.ansi_style.update("^[24m");
658 write!(handle, "\x1B[24m")?;
659 }
661 Ok(())
662 }
665const DEFAULT_GUTTER_COLOR: u8 = 238;
667#[derive(Debug, Default)]
668pub struct Colors {
669 pub grid: Style,
670 pub rule: Style,
671 pub header_value: Style,
672 pub git_added: Style,
673 pub git_removed: Style,
674 pub git_modified: Style,
675 pub line_number: Style,
678impl Colors {
679 fn plain() -> Self {
680 Colors::default()
681 }
683 fn colored(theme: &Theme, true_color: bool) -> Self {
684 let gutter_style = Style {
685 foreground: match theme.settings.gutter_foreground {
686 // If the theme provides a gutter foreground color, use it.
687 // Note: It might be the special value #00000001, in which case
688 // to_ansi_color returns None and we use an empty Style
689 // (resulting in the terminal's default foreground color).
690 Some(c) => to_ansi_color(c, true_color),
691 // Otherwise, use a specific fallback color.
692 None => Some(Fixed(DEFAULT_GUTTER_COLOR)),
693 },
694 ..Style::default()
695 };
697 Colors {
698 grid: gutter_style,
699 rule: gutter_style,
700 header_value: Style::new().bold(),
701 git_added: Green.normal(),
702 git_removed: Red.normal(),
703 git_modified: Yellow.normal(),
704 line_number: gutter_style,
705 }
706 }