Skip to main content

nmbrs_runtime/readouts/builtins/
each_close.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! `each_close` — companion to `scope_header` for the
5//! `on_each_end` slot. Where `scope_header` opens an
6//! iteration with `· {scope_name}`, `each_close` closes it
7//! with the matching trailing marker.
8//!
9//! Compact / Labeled use a thin glyph (`└─ end`); Expanded
10//! also surfaces the iteration tuple so post-run scrollback
11//! can identify which iteration finished without scanning
12//! upward to the opener.
13
14use std::fmt::Write as _;
15
16use crate::lifecycle::SubjectKind;
17use crate::readouts::buf::ReadoutBuf;
18use crate::readouts::context::ReadoutContext;
19use crate::readouts::readout::{ContentMode, Lod, Readout, ReadoutOptions};
20
21pub struct EachClose;
22
23impl Readout for EachClose {
24    fn name(&self) -> &'static str {
25        "each_close"
26    }
27    fn accepts(&self) -> &'static [SubjectKind] {
28        &[SubjectKind::Iteration]
29    }
30
31    fn render(
32        &self,
33        ctx: &dyn ReadoutContext,
34        lod: Lod,
35        mode: ContentMode,
36        _opts: &ReadoutOptions,
37        out: &mut dyn ReadoutBuf,
38    ) -> usize {
39        match (lod, mode) {
40            (Lod::Compact, ContentMode::Value) => render_compact(ctx, out),
41            (Lod::Labeled, ContentMode::Value) => render_labeled(ctx, out),
42            (Lod::Expanded, ContentMode::Value) => render_expanded(ctx, out),
43            (_, ContentMode::Explanation) => render_explanation(out),
44        }
45    }
46}
47
48fn render_compact(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
49    let color = ctx.use_color();
50    let cyan = if color { "\x1b[36m" } else { "" };
51    let reset = if color { "\x1b[0m" } else { "" };
52    let mut tmp = String::with_capacity(16);
53    let _ = write!(&mut tmp, "{cyan}└{reset}");
54    let len = tmp.len();
55    let _ = out.write_str(&tmp);
56    len
57}
58
59fn render_labeled(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
60    let color = ctx.use_color();
61    let cyan = if color { "\x1b[36m" } else { "" };
62    let italic = if color { "\x1b[3m" } else { "" };
63    let reset = if color { "\x1b[0m" } else { "" };
64    let mut tmp = String::with_capacity(32);
65    let _ = write!(&mut tmp, "{cyan}└─{reset} {italic}end{reset}");
66    let len = tmp.len();
67    let _ = out.write_str(&tmp);
68    len
69}
70
71fn render_expanded(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
72    let color = ctx.use_color();
73    let cyan = if color { "\x1b[36m" } else { "" };
74    let italic = if color { "\x1b[3m" } else { "" };
75    let reset = if color { "\x1b[0m" } else { "" };
76    let labels = ctx.subject_labels();
77    let mut tmp = String::with_capacity(64);
78    if labels.is_empty() {
79        let _ = write!(&mut tmp, "{cyan}└─{reset} {italic}end{reset}");
80    } else {
81        let _ = write!(&mut tmp, "{cyan}└─{reset} {italic}end{reset} {labels}");
82    }
83    let len = tmp.len();
84    let _ = out.write_str(&tmp);
85    len
86}
87
88fn render_explanation(out: &mut dyn ReadoutBuf) -> usize {
89    let s = "└─ end — closing marker for the surrounding for_each / for_combinations iteration";
90    let _ = out.write_str(s);
91    s.len()
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::readouts::buf::StringBuf;
98
99    #[derive(Default)]
100    struct TestCtx {
101        labels: String,
102        use_color: bool,
103    }
104    impl ReadoutContext for TestCtx {
105        fn subject_name(&self) -> &str {
106            ""
107        }
108        fn subject_seq(&self) -> Option<(usize, usize)> {
109            None
110        }
111        fn subject_labels(&self) -> &str {
112            &self.labels
113        }
114        fn cycles_completed(&self) -> u64 {
115            0
116        }
117        fn cycles_total(&self) -> u64 {
118            0
119        }
120        fn ops_ok(&self) -> u64 {
121            0
122        }
123        fn errors(&self) -> u64 {
124            0
125        }
126        fn retries(&self) -> u64 {
127            0
128        }
129        fn concurrency(&self) -> usize {
130            0
131        }
132        fn elapsed_secs(&self) -> f64 {
133            0.0
134        }
135        fn consumed(&self) -> u64 {
136            0
137        }
138        fn status_metric_chips(&self) -> String {
139            String::new()
140        }
141        fn depth_indent(&self) -> &str {
142            ""
143        }
144        fn use_color(&self) -> bool {
145            self.use_color
146        }
147        fn event(&self) -> crate::lifecycle::EventType {
148            crate::lifecycle::EventType::EachEnd
149        }
150    }
151
152    fn render(ctx: &TestCtx, lod: Lod) -> String {
153        let mut s = String::new();
154        let mut buf = StringBuf::new(&mut s);
155        EachClose.render(
156            ctx,
157            lod,
158            ContentMode::Value,
159            &ReadoutOptions::new(),
160            &mut buf,
161        );
162        s
163    }
164
165    #[test]
166    fn labeled_no_color() {
167        let ctx = TestCtx::default();
168        assert_eq!(render(&ctx, Lod::Labeled), "└─ end");
169    }
170
171    #[test]
172    fn compact_thinner_than_labeled() {
173        let ctx = TestCtx::default();
174        assert_eq!(render(&ctx, Lod::Compact), "└");
175    }
176
177    #[test]
178    fn expanded_includes_iter_labels() {
179        let ctx = TestCtx {
180            labels: "(profile=alpha, k=10)".into(),
181            ..Default::default()
182        };
183        let out = render(&ctx, Lod::Expanded);
184        assert!(out.contains("end"));
185        assert!(out.contains("(profile=alpha, k=10)"));
186    }
187
188    #[test]
189    fn ansi_emitted_when_color_enabled() {
190        let ctx = TestCtx {
191            use_color: true,
192            ..Default::default()
193        };
194        let out = render(&ctx, Lod::Labeled);
195        assert!(out.contains("\x1b[36m"));
196        assert!(out.contains("\x1b[3m"));
197    }
198}