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, SharedSearchField, StaticRowsListPanelModel,
5 fixed_section_rows, 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.has_active_overlay()
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.has_active_overlay()
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 sections = SharedListPanelSections {
111 header: vec![Line::from(Span::styled("Files".to_owned(), default_style))],
112 info: instructions,
113 search: Some(SharedSearchField {
114 label: "Search files".to_owned(),
115 placeholder: Some("filename or path".to_owned()),
116 query: filter.to_owned(),
117 }),
118 };
119 let mut model = StaticRowsListPanelModel {
120 rows: rendered_rows,
121 selected,
122 offset: 0,
123 visible_rows: 0,
124 };
125
126 render_shared_list_panel(
127 frame,
128 area,
129 sections,
130 SharedListPanelStyles {
131 base_style: default_style,
132 selected_style: Some(highlight_style),
133 text_style: default_style,
134 },
135 &mut model,
136 );
137}
138
139fn build_file_palette_rows(session: &Session, palette: &FilePalette) -> Vec<FilePaletteRenderRow> {
140 let mut rows = Vec::new();
141 let default = default_style(session);
142
143 for (_global_idx, entry, selected) in palette.current_page_items() {
144 let mut style = default;
145 let prefix = if entry.is_dir {
146 style = style.add_modifier(Modifier::BOLD);
147 "↳ "
148 } else {
149 " · "
150 };
151
152 rows.push(FilePaletteRenderRow {
153 text: format!("{}{}", prefix, entry.display_name),
154 style,
155 selectable: true,
156 selected,
157 });
158 }
159
160 if rows.is_empty() {
161 rows.push(FilePaletteRenderRow {
162 text: "No matching files".to_owned(),
163 style: default.add_modifier(Modifier::DIM),
164 selectable: false,
165 selected: false,
166 });
167 }
168
169 if palette.has_more_items() {
170 let remaining = palette
171 .total_items()
172 .saturating_sub(palette.current_page_number() * 20);
173 rows.push(FilePaletteRenderRow {
174 text: format!(" ... ({} more items)", remaining),
175 style: default.add_modifier(Modifier::DIM | Modifier::ITALIC),
176 selectable: false,
177 selected: false,
178 });
179 }
180
181 rows
182}
183
184fn file_palette_instructions(session: &Session, palette: &FilePalette) -> Vec<Line<'static>> {
185 let mut lines = vec![];
186
187 if palette.is_empty() {
188 lines.push(Line::from(Span::styled(
189 "No files found matching filter".to_owned(),
190 default_style(session).add_modifier(Modifier::DIM),
191 )));
192 } else {
193 let total = palette.total_items();
194 let count_text = if total == 1 {
195 "1 file".to_owned()
196 } else {
197 format!("{} files", total)
198 };
199
200 let nav_text = "↑↓ Navigate · PgUp/PgDn Page · Tab/Enter Select";
201
202 lines.push(Line::from(vec![Span::styled(
203 format!("{} · Esc Close", nav_text),
204 default_style(session),
205 )]));
206
207 lines.push(Line::from(vec![
208 Span::styled(
209 format!("Showing {}", count_text),
210 default_style(session).add_modifier(Modifier::DIM),
211 ),
212 Span::styled(
213 if !palette.filter_query().is_empty() {
214 format!(" matching '{}'", palette.filter_query())
215 } else {
216 String::new()
217 },
218 accent_style(session),
219 ),
220 ]));
221 }
222
223 lines
224}