zlayer_builder/tui/
logger.rs1use super::BuildEvent;
8use zlayer_tui::logger::{colorize, detect_color_support};
9use zlayer_tui::palette::ansi;
10
11#[derive(Debug, Clone)]
38pub struct PlainLogger {
39 verbose: bool,
41 color: bool,
43}
44
45impl Default for PlainLogger {
46 fn default() -> Self {
47 Self::new(false)
48 }
49}
50
51impl PlainLogger {
52 #[must_use]
59 pub fn new(verbose: bool) -> Self {
60 Self {
61 verbose,
62 color: detect_color_support(),
63 }
64 }
65
66 #[must_use]
68 pub fn with_color(verbose: bool, color: bool) -> Self {
69 Self { verbose, color }
70 }
71
72 fn colorize(&self, text: &str, color: &str) -> String {
77 colorize(text, color, self.color)
78 }
79
80 pub fn handle_event(&self, event: &BuildEvent) {
82 match event {
83 BuildEvent::BuildStarted {
84 total_stages,
85 total_instructions,
86 } => {
87 let header = format!(
88 "==> Building {total_stages} stages, {total_instructions} instructions total"
89 );
90 println!("{}", self.colorize(&header, ansi::CYAN));
91 }
92
93 BuildEvent::StageStarted {
94 index,
95 name,
96 base_image,
97 } => {
98 let stage_name = name.as_deref().unwrap_or("unnamed");
99 let header = format!("==> Stage {}: {} ({})", index + 1, stage_name, base_image);
100 println!("{}", self.colorize(&header, ansi::CYAN));
101 }
102
103 BuildEvent::InstructionStarted { instruction, .. } => {
104 let line = format!(" -> {instruction}");
105 println!("{}", self.colorize(&line, ansi::YELLOW));
106 }
107
108 BuildEvent::Output { line, is_stderr } if self.verbose => {
109 if *is_stderr {
110 eprintln!(" {}", self.colorize(line, ansi::DIM));
111 } else {
112 println!(" {line}");
113 }
114 }
115
116 BuildEvent::Output { .. } | BuildEvent::BuildPlan { .. } => {}
120
121 BuildEvent::InstructionComplete { cached, .. } => {
122 if *cached && self.verbose {
123 println!(" {}", self.colorize("[cached]", ansi::CYAN));
124 }
125 }
126
127 BuildEvent::StageComplete { index } => {
128 if self.verbose {
129 let line = format!(" Stage {} complete", index + 1);
130 println!("{}", self.colorize(&line, ansi::GREEN));
131 }
132 }
133
134 BuildEvent::BuildComplete { image_id } => {
135 println!();
136 let success = format!("Build complete: {image_id}");
137 println!("{}", self.colorize(&success, ansi::GREEN));
138 }
139
140 BuildEvent::BuildFailed { error } => {
141 println!();
142 let failure = format!("Build failed: {error}");
143 eprintln!("{}", self.colorize(&failure, ansi::RED));
144 }
145 }
146 }
147
148 pub fn process_events<I>(&self, events: I)
152 where
153 I: IntoIterator<Item = BuildEvent>,
154 {
155 for event in events {
156 self.handle_event(&event);
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_plain_logger_creation() {
167 let logger = PlainLogger::new(false);
168 assert!(!logger.verbose);
169
170 let verbose_logger = PlainLogger::new(true);
171 assert!(verbose_logger.verbose);
172 }
173
174 #[test]
175 fn test_with_color() {
176 let logger = PlainLogger::with_color(false, true);
177 assert!(logger.color);
178
179 let no_color_logger = PlainLogger::with_color(false, false);
180 assert!(!no_color_logger.color);
181 }
182
183 #[test]
184 fn test_colorize_enabled() {
185 let logger = PlainLogger::with_color(false, true);
186 let result = logger.colorize("test", ansi::GREEN);
187 assert!(result.contains("\x1b[32m"));
188 assert!(result.contains("\x1b[0m"));
189 assert!(result.contains("test"));
190 }
191
192 #[test]
193 fn test_colorize_disabled() {
194 let logger = PlainLogger::with_color(false, false);
195 let result = logger.colorize("test", ansi::GREEN);
196 assert_eq!(result, "test");
197 assert!(!result.contains("\x1b["));
198 }
199
200 #[test]
201 fn test_handle_event_does_not_panic() {
202 let logger = PlainLogger::with_color(true, false);
204
205 logger.handle_event(&BuildEvent::StageStarted {
207 index: 0,
208 name: Some("builder".to_string()),
209 base_image: "alpine".to_string(),
210 });
211
212 logger.handle_event(&BuildEvent::InstructionStarted {
213 stage: 0,
214 index: 0,
215 instruction: "RUN echo hello".to_string(),
216 });
217
218 logger.handle_event(&BuildEvent::Output {
219 line: "hello".to_string(),
220 is_stderr: false,
221 });
222
223 logger.handle_event(&BuildEvent::Output {
224 line: "warning".to_string(),
225 is_stderr: true,
226 });
227
228 logger.handle_event(&BuildEvent::InstructionComplete {
229 stage: 0,
230 index: 0,
231 cached: true,
232 });
233
234 logger.handle_event(&BuildEvent::StageComplete { index: 0 });
235
236 logger.handle_event(&BuildEvent::BuildComplete {
237 image_id: "sha256:abc".to_string(),
238 });
239
240 logger.handle_event(&BuildEvent::BuildFailed {
241 error: "test error".to_string(),
242 });
243 }
244
245 #[test]
246 fn test_default() {
247 let logger = PlainLogger::default();
248 assert!(!logger.verbose);
249 }
250}