use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
#[derive(Debug, Clone)]
pub struct Command {
pub name: String,
pub shortcut: Option<String>,
pub description: String,
pub action_id: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PaletteState {
Closed,
Open,
Filtering,
}
pub struct CommandPalette {
pub state: PaletteState,
pub commands: Vec<Command>,
pub filtered: Vec<usize>,
pub query: String,
pub selected_index: usize,
pub max_visible: usize,
pub border_color: Color,
pub bg: Color,
pub selected_bg: Color,
}
impl CommandPalette {
pub fn new() -> Self {
Self {
state: PaletteState::Closed,
commands: Vec::new(),
filtered: Vec::new(),
query: String::new(),
selected_index: 0,
max_visible: 10,
border_color: Color::rgb(88, 166, 255),
bg: Color::rgb(13, 17, 23),
selected_bg: Color::rgb(31, 111, 235),
}
}
pub fn with_commands(mut self, commands: Vec<Command>) -> Self {
self.commands = commands;
self.rebuild_filter();
self
}
pub fn add_command(&mut self, command: Command) {
self.commands.push(command);
self.rebuild_filter();
}
pub fn open(&mut self) {
self.state = PaletteState::Open;
self.query.clear();
self.selected_index = 0;
self.rebuild_filter();
}
pub fn close(&mut self) {
self.state = PaletteState::Closed;
self.query.clear();
}
pub fn is_open(&self) -> bool {
self.state != PaletteState::Closed
}
pub fn input_char(&mut self, c: char) {
if self.state == PaletteState::Closed {
return;
}
self.query.push(c);
self.selected_index = 0;
self.rebuild_filter();
}
pub fn backspace(&mut self) {
if self.state == PaletteState::Closed {
return;
}
self.query.pop();
self.selected_index = 0;
self.rebuild_filter();
}
pub fn select_next(&mut self) {
if self.filtered.is_empty() {
return;
}
self.selected_index = (self.selected_index + 1) % self.filtered.len();
}
pub fn select_prev(&mut self) {
if self.filtered.is_empty() {
return;
}
self.selected_index = if self.selected_index == 0 {
self.filtered.len() - 1
} else {
self.selected_index - 1
};
}
pub fn execute_selected(&self) -> Option<&str> {
self.filtered
.get(self.selected_index)
.and_then(|&idx| self.commands.get(idx))
.map(|cmd| cmd.action_id.as_str())
}
fn rebuild_filter(&mut self) {
self.filtered.clear();
let query_lower = self.query.to_lowercase();
for (i, cmd) in self.commands.iter().enumerate() {
if query_lower.is_empty()
|| cmd.name.to_lowercase().contains(&query_lower)
|| cmd.description.to_lowercase().contains(&query_lower)
{
self.filtered.push(i);
}
}
}
pub fn render(&self, buffer: &mut Buffer, area: Rect) {
if self.state == PaletteState::Closed {
return;
}
let w = 50.min(area.width);
let h = (self.max_visible as u16 + 4).min(area.height);
let x = area.x + (area.width.saturating_sub(w)) / 2;
let y = area.y + (area.height.saturating_sub(h)) / 2;
let rect = Rect::new(x, y, w, h);
buffer.fill(rect, ' ', Color::WHITE, Some(self.bg));
for dx in 1..w.saturating_sub(1) {
buffer.set(
(x + dx) as usize,
y as usize,
crate::core::buffer::Cell {
ch: '─',
fg: self.border_color,
bg: Some(self.bg),
bold: false,
italic: false,
underlined: false,
},
);
buffer.set(
(x + dx) as usize,
(y + h - 1) as usize,
crate::core::buffer::Cell {
ch: '─',
fg: self.border_color,
bg: Some(self.bg),
bold: false,
italic: false,
underlined: false,
},
);
}
buffer.set(
x as usize,
y as usize,
crate::core::buffer::Cell {
ch: '╭',
fg: self.border_color,
bg: Some(self.bg),
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
(x + w - 1) as usize,
y as usize,
crate::core::buffer::Cell {
ch: '╮',
fg: self.border_color,
bg: Some(self.bg),
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
x as usize,
(y + h - 1) as usize,
crate::core::buffer::Cell {
ch: '╰',
fg: self.border_color,
bg: Some(self.bg),
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
(x + w - 1) as usize,
(y + h - 1) as usize,
crate::core::buffer::Cell {
ch: '╯',
fg: self.border_color,
bg: Some(self.bg),
bold: true,
italic: false,
underlined: false,
},
);
let search_text = format!(" > {}█", self.query);
let search_display: String = search_text.chars().take((w - 4) as usize).collect();
buffer.set_str(
(x + 2) as usize,
(y + 1) as usize,
&search_display,
self.border_color,
Some(self.bg),
);
for (i, &cmd_idx) in self.filtered.iter().take(self.max_visible).enumerate() {
let opt_y = (y + 3 + i as u16) as usize;
if opt_y >= (y + h - 1) as usize {
break;
}
let cmd = &self.commands[cmd_idx];
let selected = i == self.selected_index;
let prefix = if selected { " ▸ " } else { " " };
let shortcut = cmd.shortcut.as_deref().unwrap_or("");
let text = format!("{}{} [{}]", prefix, cmd.name, shortcut);
let display: String = text.chars().take((w - 4) as usize).collect();
let color = if selected {
Color::WHITE
} else {
Color::rgb(139, 148, 158)
};
let bg = if selected {
Some(self.selected_bg)
} else {
Some(self.bg)
};
buffer.set_str((x + 2) as usize, opt_y, &display, color, bg);
}
}
}
impl Default for CommandPalette {
fn default() -> Self {
Self::new()
}
}