ansi-align 0.2.2

Text alignment library with ANSI escape sequence and Unicode support
Documentation
use ansi_align::{AlignOptions, Alignment, ansi_align_with_options};
use clap::{Parser, ValueEnum};
use colored::*;
use std::fs;
use std::io::{self, Read};
use thiserror::Error;

// Constants for better maintainability
const DEFAULT_BORDER_PADDING: usize = 1;
const DEMO_SEPARATOR_WIDTH: usize = 50;

// Custom error types for better error handling
#[derive(Error, Debug)]
enum CliError {
    #[error("Failed to read file '{path}': {source}")]
    FileRead {
        path: String,
        source: std::io::Error,
    },
    #[error("Failed to read from stdin: {0}")]
    StdinRead(std::io::Error),
    #[error("File not found: {0}")]
    FileNotFound(String),
}

// Border configuration for customizable styling
#[derive(Clone)]
struct BorderConfig {
    top_left: char,
    top_right: char,
    bottom_left: char,
    bottom_right: char,
    horizontal: char,
    vertical: char,
    padding: usize,
}

impl Default for BorderConfig {
    fn default() -> Self {
        Self {
            top_left: '',
            top_right: '',
            bottom_left: '',
            bottom_right: '',
            horizontal: '',
            vertical: '',
            padding: DEFAULT_BORDER_PADDING,
        }
    }
}

// Border renderer for better separation of concerns
struct BorderRenderer {
    config: BorderConfig,
    quiet: bool,
}

impl BorderRenderer {
    fn new(config: BorderConfig, quiet: bool) -> Self {
        Self { config, quiet }
    }

    fn calculate_content_width(text: &str) -> usize {
        text.lines()
            .map(string_width::string_width)
            .max()
            .unwrap_or(0)
    }

    fn render_border(&self, text: &str) {
        let content_width = Self::calculate_content_width(text);
        let border_width = content_width + (self.config.padding * 2);

        if !self.quiet {
            self.print_top_border(border_width);
        }

        self.print_content_lines(text);

        if !self.quiet {
            self.print_bottom_border(border_width);
        }
    }

    fn print_top_border(&self, width: usize) {
        println!(
            "{}{}{}",
            self.config.top_left.to_string().cyan().bold(),
            self.config.horizontal.to_string().repeat(width).cyan(),
            self.config.top_right.to_string().cyan().bold()
        );
    }

    fn print_content_lines(&self, text: &str) {
        for line in text.lines() {
            if !self.quiet {
                print!("{} ", self.config.vertical.to_string().cyan().bold());
            }
            print!("{}", line);
            if !self.quiet {
                print!(" {}", self.config.vertical.to_string().cyan().bold());
            }
            println!();
        }
    }

    fn print_bottom_border(&self, width: usize) {
        println!(
            "{}{}{}",
            self.config.bottom_left.to_string().cyan().bold(),
            self.config.horizontal.to_string().repeat(width).cyan(),
            self.config.bottom_right.to_string().cyan().bold()
        );
    }
}

// Demo section structure for better organization
struct DemoSection {
    title: String,
    content: Box<dyn Fn()>,
}

impl DemoSection {
    fn new(title: &str, content: Box<dyn Fn()>) -> Self {
        Self {
            title: title.to_string(),
            content,
        }
    }

    fn display(&self) {
        println!("{}", self.title.bold().blue());
        (self.content)();
        println!("\n{}", "".repeat(DEMO_SEPARATOR_WIDTH).dimmed());
    }
}

/// A beautiful CLI tool for aligning text with ANSI and Unicode support
#[derive(Parser)]
#[command(
    name = "ansi-align",
    version = "0.2.0",
    about = "🎨 Beautiful text alignment with ANSI escape sequences and Unicode support",
    long_about = "A powerful CLI tool that aligns text while preserving ANSI colors and properly handling Unicode characters including CJK. Perfect for creating beautiful terminal output, aligning code, or formatting data."
)]
struct Cli {
    /// Text to align (use '-' to read from stdin)
    #[arg(value_name = "TEXT")]
    text: Option<String>,

    /// Alignment direction
    #[arg(short, long, default_value = "center")]
    align: AlignmentArg,

    /// Character to use for padding
    #[arg(short, long, default_value = " ")]
    pad: char,

    /// String to split lines on
    #[arg(short, long, default_value = "\\n")]
    split: String,

    /// Read from file instead of argument
    #[arg(short, long, value_name = "FILE")]
    file: Option<String>,

    /// Add colorful borders around the output
    #[arg(short, long)]
    border: bool,

    /// Show example usage with colors
    #[arg(long)]
    demo: bool,

    /// Quiet mode - no decorative output
    #[arg(short, long)]
    quiet: bool,
}

#[derive(Clone, Copy, ValueEnum)]
enum AlignmentArg {
    Left,
    Center,
    Right,
}

impl From<AlignmentArg> for Alignment {
    fn from(arg: AlignmentArg) -> Self {
        match arg {
            AlignmentArg::Left => Alignment::Left,
            AlignmentArg::Center => Alignment::Center,
            AlignmentArg::Right => Alignment::Right,
        }
    }
}

impl Cli {
    fn validate(&self) -> Result<(), CliError> {
        if let Some(file_path) = &self.file {
            if !std::path::Path::new(file_path).exists() {
                return Err(CliError::FileNotFound(file_path.clone()));
            }
        }
        Ok(())
    }
}

fn main() -> Result<(), CliError> {
    let cli = Cli::parse();

    // Validate CLI arguments
    cli.validate()?;

    if cli.demo {
        show_demo();
        return Ok(());
    }

    let input_text = get_input_text(&cli)?;

    if input_text.trim().is_empty() {
        if !cli.quiet {
            eprintln!("{}", "⚠️  No input text provided".yellow());
        }
        return Ok(());
    }

    let split_str = process_escape_sequences(&cli.split);
    let alignment = cli.align.into();
    let options = AlignOptions::new(alignment)
        .with_split(split_str)
        .with_pad(cli.pad);

    let aligned_text = ansi_align_with_options(&input_text, &options);

    if cli.border {
        let border_config = BorderConfig::default();
        let renderer = BorderRenderer::new(border_config, cli.quiet);
        renderer.render_border(&aligned_text);
    } else {
        println!("{}", aligned_text);
    }

    Ok(())
}

// Optimized escape sequence processing
fn process_escape_sequences(text: &str) -> String {
    text.replace("\\n", "\n").replace("\\t", "\t")
}

fn get_input_text(cli: &Cli) -> Result<String, CliError> {
    if let Some(file_path) = &cli.file {
        fs::read_to_string(file_path).map_err(|e| CliError::FileRead {
            path: file_path.clone(),
            source: e,
        })
    } else if let Some(text) = &cli.text {
        if text == "-" {
            let mut buffer = String::new();
            io::stdin()
                .read_to_string(&mut buffer)
                .map_err(CliError::StdinRead)?;
            Ok(process_escape_sequences(&buffer))
        } else {
            Ok(process_escape_sequences(text))
        }
    } else {
        let mut buffer = String::new();
        io::stdin()
            .read_to_string(&mut buffer)
            .map_err(CliError::StdinRead)?;
        Ok(process_escape_sequences(&buffer))
    }
}

// Demo section functions for better organization
fn show_basic_alignment() {
    let basic_text = "Hello\nWorld\nRust";

    for (label, alignment) in [
        ("Left:", Alignment::Left),
        ("Center:", Alignment::Center),
        ("Right:", Alignment::Right),
    ] {
        println!("\n{}", label.green());
        println!(
            "{}",
            ansi_align_with_options(basic_text, &AlignOptions::new(alignment))
        );
    }
}

fn show_color_support() {
    let colored_text = format!(
        "{}\n{}\n{}",
        "Red Text".red().bold(),
        "Green Text".green().italic(),
        "Blue Text".blue().underline()
    );

    println!("\n{}", "Center aligned with colors:".green());
    println!(
        "{}",
        ansi_align_with_options(&colored_text, &AlignOptions::new(Alignment::Center))
    );
}

fn show_unicode_support() {
    let unicode_text = "\n古古古\nHello 世界";

    println!("\n{}", "Right aligned Unicode:".green());
    println!(
        "{}",
        ansi_align_with_options(unicode_text, &AlignOptions::new(Alignment::Right))
    );
}

fn show_custom_options() {
    let custom_text = "Name|Age|Location";
    let options = AlignOptions::new(Alignment::Center)
        .with_split("|")
        .with_pad('.');

    println!("\n{}", "Custom separator '|' and padding '.':".green());
    println!("{}", ansi_align_with_options(custom_text, &options));
}

fn show_menu_example() {
    let menu = format!(
        "{}\n{}\n{}\n{}",
        "🏠 Home".cyan(),
        "📋 About Us".yellow(),
        "📞 Contact".green(),
        "⚙️ Settings".magenta()
    );

    println!("\n{}", "Center aligned menu:".green());
    println!(
        "{}",
        ansi_align_with_options(&menu, &AlignOptions::new(Alignment::Center))
    );
}

fn show_usage_examples() {
    println!(
        "\n{}",
        "✨ Try it yourself with different options!".bold().green()
    );
    println!("{}", "Examples:".dimmed());
    println!(
        "{}",
        "  cargo run --example cli_tool -- \"Hello\\nWorld\" --align center".dimmed()
    );
    println!(
        "{}",
        "  echo \"Line 1\\nLonger Line 2\" | cargo run --example cli_tool -- - --border".dimmed()
    );
    println!(
        "{}",
        "  cargo run --example cli_tool -- --file README.md --align right --pad '.'".dimmed()
    );
}

fn show_demo() {
    println!(
        "{}",
        "🎨 ansi-align Demo - Beautiful Text Alignment"
            .bold()
            .magenta()
    );
    println!();

    let sections = vec![
        DemoSection::new("📝 Basic Alignment:", Box::new(show_basic_alignment)),
        DemoSection::new("🌈 ANSI Color Support:", Box::new(show_color_support)),
        DemoSection::new("🌏 Unicode Support:", Box::new(show_unicode_support)),
        DemoSection::new("⚙️ Custom Options:", Box::new(show_custom_options)),
        DemoSection::new("📋 Menu Example:", Box::new(show_menu_example)),
    ];

    for section in sections {
        section.display();
    }

    show_usage_examples();
}