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