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;