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;
const DEFAULT_BORDER_PADDING: usize = 1;
const DEMO_SEPARATOR_WIDTH: usize = 50;
#[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),
}
#[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,
}
}
}
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()
);
}
}
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());
}
}
#[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 {
#[arg(value_name = "TEXT")]
text: Option<String>,
#[arg(short, long, default_value = "center")]
align: AlignmentArg,
#[arg(short, long, default_value = " ")]
pad: char,
#[arg(short, long, default_value = "\\n")]
split: String,
#[arg(short, long, value_name = "FILE")]
file: Option<String>,
#[arg(short, long)]
border: bool,
#[arg(long)]
demo: bool,
#[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();
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(())
}
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))
}
}
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();
}