rskit_cli/render/
status.rs1use std::io::{self, Write};
17
18use rskit_errors::{AppError, AppResult};
19
20use crate::theme::{ColorChoice, Glyphs, Palette};
21
22pub struct StatusReporter<W> {
24 writer: W,
25 palette: Palette,
26 glyphs: Glyphs,
27}
28
29impl StatusReporter<io::Stderr> {
30 #[must_use]
36 pub fn from_env(color: ColorChoice) -> Self {
37 let stderr = io::stderr();
38 let palette = Palette::for_stream(color, &stderr);
39 Self {
40 writer: stderr,
41 palette,
42 glyphs: Glyphs::from_env(),
43 }
44 }
45}
46
47impl<W: Write> StatusReporter<W> {
48 #[must_use]
50 pub const fn new(writer: W, palette: Palette, glyphs: Glyphs) -> Self {
51 Self {
52 writer,
53 palette,
54 glyphs,
55 }
56 }
57
58 pub fn success(&mut self, message: &str) -> AppResult<()> {
64 let prefix = self.palette.success(self.glyphs.success()).into_owned();
65 self.write_line(&prefix, message)
66 }
67
68 pub fn error(&mut self, message: &str) -> AppResult<()> {
74 let prefix = self.palette.error(self.glyphs.error()).into_owned();
75 self.write_line(&prefix, message)
76 }
77
78 pub fn warn(&mut self, message: &str) -> AppResult<()> {
84 let prefix = self.palette.warn(self.glyphs.warning()).into_owned();
85 self.write_line(&prefix, message)
86 }
87
88 pub fn info(&mut self, message: &str) -> AppResult<()> {
94 let prefix = self.palette.info(self.glyphs.info()).into_owned();
95 self.write_line(&prefix, message)
96 }
97
98 pub fn bullet(&mut self, message: &str) -> AppResult<()> {
104 let prefix = format!(" {}", self.palette.dim(self.glyphs.bullet()));
105 self.write_line(&prefix, message)
106 }
107
108 pub fn step(&mut self, current: usize, total: usize, message: &str) -> AppResult<()> {
114 let counter = self
115 .palette
116 .dim(&format!("[{current}/{total}]"))
117 .into_owned();
118 self.write_line(&counter, message)
119 }
120
121 pub fn heading(&mut self, title: &str) -> AppResult<()> {
127 let styled = self.palette.bold(title).into_owned();
128 writeln!(self.writer, "\n{styled}").map_err(AppError::internal)
129 }
130
131 fn write_line(&mut self, prefix: &str, message: &str) -> AppResult<()> {
132 writeln!(self.writer, "{prefix} {message}").map_err(AppError::internal)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::StatusReporter;
139 use crate::theme::{Glyphs, Palette};
140
141 fn reporter(buffer: &mut Vec<u8>, color: bool, unicode: bool) -> StatusReporter<&mut Vec<u8>> {
142 StatusReporter::new(buffer, Palette::new(color), Glyphs::new(unicode))
143 }
144
145 fn rendered(buffer: Vec<u8>) -> String {
146 String::from_utf8(buffer).expect("utf8")
147 }
148
149 #[test]
150 fn success_line_carries_glyph_and_message() {
151 let mut buffer = Vec::new();
152 reporter(&mut buffer, false, true)
153 .success("Detected Rust")
154 .expect("write");
155 let out = rendered(buffer);
156 assert!(out.contains('✓'));
157 assert!(out.contains("Detected Rust"));
158 }
159
160 #[test]
161 fn step_line_renders_counter() {
162 let mut buffer = Vec::new();
163 reporter(&mut buffer, false, true)
164 .step(1, 4, "Selecting ecosystems")
165 .expect("write");
166 let out = rendered(buffer);
167 assert!(out.contains("[1/4]"));
168 assert!(out.contains("Selecting ecosystems"));
169 }
170
171 #[test]
172 fn heading_is_preceded_by_blank_line() {
173 let mut buffer = Vec::new();
174 reporter(&mut buffer, false, true)
175 .heading("Configuration")
176 .expect("write");
177 let out = rendered(buffer);
178 assert!(out.starts_with('\n'));
179 assert!(out.contains("Configuration"));
180 }
181
182 #[test]
183 fn ascii_fallback_avoids_unicode_glyphs() {
184 let mut buffer = Vec::new();
185 reporter(&mut buffer, false, false)
186 .warn("no toolchain found")
187 .expect("write");
188 let out = rendered(buffer);
189 assert!(out.contains('!'));
190 assert!(!out.contains('⚠'));
191 }
192
193 #[test]
194 fn disabled_palette_is_byte_clean() {
195 let mut buffer = Vec::new();
196 reporter(&mut buffer, false, true)
197 .info("nothing to do")
198 .expect("write");
199 let out = rendered(buffer);
200 assert!(!out.contains('\u{1b}'), "no color must be byte-clean");
201 }
202
203 #[test]
204 fn enabled_palette_emits_ansi_escapes() {
205 let mut buffer = Vec::new();
206 reporter(&mut buffer, true, true)
207 .error("build failed")
208 .expect("write");
209 let out = rendered(buffer);
210 assert!(out.contains('\u{1b}'), "color must emit SGR escapes");
211 }
212
213 #[test]
214 fn bullet_line_is_indented_with_its_glyph() {
215 let mut buffer = Vec::new();
216 reporter(&mut buffer, false, true)
217 .bullet("cached crate")
218 .expect("write");
219 let out = rendered(buffer);
220 assert!(out.starts_with(" "));
221 assert!(out.contains('•'));
222 assert!(out.contains("cached crate"));
223 }
224
225 #[test]
226 fn from_env_binds_to_stderr_without_writing() {
227 let _reporter = StatusReporter::from_env(crate::theme::ColorChoice::Never);
230 }
231}