Skip to main content

ic_host_text/progress/
mod.rs

1//! Pure single-line progress frames; callers own terminal admission and output.
2
3use unicode_width::UnicodeWidthChar;
4
5use crate::escape::terminal_text;
6
7/// Tracks the last rendered width and constructs carriage-return progress frames.
8///
9/// Input controls are escaped before width accounting. Callers write returned
10/// frames verbatim and own terminal detection, input limits, clipping to prevent
11/// wrapping, output failures, flushing and when to finish or clear the line.
12/// Width sums Unicode scalar columns, like [`crate::width::fit_line`],
13/// conservatively counting joined emoji separately rather than as one grapheme.
14/// No output or automatic cleanup occurs when this value is dropped.
15#[derive(Debug, Default)]
16pub struct ProgressLine {
17    last_width: usize,
18}
19
20impl ProgressLine {
21    /// Start with no outstanding progress text.
22    #[must_use]
23    pub const fn new() -> Self {
24        Self { last_width: 0 }
25    }
26
27    /// Replace the current text, blanking columns left by a longer previous frame.
28    #[must_use]
29    pub fn update(&mut self, text: &str) -> String {
30        self.render(text, false)
31    }
32
33    /// Render the final text and newline, then forget the outstanding width.
34    #[must_use]
35    pub fn finish(&mut self, text: &str) -> String {
36        self.render(text, true)
37    }
38
39    /// Blank outstanding text and return to its starting column.
40    /// Returns an empty string when no columns need clearing.
41    #[must_use]
42    pub fn clear(&mut self) -> String {
43        if self.last_width == 0 {
44            return String::new();
45        }
46        let frame = format!("\r{}\r", " ".repeat(self.last_width));
47        self.last_width = 0;
48        frame
49    }
50
51    fn render(&mut self, text: &str, newline: bool) -> String {
52        let text = terminal_text(text);
53        let width: usize = text.chars().map(|ch| ch.width().unwrap_or(0)).sum();
54        let padding = " ".repeat(self.last_width.saturating_sub(width));
55        let mut frame = format!("\r{text}{padding}");
56        if newline {
57            frame.push('\n');
58        }
59        self.last_width = if newline { 0 } else { width };
60        frame
61    }
62}
63
64#[cfg(test)]
65mod tests;