Skip to main content

steer_tui/tui/widgets/formatters/
view.rs

1use super::{
2    ToolFormatter,
3    helpers::{separator_line, tool_error_user_message, truncate_lines},
4};
5use crate::tui::theme::{Component, Theme};
6use ratatui::{
7    style::{Modifier, Style},
8    text::{Line, Span},
9};
10use serde_json::Value;
11use std::path::Path;
12use steer_grpc::client_api::ToolResult;
13use steer_tools::tools::view::ViewParams;
14
15pub struct ViewFormatter;
16const MAX_LINES: usize = 100;
17
18impl ToolFormatter for ViewFormatter {
19    fn compact(
20        &self,
21        params: &Value,
22        result: &Option<ToolResult>,
23        _wrap_width: usize,
24        theme: &Theme,
25    ) -> Vec<Line<'static>> {
26        let mut lines = Vec::new();
27
28        let Ok(params) = serde_json::from_value::<ViewParams>(params.clone()) else {
29            return vec![Line::from(Span::styled(
30                "Invalid view params",
31                theme.style(Component::ErrorText),
32            ))];
33        };
34
35        let file_name = Path::new(&params.file_path)
36            .file_name()
37            .and_then(|s| s.to_str())
38            .unwrap_or(&params.file_path);
39
40        let mut spans = vec![Span::styled(file_name.to_string(), Style::default())];
41
42        // Add line range info if present
43        if params.offset.is_some() || params.limit.is_some() {
44            let offset = params.offset.map_or(1, |o| o + 1);
45            let limit = params.limit.unwrap_or(0);
46            let end_line = if limit > 0 { offset + limit - 1 } else { 0 };
47
48            if limit > 0 {
49                spans.push(Span::styled(
50                    format!(" [{offset}-{end_line}]"),
51                    Style::default(),
52                ));
53            } else {
54                spans.push(Span::styled(format!(" [{offset}+]"), Style::default()));
55            }
56        }
57
58        // Add count info from results
59        let info = extract_view_info(result);
60        spans.push(Span::raw(" "));
61        spans.push(Span::styled(
62            format!("({info})"),
63            theme
64                .style(Component::DimText)
65                .add_modifier(Modifier::ITALIC),
66        ));
67
68        lines.push(Line::from(spans));
69        lines
70    }
71
72    fn detailed(
73        &self,
74        params: &Value,
75        result: &Option<ToolResult>,
76        wrap_width: usize,
77        theme: &Theme,
78    ) -> Vec<Line<'static>> {
79        let mut lines = Vec::new();
80
81        let Ok(params) = serde_json::from_value::<ViewParams>(params.clone()) else {
82            return vec![Line::from(Span::styled(
83                "Invalid view params",
84                theme.style(Component::ErrorText),
85            ))];
86        };
87
88        // Show file path
89        lines.push(Line::from(Span::styled(
90            format!("File: {}", params.file_path),
91            theme
92                .style(Component::CodeFilePath)
93                .add_modifier(Modifier::BOLD),
94        )));
95
96        // Show line range if specified
97        if params.offset.is_some() || params.limit.is_some() {
98            lines.push(Line::from(Span::styled(
99                format!("Lines: {}", format_line_range(params.offset, params.limit)),
100                theme.style(Component::DimText),
101            )));
102        }
103
104        // Show output if we have results
105        if let Some(result) = result {
106            match result {
107                ToolResult::FileContent(file_content) => {
108                    if file_content.content.is_empty() {
109                        lines.push(Line::from(Span::styled(
110                            "(Empty file)",
111                            theme
112                                .style(Component::DimText)
113                                .add_modifier(Modifier::ITALIC),
114                        )));
115                    } else {
116                        lines.push(separator_line(wrap_width, theme.style(Component::DimText)));
117
118                        let (output_lines, truncated) =
119                            truncate_lines(&file_content.content, MAX_LINES);
120
121                        // Trim line number & tab
122                        let trimmed_lines: Vec<&str> = output_lines
123                            .iter()
124                            .map(|line| if line.len() > 6 { &line[6..] } else { "" })
125                            .collect();
126
127                        for line in trimmed_lines {
128                            for wrapped in textwrap::wrap(line, wrap_width) {
129                                lines.push(Line::from(Span::raw(wrapped.to_string())));
130                            }
131                        }
132
133                        if truncated {
134                            lines.push(Line::from(Span::styled(
135                                format!(
136                                    "... ({} more lines)",
137                                    file_content.content.lines().count() - MAX_LINES
138                                ),
139                                theme
140                                    .style(Component::DimText)
141                                    .add_modifier(Modifier::ITALIC),
142                            )));
143                        }
144                    }
145                }
146                ToolResult::Error(error) => {
147                    lines.push(separator_line(wrap_width, theme.style(Component::DimText)));
148                    lines.push(Line::from(Span::styled(
149                        tool_error_user_message(error).into_owned(),
150                        theme.style(Component::ErrorText),
151                    )));
152                }
153                _ => {
154                    lines.push(Line::from(Span::styled(
155                        "Unexpected result type",
156                        theme.style(Component::ErrorText),
157                    )));
158                }
159            }
160        }
161
162        lines
163    }
164}
165
166fn extract_view_info(result: &Option<ToolResult>) -> String {
167    match result {
168        Some(ToolResult::FileContent(file_content)) => {
169            let line_count = file_content.content.lines().count();
170            format!("{line_count} lines")
171        }
172        Some(ToolResult::Error(_)) => "error".to_string(),
173        _ => "pending".to_string(),
174    }
175}
176
177fn format_line_range(offset: Option<u64>, limit: Option<u64>) -> String {
178    let start = offset.map_or(1, |o| o + 1);
179    match limit {
180        Some(l) if l > 0 => format!("{}-{}", start, start + l - 1),
181        _ => format!("{start}-EOF"),
182    }
183}