on 870ff7b help sign in
bat/src/main.rs
310 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 diff;
23mod printer;
24mod style;
25mod terminal;
27use std::fs::{self, File};
28use std::io::{self, BufRead, BufReader, Write};
29use std::process::{self, Child, Command, Stdio};
31#[cfg(unix)]
32use std::os::unix::fs::FileTypeExt;
34use ansi_term::Colour::{Fixed, Green, Red, White, Yellow};
35use ansi_term::Style;
37use syntect::easy::HighlightLines;
38use syntect::highlighting::Theme;
39use syntect::parsing::SyntaxSet;
41use app::{App, Config};
42use assets::{config_dir, syntax_set_path, theme_set_path, HighlightingAssets};
43use diff::get_git_diff;
44use printer::Printer;
46mod errors {
47 error_chain! {
48 foreign_links {
49 Clap(::clap::Error);
50 Io(::std::io::Error);
51 }
52 }
55use errors::*;
57enum OutputType {
58 Pager(Child),
59 Stdout(io::Stdout),
62impl OutputType {
63 fn pager() -> Result<Self> {
64 Ok(OutputType::Pager(Command::new("less")
65 .args(&["--quit-if-one-screen", "--RAW-CONTROL-CHARS", "--no-init"])
66 .stdin(Stdio::piped())
67 .spawn()
68 .chain_err(|| "Could not spawn pager")?))
69 }
71 fn stdout() -> Self {
72 OutputType::Stdout(io::stdout())
73 }
75 fn handle(&mut self) -> Result<&mut Write> {
76 Ok(match *self {
77 OutputType::Pager(ref mut command) => command
78 .stdin
79 .as_mut()
80 .chain_err(|| "Could not open stdin for pager")?,
81 OutputType::Stdout(ref mut handle) => handle,
82 })
83 }
86impl Drop for OutputType {
87 fn drop(&mut self) {
88 if let OutputType::Pager(ref mut command) = *self {
89 let _ = command.wait();
90 }
91 }
94const GRID_COLOR: u8 = 238;
95const LINE_NUMBER_COLOR: u8 = 244;
97#[derive(Default)]
98pub struct Colors {
99 pub grid: Style,
100 pub filename: Style,
101 pub git_added: Style,
102 pub git_removed: Style,
103 pub git_modified: Style,
104 pub line_number: Style,
107impl Colors {
108 fn plain() -> Self {
109 Colors::default()
110 }
112 fn colored() -> Self {
113 Colors {
114 grid: Fixed(GRID_COLOR).normal(),
115 filename: White.bold(),
116 git_added: Green.normal(),
117 git_removed: Red.normal(),
118 git_modified: Yellow.normal(),
119 line_number: Fixed(LINE_NUMBER_COLOR).normal(),
120 }
121 }
124fn print_file(
125 config: &Config,
126 theme: &Theme,
127 syntax_set: &SyntaxSet,
128 printer: &mut Printer,
129 filename: Option<&str>,
130) -> Result<()> {
131 let stdin = io::stdin(); // TODO: this is not always needed
133 let mut reader: Box<BufRead> = match filename {
134 None => Box::new(stdin.lock()),
135 Some(filename) => Box::new(BufReader::new(File::open(filename)?)),
136 };
138 let syntax = match (config.language, filename) {
139 (Some(language), _) => syntax_set.find_syntax_by_token(language),
140 (None, Some(filename)) => {
141 #[cfg(not(unix))]
142 let may_read_from_file = true;
144 // Do not peek at the file (to determine the syntax) if it is a FIFO because they can
145 // only be read once.
146 #[cfg(unix)]
147 let may_read_from_file = !fs::metadata(filename)?.file_type().is_fifo();
149 if may_read_from_file {
150 syntax_set.find_syntax_for_file(filename)?
151 } else {
152 None
153 }
154 }
155 (None, None) => None,
156 };
158 let syntax = syntax.unwrap_or_else(|| syntax_set.find_syntax_plain_text());
159 let mut highlighter = HighlightLines::new(syntax, theme);
161 printer.print_header(filename)?;
163 let mut line_nr = 1;
164 let mut line_buffer = String::new();
165 loop {
166 line_buffer.clear();
167 let num_bytes = reader.read_line(&mut line_buffer);
169 let line = match num_bytes {
170 Ok(0) => {
171 break;
172 }
173 Ok(_) => {
174 if !line_buffer.ends_with('\n') {
175 line_buffer.push('\n');
176 }
177 &line_buffer
178 }
179 Err(_) => "<bat: INVALID UTF-8>\n",
180 };
182 let regions = highlighter.highlight(line);
184 printer.print_line(line_nr, &regions)?;
186 line_nr += 1;
187 }
189 printer.print_footer()?;
191 Ok(())
194fn get_output_type(paging: bool) -> OutputType {
195 if paging {
196 OutputType::pager().unwrap_or_else(|_| OutputType::stdout())
197 } else {
198 OutputType::stdout()
199 }
202fn run() -> Result<()> {
203 let app = App::new();
205 match app.matches.subcommand() {
206 ("cache", Some(cache_matches)) => {
207 if cache_matches.is_present("init") {
208 let assets = HighlightingAssets::from_files()?;
209 assets.save()?;
210 } else if cache_matches.is_present("clear") {
211 print!("Clearing theme set cache ... ");
212 fs::remove_file(theme_set_path()).ok();
213 println!("okay");
215 print!("Clearing syntax set cache ... ");
216 fs::remove_file(syntax_set_path()).ok();
217 println!("okay");
218 } else if cache_matches.is_present("config-dir") {
219 println!("{}", config_dir());
220 }
221 }
222 _ => {
223 let config = app.config()?;
225 let assets = HighlightingAssets::new();
226 let theme = assets.get_theme(config.theme.unwrap_or("Default"))?;
228 if app.matches.is_present("list-languages") {
229 let mut languages = assets.syntax_set.syntaxes().to_owned();
230 languages.sort_by(|a, b| a.name.to_uppercase().cmp(&b.name.to_uppercase()));
232 let longest = languages
233 .iter()
234 .filter(|s| !s.hidden && !s.file_extensions.is_empty())
235 .map(|s| s.name.len())
236 .max()
237 .unwrap_or(32); // Fallback width if they have no language definitions.
239 let separator = " ";
240 for lang in languages {
241 if lang.hidden || lang.file_extensions.is_empty() {
242 continue;
243 }
244 print!("{:width$}{}", lang.name, separator, width = longest);
246 // Line-wrapping for the possible file extension overflow.
247 let desired_width = config.term_width - longest - separator.len();
248 // Number of characters on this line so far, wrap before `desired_width`
249 let mut num_chars = 0;
251 let comma_separator = ", ";
252 let mut extension = lang.file_extensions.iter().peekable();
253 while let Some(word) = extension.next() {
254 // If we can't fit this word in, then create a line break and align it in.
255 let new_chars = word.len() + comma_separator.len();
256 if num_chars + new_chars >= desired_width {
257 num_chars = 0;
258 print!("\n{:width$}{}", "", separator, width = longest);
259 }
261 num_chars += new_chars;
262 print!("{}", Green.paint(word as &str));
263 if extension.peek().is_some() {
264 print!("{}", comma_separator);
265 }
266 }
267 println!();
268 }
270 return Ok(());
271 }
273 if app.matches.is_present("list-themes") {
274 let themes = &assets.theme_set.themes;
275 for (theme, _) in themes.iter() {
276 println!("{}", theme);
277 }
278 return Ok(());
279 }
281 let mut output_type = get_output_type(config.paging);
282 let handle = output_type.handle()?;
283 let mut printer = Printer::new(handle, &config);
285 for file in &config.files {
286 printer.line_changes = file.and_then(|filename| get_git_diff(filename));
288 print_file(&config, theme, &assets.syntax_set, &mut printer, *file)?;
289 }
290 }
291 }
293 Ok(())
296fn main() {
297 let result = run();
299 if let Err(error) = result {
300 match error {
301 Error(ErrorKind::Io(ref io_error), _)
302 if io_error.kind() == io::ErrorKind::BrokenPipe => {}
303 _ => {
304 eprintln!("{}: {}", Red.paint("[bat error]"), error);
306 process::exit(1);
307 }
308 };
309 }