#![allow(dead_code)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CommandId {
NewOrchestration,
OpenOrchestration,
SwitchProvider,
OpenStage,
Approve,
Regenerate,
Edit,
ToggleTelemetry,
Help,
Quit,
}
pub struct Command {
pub id: CommandId,
pub label: &'static str,
pub shortcut: &'static str,
}
pub const COMMANDS: &[Command] = &[
Command {
id: CommandId::NewOrchestration,
label: "Nova orquestração",
shortcut: "n",
},
Command {
id: CommandId::OpenOrchestration,
label: "Abrir orquestração",
shortcut: "Enter",
},
Command {
id: CommandId::SwitchProvider,
label: "Trocar provider/modelo",
shortcut: "p",
},
Command {
id: CommandId::OpenStage,
label: "Abrir/executar etapa",
shortcut: "Enter",
},
Command {
id: CommandId::Approve,
label: "Aprovar checkpoint",
shortcut: "a",
},
Command {
id: CommandId::Regenerate,
label: "Regenerar via provider",
shortcut: "r",
},
Command {
id: CommandId::Edit,
label: "Editar no $EDITOR",
shortcut: "e",
},
Command {
id: CommandId::ToggleTelemetry,
label: "Telemetria",
shortcut: "t",
},
Command {
id: CommandId::Help,
label: "Ajuda",
shortcut: "?",
},
Command {
id: CommandId::Quit,
label: "Sair",
shortcut: "q",
},
];
fn match_quality(label_lower: &str, query_lower: &str) -> Option<u8> {
if query_lower.is_empty() {
return Some(0);
}
if label_lower.starts_with(query_lower) {
return Some(0);
}
if label_lower.contains(query_lower) {
return Some(1);
}
let mut chars = label_lower.chars();
let matched = query_lower.chars().all(|qc| chars.any(|lc| lc == qc));
if matched {
return Some(2);
}
None
}
pub fn filter(query: &str) -> Vec<usize> {
let query_lower = query.to_lowercase();
let mut matched: Vec<(usize, u8)> = COMMANDS
.iter()
.enumerate()
.filter_map(|(i, cmd)| {
let label_lower = cmd.label.to_lowercase();
match_quality(&label_lower, &query_lower).map(|g| (i, g))
})
.collect();
matched.sort_by_key(|&(_, g)| g);
matched.into_iter().map(|(i, _)| i).collect()
}
#[derive(Clone, Debug, Default)]
pub struct CommandPalette {
pub open: bool,
pub query: String,
pub selected: usize,
}
impl CommandPalette {
pub fn open(&mut self) {
self.open = true;
self.query.clear();
self.selected = 0;
}
pub fn close(&mut self) {
self.open = false;
self.query.clear();
self.selected = 0;
}
pub fn push_char(&mut self, c: char) {
self.query.push(c);
self.clamp_selected();
}
pub fn backspace(&mut self) {
self.query.pop();
self.clamp_selected();
}
pub fn move_up(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
pub fn move_down(&mut self) {
let len = self.visible().len();
if len > 0 && self.selected + 1 < len {
self.selected += 1;
}
}
pub fn visible(&self) -> Vec<usize> {
filter(&self.query)
}
pub fn selected_command(&self) -> Option<CommandId> {
let vis = self.visible();
vis.get(self.selected).map(|&i| COMMANDS[i].id)
}
fn clamp_selected(&mut self) {
let len = self.visible().len();
if len == 0 {
self.selected = 0;
} else if self.selected >= len {
self.selected = len - 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const IDX_NEW_ORCH: usize = 0; const IDX_OPEN_ORCH: usize = 1; const IDX_SWITCH: usize = 2; const IDX_OPEN_STAGE: usize = 3; const IDX_APPROVE: usize = 4; const IDX_REGEN: usize = 5; const IDX_EDIT: usize = 6; const IDX_TELEMETRY: usize = 7; const IDX_HELP: usize = 8; const IDX_QUIT: usize = 9;
#[test]
fn filter_vazio_retorna_todos() {
let result = filter("");
assert_eq!(result.len(), COMMANDS.len());
let expected: Vec<usize> = (0..COMMANDS.len()).collect();
assert_eq!(result, expected);
}
#[test]
fn filter_nov_inclui_nova_orquestracao() {
let result = filter("nov");
assert!(
result.contains(&IDX_NEW_ORCH),
"\"nov\" deve casar \"Nova orquestração\" (idx {IDX_NEW_ORCH})"
);
}
#[test]
fn filter_zzz_retorna_vazio() {
assert!(filter("zzz").is_empty());
}
#[test]
fn ranking_prefixo_antes_de_subsequencia() {
let result = filter("apr");
let pos_aprovar = result.iter().position(|&i| i == IDX_APPROVE);
assert!(
pos_aprovar.is_some(),
"\"Aprovar checkpoint\" deve casar \"apr\""
);
if let Some(pos_abrir) = result.iter().position(|&i| i == IDX_OPEN_ORCH) {
assert!(
pos_aprovar.unwrap() < pos_abrir,
"prefixo (aprovar) deve preceder subsequência (abrir) no ranking"
);
}
}
#[test]
fn filter_case_insensitive() {
let result = filter("NOVA");
assert!(result.contains(&IDX_NEW_ORCH));
}
#[test]
fn filter_substring_contigua() {
let result = filter("provider");
assert!(result.contains(&IDX_SWITCH));
assert!(result.contains(&IDX_REGEN));
}
#[test]
fn navegacao_clamp_nao_estoura() {
let mut p = CommandPalette::default();
p.open();
p.move_up();
assert_eq!(p.selected, 0);
let total = COMMANDS.len();
for _ in 0..total + 5 {
p.move_down();
}
assert_eq!(p.selected, total - 1, "não deve ultrapassar o último item");
for _ in 0..total + 5 {
p.move_up();
}
assert_eq!(p.selected, 0, "não deve ir abaixo de zero");
}
#[test]
fn navegacao_lista_vazia_nao_entra_em_indice_invalido() {
let mut p = CommandPalette::default();
p.open();
p.query = "zzz".to_string();
assert_eq!(p.visible().len(), 0);
p.move_down();
assert_eq!(p.selected, 0);
p.move_up();
assert_eq!(p.selected, 0);
assert!(p.selected_command().is_none());
}
#[test]
fn push_char_e_backspace_atualizam_query_e_clampam() {
let mut p = CommandPalette::default();
p.open();
let total = COMMANDS.len();
for _ in 0..total {
p.move_down();
}
assert_eq!(p.selected, total - 1);
p.push_char('z');
p.push_char('z');
p.push_char('z');
assert_eq!(p.query, "zzz");
assert_eq!(p.visible().len(), 0);
assert_eq!(p.selected, 0, "clamp para 0 quando lista vazia");
p.backspace();
p.backspace();
p.backspace();
assert_eq!(p.query, "");
assert_eq!(p.visible().len(), COMMANDS.len());
}
#[test]
fn selected_command_coerente_apos_filtrar() {
let mut p = CommandPalette::default();
p.open();
p.query = "nova".to_string();
let vis = p.visible();
assert!(!vis.is_empty());
let expected_id = COMMANDS[vis[0]].id;
assert_eq!(p.selected_command(), Some(expected_id));
}
#[test]
fn default_tem_open_false() {
let p = CommandPalette::default();
assert!(!p.open);
assert!(p.query.is_empty());
assert_eq!(p.selected, 0);
}
#[test]
fn open_e_close_resetam_estado() {
let mut p = CommandPalette::default();
p.open();
assert!(p.open);
assert!(p.query.is_empty());
assert_eq!(p.selected, 0);
p.push_char('a');
p.move_down();
p.close();
assert!(!p.open);
assert!(p.query.is_empty());
assert_eq!(p.selected, 0);
}
#[test]
fn todos_os_command_ids_representados_no_catalogo() {
assert_eq!(COMMANDS[IDX_NEW_ORCH].id, CommandId::NewOrchestration);
assert_eq!(COMMANDS[IDX_OPEN_ORCH].id, CommandId::OpenOrchestration);
assert_eq!(COMMANDS[IDX_SWITCH].id, CommandId::SwitchProvider);
assert_eq!(COMMANDS[IDX_OPEN_STAGE].id, CommandId::OpenStage);
assert_eq!(COMMANDS[IDX_APPROVE].id, CommandId::Approve);
assert_eq!(COMMANDS[IDX_REGEN].id, CommandId::Regenerate);
assert_eq!(COMMANDS[IDX_EDIT].id, CommandId::Edit);
assert_eq!(COMMANDS[IDX_TELEMETRY].id, CommandId::ToggleTelemetry);
assert_eq!(COMMANDS[IDX_HELP].id, CommandId::Help);
assert_eq!(COMMANDS[IDX_QUIT].id, CommandId::Quit);
}
}