use crate::app::AppState;
use crate::ui::style::{self, Emphasis};
use crate::palette::filter::ScoredCommand;
use crate::palette::command::{CommandDef, CommandCategory};
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::widgets::{List, ListItem, ListState, Clear, Paragraph};
use ratatui::text::{Line, Span};
use ratatui::style::Modifier;
use std::collections::HashMap;
use super::utils::{category_color, category_abbreviation, highlight_matches};
use super::layout::center_rect;
pub fn render_command_list_with_custom(
frame: &mut Frame,
area: Rect,
state: &AppState,
commands: &[ScoredCommand],
custom_commands: &[(String, String)],
selected: usize,
) {
let mut items = Vec::new();
for (i, scored) in commands.iter().enumerate() {
let cmd = scored.command;
let is_selected = i == selected;
items.push(build_list_item(cmd, scored, &cmd.category, is_selected, false, true, state));
}
if !commands.is_empty() && !custom_commands.is_empty() {
items.push(ListItem::new(Line::from(vec![
Span::styled("─── Custom Commands ───", style::text(&state.theme, Emphasis::Muted)),
])));
}
let custom_start = commands.len();
for (i, (name, _)) in custom_commands.iter().enumerate() {
let idx = custom_start + i;
let is_selected = idx == selected;
let style = if is_selected {
style::selection(&state.theme)
} else {
style::body_style(&state.theme)
};
items.push(ListItem::new(Line::from(vec![
Span::styled(format!("🔧 {}", name), style),
])).style(style));
}
let list = List::new(items)
.style(style::body_style(&state.theme))
.block(style::pane_block(&state.theme, "Commands", false));
frame.render_widget(list, area);
}
pub fn render_command_list_grouped(
frame: &mut Frame,
area: Rect,
state: &AppState,
grouped: &HashMap<CommandCategory, Vec<&ScoredCommand>>,
selected: usize,
_use_two_column: bool,
original_commands: &[ScoredCommand],
) -> (usize, usize) {
let mut flat_commands: Vec<(CommandCategory, &ScoredCommand, usize)> = Vec::new();
let mut categories: Vec<_> = grouped.keys().collect();
categories.sort_by_key(|c| format!("{:?}", c));
for category in categories {
let commands_in_category = &grouped[category];
for cmd in commands_in_category {
let original_idx = original_commands
.iter()
.position(|c| std::ptr::eq(c.command, cmd.command))
.unwrap_or(flat_commands.len());
flat_commands.push((*category, *cmd, original_idx));
}
}
let visual_selected = selected.min(flat_commands.len().saturating_sub(1));
let original_idx = flat_commands
.get(visual_selected)
.map(|(_, _, idx)| *idx)
.unwrap_or(selected);
let items: Vec<ListItem> = flat_commands
.iter()
.enumerate()
.map(|(i, (category, scored, _global_idx))| {
let cmd = scored.command;
let is_selected = i == visual_selected;
let show_full_details = i < 5; let show_description = i < 15;
build_list_item(cmd, scored, category, is_selected, show_full_details, show_description, state)
})
.collect();
let title = if state.palette_input.is_empty() {
"Command Palette".to_string()
} else {
format!("Palette: {} [{} / {}]", state.palette_input, visual_selected + 1, flat_commands.len().max(1))
};
let mut list_state = ListState::default();
list_state.select(Some(visual_selected));
let list = List::new(items)
.style(style::body_style(&state.theme))
.block(style::pane_block(&state.theme, title.as_str(), true));
frame.render_stateful_widget(list, area, &mut list_state);
(original_idx, visual_selected)
}
fn build_list_item(
cmd: &CommandDef,
scored: &ScoredCommand,
category: &CommandCategory,
is_selected: bool,
show_full_details: bool,
show_description: bool,
state: &AppState,
) -> ListItem<'static> {
let mut spans = Vec::new();
let category_color = category_color(category, &state.theme);
let category_abbr = category_abbreviation(category);
spans.push(Span::styled(
format!("[{}] ", category_abbr),
category_color,
));
let name_spans = highlight_matches(cmd.name, &scored.match_ranges, state);
spans.extend(name_spans);
spans.push(Span::styled(
format!(" ({})", cmd.key),
style::text(&state.theme, Emphasis::Muted).add_modifier(Modifier::BOLD),
));
if show_full_details && show_description {
spans.push(Span::styled(
format!(" — {}", cmd.description),
style::text(&state.theme, Emphasis::Muted),
));
}
let line = Line::from(spans);
let item_style = if is_selected {
style::selection(&state.theme)
} else {
style::text(&state.theme, Emphasis::Normal)
};
ListItem::new(line).style(item_style)
}
pub fn render_empty(frame: &mut Frame, area: Rect, state: &AppState) {
let popup = center_rect(55, 25, area);
frame.render_widget(Clear, popup);
let block = style::pane_block(&state.theme, "Command Palette", true);
let text = if state.palette_input.is_empty() {
vec![
Line::from("Type to search commands..."),
Line::from(""),
Line::from(vec![
Span::styled("Tip: ", style::text(&state.theme, Emphasis::Header)),
Span::styled("Use @category to filter (e.g., @git, @staging)", style::text(&state.theme, Emphasis::Muted)),
]),
Line::from(""),
Line::from(vec![
Span::styled("Examples: ", style::text(&state.theme, Emphasis::Header)),
Span::styled("@git push", style::text(&state.theme, Emphasis::Normal)),
]),
]
} else {
vec![
Line::from("No matching commands found"),
Line::from(""),
Line::from(vec![
Span::styled("Try: ", style::text(&state.theme, Emphasis::Header)),
Span::styled("• Different keywords", style::text(&state.theme, Emphasis::Muted)),
]),
Line::from(vec![
Span::styled(" ", style::text(&state.theme, Emphasis::Muted)),
Span::styled("• Category filter: @git, @staging, etc.", style::text(&state.theme, Emphasis::Muted)),
]),
]
};
let paragraph = Paragraph::new(text)
.block(block)
.style(style::body_style(&state.theme))
.alignment(ratatui::layout::Alignment::Left);
frame.render_widget(paragraph, popup);
}