Skip to main content

cfgd_core/output/
section_guard.rs

1//! `SectionGuard` is the only path to indented output. Its lifetime is tied
2//! to `&Printer`, and Drop closes the section.
3use std::sync::Arc;
4
5use super::renderer::{Renderer, StatusFields, Table, Writer};
6use super::{Printer, Role};
7
8/// Open section. Holds a reference to Printer and the renderer's depth.
9/// Drop closes the section: emits a deferred `(none)` placeholder if no
10/// children rendered (and `keep_when_empty` was true), or leaves no trace
11/// (if `keep_when_empty` was false).
12pub struct SectionGuard<'p> {
13    pub(crate) printer: &'p Printer,
14    pub(crate) renderer: Arc<Renderer>,
15    pub(crate) sink: Arc<dyn Writer>,
16    pub(crate) depth: usize,
17}
18
19impl<'p> SectionGuard<'p> {
20    pub fn bullet(&self, text: impl Into<String>) -> &Self {
21        self.renderer
22            .render_bullet(self.sink.as_ref(), self.depth, &text.into());
23        self
24    }
25
26    pub fn kv(&self, key: impl Into<String>, value: impl Into<String>) -> &Self {
27        // Defer to the buffer so consecutive kvs at this depth coalesce.
28        self.renderer.render_kv(&key.into(), &value.into());
29        self
30    }
31
32    pub fn kv_block<I, K, V>(&self, pairs: I) -> &Self
33    where
34        I: IntoIterator<Item = (K, V)>,
35        K: Into<String>,
36        V: Into<String>,
37    {
38        let pairs: Vec<(String, String)> = pairs
39            .into_iter()
40            .map(|(k, v)| (k.into(), v.into()))
41            .collect();
42        self.renderer
43            .render_kv_block(self.sink.as_ref(), self.depth, &pairs);
44        self
45    }
46
47    pub fn hint(&self, text: impl Into<String>) -> &Self {
48        self.renderer
49            .render_hint(self.sink.as_ref(), self.depth, &text.into());
50        self
51    }
52
53    pub fn note(&self, text: impl Into<String>) -> &Self {
54        self.renderer
55            .render_note(self.sink.as_ref(), self.depth, &text.into());
56        self
57    }
58
59    pub fn table(&self, table: Table) -> &Self {
60        self.renderer
61            .render_table(self.sink.as_ref(), self.depth, &table);
62        self
63    }
64
65    /// Set the empty-state placeholder for this section (overrides the default
66    /// "(none)"). Only meaningful for sections opened with `section()` (not
67    /// `section_or_collapse()`).
68    pub fn empty_state(&self, text: impl Into<String>) -> &Self {
69        self.renderer.render_section_empty_state(&text.into());
70        self
71    }
72
73    /// Status with no extra fields. For chained detail/duration/target, use
74    /// `status` for the chainable builder.
75    pub fn status_simple(&self, role: Role, subject: impl Into<String>) -> &Self {
76        let subject = subject.into();
77        self.renderer.render_status(
78            self.sink.as_ref(),
79            self.depth,
80            &StatusFields {
81                role,
82                subject: &subject,
83                detail: None,
84                duration: None,
85                target: None,
86            },
87        );
88        self
89    }
90
91    /// Status builder at this section's depth. Commits on Drop.
92    pub fn status(
93        &self,
94        role: Role,
95        subject: impl Into<String>,
96    ) -> super::status_builder::StatusBuilder<'_> {
97        super::status_builder::StatusBuilder::new(
98            self.renderer.clone(),
99            self.sink.clone(),
100            self.depth,
101            role,
102            subject,
103        )
104    }
105
106    /// Open a child section. Returns a guard that borrows `&self` so the parent
107    /// is locked until the child drops.
108    #[must_use = "section closes when SectionGuard is dropped; bind it"]
109    pub fn section(&self, name: impl Into<String>) -> SectionGuard<'_> {
110        self.renderer
111            .render_section_open(&name.into(), /*keep_when_empty=*/ true);
112        SectionGuard {
113            printer: self.printer,
114            renderer: self.renderer.clone(),
115            sink: self.sink.clone(),
116            depth: self.depth + 1,
117        }
118    }
119
120    #[must_use = "section closes when SectionGuard is dropped; bind it"]
121    pub fn section_or_collapse(&self, name: impl Into<String>) -> SectionGuard<'_> {
122        self.renderer
123            .render_section_open(&name.into(), /*keep_when_empty=*/ false);
124        SectionGuard {
125            printer: self.printer,
126            renderer: self.renderer.clone(),
127            sink: self.sink.clone(),
128            depth: self.depth + 1,
129        }
130    }
131
132    /// Section-scoped spinner. Inherits the section's depth so the eventual
133    /// Status emitted by `finish_*` lands at the right indentation.
134    #[must_use]
135    pub fn spinner(&self, message: impl Into<String>) -> super::spinner::Spinner<'_> {
136        let message = message.into();
137        let bar = super::spinner::make_spinner_bar(
138            &self.printer.multi_progress,
139            &self.renderer,
140            self.printer.verbosity(),
141            &message,
142        );
143        super::spinner::Spinner {
144            renderer: self.renderer.clone(),
145            sink: self.sink.clone(),
146            depth: self.depth,
147            bar,
148            message,
149            finished: false,
150            _phantom: std::marker::PhantomData,
151        }
152    }
153
154    /// Section-scoped progress bar.
155    #[must_use]
156    pub fn progress_bar(
157        &self,
158        total: u64,
159        message: impl Into<String>,
160    ) -> super::spinner::ProgressBar<'_> {
161        let bar = super::spinner::make_progress_bar(
162            &self.printer.multi_progress,
163            total,
164            self.printer.verbosity(),
165            &message.into(),
166        );
167        super::spinner::ProgressBar {
168            bar,
169            _phantom: std::marker::PhantomData,
170        }
171    }
172
173    /// Run an external command at this section's depth with live output.
174    /// TTY+non-quiet → spinner with tailing ring indented under the section;
175    /// otherwise → streaming lines. Either path captures full stdout/stderr.
176    pub fn run(
177        &self,
178        cmd: &mut std::process::Command,
179        label: impl Into<String>,
180    ) -> std::io::Result<super::process::CommandOutput> {
181        super::process::run_command(
182            &self.renderer,
183            self.sink.as_ref(),
184            &self.printer.multi_progress,
185            self.depth,
186            cmd,
187            &label.into(),
188        )
189    }
190
191    /// Manually close (alternative to drop). Useful when the caller needs the
192    /// section to close before the binding goes out of scope.
193    pub fn close(self) { /* drop happens here */
194    }
195}
196
197impl Drop for SectionGuard<'_> {
198    fn drop(&mut self) {
199        self.renderer.render_section_close(self.sink.as_ref());
200    }
201}