1pub mod args;
2pub mod builder;
3pub mod id;
4pub mod meta;
5
6pub use args::{ArgKind, ArgValue, CommandArg};
7pub use builder::command;
8pub use id::CommandId;
9pub use meta::CommandMeta;
10
11use std::collections::{HashMap, VecDeque};
12use std::time::Instant;
13
14use crate::app_state::AppState;
15use crate::operation::Operation;
16
17pub type CommandHandler =
18 Box<dyn Fn(&AppState, &HashMap<String, ArgValue>) -> Vec<Operation>>;
19
20pub struct Command {
21 pub meta: CommandMeta,
22 pub handler: CommandHandler,
24}
25
26const HISTORY_LIMIT: usize = 50;
27
28#[derive(Debug, Clone)]
29pub struct HistoryEntry {
30 pub id: CommandId,
31 pub args: HashMap<String, ArgValue>,
32 pub timestamp: Instant,
33 pub count: u32,
34}
35
36pub struct CommandRegistry {
37 commands: HashMap<CommandId, Command>,
38 history: VecDeque<HistoryEntry>,
39}
40
41impl CommandRegistry {
42 pub fn new() -> Self {
43 Self {
44 commands: HashMap::new(),
45 history: VecDeque::new(),
46 }
47 }
48
49 pub fn register(&mut self, command: Command) {
50 self.commands.insert(command.meta.id.clone(), command);
51 }
52
53 pub fn select_command_in_context<'a>(
56 &self,
57 ids: &'a Vec<CommandId>,
58 active_contexts: &std::collections::HashSet<String>,
59 ) -> Option<&'a CommandId> {
60 log::debug!("Select commands in context {ids:?} {active_contexts:?}");
61 for id in ids {
62 if let Some(cmd) = self.commands.get(id) {
63 let active = cmd.meta.contexts.is_empty()
64 || cmd
65 .meta
66 .contexts
67 .iter()
68 .any(|c| active_contexts.contains(c.as_ref()));
69 log::debug!("{id:?} {active:?}");
70 if active {
71 return Some(id);
72 }
73 }
74 }
75 None
76 }
77
78 #[allow(dead_code)]
79 pub fn active_commands<'a>(
80 &'a self,
81 app: &'a AppState,
82 ) -> impl Iterator<Item = &'a Command> + 'a {
83 self.commands.values().filter(move |cmd| {
84 cmd.meta.contexts.is_empty()
85 || cmd
86 .meta
87 .contexts
88 .iter()
89 .any(|c| app.active_contexts.contains(c.as_ref()))
90 })
91 }
92
93 #[allow(dead_code)]
94 pub fn all_commands_sorted(&self) -> Vec<&Command> {
95 let mut cmds: Vec<&Command> = self.commands.values().collect();
96 cmds.sort_by(|a, b| {
97 a.meta
98 .id
99 .group
100 .cmp(&b.meta.id.group)
101 .then(a.meta.id.name.cmp(&b.meta.id.name))
102 });
103 cmds
104 }
105
106 pub fn user_commands_sorted(&self) -> Vec<&Command> {
107 let mut cmds: Vec<&Command> = self
108 .commands
109 .values()
110 .filter(|c| matches!(c.meta.visibility, meta::Visibility::UserVisible))
111 .collect();
112 cmds.sort_by(|a, b| {
113 a.meta
114 .id
115 .group
116 .cmp(&b.meta.id.group)
117 .then(a.meta.id.name.cmp(&b.meta.id.name))
118 });
119 cmds
120 }
121
122 pub fn history(&self) -> &VecDeque<HistoryEntry> {
123 &self.history
124 }
125
126 pub fn execute(
129 &mut self,
130 id: &CommandId,
131 args: HashMap<String, ArgValue>,
132 app: &AppState,
133 ) -> Vec<Operation> {
134 if let Some(cmd) = self.commands.get(id) {
135
136 (cmd.handler)(app, &args)
138 } else {
139 log::warn!("Unknown command: {}", id);
140 vec![]
141 }
142 }
143
144 fn push_history(&mut self, id: CommandId, args: HashMap<String, ArgValue>) {
145 if let Some(cmd) = self.commands.get(&id) {
147 if !matches!(cmd.meta.visibility, meta::Visibility::UserVisible) {
148 return;
149 }
150 } else {
151 return;
153 }
154
155 if let Some(front) = self.history.front_mut()
156 && front.id == id
157 && front.args == args
158 {
159 front.count += 1;
160 front.timestamp = Instant::now();
161 return;
162 }
163 self.history.push_front(HistoryEntry {
164 id,
165 args,
166 timestamp: Instant::now(),
167 count: 1,
168 });
169 if self.history.len() > HISTORY_LIMIT {
170 self.history.pop_back();
171 }
172 }
173
174 pub fn record_palette_selection(&mut self, id: CommandId, args: HashMap<String, ArgValue>) {
177 self.push_history(id, args);
178 }
179}
180
181impl Default for CommandRegistry {
182 fn default() -> Self {
183 Self::new()
184 }
185}