vtcode_tui/core_tui/session/render/
palettes.rs1use super::*;
2use crate::ui::tui::session::inline_list::{InlineListRow, selection_padding};
3use crate::ui::tui::session::list_panel::{
4 SharedListPanelSections, SharedListPanelStyles, StaticRowsListPanelModel, fixed_section_rows,
5 render_shared_list_panel, rows_to_u16, split_bottom_list_panel,
6};
7
8#[derive(Clone)]
9struct FilePaletteRenderRow {
10 text: String,
11 style: Style,
12 selectable: bool,
13 selected: bool,
14}
15
16pub fn split_inline_file_palette_area(session: &mut Session, area: Rect) -> (Rect, Option<Rect>) {
17 if area.height == 0
18 || area.width == 0
19 || session.modal.is_some()
20 || !session.inline_lists_visible()
21 || session.history_picker_state.active
22 || !session.file_palette_active
23 {
24 return (area, None);
25 }
26
27 let Some(palette) = session.file_palette.as_ref() else {
28 return (area, None);
29 };
30
31 let info_rows = if palette.has_files() {
32 file_palette_instructions(session, palette).len()
33 } else {
34 1
35 };
36 let fixed_rows = fixed_section_rows(1, info_rows, palette.has_files());
37
38 let list_rows = if palette.has_files() {
39 let mut rows = palette.current_page_items().len().max(1);
40 if palette.has_more_items() {
41 rows += 1;
42 }
43 rows.min(ui::INLINE_LIST_MAX_ROWS)
44 } else {
45 1
46 };
47
48 split_bottom_list_panel(area, fixed_rows, rows_to_u16(list_rows))
49}
50
51pub fn render_file_palette(session: &mut Session, frame: &mut Frame<'_>, area: Rect) {
52 if !session.file_palette_active
53 || !session.inline_lists_visible()
54 || area.height == 0
55 || area.width == 0
56 || session.modal.is_some()
57 {
58 return;
59 }
60
61 let Some(palette) = session.file_palette.as_ref() else {
62 return;
63 };
64
65 frame.render_widget(Clear, area);
66
67 if !palette.has_files() {
68 let loading = Paragraph::new(Line::from(Span::styled(
69 "Loading workspace files...".to_owned(),
70 default_style(session).add_modifier(Modifier::DIM),
71 )))
72 .wrap(Wrap { trim: true });
73 frame.render_widget(loading, area);
74 return;
75 }
76
77 let instructions = file_palette_instructions(session, palette);
78 let rows = build_file_palette_rows(session, palette);
79 let item_count = rows.len();
80 if item_count == 0 {
81 return;
82 }
83
84 let default_style = default_style(session);
85 let highlight_style = modal_list_highlight_style(session);
86 let unselected_prefix = selection_padding();
87
88 let selected = rows.iter().position(|row| row.selectable && row.selected);
89 let rendered_rows = rows
90 .into_iter()
91 .map(|row| {
92 (
93 InlineListRow::single(
94 Line::from(vec![
95 Span::styled(unselected_prefix.clone(), default_style),
96 Span::styled(row.text, row.style),
97 ]),
98 if row.selectable {
99 default_style
100 } else {
101 default_style.add_modifier(Modifier::DIM)
102 },
103 ),
104 1_u16,
105 )
106 })
107 .collect::<Vec<_>>();
108
109 let filter = palette.filter_query();
110 let search_line = if filter.is_empty() {
111 "Filter: (type @path in input)".to_owned()
112 } else {
113 format!("Filter: {}", filter)
114 };
115 let sections = SharedListPanelSections {
116 header: vec![Line::from(Span::styled("Files".to_owned(), default_style))],
117 info: instructions,
118 search: Some(Line::from(Span::styled(search_line, default_style))),
119 };
120 let mut model = StaticRowsListPanelModel {
121 rows: rendered_rows,
122 selected,
123 offset: 0,
124 visible_rows: 0,
125 };
126
127 render_shared_list_panel(
128 frame,
129 area,
130 sections,
131 SharedListPanelStyles {
132 base_style: default_style,
133 selected_style: Some(highlight_style),
134 text_style: default_style,
135 },
136 &mut model,
137 );
138}
139
140fn build_file_palette_rows(session: &Session, palette: &FilePalette) -> Vec<FilePaletteRenderRow> {
141 let mut rows = Vec::new();
142 let default = default_style(session);
143
144 for (_global_idx, entry, selected) in palette.current_page_items() {
145 let mut style = default;
146 let prefix = if entry.is_dir {
147 style = style.add_modifier(Modifier::BOLD);
148 "↳ "
149 } else {
150 " · "
151 };
152
153 rows.push(FilePaletteRenderRow {
154 text: format!("{}{}", prefix, entry.display_name),
155 style,
156 selectable: true,
157 selected,
158 });
159 }
160
161 if rows.is_empty() {
162 rows.push(FilePaletteRenderRow {
163 text: "No matching files".to_owned(),
164 style: default.add_modifier(Modifier::DIM),
165 selectable: false,
166 selected: false,
167 });
168 }
169
170 if palette.has_more_items() {
171 let remaining = palette
172 .total_items()
173 .saturating_sub(palette.current_page_number() * 20);
174 rows.push(FilePaletteRenderRow {
175 text: format!(" ... ({} more items)", remaining),
176 style: default.add_modifier(Modifier::DIM | Modifier::ITALIC),
177 selectable: false,
178 selected: false,
179 });
180 }
181
182 rows
183}
184
185fn file_palette_instructions(session: &Session, palette: &FilePalette) -> Vec<Line<'static>> {
186 let mut lines = vec![];
187
188 if palette.is_empty() {
189 lines.push(Line::from(Span::styled(
190 "No files found matching filter".to_owned(),
191 default_style(session).add_modifier(Modifier::DIM),
192 )));
193 } else {
194 let total = palette.total_items();
195 let count_text = if total == 1 {
196 "1 file".to_owned()
197 } else {
198 format!("{} files", total)
199 };
200
201 let nav_text = "↑↓ Navigate · PgUp/PgDn Page · Tab/Enter Select";
202
203 lines.push(Line::from(vec![Span::styled(
204 format!("{} · Esc Close", nav_text),
205 default_style(session),
206 )]));
207
208 lines.push(Line::from(vec![
209 Span::styled(
210 format!("Showing {}", count_text),
211 default_style(session).add_modifier(Modifier::DIM),
212 ),
213 Span::styled(
214 if !palette.filter_query().is_empty() {
215 format!(" matching '{}'", palette.filter_query())
216 } else {
217 String::new()
218 },
219 accent_style(session),
220 ),
221 ]));
222 }
223
224 lines
225}