use ratatui::style::Style;
use ratatui::widgets::ListState;
use ratatui::widgets::{Block, Borders};
use ratatui_textarea::TextArea;
pub const PALETTE_TABS: &[(&str, &str, Option<crate::actions::ActionCategory>)] = &[
("All", "\u{f0ca}", None),
(
"Notes",
"\u{f15c}",
Some(crate::actions::ActionCategory::Notes),
),
(
"Import",
"\u{f019}",
Some(crate::actions::ActionCategory::Import),
),
(
"Append",
"\u{f067}",
Some(crate::actions::ActionCategory::Append),
),
(
"Views",
"\u{f06e}",
Some(crate::actions::ActionCategory::Views),
),
(
"Settings",
"\u{f013}",
Some(crate::actions::ActionCategory::Settings),
),
];
pub struct PaletteItem {
pub id: String,
pub name: String,
pub description: String,
pub glyph: String,
pub score: i64,
}
pub struct CommandPalette {
pub input: TextArea<'static>,
pub items: Vec<PaletteItem>,
pub state: ListState,
pub context_note_id: Option<String>,
pub active_tab: usize,
}
impl CommandPalette {
pub fn new(context_note_id: Option<String>, app: &crate::app::App) -> Self {
let mut input = TextArea::default();
input.set_cursor_line_style(Style::default());
input.set_placeholder_text("Search commands...");
input.set_style(app.app_theme.bg_style());
input.set_block(
Block::default()
.style(app.app_theme.bg_style())
.borders(Borders::ALL)
.border_style(Style::default().fg(app.app_theme.muted)),
);
let mut p = Self {
input,
items: Vec::new(),
state: ListState::default(),
context_note_id,
active_tab: 0,
};
p.refresh_items(app);
p
}
pub fn refresh_items(&mut self, app: &crate::app::App) {
let query = self.input.lines()[0].as_str();
let actions = crate::actions::get_all_action_infos(app);
let mut matched = Vec::with_capacity(actions.len());
let category_filter = PALETTE_TABS[self.active_tab].2;
if query.is_empty() {
for action in actions {
if category_filter.is_some_and(|cat| action.category != cat) {
continue;
}
matched.push(PaletteItem {
id: action.id.clone(),
name: action.name.clone(),
description: action.description.clone(),
glyph: action.glyph.clone(),
score: 0,
});
}
} else {
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
let matcher = SkimMatcherV2::default();
for action in actions {
if category_filter.is_some_and(|cat| action.category != cat) {
continue;
}
if let Some(score) = matcher.fuzzy_match(&action.name, query) {
matched.push(PaletteItem {
id: action.id.clone(),
name: action.name.clone(),
description: action.description.clone(),
glyph: action.glyph.clone(),
score,
});
}
}
matched.sort_by_key(|b| std::cmp::Reverse(b.score));
}
self.items = matched;
if self.items.is_empty() {
self.state.select(None);
} else {
self.state.select(Some(0));
}
}
pub fn handle_input(&mut self, key: crossterm::event::KeyEvent, app: &crate::app::App) -> bool {
use crossterm::event::KeyCode;
match key.code {
KeyCode::Esc => return true,
KeyCode::Tab => {
self.active_tab = (self.active_tab + 1) % PALETTE_TABS.len();
self.refresh_items(app);
}
KeyCode::BackTab => {
if self.active_tab == 0 {
self.active_tab = PALETTE_TABS.len() - 1;
} else {
self.active_tab -= 1;
}
self.refresh_items(app);
}
KeyCode::Down => {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len().saturating_sub(1) {
0
} else {
i + 1
}
}
None => 0,
};
if !self.items.is_empty() {
self.state.select(Some(i));
}
}
KeyCode::Up => {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len().saturating_sub(1)
} else {
i - 1
}
}
None => 0,
};
if !self.items.is_empty() {
self.state.select(Some(i));
}
}
KeyCode::Enter => {
return true;
}
_ => {
self.input.input(key);
self.refresh_items(app);
}
}
false
}
}