mod cli;
mod config;
mod follow;
mod git;
mod highlight;
mod input;
mod interactive;
mod markdown;
mod pager;
mod printer;
mod syntax;
use anyhow::Result;
use clap::Parser;
use cli::{Cli, ColorWhen};
use highlight::{Highlighter, resolve_theme, theme_set};
use input::{InputKind, LineRange};
use printer::{PrinterConfig, StyleFlags, print};
use std::collections::HashSet;
use std::io::Write;
use std::path::PathBuf;
fn main() {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
if let Err(e) = run() {
eprintln!("batty: error: {:#}", e);
std::process::exit(1);
}
}
fn run() -> Result<()> {
let mut all: Vec<std::ffi::OsString> = std::env::args_os().take(1).collect();
all.extend(config::load_args().into_iter().map(Into::into));
all.extend(std::env::args_os().skip(1));
let args = Cli::parse_from(all);
let syntax_set = syntax::build_syntax_set()?;
let theme_set = theme_set();
if args.list_languages {
let mut names: Vec<&str> = syntax_set.syntaxes().iter().map(|s| s.name.as_str()).collect();
names.sort_unstable();
for n in names { println!("{}", n); }
return Ok(());
}
if args.list_themes {
let mut names: Vec<&String> = theme_set.themes.keys().collect();
names.sort();
for n in names { println!("{}", n); }
return Ok(());
}
if args.interactive && args.follow {
anyhow::bail!("--interactive and --follow are mutually exclusive");
}
let want_interactive = args.interactive && args.paging != cli::PagingWhen::Never;
if want_interactive {
return run_interactive(&args, &syntax_set, &theme_set);
}
if args.follow {
return follow::run(&args, &syntax_set, &theme_set);
}
let stdout_was_tty = {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
};
pager::setup(args.paging);
let use_color = match args.color {
ColorWhen::Always => true,
ColorWhen::Never => false,
ColorWhen::Auto => stdout_was_tty && std::env::var_os("NO_COLOR").is_none(),
};
let force_plain = args.plain
|| matches!(args.decorations, cli::DecorationsWhen::Never);
let mut style = StyleFlags::parse(&args.style, force_plain, args.number, args.diff);
if args.no_gutter {
style.numbers = false;
style.changes = false;
style.grid = false;
}
let theme = resolve_theme(&theme_set, args.theme.as_deref());
let line_range = match &args.line_range {
Some(s) => Some(LineRange::parse(s)?),
None => None,
};
let highlight_lines: HashSet<usize> = args.highlight_line.iter().copied().collect();
let cursor: Option<usize> = args.highlight_line.first().copied();
let width = term_width();
let inputs: Vec<InputKind> = if args.files.is_empty() {
vec![InputKind::Stdin]
} else {
args.files.iter().map(|p: &PathBuf| InputKind::from_path(p)).collect()
};
let mut stdout = std::io::stdout().lock();
for (idx, input) in inputs.iter().enumerate() {
if idx > 0 && style.rule {
writeln!(stdout, "{}", "─".repeat(width.saturating_sub(1)))?;
}
let contents = input.read(args.encoding)?;
let path = match input { InputKind::File(p) => Some(p.as_path()), InputKind::Stdin => None };
let first_line = contents.lines().next();
let syntax = syntax::detect_syntax(&syntax_set, path, args.language.as_deref(), first_line);
let mut hl = Highlighter::new(syntax, theme, &syntax_set);
let cfg = PrinterConfig {
style,
line_range,
highlight_lines: highlight_lines.clone(),
tabs: args.tabs,
wrap: args.wrap,
show_all: args.show_all,
use_color,
width,
language_name: &syntax.name,
cursor,
line_numbers: args.line_numbers,
markdown: resolve_markdown(&args, path),
};
print(&mut stdout, input, &contents, &mut hl, &cfg)?;
stdout.flush()?;
}
Ok(())
}
fn run_interactive(
args: &Cli,
syntax_set: &syntect::parsing::SyntaxSet,
theme_set: &syntect::highlighting::ThemeSet,
) -> Result<()> {
if args.files.len() > 1 {
anyhow::bail!("interactive mode supports a single file");
}
let path = match args.files.first() {
Some(p) if p.as_os_str() != "-" => p.clone(),
_ => anyhow::bail!("interactive mode requires a file path"),
};
let input = InputKind::from_path(&path);
let contents = input.read(args.encoding)?;
let first_line = contents.lines().next();
let syntax = syntax::detect_syntax(syntax_set, Some(&path), args.language.as_deref(), first_line);
let theme = resolve_theme(theme_set, args.theme.as_deref());
let is_md = markdown::is_markdown_path(&path);
let initial_markdown = resolve_markdown(args, Some(&path));
let can_toggle = is_md || initial_markdown;
interactive::run(
&input.display_name(),
&contents,
syntax,
syntax_set,
theme,
args.line_numbers,
args.tabs,
args.show_all,
args.top_pad,
initial_markdown,
can_toggle,
!args.no_gutter,
)
}
fn resolve_markdown(args: &Cli, path: Option<&std::path::Path>) -> bool {
if args.no_markdown {
return false;
}
if args.markdown {
return true;
}
if args.markdown_on_extension {
return path.map(markdown::is_markdown_path).unwrap_or(false);
}
false
}
pub fn term_width() -> usize {
crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80)
}