Skip to main content

vtcode_tui/core_tui/session/render/
palettes.rs

1use super::*;
2
3/// Generic palette rendering helper to avoid duplication
4struct PaletteRenderParams<F>
5where
6    F: for<'a> Fn(&Session, &'a str, bool) -> ListItem<'static>,
7{
8    is_active: bool,
9    title: String,
10    items: Vec<(usize, String, bool)>, // (index, display_text, is_selected)
11    instructions: Vec<Line<'static>>,
12    has_more: bool,
13    more_text: String,
14    render_item: F,
15}
16
17fn render_palette_generic<F>(
18    session: &mut Session,
19    frame: &mut Frame<'_>,
20    viewport: Rect,
21    params: PaletteRenderParams<F>,
22) where
23    F: for<'a> Fn(&Session, &'a str, bool) -> ListItem<'static>,
24{
25    if !params.is_active || viewport.height == 0 || viewport.width == 0 || session.modal.is_some() {
26        return;
27    }
28
29    if params.items.is_empty() {
30        return;
31    }
32
33    let modal_height =
34        params.items.len() + params.instructions.len() + 2 + if params.has_more { 1 } else { 0 };
35    let area = compute_modal_area(viewport, modal_height, 0, 0, true);
36
37    frame.render_widget(Clear, area);
38    let block = Block::bordered()
39        .title(params.title)
40        .border_type(terminal_capabilities::get_border_type())
41        .style(default_style(session))
42        .border_style(border_style(session));
43    let inner = block.inner(area);
44    frame.render_widget(block, area);
45
46    if inner.height == 0 || inner.width == 0 {
47        return;
48    }
49
50    let layout = ModalListLayout::new(inner, params.instructions.len());
51    if let Some(text_area) = layout.text_area {
52        let paragraph = Paragraph::new(params.instructions).wrap(Wrap { trim: true });
53        frame.render_widget(paragraph, text_area);
54    }
55
56    let mut list_items: Vec<ListItem> = params
57        .items
58        .iter()
59        .map(|(_, display_text, is_selected)| {
60            (params.render_item)(session, display_text.as_str(), *is_selected)
61        })
62        .collect();
63
64    if params.has_more {
65        let continuation_style =
66            default_style(session).add_modifier(Modifier::DIM | Modifier::ITALIC);
67        list_items.push(ListItem::new(Line::from(Span::styled(
68            params.more_text,
69            continuation_style,
70        ))));
71    }
72
73    let list = List::new(list_items)
74        .style(default_style(session))
75        .highlight_symbol(ui::MODAL_LIST_HIGHLIGHT_FULL)
76        .repeat_highlight_symbol(true);
77    frame.render_widget(list, layout.list_area);
78}
79
80pub fn render_file_palette(session: &mut Session, frame: &mut Frame<'_>, viewport: Rect) {
81    if !session.file_palette_active {
82        return;
83    }
84
85    let Some(palette) = session.file_palette.as_ref() else {
86        return;
87    };
88
89    if viewport.height == 0 || viewport.width == 0 || session.modal.is_some() {
90        return;
91    }
92
93    // Show loading state if no files loaded yet
94    if !palette.has_files() {
95        render_file_palette_loading(session, frame, viewport);
96        return;
97    }
98
99    let items = palette.current_page_items();
100    if items.is_empty() && palette.filter_query().is_empty() {
101        return;
102    }
103
104    // Convert items to generic format
105    let generic_items: Vec<(usize, String, bool)> = items
106        .iter()
107        .map(|(idx, entry, selected)| {
108            let display = if entry.is_dir {
109                format!("{}/ ", entry.display_name)
110            } else {
111                entry.display_name.clone()
112            };
113            (*idx, display, *selected)
114        })
115        .collect();
116
117    let title = format!(
118        "File Browser (Page {}/{})",
119        palette.current_page_number(),
120        palette.total_pages()
121    );
122
123    let instructions = file_palette_instructions(session, palette);
124    let has_more = palette.has_more_items();
125    let more_text = format!(
126        "  ... ({} more items)",
127        palette
128            .total_items()
129            .saturating_sub(palette.current_page_number() * 20)
130    );
131
132    // Render using generic helper
133    render_palette_generic(
134        session,
135        frame,
136        viewport,
137        PaletteRenderParams {
138            is_active: true, // is_active already checked above
139            title,
140            items: generic_items,
141            instructions,
142            has_more,
143            more_text,
144            render_item: |session, display_text: &str, is_selected| {
145                let base_style = if is_selected {
146                    modal_list_highlight_style(session)
147                } else {
148                    default_style(session)
149                };
150
151                // Apply file-specific styling
152                let mut style = base_style;
153
154                // Add icon prefix based on file type
155                let (prefix, is_dir) = if display_text.ends_with("/ ") {
156                    ("↳  ", true)
157                } else {
158                    ("  · ", false)
159                };
160
161                if is_dir {
162                    style = style.add_modifier(Modifier::BOLD);
163                }
164
165                let display = format!("{}{}", prefix, display_text.trim_end_matches("/ "));
166                ListItem::new(Line::from(display).style(style))
167            },
168        },
169    );
170}
171
172fn render_file_palette_loading(session: &Session, frame: &mut Frame<'_>, viewport: Rect) {
173    let modal_height = 3;
174    let area = compute_modal_area(viewport, modal_height, 0, 0, true);
175
176    frame.render_widget(Clear, area);
177    let block = Block::bordered()
178        .title("File Browser")
179        .border_type(terminal_capabilities::get_border_type())
180        .style(default_style(session))
181        .border_style(border_style(session));
182    let inner = block.inner(area);
183    frame.render_widget(block, area);
184
185    if inner.height > 0 && inner.width > 0 {
186        let loading_text = vec![Line::from(Span::styled(
187            "Loading workspace files...".to_owned(),
188            default_style(session).add_modifier(Modifier::DIM),
189        ))];
190        let paragraph = Paragraph::new(loading_text).wrap(Wrap { trim: true });
191        frame.render_widget(paragraph, inner);
192    }
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}