ecd 0.1.1

Fast CLI to detect text file encodings (UTF-8, GBK, Shift_JIS, and 35+ more)
Documentation
use std::io::{self, Write};
use std::path::Path;

use crate::detect::{Detection, SkipReason};

#[derive(Debug, Clone)]
pub struct FileResult {
    pub path: std::path::PathBuf,
    pub detection: Detection,
}

pub struct OutputConfig<'a> {
    pub single_file_mode: bool,
    pub ignore_encoding: Option<&'a str>,
    pub quiet: bool,
    pub verbose: bool,
}

pub struct Stats {
    pub checked: usize,
    pub printed: usize,
    pub skipped: usize,
    pub ignored: usize,
}

pub fn print_results(
    results: &[FileResult],
    config: &OutputConfig<'_>,
    stderr: &mut dyn Write,
) -> io::Result<Stats> {
    let mut stats = Stats {
        checked: results.len(),
        printed: 0,
        skipped: 0,
        ignored: 0,
    };

    if config.single_file_mode {
        if let Some(result) = results.first() {
            match &result.detection {
                Detection::Encoded(enc) => {
                    if should_ignore(config.ignore_encoding, enc) {
                        stats.ignored += 1;
                    } else if !config.quiet {
                        println!("{enc}");
                        stats.printed += 1;
                    }
                }
                Detection::Skip(reason) => {
                    stats.skipped += 1;
                    if config.verbose {
                        writeln!(
                            stderr,
                            "skip {}: {}",
                            result.path.display(),
                            skip_reason_message(reason)
                        )?;
                    }
                }
            }
        }
        return Ok(stats);
    }

    for result in results {
        match &result.detection {
            Detection::Encoded(enc) => {
                if should_ignore(config.ignore_encoding, enc) {
                    stats.ignored += 1;
                    continue;
                }
                if !config.quiet {
                    println!("[{}] {}", display_encoding(enc), display_path(&result.path));
                    stats.printed += 1;
                }
            }
            Detection::Skip(_) => {
                stats.skipped += 1;
                if !config.quiet {
                    println!("[SKIP] {}", display_path(&result.path));
                }
                if config.verbose
                    && let Detection::Skip(reason) = &result.detection
                {
                    writeln!(
                        stderr,
                        "skip {}: {}",
                        result.path.display(),
                        skip_reason_message(reason)
                    )?;
                }
            }
        }
    }

    Ok(stats)
}

pub fn print_stats(stats: &Stats, stderr: &mut dyn Write) -> io::Result<()> {
    writeln!(
        stderr,
        "checked: {}, printed: {}, skipped: {}, ignored: {}",
        stats.checked, stats.printed, stats.skipped, stats.ignored
    )
}

fn should_ignore(ignore: Option<&str>, encoding: &str) -> bool {
    ignore.is_some_and(|i| i.eq_ignore_ascii_case(encoding))
}

fn display_encoding(enc: &str) -> String {
    enc.to_ascii_uppercase()
}

fn display_path(path: &Path) -> String {
    path.to_string_lossy().into_owned()
}

fn skip_reason_message(reason: &SkipReason) -> &'static str {
    match reason {
        SkipReason::Empty => "empty file",
        SkipReason::Binary => "binary content",
        SkipReason::Unknown => "encoding unknown",
        SkipReason::ReadError(_) => "read error",
    }
}