langcodec_cli/tui/
reporter.rs1use 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}