Skip to main content

langcodec_cli/tui/
reporter.rs

1use std::{
2    io::{self, IsTerminal, Write},
3    sync::mpsc::{self, Sender},
4    thread::{self, JoinHandle},
5};
6
7use crate::{
8    tui::{
9        DashboardEvent, DashboardInit, DashboardKind, DashboardLogTone, DashboardState,
10        terminal::run_dashboard,
11    },
12    ui,
13};
14
15pub trait RunReporter {
16    fn emit(&mut self, event: DashboardEvent);
17    fn finish(&mut self) -> Result<(), String>;
18}
19
20pub struct PlainReporter {
21    state: DashboardState,
22    interactive: bool,
23    last_width: usize,
24}
25
26impl PlainReporter {
27    pub fn new(init: DashboardInit) -> Self {
28        Self {
29            state: DashboardState::new(init),
30            interactive: io::stderr().is_terminal(),
31            last_width: 0,
32        }
33    }
34
35    fn update_status_line(&mut self) {
36        let line = match self.state.kind {
37            DashboardKind::Translate => {
38                let counts = self.state.counts();
39                let skipped = self
40                    .state
41                    .summary_value("Skipped total")
42                    .or_else(|| self.state.summary_value("Skipped"))
43                    .unwrap_or("0");
44                format!(
45                    "Progress: {}/{} translated={} skipped={} failed={}",
46                    counts.succeeded + counts.failed,
47                    self.state.items.len(),
48                    counts.succeeded,
49                    skipped,
50                    counts.failed
51                )
52            }
53            DashboardKind::Annotate => {
54                let counts = self.state.counts();
55                format!(
56                    "Annotate progress: {}/{} processed generated={} skipped={}",
57                    counts.succeeded + counts.failed + counts.skipped,
58                    self.state.items.len(),
59                    counts.succeeded,
60                    counts.skipped
61                )
62            }
63        };
64        if self.interactive {
65            let padding = self.last_width.saturating_sub(line.len());
66            eprint!("\r{}{}", line, " ".repeat(padding));
67            let _ = io::stderr().flush();
68            self.last_width = line.len();
69        } else {
70            eprintln!("{}", line);
71        }
72    }
73
74    fn finish_line(&mut self) {
75        if self.interactive && self.last_width > 0 {
76            eprintln!();
77            self.last_width = 0;
78        }
79    }
80
81    fn print_log(&mut self, tone: DashboardLogTone, message: &str) {
82        self.finish_line();
83        match self.state.kind {
84            DashboardKind::Translate => {
85                if matches!(tone, DashboardLogTone::Error | DashboardLogTone::Warning) {
86                    eprintln!("{}", ui::status_line_stderr(map_tone(tone), message));
87                }
88            }
89            DashboardKind::Annotate => {
90                eprintln!("{}", message);
91            }
92        }
93    }
94}
95
96impl RunReporter for PlainReporter {
97    fn emit(&mut self, event: DashboardEvent) {
98        if let DashboardEvent::Log { tone, message } = &event {
99            self.print_log(*tone, message);
100        }
101        self.state.apply(event.clone());
102        match event {
103            DashboardEvent::UpdateItem { .. } | DashboardEvent::SummaryRows { .. } => {
104                self.update_status_line();
105            }
106            DashboardEvent::Completed => self.finish_line(),
107            DashboardEvent::Log { .. } => {}
108        }
109    }
110
111    fn finish(&mut self) -> Result<(), String> {
112        self.finish_line();
113        Ok(())
114    }
115}
116
117fn map_tone(tone: DashboardLogTone) -> ui::Tone {
118    match tone {
119        DashboardLogTone::Info => ui::Tone::Info,
120        DashboardLogTone::Success => ui::Tone::Success,
121        DashboardLogTone::Warning => ui::Tone::Warning,
122        DashboardLogTone::Error => ui::Tone::Error,
123    }
124}
125
126pub(super) enum DashboardMessage {
127    Event(DashboardEvent),
128}
129
130pub struct TuiReporter {
131    sender: Sender<DashboardMessage>,
132    join_handle: Option<JoinHandle<Result<(), String>>>,
133}
134
135impl TuiReporter {
136    pub fn new(init: DashboardInit) -> Result<Self, String> {
137        let (tx, rx) = mpsc::channel::<DashboardMessage>();
138        let join_handle = thread::spawn(move || run_dashboard(DashboardState::new(init), rx));
139        Ok(Self {
140            sender: tx,
141            join_handle: Some(join_handle),
142        })
143    }
144}
145
146impl RunReporter for TuiReporter {
147    fn emit(&mut self, event: DashboardEvent) {
148        let _ = self.sender.send(DashboardMessage::Event(event));
149    }
150
151    fn finish(&mut self) -> Result<(), String> {
152        let _ = self
153            .sender
154            .send(DashboardMessage::Event(DashboardEvent::Completed));
155        if let Some(handle) = self.join_handle.take() {
156            match handle.join() {
157                Ok(result) => result,
158                Err(_) => Err("TUI thread panicked".to_string()),
159            }
160        } else {
161            Ok(())
162        }
163    }
164}