on 2eee685 help sign in
bat/src/main.rs
311 lines 8.9 kB history raw
1// `error_chain!` can recurse deeply
2#![recursion_limit = "1024"]
4#[macro_use]
5extern crate error_chain;
7#[macro_use]
8extern crate clap;
10#[macro_use]
11extern crate lazy_static;
13extern crate ansi_term;
14extern crate atty;
15extern crate console;
16extern crate directories;
17extern crate git2;
18extern crate syntect;
20mod app;
21mod assets;
22mod decorations;
23mod diff;
24mod printer;
25mod style;
26mod terminal;
28use std::fs::{self, File};
29use std::io::{self, BufRead, BufReader, Write};
30use std::process::{self, Child, Command, Stdio};
32#[cfg(unix)]
33use std::os::unix::fs::FileTypeExt;
35use ansi_term::Colour::{Fixed, Green, Red, White, Yellow};
36use ansi_term::Style;
38use syntect::easy::HighlightLines;
39use syntect::highlighting::Theme;
40use syntect::parsing::SyntaxSet;
42use app::{App, Config};
43use assets::{config_dir, syntax_set_path, theme_set_path, HighlightingAssets};
44use diff::get_git_diff;
45use printer::Printer;
47mod errors {
48 error_chain! {
49 foreign_links {
50 Clap(::clap::Error);
51 Io(::std::io::Error);
52 }
53 }
56use errors::*;
58enum OutputType {
59 Pager(Child),
60 Stdout(io::Stdout),
63impl OutputType {
64 fn pager() -> Result<Self> {
65 Ok(OutputType::Pager(Command::new("less")
66 .args(&["--quit-if-one-screen", "--RAW-CONTROL-CHARS", "--no-init"])
67 .stdin(Stdio::piped())
68 .spawn()
69 .chain_err(|| "Could not spawn pager")?))
70 }
72 fn stdout() -> Self {
73 OutputType::Stdout(io::stdout())
74 }
76 fn handle(&mut self) -> Result<&mut Write> {
77 Ok(match *self {
78 OutputType::Pager(ref mut command) => command
79 .stdin
80 .as_mut()
81 .chain_err(|| "Could not open stdin for pager")?,
82 OutputType::Stdout(ref mut handle) => handle,
83 })
84 }
87impl Drop for OutputType {
88 fn drop(&mut self) {
89 if let OutputType::Pager(ref mut command) = *self {
90 let _ = command.wait();
91 }
92 }
95const GRID_COLOR: u8 = 238;
96const LINE_NUMBER_COLOR: u8 = 244;
98#[derive(Default)]
99pub struct Colors {
100 pub grid: Style,
101 pub filename: Style,
102 pub git_added: Style,
103 pub git_removed: Style,
104 pub git_modified: Style,
105 pub line_number: Style,
108impl Colors {
109 fn plain() -> Self {
110 Colors::default()
111 }
113 fn colored() -> Self {
114 Colors {
115 grid: Fixed(GRID_COLOR).normal(),
116 filename: White.bold(),
117 git_added: Green.normal(),
118 git_removed: Red.normal(),
119 git_modified: Yellow.normal(),
120 line_number: Fixed(LINE_NUMBER_COLOR).normal(),
121 }
122 }
125fn print_file(
126 config: &Config,
127 theme: &Theme,
128 syntax_set: &SyntaxSet,
129 printer: &mut Printer,
130 filename: Option<&str>,
131) -> Result<()> {
132 let stdin = io::stdin(); // TODO: this is not always needed
134 let mut reader: Box<BufRead> = match filename {
135 None => Box::new(stdin.lock()),
136 Some(filename) => Box::new(BufReader::new(File::open(filename)?)),
137 };
139 let syntax = match (config.language, filename) {
140 (Some(language), _) => syntax_set.find_syntax_by_token(language),
141 (None, Some(filename)) => {
142 #[cfg(not(unix))]
143 let may_read_from_file = true;
145 // Do not peek at the file (to determine the syntax) if it is a FIFO because they can
146 // only be read once.
147 #[cfg(unix)]
148 let may_read_from_file = !fs::metadata(filename)?.file_type().is_fifo();
150 if may_read_from_file {
151 syntax_set.find_syntax_for_file(filename)?
152 } else {
153 None
154 }
155 }
156 (None, None) => None,
157 };
159 let syntax = syntax.unwrap_or_else(|| syntax_set.find_syntax_plain_text());
160 let mut highlighter = HighlightLines::new(syntax, theme);
162 printer.print_header(filename)?;
164 let mut line_nr = 1;
165 let mut line_buffer = String::new();
166 loop {
167 line_buffer.clear();
168 let num_bytes = reader.read_line(&mut line_buffer);
170 let line = match num_bytes {
171 Ok(0) => {
172 break;
173 }
174 Ok(_) => {
175 if !line_buffer.ends_with('\n') {
176 line_buffer.push('\n');
177 }
178 &line_buffer
179 }
180 Err(_) => "<bat: INVALID UTF-8>\n",
181 };
183 let regions = highlighter.highlight(line);
185 printer.print_line(line_nr, &regions)?;
187 line_nr += 1;
188 }
190 printer.print_footer()?;
192 Ok(())
195fn get_output_type(paging: bool) -> OutputType {
196 if paging {
197 OutputType::pager().unwrap_or_else(|_| OutputType::stdout())
198 } else {
199 OutputType::stdout()
200 }
203fn run() -> Result<()> {
204 let app = App::new();
206 match app.matches.subcommand() {
207 ("cache", Some(cache_matches)) => {
208 if cache_matches.is_present("init") {
209 let assets = HighlightingAssets::from_files()?;
210 assets.save()?;
211 } else if cache_matches.is_present("clear") {
212 print!("Clearing theme set cache ... ");
213 fs::remove_file(theme_set_path()).ok();
214 println!("okay");
216 print!("Clearing syntax set cache ... ");
217 fs::remove_file(syntax_set_path()).ok();
218 println!("okay");
219 } else if cache_matches.is_present("config-dir") {
220 println!("{}", config_dir());
221 }
222 }
223 _ => {
224 let config = app.config()?;
226 let assets = HighlightingAssets::new();
227 let theme = assets.get_theme(config.theme.unwrap_or("Default"))?;
229 if app.matches.is_present("list-languages") {
230 let mut languages = assets.syntax_set.syntaxes().to_owned();
231 languages.sort_by_key(|lang| lang.name.to_uppercase());
233 let longest = languages
234 .iter()
235 .filter(|s| !s.hidden && !s.file_extensions.is_empty())
236 .map(|s| s.name.len())
237 .max()
238 .unwrap_or(32); // Fallback width if they have no language definitions.
240 let separator = " ";
241 for lang in languages {
242 if lang.hidden || lang.file_extensions.is_empty() {
243 continue;
244 }
245 print!("{:width$}{}", lang.name, separator, width = longest);
247 // Line-wrapping for the possible file extension overflow.
248 let desired_width = config.term_width - longest - separator.len();
249 // Number of characters on this line so far, wrap before `desired_width`
250 let mut num_chars = 0;
252 let comma_separator = ", ";
253 let mut extension = lang.file_extensions.iter().peekable();
254 while let Some(word) = extension.next() {
255 // If we can't fit this word in, then create a line break and align it in.
256 let new_chars = word.len() + comma_separator.len();
257 if num_chars + new_chars >= desired_width {
258 num_chars = 0;
259 print!("\n{:width$}{}", "", separator, width = longest);
260 }
262 num_chars += new_chars;
263 print!("{}", Green.paint(word as &str));
264 if extension.peek().is_some() {
265 print!("{}", comma_separator);
266 }
267 }
268 println!();
269 }
271 return Ok(());
272 }
274 if app.matches.is_present("list-themes") {
275 let themes = &assets.theme_set.themes;
276 for (theme, _) in themes.iter() {
277 println!("{}", theme);
278 }
279 return Ok(());
280 }
282 let mut output_type = get_output_type(config.paging);
283 let handle = output_type.handle()?;
284 let mut printer = Printer::new(handle, &config);
286 for file in &config.files {
287 printer.line_changes = file.and_then(|filename| get_git_diff(filename));
289 print_file(&config, theme, &assets.syntax_set, &mut printer, *file)?;
290 }
291 }
292 }
294 Ok(())
297fn main() {
298 let result = run();
300 if let Err(error) = result {
301 match error {
302 Error(ErrorKind::Io(ref io_error), _)
303 if io_error.kind() == io::ErrorKind::BrokenPipe => {}
304 _ => {
305 eprintln!("{}: {}", Red.paint("[bat error]"), error);
307 process::exit(1);
308 }
309 };
310 }