use clap::Parser;
use console::{style, Term};
use regex::Regex;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, ThemeSet};
use syntect::parsing::SyntaxSet;
use syntect::util::as_24_bit_terminal_escaped;
#[derive(Parser)]
#[command(name = "code-preview")]
#[command(about = "A CLI tool to preview files with syntax highlighting and search", long_about = None)]
struct Args {
#[arg(value_name = "FILE")]
path: String,
#[arg(short, long)]
search: Option<String>,
#[arg(short, long, default_value = "base16-ocean.dark")]
theme: String,
#[arg(long)]
list_themes: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
if args.list_themes {
println!("Available themes:");
for theme in ts.themes.keys() {
println!(" - {}", theme);
}
return Ok(());
}
let path = Path::new(&args.path);
if !path.exists() {
eprintln!("Error: File '{}' not found.", args.path);
std::process::exit(1);
}
let syntax = ps
.find_syntax_for_file(path)?
.unwrap_or_else(|| ps.find_syntax_plain_text());
let theme = ts.themes.get(&args.theme).unwrap_or_else(|| {
eprintln!("Warning: Theme '{}' not found, using default.", args.theme);
&ts.themes["base16-ocean.dark"]
});
let mut h = HighlightLines::new(syntax, theme);
let file = File::open(path)?;
let reader = BufReader::new(file);
let lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
let search_regex = args.search.as_ref().map(|s| Regex::new(s)).transpose()?;
let term = Term::stdout();
let (_height, _width) = term.size();
println!("Previewing: {}", style(&args.path).cyan().bold());
println!("{}", "─".repeat(args.path.len() + 12));
for (i, line) in lines.iter().enumerate() {
let line_number = i + 1;
let line_with_ending = format!("{}\n", line);
let ranges: Vec<(Style, &str)> = h.highlight_line(&line_with_ending, &ps)?;
let mut escaped = as_24_bit_terminal_escaped(&ranges[..], false);
if escaped.ends_with('\n') {
escaped.pop();
}
let highlighted_line = if let Some(ref re) = search_regex {
if re.is_match(line) {
let mut result = String::new();
let mut last_end = 0;
for mat in re.find_iter(line) {
result.push_str(&line[last_end..mat.start()]);
result.push_str(&style(&line[mat.start()..mat.end()]).on_yellow().black().to_string());
last_end = mat.end();
}
result.push_str(&line[last_end..]);
style(result).bold().to_string()
} else {
escaped
}
} else {
escaped
};
println!("{:>4} │ {}", style(line_number).dim(), highlighted_line);
}
Ok(())
}