Skip to main content

vtcode_tui/core_tui/session/render/
palettes.rs

1use 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}