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, 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}