vtcode_tui/core_tui/widgets/
slash.rs1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, Clear, List, ListItem, Paragraph, Widget, Wrap},
7};
8
9use crate::config::constants::ui;
10use crate::ui::tui::session::{
11 Session,
12 modal::compute_modal_area,
13 slash_palette::{SlashPalette, SlashPaletteSuggestion},
14 terminal_capabilities,
15};
16use crate::ui::tui::style::{ratatui_color_from_ansi, ratatui_style_from_inline};
17use crate::ui::tui::types::InlineTextStyle;
18
19pub struct SlashWidget<'a> {
28 session: &'a Session,
29 palette: &'a SlashPalette,
30 viewport: Rect,
31 highlight_style: Option<Style>,
32}
33
34impl<'a> SlashWidget<'a> {
35 pub fn new(session: &'a Session, palette: &'a SlashPalette, viewport: Rect) -> Self {
37 Self {
38 session,
39 palette,
40 viewport,
41 highlight_style: None,
42 }
43 }
44
45 #[must_use]
47 pub fn highlight_style(mut self, style: Style) -> Self {
48 self.highlight_style = Some(style);
49 self
50 }
51}
52
53impl<'a> Widget for SlashWidget<'a> {
54 fn render(self, _area: Rect, buf: &mut Buffer) {
55 if self.viewport.height == 0 || self.viewport.width == 0 || self.palette.is_empty() {
56 return;
57 }
58
59 let suggestions = self.palette.suggestions();
60 if suggestions.is_empty() {
61 return;
62 }
63
64 let instructions = self.instructions();
65 let modal_height = suggestions.len() + instructions.len() + 2;
66 let area = compute_modal_area(self.viewport, modal_height, 0, 0, true);
67
68 Clear.render(area, buf);
70
71 let block = Block::bordered()
73 .title("Slash Commands")
74 .border_type(terminal_capabilities::get_border_type())
75 .style(self.session.styles.default_style())
76 .border_style(self.session.styles.border_style());
77 let inner = block.inner(area);
78 block.render(area, buf);
79
80 if inner.height == 0 || inner.width == 0 {
81 return;
82 }
83
84 let inst_height = instructions.len().min(inner.height as usize);
86 if inst_height > 0 {
87 let inst_area = Rect {
88 x: inner.x,
89 y: inner.y,
90 width: inner.width,
91 height: inst_height as u16,
92 };
93 let paragraph = Paragraph::new(instructions).wrap(Wrap { trim: true });
94 paragraph.render(inst_area, buf);
95 }
96
97 let list_y = inner.y + inst_height as u16;
99 let list_height = inner.height.saturating_sub(inst_height as u16);
100 if list_height > 0 {
101 let list_area = Rect {
102 x: inner.x,
103 y: list_y,
104 width: inner.width,
105 height: list_height,
106 };
107
108 let list_items = self.create_list_items(suggestions);
109 let list = List::new(list_items)
110 .style(self.session.styles.default_style())
111 .highlight_style(
112 self.highlight_style
113 .unwrap_or_else(|| self.slash_highlight_style()),
114 )
115 .highlight_symbol(ui::MODAL_LIST_HIGHLIGHT_FULL)
116 .repeat_highlight_symbol(true);
117
118 list.render(list_area, buf);
121 }
122 }
123}
124
125impl<'a> SlashWidget<'a> {
126 fn create_list_items(&self, suggestions: &[SlashPaletteSuggestion]) -> Vec<ListItem<'static>> {
128 suggestions
129 .iter()
130 .map(|suggestion| match suggestion {
131 SlashPaletteSuggestion::Static(command) => ListItem::new(Line::from(vec![
132 Span::styled(format!("/ {}", command.name), self.slash_name_style()),
133 Span::raw(" "),
134 Span::styled(
135 command.description.to_owned(),
136 self.slash_description_style(),
137 ),
138 ])),
139 })
140 .collect()
141 }
142
143 fn instructions(&self) -> Vec<Line<'static>> {
145 vec![
146 Line::from(Span::styled(
147 ui::SLASH_PALETTE_HINT_PRIMARY.to_owned(),
148 self.session.styles.default_style(),
149 )),
150 Line::from(Span::styled(
151 ui::SLASH_PALETTE_HINT_SECONDARY.to_owned(),
152 self.session
153 .styles
154 .default_style()
155 .add_modifier(Modifier::DIM),
156 )),
157 ]
158 }
159
160 fn slash_highlight_style(&self) -> Style {
162 let mut style = Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD);
163 if let Some(primary) = self.session.theme.primary.or(self.session.theme.secondary) {
164 style = style.fg(ratatui_color_from_ansi(primary));
165 }
166 style
167 }
168
169 fn slash_name_style(&self) -> Style {
171 let style = InlineTextStyle::default()
172 .bold()
173 .with_color(self.session.theme.primary.or(self.session.theme.foreground));
174 ratatui_style_from_inline(&style, self.session.theme.foreground)
175 }
176
177 fn slash_description_style(&self) -> Style {
179 self.session
180 .styles
181 .default_style()
182 .add_modifier(Modifier::DIM)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::ui::tui::InlineTheme;
190 use crate::ui::tui::types::SlashCommandItem;
191 use ratatui::Terminal;
192 use ratatui::backend::TestBackend;
193
194 fn create_test_session() -> Session {
195 let theme = InlineTheme::default();
196 Session::new(theme, None, 24)
197 }
198
199 fn create_test_palette() -> SlashPalette {
200 let mut palette = SlashPalette::with_commands(vec![
201 SlashCommandItem::new("help", "Show help"),
202 SlashCommandItem::new("hello", "Demo command"),
203 ]);
204 palette.update(Some("he"), 5);
206 palette
207 }
208
209 #[test]
210 fn test_slash_widget_creation() {
211 let session = create_test_session();
212 let palette = create_test_palette();
213 let viewport = ratatui::layout::Rect::new(0, 0, 80, 24);
214
215 let _widget = SlashWidget::new(&session, &palette, viewport);
216
217 assert_eq!(viewport.width, 80);
219 assert_eq!(viewport.height, 24);
220 }
221
222 #[test]
223 fn test_slash_widget_render_empty() {
224 let session = create_test_session();
225 let palette = SlashPalette::new(); let viewport = ratatui::layout::Rect::new(0, 0, 80, 24);
227
228 let backend = TestBackend::new(80, 24);
229 let mut terminal = Terminal::new(backend).unwrap();
230
231 terminal
232 .draw(|frame| {
233 let widget = SlashWidget::new(&session, &palette, viewport);
234 widget.render(viewport, frame.buffer_mut());
235 })
236 .unwrap();
237
238 assert!(true);
240 }
241
242 #[test]
243 fn test_slash_widget_render_with_suggestions() {
244 let session = create_test_session();
245 let mut palette = SlashPalette::with_commands(vec![
246 SlashCommandItem::new("help", "Show help"),
247 SlashCommandItem::new("hello", "Demo command"),
248 ]);
249 palette.update(Some(""), 5); let viewport = ratatui::layout::Rect::new(0, 0, 80, 24);
251
252 let backend = TestBackend::new(80, 24);
253 let mut terminal = Terminal::new(backend).unwrap();
254
255 terminal
256 .draw(|frame| {
257 let widget = SlashWidget::new(&session, &palette, viewport);
258 widget.render(viewport, frame.buffer_mut());
259 })
260 .unwrap();
261
262 assert!(true);
264 }
265}