on 026a9eb help sign in
bat/src/main.rs
309 lines 8.8 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())?;
213 println!("okay");
215 print!("Clearing syntax set cache ... ");
216 fs::remove_file(syntax_set_path())?;
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 languages = assets.syntax_set.syntaxes();
231 let longest = languages
232 .iter()
233 .filter(|s| !s.hidden && !s.file_extensions.is_empty())
234 .map(|s| s.name.len())
235 .max()
236 .unwrap_or(32); // Fallback width if they have no language definitions.
238 let separator = " ";
239 for lang in languages {
240 if lang.hidden || lang.file_extensions.is_empty() {
241 continue;
242 }
243 print!("{:width$}{}", lang.name, separator, width = longest);
245 // Line-wrapping for the possible file extension overflow.
246 let desired_width = config.term_width - longest - separator.len();
247 // Number of characters on this line so far, wrap before `desired_width`
248 let mut num_chars = 0;
250 let comma_separator = ", ";
251 let mut extension = lang.file_extensions.iter().peekable();
252 while let Some(word) = extension.next() {
253 // If we can't fit this word in, then create a line break and align it in.
254 let new_chars = word.len() + comma_separator.len();
255 if num_chars + new_chars >= desired_width {
256 num_chars = 0;
257 print!("\n{:width$}{}", "", separator, width = longest);
258 }
260 num_chars += new_chars;
261 print!("{}", Green.paint(word as &str));
262 if extension.peek().is_some() {
263 print!("{}", comma_separator);
264 }
265 }
266 println!();
267 }
269 return Ok(());
270 }
272 if app.matches.is_present("list-themes") {
273 let themes = &assets.theme_set.themes;
274 for (theme, _) in themes.iter() {
275 println!("{}", theme);
276 }
277 return Ok(());
278 }
280 let mut output_type = get_output_type(config.paging);
281 let handle = output_type.handle()?;
282 let mut printer = Printer::new(handle, &config);
284 for file in &config.files {
285 printer.line_changes = file.and_then(|filename| get_git_diff(filename));
287 print_file(&config, theme, &assets.syntax_set, &mut printer, *file)?;
288 }
289 }
290 }
292 Ok(())
295fn main() {
296 let result = run();
298 if let Err(error) = result {
299 match error {
300 Error(ErrorKind::Io(ref io_error), _)
301 if io_error.kind() == io::ErrorKind::BrokenPipe => {}
302 _ => {
303 eprintln!("{}: {}", Red.paint("[bat error]"), error);
305 process::exit(1);
306 }
307 };
308 }