vtcode_tui/core_tui/session/render/
palettes.rs1use super::*;
2use crate::ui::tui::session::inline_list::{InlineListRow, render_inline_list};
3
4#[derive(Clone)]
5struct FilePaletteRenderRow {
6 text: String,
7 style: Style,
8 selectable: bool,
9 selected: bool,
10}
11
12pub fn split_inline_file_palette_area(session: &mut Session, area: Rect) -> (Rect, Option<Rect>) {
13 if area.height == 0
14 || area.width == 0
15 || session.modal.is_some()
16 || session.history_picker_state.active
17 || !session.file_palette_active
18 {
19 return (area, None);
20 }
21
22 let Some(palette) = session.file_palette.as_ref() else {
23 return (area, None);
24 };
25
26 let instruction_rows = if palette.has_files() {
27 file_palette_instructions(session, palette).len()
28 } else {
29 1
30 };
31
32 let list_rows = if palette.has_files() {
33 let mut rows = palette.current_page_items().len().max(1);
34 if palette.has_more_items() {
35 rows += 1;
36 }
37 rows.min(ui::INLINE_LIST_MAX_ROWS)
38 } else {
39 1
40 };
41
42 let instruction_rows = instruction_rows.min(u16::MAX as usize) as u16;
43 let desired_height = instruction_rows.saturating_add(list_rows.min(u16::MAX as usize) as u16);
44 let max_panel_height = area.height.saturating_sub(1);
45 if max_panel_height <= instruction_rows {
46 return (area, None);
47 }
48
49 let panel_height = desired_height.min(max_panel_height);
50 let chunks =
51 Layout::vertical([Constraint::Min(1), Constraint::Length(panel_height)]).split(area);
52 (chunks[0], Some(chunks[1]))
53}
54
55pub fn render_file_palette(session: &mut Session, frame: &mut Frame<'_>, area: Rect) {
56 if !session.file_palette_active
57 || area.height == 0
58 || area.width == 0
59 || session.modal.is_some()
60 {
61 return;
62 }
63
64 let Some(palette) = session.file_palette.as_ref() else {
65 return;
66 };
67
68 frame.render_widget(Clear, area);
69
70 if !palette.has_files() {
71 let loading = Paragraph::new(Line::from(Span::styled(
72 "Loading workspace files...".to_owned(),
73 default_style(session).add_modifier(Modifier::DIM),
74 )))
75 .wrap(Wrap { trim: true });
76 frame.render_widget(loading, area);
77 return;
78 }
79
80 let instructions = file_palette_instructions(session, palette);
81 let instruction_rows = instructions.len().min(u16::MAX as usize) as u16;
82 let layout = if instruction_rows == 0 {
83 Layout::vertical([Constraint::Min(1)]).split(area)
84 } else {
85 Layout::vertical([Constraint::Length(instruction_rows), Constraint::Min(1)]).split(area)
86 };
87
88 if layout.is_empty() {
89 return;
90 }
91
92 let list_area = if instruction_rows == 0 {
93 layout[0]
94 } else {
95 frame.render_widget(
96 Paragraph::new(instructions).wrap(Wrap { trim: true }),
97 layout[0],
98 );
99 if layout.len() < 2 {
100 return;
101 }
102 layout[1]
103 };
104
105 if list_area.width == 0 || list_area.height == 0 {
106 return;
107 }
108
109 let rows = build_file_palette_rows(session, palette);
110 let item_count = rows.len();
111 if item_count == 0 {
112 return;
113 }
114
115 let default_style = default_style(session);
116 let highlight_style = modal_list_highlight_style(session);
117 let selected_prefix = ui::MODAL_LIST_HIGHLIGHT_FULL.to_owned();
118 let unselected_prefix = " ".repeat(selected_prefix.chars().count());
119
120 let selected = rows.iter().position(|row| row.selectable && row.selected);
121 let rendered_rows = rows
122 .into_iter()
123 .map(|row| {
124 (
125 InlineListRow::single(
126 Line::from(vec![
127 Span::styled(unselected_prefix.clone(), default_style),
128 Span::styled(row.text, row.style),
129 ]),
130 if row.selectable {
131 default_style
132 } else {
133 default_style.add_modifier(Modifier::DIM)
134 },
135 ),
136 1_u16,
137 )
138 })
139 .collect::<Vec<_>>();
140
141 let _ = render_inline_list(
142 frame,
143 list_area,
144 rendered_rows,
145 selected,
146 Some(highlight_style),
147 );
148}
149
150fn build_file_palette_rows(session: &Session, palette: &FilePalette) -> Vec<FilePaletteRenderRow> {
151 let mut rows = Vec::new();
152 let default = default_style(session);
153
154 for (_global_idx, entry, selected) in palette.current_page_items() {
155 let mut style = default;
156 let prefix = if entry.is_dir {
157 style = style.add_modifier(Modifier::BOLD);
158 "↳ "
159 } else {
160 " · "
161 };
162
163 rows.push(FilePaletteRenderRow {
164 text: format!("{}{}", prefix, entry.display_name),
165 style,
166 selectable: true,
167 selected,
168 });
169 }
170
171 if rows.is_empty() {
172 rows.push(FilePaletteRenderRow {
173 text: "No matching files".to_owned(),
174 style: default.add_modifier(Modifier::DIM),
175 selectable: false,
176 selected: false,
177 });
178 }
179
180 if palette.has_more_items() {
181 let remaining = palette
182 .total_items()
183 .saturating_sub(palette.current_page_number() * 20);
184 rows.push(FilePaletteRenderRow {
185 text: format!(" ... ({} more items)", remaining),
186 style: default.add_modifier(Modifier::DIM | Modifier::ITALIC),
187 selectable: false,
188 selected: false,
189 });
190 }
191
192 rows
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}