cuenv_events/renderers/
cli.rs1#![allow(clippy::print_stdout, clippy::print_stderr)]
7
8use crate::bus::EventReceiver;
9use crate::event::{
10 CiEvent, CommandEvent, CuenvEvent, EventCategory, InteractiveEvent, OutputEvent, Stream,
11 SystemEvent, TaskEvent,
12};
13use std::io::{self, IsTerminal, Write};
14
15#[derive(Debug, Clone)]
17pub struct CliRendererConfig {
18 pub colors: bool,
20 pub verbose: bool,
22}
23
24impl Default for CliRendererConfig {
25 fn default() -> Self {
26 Self {
27 colors: io::stdout().is_terminal(),
28 verbose: false,
29 }
30 }
31}
32
33#[derive(Debug)]
35pub struct CliRenderer {
36 config: CliRendererConfig,
37}
38
39impl CliRenderer {
40 #[must_use]
42 pub fn new() -> Self {
43 Self {
44 config: CliRendererConfig::default(),
45 }
46 }
47
48 #[must_use]
50 pub fn with_config(config: CliRendererConfig) -> Self {
51 Self { config }
52 }
53
54 pub async fn run(self, mut receiver: EventReceiver) {
56 while let Some(event) = receiver.recv().await {
57 self.render(&event);
58 }
59 }
60
61 pub fn render(&self, event: &CuenvEvent) {
63 match &event.category {
64 EventCategory::Task(task_event) => self.render_task(task_event),
65 EventCategory::Ci(ci_event) => self.render_ci(ci_event),
66 EventCategory::Command(cmd_event) => self.render_command(cmd_event),
67 EventCategory::Interactive(interactive_event) => {
68 self.render_interactive(interactive_event);
69 }
70 EventCategory::System(system_event) => self.render_system(system_event),
71 EventCategory::Output(output_event) => self.render_output(output_event),
72 }
73 }
74
75 fn render_task(&self, event: &TaskEvent) {
76 match event {
77 TaskEvent::Started {
78 name,
79 command,
80 hermetic,
81 } => {
82 let hermetic_indicator = if *hermetic { " (hermetic)" } else { "" };
83 eprintln!("> [{name}] {command}{hermetic_indicator}");
84 }
85 TaskEvent::CacheHit { name, .. } => {
86 eprintln!("> [{name}] (cached)");
87 }
88 TaskEvent::CacheMiss { name } => {
89 if self.config.verbose {
90 eprintln!("> [{name}] cache miss, executing...");
91 }
92 }
93 TaskEvent::Output {
94 stream, content, ..
95 } => match stream {
96 Stream::Stdout => {
97 print!("{content}");
98 let _ = io::stdout().flush();
99 }
100 Stream::Stderr => {
101 eprint!("{content}");
102 let _ = io::stderr().flush();
103 }
104 },
105 TaskEvent::Completed {
106 name,
107 success,
108 duration_ms,
109 ..
110 } => {
111 if self.config.verbose {
112 let status = if *success { "completed" } else { "failed" };
113 eprintln!("> [{name}] {status} in {duration_ms}ms");
114 }
115 }
116 TaskEvent::GroupStarted {
117 name,
118 sequential,
119 task_count,
120 } => {
121 let mode = if *sequential {
122 "sequential"
123 } else {
124 "parallel"
125 };
126 eprintln!("> Running {mode} group: {name} ({task_count} tasks)");
127 }
128 TaskEvent::GroupCompleted {
129 name,
130 success,
131 duration_ms,
132 } => {
133 if self.config.verbose {
134 let status = if *success { "completed" } else { "failed" };
135 eprintln!("> Group {name} {status} in {duration_ms}ms");
136 }
137 }
138 }
139 }
140
141 fn render_ci(&self, event: &CiEvent) {
142 let _ = &self.config; match event {
144 CiEvent::ContextDetected {
145 provider,
146 event_type,
147 ref_name,
148 } => {
149 println!("Context: {provider} (event: {event_type}, ref: {ref_name})");
150 }
151 CiEvent::ChangedFilesFound { count } => {
152 println!("Changed files: {count}");
153 }
154 CiEvent::ProjectsDiscovered { count } => {
155 println!("Found {count} projects");
156 }
157 CiEvent::ProjectSkipped { path, reason } => {
158 println!("Project {path}: {reason}");
159 }
160 CiEvent::TaskExecuting { task, .. } => {
161 println!(" -> Executing {task}");
162 }
163 CiEvent::TaskResult {
164 task,
165 success,
166 error,
167 ..
168 } => {
169 if *success {
170 println!(" -> {task} passed");
171 } else if let Some(err) = error {
172 println!(" -> {task} failed: {err}");
173 } else {
174 println!(" -> {task} failed");
175 }
176 }
177 CiEvent::ReportGenerated { path } => {
178 println!("Report written to: {path}");
179 }
180 }
181 }
182
183 fn render_command(&self, event: &CommandEvent) {
184 match event {
185 CommandEvent::Started { command, .. } => {
186 if self.config.verbose {
187 eprintln!("Starting command: {command}");
188 }
189 }
190 CommandEvent::Progress {
191 progress, message, ..
192 } => {
193 if self.config.verbose {
194 let pct = progress * 100.0;
195 eprintln!("[{pct:.0}%] {message}");
196 }
197 }
198 CommandEvent::Completed {
199 command,
200 success,
201 duration_ms,
202 } => {
203 if self.config.verbose {
204 let status = if *success { "completed" } else { "failed" };
205 eprintln!("Command {command} {status} in {duration_ms}ms");
206 }
207 }
208 }
209 }
210
211 fn render_interactive(&self, event: &InteractiveEvent) {
212 let _ = &self.config; match event {
214 InteractiveEvent::PromptRequested {
215 message, options, ..
216 } => {
217 println!("{message}");
218 for (i, option) in options.iter().enumerate() {
219 println!(" [{i}] {option}");
220 }
221 print!("> ");
222 let _ = io::stdout().flush();
223 }
224 InteractiveEvent::PromptResolved { .. } => {
225 }
227 InteractiveEvent::WaitProgress {
228 target,
229 elapsed_secs,
230 } => {
231 eprint!("\r\x1b[KWaiting for `{target}`... [{elapsed_secs}s]");
232 let _ = io::stderr().flush();
233 }
234 }
235 }
236
237 fn render_system(&self, event: &SystemEvent) {
238 match event {
239 SystemEvent::SupervisorLog { tag, message } => {
240 eprintln!("[{tag}] {message}");
241 }
242 SystemEvent::Shutdown => {
243 if self.config.verbose {
244 eprintln!("System shutdown");
245 }
246 }
247 }
248 }
249
250 fn render_output(&self, event: &OutputEvent) {
251 let _ = &self.config; match event {
253 OutputEvent::Stdout { content } => {
254 println!("{content}");
255 }
256 OutputEvent::Stderr { content } => {
257 eprintln!("{content}");
258 }
259 }
260 }
261}
262
263impl Default for CliRenderer {
264 fn default() -> Self {
265 Self::new()
266 }
267}