pub mod args;
pub mod builder;
pub mod id;
pub mod meta;
pub use args::{ArgKind, ArgValue, CommandArg};
pub use builder::command;
pub use id::CommandId;
pub use meta::CommandMeta;
use std::collections::{HashMap, VecDeque};
use std::time::Instant;
use crate::app_state::AppState;
use crate::operation::Operation;
pub type CommandHandler =
Box<dyn Fn(&AppState, &HashMap<String, ArgValue>) -> Vec<Operation>>;
pub struct Command {
pub meta: CommandMeta,
pub handler: CommandHandler,
}
const HISTORY_LIMIT: usize = 50;
#[derive(Debug, Clone)]
pub struct HistoryEntry {
pub id: CommandId,
pub args: HashMap<String, ArgValue>,
pub timestamp: Instant,
pub count: u32,
}
pub struct CommandRegistry {
commands: HashMap<CommandId, Command>,
history: VecDeque<HistoryEntry>,
}
impl CommandRegistry {
pub fn new() -> Self {
Self {
commands: HashMap::new(),
history: VecDeque::new(),
}
}
pub fn register(&mut self, command: Command) {
self.commands.insert(command.meta.id.clone(), command);
}
pub fn select_command_in_context<'a>(
&self,
ids: &'a Vec<CommandId>,
active_contexts: &std::collections::HashSet<String>,
) -> Option<&'a CommandId> {
log::debug!("Select commands in context {ids:?} {active_contexts:?}");
for id in ids {
if let Some(cmd) = self.commands.get(id) {
let active = cmd.meta.contexts.is_empty()
|| cmd
.meta
.contexts
.iter()
.any(|c| active_contexts.contains(c.as_ref()));
log::debug!("{id:?} {active:?}");
if active {
return Some(id);
}
}
}
None
}
#[allow(dead_code)]
pub fn active_commands<'a>(
&'a self,
app: &'a AppState,
) -> impl Iterator<Item = &'a Command> + 'a {
self.commands.values().filter(move |cmd| {
cmd.meta.contexts.is_empty()
|| cmd
.meta
.contexts
.iter()
.any(|c| app.active_contexts.contains(c.as_ref()))
})
}
#[allow(dead_code)]
pub fn all_commands_sorted(&self) -> Vec<&Command> {
let mut cmds: Vec<&Command> = self.commands.values().collect();
cmds.sort_by(|a, b| {
a.meta
.id
.group
.cmp(&b.meta.id.group)
.then(a.meta.id.name.cmp(&b.meta.id.name))
});
cmds
}
pub fn user_commands_sorted(&self) -> Vec<&Command> {
let mut cmds: Vec<&Command> = self
.commands
.values()
.filter(|c| matches!(c.meta.visibility, meta::Visibility::UserVisible))
.collect();
cmds.sort_by(|a, b| {
a.meta
.id
.group
.cmp(&b.meta.id.group)
.then(a.meta.id.name.cmp(&b.meta.id.name))
});
cmds
}
pub fn history(&self) -> &VecDeque<HistoryEntry> {
&self.history
}
pub fn execute(
&mut self,
id: &CommandId,
args: HashMap<String, ArgValue>,
app: &AppState,
) -> Vec<Operation> {
if let Some(cmd) = self.commands.get(id) {
(cmd.handler)(app, &args)
} else {
log::warn!("Unknown command: {}", id);
vec![]
}
}
fn push_history(&mut self, id: CommandId, args: HashMap<String, ArgValue>) {
if let Some(cmd) = self.commands.get(&id) {
if !matches!(cmd.meta.visibility, meta::Visibility::UserVisible) {
return;
}
} else {
return;
}
if let Some(front) = self.history.front_mut()
&& front.id == id
&& front.args == args
{
front.count += 1;
front.timestamp = Instant::now();
return;
}
self.history.push_front(HistoryEntry {
id,
args,
timestamp: Instant::now(),
count: 1,
});
if self.history.len() > HISTORY_LIMIT {
self.history.pop_back();
}
}
pub fn record_palette_selection(&mut self, id: CommandId, args: HashMap<String, ArgValue>) {
self.push_history(id, args);
}
}
impl Default for CommandRegistry {
fn default() -> Self {
Self::new()
}
}