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::StageStarted {
84 index,
85 name,
86 base_image,
87 } => {
88 let stage_name = name.as_deref().unwrap_or("unnamed");
89 let header = format!("==> Stage {}: {} ({})", index + 1, stage_name, base_image);
90 println!("{}", self.colorize(&header, ansi::CYAN));
91 }
92
93 BuildEvent::InstructionStarted { instruction, .. } => {
94 let line = format!(" -> {instruction}");
95 println!("{}", self.colorize(&line, ansi::YELLOW));
96 }
97
98 BuildEvent::Output { line, is_stderr } if self.verbose => {
99 if *is_stderr {
100 eprintln!(" {}", self.colorize(line, ansi::DIM));
101 } else {
102 println!(" {line}");
103 }
104 }
105
106 BuildEvent::Output { .. } => {
107 }
109
110 BuildEvent::InstructionComplete { cached, .. } => {
111 if *cached && self.verbose {
112 println!(" {}", self.colorize("[cached]", ansi::CYAN));
113 }
114 }
115
116 BuildEvent::StageComplete { index } => {
117 if self.verbose {
118 let line = format!(" Stage {} complete", index + 1);
119 println!("{}", self.colorize(&line, ansi::GREEN));
120 }
121 }
122
123 BuildEvent::BuildComplete { image_id } => {
124 println!();
125 let success = format!("Build complete: {image_id}");
126 println!("{}", self.colorize(&success, ansi::GREEN));
127 }
128
129 BuildEvent::BuildFailed { error } => {
130 println!();
131 let failure = format!("Build failed: {error}");
132 eprintln!("{}", self.colorize(&failure, ansi::RED));
133 }
134 }
135 }
136
137 pub fn process_events<I>(&self, events: I)
141 where
142 I: IntoIterator<Item = BuildEvent>,
143 {
144 for event in events {
145 self.handle_event(&event);
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_plain_logger_creation() {
156 let logger = PlainLogger::new(false);
157 assert!(!logger.verbose);
158
159 let verbose_logger = PlainLogger::new(true);
160 assert!(verbose_logger.verbose);
161 }
162
163 #[test]
164 fn test_with_color() {
165 let logger = PlainLogger::with_color(false, true);
166 assert!(logger.color);
167
168 let no_color_logger = PlainLogger::with_color(false, false);
169 assert!(!no_color_logger.color);
170 }
171
172 #[test]
173 fn test_colorize_enabled() {
174 let logger = PlainLogger::with_color(false, true);
175 let result = logger.colorize("test", ansi::GREEN);
176 assert!(result.contains("\x1b[32m"));
177 assert!(result.contains("\x1b[0m"));
178 assert!(result.contains("test"));
179 }
180
181 #[test]
182 fn test_colorize_disabled() {
183 let logger = PlainLogger::with_color(false, false);
184 let result = logger.colorize("test", ansi::GREEN);
185 assert_eq!(result, "test");
186 assert!(!result.contains("\x1b["));
187 }
188
189 #[test]
190 fn test_handle_event_does_not_panic() {
191 let logger = PlainLogger::with_color(true, false);
193
194 logger.handle_event(&BuildEvent::StageStarted {
196 index: 0,
197 name: Some("builder".to_string()),
198 base_image: "alpine".to_string(),
199 });
200
201 logger.handle_event(&BuildEvent::InstructionStarted {
202 stage: 0,
203 index: 0,
204 instruction: "RUN echo hello".to_string(),
205 });
206
207 logger.handle_event(&BuildEvent::Output {
208 line: "hello".to_string(),
209 is_stderr: false,
210 });
211
212 logger.handle_event(&BuildEvent::Output {
213 line: "warning".to_string(),
214 is_stderr: true,
215 });
216
217 logger.handle_event(&BuildEvent::InstructionComplete {
218 stage: 0,
219 index: 0,
220 cached: true,
221 });
222
223 logger.handle_event(&BuildEvent::StageComplete { index: 0 });
224
225 logger.handle_event(&BuildEvent::BuildComplete {
226 image_id: "sha256:abc".to_string(),
227 });
228
229 logger.handle_event(&BuildEvent::BuildFailed {
230 error: "test error".to_string(),
231 });
232 }
233
234 #[test]
235 fn test_default() {
236 let logger = PlainLogger::default();
237 assert!(!logger.verbose);
238 }
239}