stynx_code_tui/widgets/
tool_detail.rs1use 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}