Skip to main content

nmbrs_runtime/readouts/builtins/
scope_open.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! `scope_open` — opening row for non-iteration scopes
5//! (`do_while`, `do_until`, plain group blocks). Bound by
6//! default to `on_scope_start`.
7//!
8//! Distinct from `scope_header`, which opens iterations
9//! inside `for_each` / `for_combinations`. The non-iteration
10//! variants don't carry a tuple label, so this readout is
11//! thinner — just the scope name and an opening glyph.
12
13use 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 ScopeOpen;
21
22impl Readout for ScopeOpen {
23    fn name(&self) -> &'static str {
24        "scope_open"
25    }
26    fn accepts(&self) -> &'static [SubjectKind] {
27        &[SubjectKind::Scope]
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_value(ctx, out, false),
40            (Lod::Labeled, ContentMode::Value) => render_value(ctx, out, false),
41            (Lod::Expanded, ContentMode::Value) => render_value(ctx, out, true),
42            (_, ContentMode::Explanation) => render_explanation(out),
43        }
44    }
45}
46
47fn render_value(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf, expanded: bool) -> usize {
48    let color = ctx.use_color();
49    let cyan = if color { "\x1b[36m" } else { "" };
50    let italic = if color { "\x1b[3m" } else { "" };
51    let reset = if color { "\x1b[0m" } else { "" };
52    let name = ctx.subject_name();
53    if name.is_empty() {
54        return 0;
55    }
56
57    let mut tmp = String::with_capacity(48);
58    if expanded {
59        let _ = write!(
60            &mut tmp,
61            "{cyan}┌─{reset} {italic}{name}{reset}\n  scope opens"
62        );
63    } else {
64        let _ = write!(&mut tmp, "{cyan}┌─{reset} {italic}{name}{reset}");
65    }
66    let len = tmp.len();
67    let _ = out.write_str(&tmp);
68    len
69}
70
71fn render_explanation(out: &mut dyn ReadoutBuf) -> usize {
72    let s = "┌─ {scope-name} — opening marker for a do_while / do_until / group scope";
73    let _ = out.write_str(s);
74    s.len()
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::readouts::buf::StringBuf;
81
82    #[derive(Default)]
83    struct TestCtx {
84        name: String,
85        use_color: bool,
86    }
87    impl ReadoutContext for TestCtx {
88        fn subject_name(&self) -> &str {
89            &self.name
90        }
91        fn subject_seq(&self) -> Option<(usize, usize)> {
92            None
93        }
94        fn subject_labels(&self) -> &str {
95            ""
96        }
97        fn cycles_completed(&self) -> u64 {
98            0
99        }
100        fn cycles_total(&self) -> u64 {
101            0
102        }
103        fn ops_ok(&self) -> u64 {
104            0
105        }
106        fn errors(&self) -> u64 {
107            0
108        }
109        fn retries(&self) -> u64 {
110            0
111        }
112        fn concurrency(&self) -> usize {
113            0
114        }
115        fn elapsed_secs(&self) -> f64 {
116            0.0
117        }
118        fn consumed(&self) -> u64 {
119            0
120        }
121        fn status_metric_chips(&self) -> String {
122            String::new()
123        }
124        fn depth_indent(&self) -> &str {
125            ""
126        }
127        fn use_color(&self) -> bool {
128            self.use_color
129        }
130        fn event(&self) -> crate::lifecycle::EventType {
131            crate::lifecycle::EventType::ScopeStart
132        }
133    }
134
135    fn render(ctx: &TestCtx, lod: Lod) -> String {
136        let mut s = String::new();
137        let mut buf = StringBuf::new(&mut s);
138        ScopeOpen.render(
139            ctx,
140            lod,
141            ContentMode::Value,
142            &ReadoutOptions::new(),
143            &mut buf,
144        );
145        s
146    }
147
148    #[test]
149    fn labeled_renders_open_glyph_and_name() {
150        let ctx = TestCtx {
151            name: "do_while".into(),
152            ..Default::default()
153        };
154        assert_eq!(render(&ctx, Lod::Labeled), "┌─ do_while");
155    }
156
157    #[test]
158    fn compact_matches_labeled() {
159        let ctx = TestCtx {
160            name: "do_until".into(),
161            ..Default::default()
162        };
163        assert_eq!(render(&ctx, Lod::Compact), render(&ctx, Lod::Labeled),);
164    }
165
166    #[test]
167    fn expanded_breaks_onto_lines() {
168        let ctx = TestCtx {
169            name: "do_while".into(),
170            ..Default::default()
171        };
172        let out = render(&ctx, Lod::Expanded);
173        assert!(out.contains("do_while"));
174        assert!(out.lines().count() >= 2);
175    }
176
177    #[test]
178    fn ansi_when_color_enabled() {
179        let ctx = TestCtx {
180            name: "x".into(),
181            use_color: true,
182        };
183        let out = render(&ctx, Lod::Labeled);
184        assert!(out.contains("\x1b[36m"));
185    }
186}