nmbrs_runtime/readouts/builtins/
phase_starting.rs1use std::fmt::Write as _;
14
15use crate::lifecycle::SubjectKind;
16use crate::readouts::buf::ReadoutBuf;
17use crate::readouts::context::ReadoutContext;
18use crate::readouts::readout::{ContentMode, Lod, Readout, ReadoutOptions};
19
20pub struct PhaseStarting;
21
22impl Readout for PhaseStarting {
23 fn name(&self) -> &'static str {
24 "phase_starting"
25 }
26 fn accepts(&self) -> &'static [SubjectKind] {
27 &[SubjectKind::Phase]
28 }
29
30 fn render(
31 &self,
32 ctx: &dyn ReadoutContext,
33 lod: Lod,
34 mode: ContentMode,
35 _opts: &ReadoutOptions,
36 out: &mut dyn ReadoutBuf,
37 ) -> usize {
38 match (lod, mode) {
39 (Lod::Compact, ContentMode::Value) => render_compact(ctx, out),
40 (Lod::Labeled, ContentMode::Value) => render_labeled(ctx, out),
41 (Lod::Expanded, ContentMode::Value) => render_expanded(ctx, out),
42 (_, ContentMode::Explanation) => render_explanation(out),
43 }
44 }
45}
46
47fn render_compact(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
48 let color = ctx.use_color();
49 let green = if color { "\x1b[32m" } else { "" };
50 let reset = if color { "\x1b[0m" } else { "" };
51 let name = ctx.subject_name();
52 let depth_indent = ctx.depth_indent();
53 let mut tmp = String::with_capacity(64);
54 let _ = write!(&mut tmp, "{depth_indent}{green}▶{reset} {name}");
55 let len = tmp.len();
56 let _ = out.write_str(&tmp);
57 len
58}
59
60fn render_labeled(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
61 let color = ctx.use_color();
66 let green = if color { "\x1b[32m" } else { "" };
67 let bold = if color { "\x1b[1m" } else { "" };
68 let dim = if color { "\x1b[2m" } else { "" };
69 let blue = if color { "\x1b[34m" } else { "" };
70 let reset = if color { "\x1b[0m" } else { "" };
71 let name = ctx.subject_name();
72 let labels = ctx.subject_labels();
73 let depth_indent = ctx.depth_indent();
74 let seq_part: String = match ctx.subject_seq() {
75 Some((s, t)) => format!("{dim}[{s}/{t}]{reset} "),
76 None => String::new(),
77 };
78 let seq_visible: usize = match ctx.subject_seq() {
79 Some((s, t)) => format!("[{s}/{t}] ").chars().count(),
80 None => 0,
81 };
82 let head_consumed: usize = depth_indent.chars().count()
91 + 2 + seq_visible
93 + 2 + name.chars().count();
95 let coords_part = super::phase_outcome::format_coords_block(
96 labels,
97 color,
98 head_consumed,
99 &format!("{depth_indent} "),
100 false,
101 );
102 let mut tmp = String::with_capacity(160);
103 let _ = write!(
104 &mut tmp,
105 "{depth_indent}{green}▶{reset} {seq_part}{bold}{blue}[{name}]{reset}{coords_part} starting",
106 );
107 let len = tmp.len();
108 let _ = out.write_str(&tmp);
109 len
110}
111
112fn render_expanded(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
113 let color = ctx.use_color();
116 let green = if color { "\x1b[32m" } else { "" };
117 let bold = if color { "\x1b[1m" } else { "" };
118 let dim = if color { "\x1b[2m" } else { "" };
119 let blue = if color { "\x1b[34m" } else { "" };
120 let reset = if color { "\x1b[0m" } else { "" };
121 let name = ctx.subject_name();
122 let labels = ctx.subject_labels();
123 let depth_indent = ctx.depth_indent();
124 let seq_part: String = match ctx.subject_seq() {
125 Some((s, t)) => format!("{dim}[{s}/{t}]{reset} "),
126 None => String::new(),
127 };
128 let mut tmp = String::with_capacity(192);
129 let _ = write!(
130 &mut tmp,
131 "{depth_indent}{green}▶{reset} {seq_part}{bold}{blue}[{name}]{reset} starting",
132 );
133 if !labels.is_empty() {
134 let coords_continuation_indent = format!("{depth_indent} ");
139 let coords_head_consumed = depth_indent.chars().count() + 2;
140 let coords_payload = super::phase_outcome::format_coords_block(
141 labels,
142 color,
143 coords_head_consumed,
144 &coords_continuation_indent,
145 false,
146 );
147 let payload = coords_payload.strip_prefix(' ').unwrap_or(&coords_payload);
151 if !payload.is_empty() {
152 let _ = write!(&mut tmp, "\n{depth_indent} {payload}");
153 }
154 }
155 let len = tmp.len();
156 let _ = out.write_str(&tmp);
157 len
158}
159
160fn render_explanation(out: &mut dyn ReadoutBuf) -> usize {
161 let s = "▶ {phase-name} ({idx}/{total}) starting — opt-in pre-phase header";
162 let _ = out.write_str(s);
163 s.len()
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::readouts::buf::StringBuf;
170
171 #[derive(Default)]
172 struct TestCtx {
173 name: String,
174 seq: Option<(usize, usize)>,
175 labels: String,
176 use_color: bool,
177 }
178 impl ReadoutContext for TestCtx {
179 fn subject_name(&self) -> &str {
180 &self.name
181 }
182 fn subject_seq(&self) -> Option<(usize, usize)> {
183 self.seq
184 }
185 fn subject_labels(&self) -> &str {
186 &self.labels
187 }
188 fn cycles_completed(&self) -> u64 {
189 0
190 }
191 fn cycles_total(&self) -> u64 {
192 0
193 }
194 fn ops_ok(&self) -> u64 {
195 0
196 }
197 fn errors(&self) -> u64 {
198 0
199 }
200 fn retries(&self) -> u64 {
201 0
202 }
203 fn concurrency(&self) -> usize {
204 0
205 }
206 fn elapsed_secs(&self) -> f64 {
207 0.0
208 }
209 fn consumed(&self) -> u64 {
210 0
211 }
212 fn status_metric_chips(&self) -> String {
213 String::new()
214 }
215 fn depth_indent(&self) -> &str {
216 ""
217 }
218 fn use_color(&self) -> bool {
219 self.use_color
220 }
221 fn event(&self) -> crate::lifecycle::EventType {
222 crate::lifecycle::EventType::PhaseStart
223 }
224 }
225
226 fn render(ctx: &TestCtx, lod: Lod) -> String {
227 let mut s = String::new();
228 let mut buf = StringBuf::new(&mut s);
229 PhaseStarting.render(
230 ctx,
231 lod,
232 ContentMode::Value,
233 &ReadoutOptions::new(),
234 &mut buf,
235 );
236 s
237 }
238
239 #[test]
240 fn labeled_with_seq() {
241 let ctx = TestCtx {
242 name: "run".into(),
243 seq: Some((3, 8)),
244 ..Default::default()
245 };
246 assert_eq!(render(&ctx, Lod::Labeled), "▶ [3/8] [run] starting");
249 }
250
251 #[test]
252 fn labeled_without_seq() {
253 let ctx = TestCtx {
254 name: "setup".into(),
255 ..Default::default()
256 };
257 assert_eq!(render(&ctx, Lod::Labeled), "▶ [setup] starting");
258 }
259
260 #[test]
261 fn compact_drops_seq_and_starting_word() {
262 let ctx = TestCtx {
263 name: "run".into(),
264 seq: Some((3, 8)),
265 ..Default::default()
266 };
267 assert_eq!(render(&ctx, Lod::Compact), "▶ run");
268 }
269
270 #[test]
271 fn expanded_includes_iter_labels() {
272 let ctx = TestCtx {
273 name: "run".into(),
274 seq: Some((3, 8)),
275 labels: "(profile=alpha, k=10)".into(),
276 ..Default::default()
277 };
278 let out = render(&ctx, Lod::Expanded);
279 assert!(out.contains("[3/8]"), "got: {out:?}");
280 assert!(out.contains("[run]"), "got: {out:?}");
281 assert!(out.contains("(profile=alpha, k=10)"), "got: {out:?}");
282 assert!(out.lines().count() >= 2);
283 }
284}