envelope_cli/tui/dialogs/
command_palette.rs

1//! Command palette dialog
2//!
3//! Provides fuzzy search for commands
4
5use ratatui::{
6    layout::Rect,
7    style::{Color, Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
10    Frame,
11};
12
13use crate::tui::app::App;
14use crate::tui::commands::COMMANDS;
15use crate::tui::layout::centered_rect_fixed;
16
17/// Render the command palette
18pub fn render(frame: &mut Frame, app: &mut App) {
19    let width = 60;
20    let height = 20;
21    let area = centered_rect_fixed(width, height, frame.area());
22
23    // Clear the background
24    frame.render_widget(Clear, area);
25
26    let block = Block::default()
27        .title(" Command Palette ")
28        .title_style(
29            Style::default()
30                .fg(Color::Cyan)
31                .add_modifier(Modifier::BOLD),
32        )
33        .borders(Borders::ALL)
34        .border_style(Style::default().fg(Color::Cyan));
35
36    frame.render_widget(block, area);
37
38    // Input area
39    let input_area = Rect {
40        x: area.x + 1,
41        y: area.y + 1,
42        width: area.width - 2,
43        height: 1,
44    };
45
46    let input_line = Line::from(vec![
47        Span::styled("> ", Style::default().fg(Color::Cyan)),
48        Span::styled(app.command_input.clone(), Style::default().fg(Color::White)),
49        Span::styled("_", Style::default().fg(Color::Cyan)), // Cursor
50    ]);
51
52    frame.render_widget(Paragraph::new(input_line), input_area);
53
54    // Results area
55    let results_area = Rect {
56        x: area.x + 1,
57        y: area.y + 3,
58        width: area.width - 2,
59        height: area.height - 4,
60    };
61
62    // Filter commands based on input
63    let filtered_commands: Vec<(usize, &crate::tui::commands::Command)> = COMMANDS
64        .iter()
65        .enumerate()
66        .filter(|(_, cmd)| {
67            if app.command_input.is_empty() {
68                true
69            } else {
70                let query = app.command_input.to_lowercase();
71                cmd.name.to_lowercase().contains(&query)
72                    || cmd.description.to_lowercase().contains(&query)
73            }
74        })
75        .collect();
76
77    if filtered_commands.is_empty() {
78        let text = Paragraph::new("No matching commands").style(Style::default().fg(Color::Yellow));
79        frame.render_widget(text, results_area);
80        return;
81    }
82
83    // Build list items
84    let items: Vec<ListItem> = filtered_commands
85        .iter()
86        .map(|(_, cmd)| {
87            let line = Line::from(vec![
88                Span::styled(
89                    format!("{:<20}", cmd.name),
90                    Style::default().fg(Color::Cyan),
91                ),
92                Span::raw(" "),
93                Span::styled(cmd.description, Style::default().fg(Color::Yellow)),
94            ]);
95            ListItem::new(line)
96        })
97        .collect();
98
99    let list = List::new(items)
100        .highlight_style(
101            Style::default()
102                .bg(Color::DarkGray)
103                .add_modifier(Modifier::BOLD),
104        )
105        .highlight_symbol("▶ ");
106
107    let mut state = ListState::default();
108    let selected = app
109        .selected_command_index
110        .min(filtered_commands.len().saturating_sub(1));
111    state.select(Some(selected));
112
113    frame.render_stateful_widget(list, results_area, &mut state);
114}