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 { .. } => {
117 }
119
120 BuildEvent::InstructionComplete { cached, .. } => {
121 if *cached && self.verbose {
122 println!(" {}", self.colorize("[cached]", ansi::CYAN));
123 }
124 }
125
126 BuildEvent::StageComplete { index } => {
127 if self.verbose {
128 let line = format!(" Stage {} complete", index + 1);
129 println!("{}", self.colorize(&line, ansi::GREEN));
130 }
131 }
132
133 BuildEvent::BuildComplete { image_id } => {
134 println!();
135 let success = format!("Build complete: {image_id}");
136 println!("{}", self.colorize(&success, ansi::GREEN));
137 }
138
139 BuildEvent::BuildFailed { error } => {
140 println!();
141 let failure = format!("Build failed: {error}");
142 eprintln!("{}", self.colorize(&failure, ansi::RED));
143 }
144 }
145 }
146
147 pub fn process_events<I>(&self, events: I)
151 where
152 I: IntoIterator<Item = BuildEvent>,
153 {
154 for event in events {
155 self.handle_event(&event);
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_plain_logger_creation() {
166 let logger = PlainLogger::new(false);
167 assert!(!logger.verbose);
168
169 let verbose_logger = PlainLogger::new(true);
170 assert!(verbose_logger.verbose);
171 }
172
173 #[test]
174 fn test_with_color() {
175 let logger = PlainLogger::with_color(false, true);
176 assert!(logger.color);
177
178 let no_color_logger = PlainLogger::with_color(false, false);
179 assert!(!no_color_logger.color);
180 }
181
182 #[test]
183 fn test_colorize_enabled() {
184 let logger = PlainLogger::with_color(false, true);
185 let result = logger.colorize("test", ansi::GREEN);
186 assert!(result.contains("\x1b[32m"));
187 assert!(result.contains("\x1b[0m"));
188 assert!(result.contains("test"));
189 }
190
191 #[test]
192 fn test_colorize_disabled() {
193 let logger = PlainLogger::with_color(false, false);
194 let result = logger.colorize("test", ansi::GREEN);
195 assert_eq!(result, "test");
196 assert!(!result.contains("\x1b["));
197 }
198
199 #[test]
200 fn test_handle_event_does_not_panic() {
201 let logger = PlainLogger::with_color(true, false);
203
204 logger.handle_event(&BuildEvent::StageStarted {
206 index: 0,
207 name: Some("builder".to_string()),
208 base_image: "alpine".to_string(),
209 });
210
211 logger.handle_event(&BuildEvent::InstructionStarted {
212 stage: 0,
213 index: 0,
214 instruction: "RUN echo hello".to_string(),
215 });
216
217 logger.handle_event(&BuildEvent::Output {
218 line: "hello".to_string(),
219 is_stderr: false,
220 });
221
222 logger.handle_event(&BuildEvent::Output {
223 line: "warning".to_string(),
224 is_stderr: true,
225 });
226
227 logger.handle_event(&BuildEvent::InstructionComplete {
228 stage: 0,
229 index: 0,
230 cached: true,
231 });
232
233 logger.handle_event(&BuildEvent::StageComplete { index: 0 });
234
235 logger.handle_event(&BuildEvent::BuildComplete {
236 image_id: "sha256:abc".to_string(),
237 });
238
239 logger.handle_event(&BuildEvent::BuildFailed {
240 error: "test error".to_string(),
241 });
242 }
243
244 #[test]
245 fn test_default() {
246 let logger = PlainLogger::default();
247 assert!(!logger.verbose);
248 }
249}