nmbrs_runtime/readouts/builtins/
scope_open.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 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}