Skip to main content

stynx_code_tui/widgets/
tool_detail.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::{Margin, Rect},
4    style::{Modifier, Style},
5    text::{Line, Span},
6    widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap},
7};
8
9use crate::state::{DiffLineKind, DisplayToolUse, ToolUseStatus};
10use crate::theme;
11
12pub struct ToolDetail<'a> {
13    pub tool: &'a DisplayToolUse,
14}
15
16impl<'a> ToolDetail<'a> {
17    pub fn new(tool: &'a DisplayToolUse) -> Self {
18        Self { tool }
19    }
20}
21
22fn status_label(s: ToolUseStatus) -> (&'static str, ratatui::style::Color) {
23    match s {
24        ToolUseStatus::Running => ("running", theme::GOLD()),
25        ToolUseStatus::Completed => ("done", theme::SUCCESS()),
26        ToolUseStatus::Error => ("error", theme::ERROR()),
27    }
28}
29
30impl<'a> Widget for ToolDetail<'a> {
31    fn render(self, area: Rect, buf: &mut Buffer) {
32        let popup_w = area.width.saturating_sub(8).min(160);
33        let popup_h = area.height.saturating_sub(4).min(40);
34        let popup = Rect {
35            x: area.x + (area.width.saturating_sub(popup_w)) / 2,
36            y: area.y + (area.height.saturating_sub(popup_h)) / 2,
37            width: popup_w,
38            height: popup_h,
39        };
40
41        Clear.render(popup, buf);
42
43        let (status_text, status_col) = status_label(self.tool.status);
44        let title_line = Line::from(vec![
45            Span::styled(" ", Style::default()),
46            Span::styled(self.tool.name.clone(), Style::default().fg(theme::FOAM()).add_modifier(Modifier::BOLD)),
47            Span::styled("  ", Style::default()),
48            Span::styled(status_text, Style::default().fg(status_col).add_modifier(Modifier::BOLD)),
49            Span::styled(" ", Style::default()),
50        ]);
51
52        let block = Block::default()
53            .borders(Borders::ALL)
54            .title(title_line)
55            .border_style(Style::default().fg(theme::IRIS()))
56            .style(Style::default().bg(theme::BACKGROUND()));
57
58        let inner = block.inner(popup);
59        block.render(popup, buf);
60
61        let body = inner.inner(Margin { vertical: 1, horizontal: 2 });
62
63        let mut lines: Vec<Line<'static>> = Vec::new();
64
65        if !self.tool.input_summary.is_empty() {
66            lines.push(Line::from(vec![
67                Span::styled("input  ", Style::default().fg(theme::SUBTLE()).add_modifier(Modifier::BOLD)),
68                Span::styled(self.tool.input_summary.clone(), Style::default().fg(theme::TEXT())),
69            ]));
70            lines.push(Line::from(""));
71        }
72
73        if !self.tool.diff.is_empty() {
74            lines.push(Line::from(Span::styled(
75                "diff",
76                Style::default().fg(theme::SUBTLE()).add_modifier(Modifier::BOLD),
77            )));
78            for d in &self.tool.diff {
79                let (sign, sign_col, body_col) = match d.kind {
80                    DiffLineKind::Added => ("+", theme::SUCCESS(), theme::TEXT()),
81                    DiffLineKind::Removed => ("-", theme::ERROR(), theme::TEXT()),
82                    DiffLineKind::Context => (" ", theme::TEXT_MUTED(), theme::TEXT_MUTED()),
83                };
84                lines.push(Line::from(vec![
85                    Span::styled(format!("{sign} "), Style::default().fg(sign_col).add_modifier(Modifier::BOLD)),
86                    Span::styled(d.text.clone(), Style::default().fg(body_col)),
87                ]));
88            }
89            lines.push(Line::from(""));
90        }
91
92        if !self.tool.output_excerpt.is_empty() {
93            lines.push(Line::from(Span::styled(
94                "output",
95                Style::default().fg(theme::SUBTLE()).add_modifier(Modifier::BOLD),
96            )));
97            for l in &self.tool.output_excerpt {
98                lines.push(Line::from(Span::styled(l.clone(), Style::default().fg(theme::TEXT_MUTED()))));
99            }
100            lines.push(Line::from(""));
101        } else if !self.tool.output_preview.is_empty() {
102            lines.push(Line::from(Span::styled(
103                "output",
104                Style::default().fg(theme::SUBTLE()).add_modifier(Modifier::BOLD),
105            )));
106            lines.push(Line::from(Span::styled(
107                self.tool.output_preview.clone(),
108                Style::default().fg(theme::TEXT_MUTED()),
109            )));
110            lines.push(Line::from(""));
111        }
112
113        if !self.tool.sub_progress.is_empty() {
114            lines.push(Line::from(Span::styled(
115                "delegated steps",
116                Style::default().fg(theme::SUBTLE()).add_modifier(Modifier::BOLD),
117            )));
118            for l in &self.tool.sub_progress {
119                lines.push(Line::from(vec![
120                    Span::styled("↪ ", Style::default().fg(theme::IRIS())),
121                    Span::styled(l.clone(), Style::default().fg(theme::TEXT_MUTED())),
122                ]));
123            }
124        }
125
126        let footer = Line::from(Span::styled(
127            "  esc to close",
128            Style::default().fg(theme::SUBTLE()).add_modifier(Modifier::ITALIC | Modifier::DIM),
129        ));
130        lines.push(Line::from(""));
131        lines.push(footer);
132
133        Paragraph::new(lines).wrap(Wrap { trim: false }).render(body, buf);
134    }
135}