Skip to main content

nmbrs_runtime/readouts/builtins/
scope_close.rs

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