langcodec-cli 0.12.0

A universal CLI tool for converting and inspecting localization files (Apple, Android, CSV, etc.)
Documentation
use atty::Stream;
use clap::builder::styling::{AnsiColor, Effects, Styles};
use crossterm::style::{Attribute, Color, Stylize, style};
use std::fmt::Display;
use unicode_width::UnicodeWidthStr;

#[derive(Clone, Copy)]
pub enum Tone {
    Success,
    Error,
    Warning,
    Info,
    Accent,
    Muted,
}

pub fn clap_styles() -> Styles {
    Styles::styled()
        .header(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
        .usage(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
        .literal(AnsiColor::Yellow.on_default().effects(Effects::BOLD))
        .placeholder(AnsiColor::Green.on_default())
        .valid(AnsiColor::Green.on_default().effects(Effects::BOLD))
        .invalid(AnsiColor::Red.on_default().effects(Effects::BOLD))
        .error(AnsiColor::Red.on_default().effects(Effects::BOLD))
}

fn colors_enabled(stream: Stream) -> bool {
    std::env::var_os("NO_COLOR").is_none() && atty::is(stream)
}

pub fn stdout_styled() -> bool {
    colors_enabled(Stream::Stdout)
}

pub fn stderr_styled() -> bool {
    colors_enabled(Stream::Stderr)
}

fn tone_color(tone: Tone) -> Color {
    match tone {
        Tone::Success => Color::Green,
        Tone::Error => Color::Red,
        Tone::Warning => Color::Yellow,
        Tone::Info => Color::Blue,
        Tone::Accent => Color::Cyan,
        Tone::Muted => Color::DarkGrey,
    }
}

fn tone_label(tone: Tone) -> &'static str {
    match tone {
        Tone::Success => "OK",
        Tone::Error => "ERR",
        Tone::Warning => "WARN",
        Tone::Info => "INFO",
        Tone::Accent => "NOTE",
        Tone::Muted => "··",
    }
}

fn plain_prefix(tone: Tone) -> &'static str {
    match tone {
        Tone::Success => "",
        Tone::Error => "",
        Tone::Warning => "⚠️",
        Tone::Info => "ℹ️",
        Tone::Accent => "",
        Tone::Muted => "·",
    }
}

pub fn tone_text(text: &str, tone: Tone) -> String {
    if stdout_styled() {
        format!(
            "{}",
            style(text)
                .with(tone_color(tone))
                .attribute(Attribute::Bold)
        )
    } else {
        text.to_string()
    }
}

pub fn accent(text: &str) -> String {
    if stdout_styled() {
        format!(
            "{}",
            style(text).with(Color::Cyan).attribute(Attribute::Bold)
        )
    } else {
        text.to_string()
    }
}

pub fn muted(text: &str) -> String {
    if stdout_styled() {
        format!("{}", style(text).with(Color::DarkGrey))
    } else {
        text.to_string()
    }
}

pub fn divider(width: usize) -> String {
    if stdout_styled() {
        format!("{}", style("".repeat(width)).with(Color::DarkGrey))
    } else {
        "-".repeat(width)
    }
}

pub fn header(title: &str) -> String {
    if stdout_styled() {
        format!(
            "{}\n{}",
            accent(title),
            divider(title.chars().count().max(24))
        )
    } else {
        format!("=== {} ===", title)
    }
}

pub fn section(title: &str) -> String {
    if stdout_styled() {
        format!("\n{}", accent(title))
    } else {
        format!("\n=== {} ===", title)
    }
}

pub fn key_value(label: &str, value: impl Display) -> String {
    if stdout_styled() {
        let width = UnicodeWidthStr::width(label).min(18);
        let padding = 18usize.saturating_sub(width).max(1);
        format!("{}{}{}", muted(label), " ".repeat(padding), value)
    } else {
        format!("{label}: {value}")
    }
}

pub fn status_line_stdout(tone: Tone, message: &str) -> String {
    if stdout_styled() {
        format!(
            "{} {}",
            style(format!(" {} ", tone_label(tone)))
                .with(tone_color(tone))
                .attribute(Attribute::Bold),
            message
        )
    } else {
        format!("{} {}", plain_prefix(tone), message)
    }
}

pub fn status_line_stderr(tone: Tone, message: &str) -> String {
    if stderr_styled() {
        format!(
            "{} {}",
            style(format!(" {} ", tone_label(tone)))
                .with(tone_color(tone))
                .attribute(Attribute::Bold),
            message
        )
    } else {
        format!("{} {}", plain_prefix(tone), message)
    }
}

pub fn progress_bar(ratio: f64, width: usize) -> String {
    let clamped = ratio.clamp(0.0, 1.0);
    if stdout_styled() {
        let filled = (clamped * width as f64).round() as usize;
        let empty = width.saturating_sub(filled);
        let left = format!("{}", style("".repeat(filled)).with(Color::Green));
        let right = format!("{}", style("".repeat(empty)).with(Color::DarkGrey));
        format!("{left}{right}")
    } else {
        let percent = (clamped * 100.0).round() as usize;
        format!("{percent}%")
    }
}