langcodec-cli 0.12.0

A universal CLI tool for converting and inspecting localization files (Apple, Android, CSV, etc.)
Documentation
use std::{
    io::{self, IsTerminal, Write},
    sync::mpsc::{self, Sender},
    thread::{self, JoinHandle},
};

use crate::{
    tui::{
        DashboardEvent, DashboardInit, DashboardKind, DashboardLogTone, DashboardState,
        terminal::run_dashboard,
    },
    ui,
};

pub trait RunReporter {
    fn emit(&mut self, event: DashboardEvent);
    fn finish(&mut self) -> Result<(), String>;
}

pub struct PlainReporter {
    state: DashboardState,
    interactive: bool,
    last_width: usize,
}

impl PlainReporter {
    pub fn new(init: DashboardInit) -> Self {
        Self {
            state: DashboardState::new(init),
            interactive: io::stderr().is_terminal(),
            last_width: 0,
        }
    }

    fn update_status_line(&mut self) {
        let line = match self.state.kind {
            DashboardKind::Translate => {
                let counts = self.state.counts();
                let skipped = self
                    .state
                    .summary_value("Skipped total")
                    .or_else(|| self.state.summary_value("Skipped"))
                    .unwrap_or("0");
                format!(
                    "Progress: {}/{} translated={} skipped={} failed={}",
                    counts.succeeded + counts.failed,
                    self.state.items.len(),
                    counts.succeeded,
                    skipped,
                    counts.failed
                )
            }
            DashboardKind::Annotate => {
                let counts = self.state.counts();
                format!(
                    "Annotate progress: {}/{} processed generated={} skipped={}",
                    counts.succeeded + counts.failed + counts.skipped,
                    self.state.items.len(),
                    counts.succeeded,
                    counts.skipped
                )
            }
        };
        if self.interactive {
            let padding = self.last_width.saturating_sub(line.len());
            eprint!("\r{}{}", line, " ".repeat(padding));
            let _ = io::stderr().flush();
            self.last_width = line.len();
        } else {
            eprintln!("{}", line);
        }
    }

    fn finish_line(&mut self) {
        if self.interactive && self.last_width > 0 {
            eprintln!();
            self.last_width = 0;
        }
    }

    fn print_log(&mut self, tone: DashboardLogTone, message: &str) {
        self.finish_line();
        match self.state.kind {
            DashboardKind::Translate => {
                if matches!(tone, DashboardLogTone::Error | DashboardLogTone::Warning) {
                    eprintln!("{}", ui::status_line_stderr(map_tone(tone), message));
                }
            }
            DashboardKind::Annotate => {
                eprintln!("{}", message);
            }
        }
    }
}

impl RunReporter for PlainReporter {
    fn emit(&mut self, event: DashboardEvent) {
        if let DashboardEvent::Log { tone, message } = &event {
            self.print_log(*tone, message);
        }
        self.state.apply(event.clone());
        match event {
            DashboardEvent::UpdateItem { .. } | DashboardEvent::SummaryRows { .. } => {
                self.update_status_line();
            }
            DashboardEvent::Completed => self.finish_line(),
            DashboardEvent::Log { .. } => {}
        }
    }

    fn finish(&mut self) -> Result<(), String> {
        self.finish_line();
        Ok(())
    }
}

fn map_tone(tone: DashboardLogTone) -> ui::Tone {
    match tone {
        DashboardLogTone::Info => ui::Tone::Info,
        DashboardLogTone::Success => ui::Tone::Success,
        DashboardLogTone::Warning => ui::Tone::Warning,
        DashboardLogTone::Error => ui::Tone::Error,
    }
}

pub(super) enum DashboardMessage {
    Event(DashboardEvent),
}

pub struct TuiReporter {
    sender: Sender<DashboardMessage>,
    join_handle: Option<JoinHandle<Result<(), String>>>,
}

impl TuiReporter {
    pub fn new(init: DashboardInit) -> Result<Self, String> {
        let (tx, rx) = mpsc::channel::<DashboardMessage>();
        let join_handle = thread::spawn(move || run_dashboard(DashboardState::new(init), rx));
        Ok(Self {
            sender: tx,
            join_handle: Some(join_handle),
        })
    }
}

impl RunReporter for TuiReporter {
    fn emit(&mut self, event: DashboardEvent) {
        let _ = self.sender.send(DashboardMessage::Event(event));
    }

    fn finish(&mut self) -> Result<(), String> {
        let _ = self
            .sender
            .send(DashboardMessage::Event(DashboardEvent::Completed));
        if let Some(handle) = self.join_handle.take() {
            match handle.join() {
                Ok(result) => result,
                Err(_) => Err("TUI thread panicked".to_string()),
            }
        } else {
            Ok(())
        }
    }
}