vtcode_tui/core_tui/session/render/
palettes.rs1use super::*;
2
3struct PaletteRenderParams<F>
5where
6 F: for<'a> Fn(&Session, &'a str, bool) -> ListItem<'static>,
7{
8 is_active: bool,
9 title: String,
10 items: Vec<(usize, String, bool)>, instructions: Vec<Line<'static>>,
12 has_more: bool,
13 more_text: String,
14 render_item: F,
15}
16
17fn render_palette_generic<F>(
18 session: &mut Session,
19 frame: &mut Frame<'_>,
20 viewport: Rect,
21 params: PaletteRenderParams<F>,
22) where
23 F: for<'a> Fn(&Session, &'a str, bool) -> ListItem<'static>,
24{
25 if !params.is_active || viewport.height == 0 || viewport.width == 0 || session.modal.is_some() {
26 return;
27 }
28
29 if params.items.is_empty() {
30 return;
31 }
32
33 let modal_height =
34 params.items.len() + params.instructions.len() + 2 + if params.has_more { 1 } else { 0 };
35 let area = compute_modal_area(viewport, modal_height, 0, 0, true);
36
37 frame.render_widget(Clear, area);
38 let block = Block::bordered()
39 .title(params.title)
40 .border_type(terminal_capabilities::get_border_type())
41 .style(default_style(session))
42 .border_style(border_style(session));
43 let inner = block.inner(area);
44 frame.render_widget(block, area);
45
46 if inner.height == 0 || inner.width == 0 {
47 return;
48 }
49
50 let layout = ModalListLayout::new(inner, params.instructions.len());
51 if let Some(text_area) = layout.text_area {
52 let paragraph = Paragraph::new(params.instructions).wrap(Wrap { trim: true });
53 frame.render_widget(paragraph, text_area);
54 }
55
56 let mut list_items: Vec<ListItem> = params
57 .items
58 .iter()
59 .map(|(_, display_text, is_selected)| {
60 (params.render_item)(session, display_text.as_str(), *is_selected)
61 })
62 .collect();
63
64 if params.has_more {
65 let continuation_style =
66 default_style(session).add_modifier(Modifier::DIM | Modifier::ITALIC);
67 list_items.push(ListItem::new(Line::from(Span::styled(
68 params.more_text,
69 continuation_style,
70 ))));
71 }
72
73 let list = List::new(list_items)
74 .style(default_style(session))
75 .highlight_symbol(ui::MODAL_LIST_HIGHLIGHT_FULL)
76 .repeat_highlight_symbol(true);
77 frame.render_widget(list, layout.list_area);
78}
79
80pub fn render_file_palette(session: &mut Session, frame: &mut Frame<'_>, viewport: Rect) {
81 if !session.file_palette_active {
82 return;
83 }
84
85 let Some(palette) = session.file_palette.as_ref() else {
86 return;
87 };
88
89 if viewport.height == 0 || viewport.width == 0 || session.modal.is_some() {
90 return;
91 }
92
93 if !palette.has_files() {
95 render_file_palette_loading(session, frame, viewport);
96 return;
97 }
98
99 let items = palette.current_page_items();
100 if items.is_empty() && palette.filter_query().is_empty() {
101 return;
102 }
103
104 let generic_items: Vec<(usize, String, bool)> = items
106 .iter()
107 .map(|(idx, entry, selected)| {
108 let display = if entry.is_dir {
109 format!("{}/ ", entry.display_name)
110 } else {
111 entry.display_name.clone()
112 };
113 (*idx, display, *selected)
114 })
115 .collect();
116
117 let title = format!(
118 "File Browser (Page {}/{})",
119 palette.current_page_number(),
120 palette.total_pages()
121 );
122
123 let instructions = file_palette_instructions(session, palette);
124 let has_more = palette.has_more_items();
125 let more_text = format!(
126 " ... ({} more items)",
127 palette
128 .total_items()
129 .saturating_sub(palette.current_page_number() * 20)
130 );
131
132 render_palette_generic(
134 session,
135 frame,
136 viewport,
137 PaletteRenderParams {
138 is_active: true, title,
140 items: generic_items,
141 instructions,
142 has_more,
143 more_text,
144 render_item: |session, display_text: &str, is_selected| {
145 let base_style = if is_selected {
146 modal_list_highlight_style(session)
147 } else {
148 default_style(session)
149 };
150
151 let mut style = base_style;
153
154 let (prefix, is_dir) = if display_text.ends_with("/ ") {
156 ("↳ ", true)
157 } else {
158 (" · ", false)
159 };
160
161 if is_dir {
162 style = style.add_modifier(Modifier::BOLD);
163 }
164
165 let display = format!("{}{}", prefix, display_text.trim_end_matches("/ "));
166 ListItem::new(Line::from(display).style(style))
167 },
168 },
169 );
170}
171
172fn render_file_palette_loading(session: &Session, frame: &mut Frame<'_>, viewport: Rect) {
173 let modal_height = 3;
174 let area = compute_modal_area(viewport, modal_height, 0, 0, true);
175
176 frame.render_widget(Clear, area);
177 let block = Block::bordered()
178 .title("File Browser")
179 .border_type(terminal_capabilities::get_border_type())
180 .style(default_style(session))
181 .border_style(border_style(session));
182 let inner = block.inner(area);
183 frame.render_widget(block, area);
184
185 if inner.height > 0 && inner.width > 0 {
186 let loading_text = vec![Line::from(Span::styled(
187 "Loading workspace files...".to_owned(),
188 default_style(session).add_modifier(Modifier::DIM),
189 ))];
190 let paragraph = Paragraph::new(loading_text).wrap(Wrap { trim: true });
191 frame.render_widget(paragraph, inner);
192 }
193}
194
195fn file_palette_instructions(session: &Session, palette: &FilePalette) -> Vec<Line<'static>> {
196 let mut lines = vec![];
197
198 if palette.is_empty() {
199 lines.push(Line::from(Span::styled(
200 "No files found matching filter".to_owned(),
201 default_style(session).add_modifier(Modifier::DIM),
202 )));
203 } else {
204 let total = palette.total_items();
205 let count_text = if total == 1 {
206 "1 file".to_owned()
207 } else {
208 format!("{} files", total)
209 };
210
211 let nav_text = "↑↓ Navigate · PgUp/PgDn Page · Tab/Enter Select";
212
213 lines.push(Line::from(vec![Span::styled(
214 format!("{} · Esc Close", nav_text),
215 default_style(session),
216 )]));
217
218 lines.push(Line::from(vec![
219 Span::styled(
220 format!("Showing {}", count_text),
221 default_style(session).add_modifier(Modifier::DIM),
222 ),
223 Span::styled(
224 if !palette.filter_query().is_empty() {
225 format!(" matching '{}'", palette.filter_query())
226 } else {
227 String::new()
228 },
229 accent_style(session),
230 ),
231 ]));
232 }
233
234 lines
235}
236
237pub(super) fn has_input_status(session: &Session) -> bool {
238 let left_present = session
239 .input_status_left
240 .as_ref()
241 .is_some_and(|value| !value.trim().is_empty());
242 if left_present {
243 return true;
244 }
245 session
246 .input_status_right
247 .as_ref()
248 .is_some_and(|value| !value.trim().is_empty())
249}