code-preview 0.1.0

A powerful CLI tool to preview code files with syntax highlighting, line numbers, and built-in search.
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 {
    /// The file to preview
    #[arg(value_name = "FILE")]
    path: String,

    /// Search for a pattern in the file
    #[arg(short, long)]
    search: Option<String>,

    /// The theme to use for highlighting (default: base16-ocean.dark)
    #[arg(short, long, default_value = "base16-ocean.dark")]
    theme: String,

    /// List all available themes
    #[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);
        
        // Remove trailing newline added by highlight_line for easier formatting
        if escaped.ends_with('\n') {
            escaped.pop();
        }

        let highlighted_line = if let Some(ref re) = search_regex {
            if re.is_match(line) {
                // Simplistic highlight: wrap matches in red
                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..]);
                // This replaces the highlighted syntax with plain text search highlight
                // In a more advanced version, we'd mix both.
                style(result).bold().to_string()
            } else {
                escaped
            }
        } else {
            escaped
        };

        println!("{:>4}{}", style(line_number).dim(), highlighted_line);
    }

    Ok(())
}