rskit_cli/render/
status.rs1use std::io::{self, Write};
14
15use rskit_errors::{AppError, AppResult};
16
17use crate::theme::{ColorChoice, Glyphs, Palette};
18
19pub struct StatusReporter<W> {
21 writer: W,
22 palette: Palette,
23 glyphs: Glyphs,
24}
25
26impl StatusReporter<io::Stderr> {
27 #[must_use]
32 pub fn from_env(color: ColorChoice) -> Self {
33 let stderr = io::stderr();
34 let palette = Palette::for_stream(color, &stderr);
35 Self {
36 writer: stderr,
37 palette,
38 glyphs: Glyphs::from_env(),
39 }
40 }
41}
42
43impl<W: Write> StatusReporter<W> {
44 #[must_use]
46 pub const fn new(writer: W, palette: Palette, glyphs: Glyphs) -> Self {
47 Self {
48 writer,
49 palette,
50 glyphs,
51 }
52 }
53
54 pub fn success(&mut self, message: &str) -> AppResult<()> {
60 let prefix = self.palette.success(self.glyphs.success()).into_owned();
61 self.write_line(&prefix, message)
62 }
63
64 pub fn error(&mut self, message: &str) -> AppResult<()> {
70 let prefix = self.palette.error(self.glyphs.error()).into_owned();
71 self.write_line(&prefix, message)
72 }
73
74 pub fn warn(&mut self, message: &str) -> AppResult<()> {
80 let prefix = self.palette.warn(self.glyphs.warning()).into_owned();
81 self.write_line(&prefix, message)
82 }
83
84 pub fn info(&mut self, message: &str) -> AppResult<()> {
90 let prefix = self.palette.info(self.glyphs.info()).into_owned();
91 self.write_line(&prefix, message)
92 }
93
94 pub fn bullet(&mut self, message: &str) -> AppResult<()> {
100 let prefix = format!(" {}", self.palette.dim(self.glyphs.bullet()));
101 self.write_line(&prefix, message)
102 }
103
104 pub fn step(&mut self, current: usize, total: usize, message: &str) -> AppResult<()> {
110 let counter = self
111 .palette
112 .dim(&format!("[{current}/{total}]"))
113 .into_owned();
114 self.write_line(&counter, message)
115 }
116
117 pub fn heading(&mut self, title: &str) -> AppResult<()> {
123 let styled = self.palette.bold(title).into_owned();
124 writeln!(self.writer, "\n{styled}").map_err(AppError::internal)
125 }
126
127 fn write_line(&mut self, prefix: &str, message: &str) -> AppResult<()> {
128 writeln!(self.writer, "{prefix} {message}").map_err(AppError::internal)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::StatusReporter;
135 use crate::theme::{Glyphs, Palette};
136
137 fn reporter(buffer: &mut Vec<u8>, color: bool, unicode: bool) -> StatusReporter<&mut Vec<u8>> {
138 StatusReporter::new(buffer, Palette::new(color), Glyphs::new(unicode))
139 }
140
141 fn rendered(buffer: Vec<u8>) -> String {
142 String::from_utf8(buffer).expect("utf8")
143 }
144
145 #[test]
146 fn success_line_carries_glyph_and_message() {
147 let mut buffer = Vec::new();
148 reporter(&mut buffer, false, true)
149 .success("Detected Rust")
150 .expect("write");
151 let out = rendered(buffer);
152 assert!(out.contains('✓'));
153 assert!(out.contains("Detected Rust"));
154 }
155
156 #[test]
157 fn step_line_renders_counter() {
158 let mut buffer = Vec::new();
159 reporter(&mut buffer, false, true)
160 .step(1, 4, "Selecting ecosystems")
161 .expect("write");
162 let out = rendered(buffer);
163 assert!(out.contains("[1/4]"));
164 assert!(out.contains("Selecting ecosystems"));
165 }
166
167 #[test]
168 fn heading_is_preceded_by_blank_line() {
169 let mut buffer = Vec::new();
170 reporter(&mut buffer, false, true)
171 .heading("Configuration")
172 .expect("write");
173 let out = rendered(buffer);
174 assert!(out.starts_with('\n'));
175 assert!(out.contains("Configuration"));
176 }
177
178 #[test]
179 fn ascii_fallback_avoids_unicode_glyphs() {
180 let mut buffer = Vec::new();
181 reporter(&mut buffer, false, false)
182 .warn("no toolchain found")
183 .expect("write");
184 let out = rendered(buffer);
185 assert!(out.contains('!'));
186 assert!(!out.contains('⚠'));
187 }
188
189 #[test]
190 fn disabled_palette_is_byte_clean() {
191 let mut buffer = Vec::new();
192 reporter(&mut buffer, false, true)
193 .info("nothing to do")
194 .expect("write");
195 let out = rendered(buffer);
196 assert!(!out.contains('\u{1b}'), "no color must be byte-clean");
197 }
198
199 #[test]
200 fn enabled_palette_emits_ansi_escapes() {
201 let mut buffer = Vec::new();
202 reporter(&mut buffer, true, true)
203 .error("build failed")
204 .expect("write");
205 let out = rendered(buffer);
206 assert!(out.contains('\u{1b}'), "color must emit SGR escapes");
207 }
208
209 #[test]
210 fn bullet_line_is_indented_with_its_glyph() {
211 let mut buffer = Vec::new();
212 reporter(&mut buffer, false, true)
213 .bullet("cached crate")
214 .expect("write");
215 let out = rendered(buffer);
216 assert!(out.starts_with(" "));
217 assert!(out.contains('•'));
218 assert!(out.contains("cached crate"));
219 }
220
221 #[test]
222 fn from_env_binds_to_stderr_without_writing() {
223 let _reporter = StatusReporter::from_env(crate::theme::ColorChoice::Never);
226 }
227}