Skip to main content

opendev_tui/widgets/
progress.rs

1//! Task progress display widget.
2//!
3//! Mirrors the Python `TaskProgressDisplay` — shows an animated spinner with
4//! task description, elapsed time, and token usage during agent/tool execution.
5
6use ratatui::{
7    buffer::Buffer,
8    layout::Rect,
9    style::Style,
10    text::{Line, Span},
11    widgets::Widget,
12};
13
14use super::spinner::SpinnerState;
15use crate::formatters::style_tokens;
16
17/// Task progress display data.
18#[derive(Debug, Clone)]
19pub struct TaskProgress {
20    /// Task description (e.g., "Thinking...", "Running bash").
21    pub description: String,
22    /// Elapsed seconds since task started.
23    pub elapsed_secs: u64,
24    /// Token usage display string (e.g., "1.2k tokens").
25    pub token_display: Option<String>,
26    /// Whether the task was interrupted.
27    pub interrupted: bool,
28    /// Wall-clock start time for accurate elapsed calculation.
29    pub started_at: std::time::Instant,
30}
31
32/// Widget that renders task progress with animated spinner.
33pub struct TaskProgressWidget<'a> {
34    progress: &'a TaskProgress,
35    spinner_char: char,
36}
37
38impl<'a> TaskProgressWidget<'a> {
39    pub fn new(progress: &'a TaskProgress, spinner_state: &SpinnerState) -> Self {
40        Self {
41            progress,
42            spinner_char: spinner_state.current(),
43        }
44    }
45}
46
47impl Widget for TaskProgressWidget<'_> {
48    fn render(self, area: Rect, buf: &mut Buffer) {
49        if area.height == 0 || area.width == 0 {
50            return;
51        }
52
53        let mut spans: Vec<Span> = Vec::new();
54
55        // Spinner character
56        spans.push(Span::styled(
57            format!("{} ", self.spinner_char),
58            Style::default().fg(style_tokens::BLUE_BRIGHT),
59        ));
60
61        // Task description
62        spans.push(Span::styled(
63            format!("{}... ", self.progress.description),
64            Style::default().fg(style_tokens::SUBTLE),
65        ));
66
67        // Info section: esc to interrupt · Xs · token_display
68        let mut info_parts = Vec::new();
69        info_parts.push("esc to interrupt".to_string());
70        info_parts.push(format!("{}s", self.progress.elapsed_secs));
71
72        if let Some(ref token_display) = self.progress.token_display {
73            info_parts.push(token_display.clone());
74        }
75
76        let info_str = info_parts.join(" \u{00b7} "); // middle dot separator
77        spans.push(Span::styled(
78            info_str,
79            Style::default().fg(style_tokens::SUBTLE),
80        ));
81
82        let line = Line::from(spans);
83        buf.set_line(area.left(), area.top(), &line, area.width);
84    }
85}
86
87/// Format a final status line after task completion.
88///
89/// Returns a formatted string like "⏺ completed in 5s (1.2k tokens)".
90pub fn format_final_status(progress: &TaskProgress) -> String {
91    let symbol = if progress.interrupted {
92        "\u{23f9}" // ⏹
93    } else {
94        "\u{23fa}" // ⏺
95    };
96
97    let status = if progress.interrupted {
98        "interrupted"
99    } else {
100        "completed"
101    };
102
103    let mut parts = vec![format!("{status} in {}s", progress.elapsed_secs)];
104    if let Some(ref token_display) = progress.token_display {
105        parts.push(token_display.clone());
106    }
107
108    format!("{symbol} {}", parts.join(", "))
109}
110
111#[cfg(test)]
112#[path = "progress_tests.rs"]
113mod tests;