#![allow(dead_code)]
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState};
use ratatui::Frame;
use crate::command::{CommandRegistry, CommandSpec};
use crate::tui::theme::Theme;
pub struct Palette {
matcher: SkimMatcherV2,
matches: Vec<&'static str>,
selected: usize,
}
impl Palette {
pub fn new() -> Self {
Palette {
matcher: SkimMatcherV2::default(),
matches: Vec::new(),
selected: 0,
}
}
pub fn update(&mut self, registry: &CommandRegistry, query: &str) {
let q = query.trim();
let mut scored: Vec<(i64, &'static str)> = registry
.all()
.iter()
.filter_map(|spec| {
if q.is_empty() {
Some((0, spec.name))
} else {
self.matcher
.fuzzy_match(spec.name, q)
.or_else(|| {
spec.aliases
.iter()
.filter_map(|a| self.matcher.fuzzy_match(a, q))
.max()
})
.map(|score| (score, spec.name))
}
})
.collect();
scored.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(b.1)));
self.matches = scored.into_iter().map(|(_, n)| n).collect();
if self.selected >= self.matches.len() {
self.selected = self.matches.len().saturating_sub(1);
}
}
pub fn is_empty(&self) -> bool {
self.matches.is_empty()
}
pub fn select_next(&mut self) {
if !self.matches.is_empty() {
self.selected = (self.selected + 1) % self.matches.len();
}
}
pub fn select_prev(&mut self) {
if !self.matches.is_empty() {
self.selected = (self.selected + self.matches.len() - 1) % self.matches.len();
}
}
pub fn selected_name(&self) -> Option<&'static str> {
self.matches.get(self.selected).copied()
}
pub fn reset(&mut self) {
self.matches.clear();
self.selected = 0;
}
pub fn render(
&self,
frame: &mut Frame,
anchor: Rect,
registry: &CommandRegistry,
theme: &Theme,
) {
let max_rows = 8usize;
let shown = self.matches.len().min(max_rows);
let height = (shown as u16) + 2; let width = anchor.width.min(64);
let x = anchor.x;
let y = anchor.y.saturating_sub(height);
let area = Rect::new(x, y, width, height);
let items: Vec<ListItem> = self
.matches
.iter()
.take(max_rows)
.map(|name| {
let spec = registry.resolve(name);
let usage = spec
.map(CommandSpec::usage)
.unwrap_or_else(|| name.to_string());
let summary = spec.map(|s| s.summary).unwrap_or("");
ListItem::new(Line::from(vec![
Span::styled(format!("/{:<22}", usage), Style::default().fg(theme.accent)),
Span::styled(summary.to_string(), Style::default().fg(theme.muted)),
]))
})
.collect();
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.warning))
.title(Span::styled(
" commands ",
Style::default()
.fg(theme.warning)
.add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(theme.overlay_bg));
let list = List::new(items).block(block).highlight_style(
Style::default()
.bg(theme.highlight)
.add_modifier(Modifier::BOLD),
);
let mut state = ListState::default();
if !self.matches.is_empty() {
state.select(Some(self.selected.min(shown.saturating_sub(1))));
}
frame.render_widget(Clear, area);
frame.render_stateful_widget(list, area, &mut state);
}
}
impl Default for Palette {
fn default() -> Self {
Palette::new()
}
}