use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::fmt;
use std::rc::Rc;
use std::sync::Arc;
use crate::app::input::keymap::{Action, Keymap};
use crate::callback::Callback;
use crate::callback::ScopeId;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct CommandId(Arc<str>);
impl CommandId {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for CommandId {
fn from(value: &str) -> Self {
Self(Arc::from(value))
}
}
impl From<String> for CommandId {
fn from(value: String) -> Self {
Self(Arc::from(value))
}
}
impl From<Arc<str>> for CommandId {
fn from(value: Arc<str>) -> Self {
Self(value)
}
}
impl fmt::Display for CommandId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone)]
pub struct CommandEntry {
pub id: CommandId,
pub label: Arc<str>,
pub description: Option<Arc<str>>,
pub category: Option<Arc<str>>,
pub keybinding_hint: Option<Arc<str>>,
pub enabled: bool,
pub(crate) scope: Option<ScopeId>,
pub handler: Callback<()>,
}
impl fmt::Debug for CommandEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CommandEntry")
.field("id", &self.id)
.field("label", &self.label)
.field("description", &self.description)
.field("category", &self.category)
.field("keybinding_hint", &self.keybinding_hint)
.field("enabled", &self.enabled)
.field("scope", &self.scope)
.finish()
}
}
impl CommandEntry {
pub fn builder(id: impl Into<CommandId>) -> CommandBuilder {
CommandBuilder::new(id)
}
}
#[derive(Clone)]
pub struct CommandBuilder {
id: CommandId,
label: Arc<str>,
description: Option<Arc<str>>,
category: Option<Arc<str>>,
keybinding_hint: Option<Arc<str>>,
enabled: bool,
scope: Option<ScopeId>,
handler: Callback<()>,
}
impl CommandBuilder {
pub fn new(id: impl Into<CommandId>) -> Self {
Self {
id: id.into(),
label: Arc::from(""),
description: None,
category: None,
keybinding_hint: None,
enabled: true,
scope: None,
handler: Callback::new(|_| {}),
}
}
pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
self.label = label.into();
self
}
pub fn description(mut self, description: impl Into<Arc<str>>) -> Self {
self.description = Some(description.into());
self
}
pub fn category(mut self, category: impl Into<Arc<str>>) -> Self {
self.category = Some(category.into());
self
}
pub fn keybinding(mut self, hint: impl Into<Arc<str>>) -> Self {
self.keybinding_hint = Some(hint.into());
self
}
pub fn keybinding_hint_opt(mut self, hint: Option<Arc<str>>) -> Self {
self.keybinding_hint = hint;
self
}
pub fn keybinding_from_keymap(mut self, keymap: &Keymap, action: Action) -> Self {
self.keybinding_hint = keymap
.binding_for_action(action)
.map(|binding| Arc::<str>::from(binding.canonical_lowercase()));
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn handler(mut self, handler: Callback<()>) -> Self {
self.handler = handler;
self
}
pub fn build(self) -> CommandEntry {
CommandEntry {
id: self.id,
label: self.label,
description: self.description,
category: self.category,
keybinding_hint: self.keybinding_hint,
enabled: self.enabled,
scope: self.scope,
handler: self.handler,
}
}
}
#[derive(Clone, Default)]
pub struct CommandRegistry {
entries: Rc<RefCell<HashMap<CommandId, CommandEntry>>>,
generation: Rc<Cell<u64>>,
}
impl CommandRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, entry: CommandEntry) {
self.entries.borrow_mut().insert(entry.id.clone(), entry);
self.bump_generation();
}
pub(crate) fn register_for_scope(&self, scope: ScopeId, entry: CommandEntry) {
self.register(CommandEntry {
scope: Some(scope),
..entry
});
}
pub fn unregister(&self, id: impl Into<CommandId>) -> Option<CommandEntry> {
let removed = self.entries.borrow_mut().remove(&id.into());
if removed.is_some() {
self.bump_generation();
}
removed
}
pub fn set_enabled(&self, id: impl Into<CommandId>, enabled: bool) -> bool {
let id = id.into();
let mut entries = self.entries.borrow_mut();
let Some(entry) = entries.get_mut(&id) else {
return false;
};
if entry.enabled != enabled {
entry.enabled = enabled;
self.bump_generation();
}
true
}
pub fn execute(&self, id: impl Into<CommandId>) -> bool {
let id = id.into();
let to_run = {
let entries = self.entries.borrow();
let Some(entry) = entries.get(&id) else {
return false;
};
if !entry.enabled {
return false;
}
entry.handler.clone()
};
to_run.emit(());
true
}
pub fn entries(&self) -> Vec<CommandEntry> {
self.entries.borrow().values().cloned().collect()
}
pub(crate) fn unregister_scope(&self, scope: ScopeId) {
let before = self.entries.borrow().len();
self.entries
.borrow_mut()
.retain(|_, entry| entry.scope != Some(scope));
if self.entries.borrow().len() != before {
self.bump_generation();
}
}
pub fn generation(&self) -> u64 {
self.generation.get()
}
fn bump_generation(&self) {
self.generation
.set(self.generation.get().wrapping_add(1).max(1));
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::rc::Rc;
use super::{CommandBuilder, CommandRegistry};
use crate::callback::Callback;
#[test]
fn register_replaces_existing_entry_by_id() {
let registry = CommandRegistry::new();
let hit_a = Rc::new(Cell::new(false));
let hit_b = Rc::new(Cell::new(false));
let hit_a_cb = Rc::clone(&hit_a);
registry.register(
CommandBuilder::new("app.test")
.label("A")
.handler(Callback::new(move |_| hit_a_cb.set(true)))
.build(),
);
let hit_b_cb = Rc::clone(&hit_b);
registry.register(
CommandBuilder::new("app.test")
.label("B")
.handler(Callback::new(move |_| hit_b_cb.set(true)))
.build(),
);
assert_eq!(registry.entries().len(), 1);
assert!(registry.execute("app.test"));
assert!(!hit_a.get());
assert!(hit_b.get());
}
#[test]
fn generation_increments_on_mutations() {
let registry = CommandRegistry::new();
assert_eq!(registry.generation(), 0);
registry.register(CommandBuilder::new("a").label("A").build());
let g1 = registry.generation();
assert!(g1 > 0);
assert!(registry.set_enabled("a", false));
let g2 = registry.generation();
assert!(g2 > g1);
assert!(registry.unregister("a").is_some());
let g3 = registry.generation();
assert!(g3 > g2);
}
#[test]
fn execute_runs_handler_when_enabled() {
let registry = CommandRegistry::new();
let called = Rc::new(Cell::new(false));
let called_cb = Rc::clone(&called);
registry.register(
CommandBuilder::new("run")
.label("Run")
.handler(Callback::new(move |_| called_cb.set(true)))
.build(),
);
assert!(registry.execute("run"));
assert!(called.get());
}
#[test]
fn disabled_command_does_not_execute_until_enabled() {
let registry = CommandRegistry::new();
let called = Rc::new(Cell::new(false));
let called_cb = Rc::clone(&called);
registry.register(
CommandBuilder::new("toggle")
.label("Toggle")
.enabled(false)
.handler(Callback::new(move |_| called_cb.set(true)))
.build(),
);
assert!(!registry.execute("toggle"));
assert!(!called.get());
assert!(registry.set_enabled("toggle", true));
assert!(registry.execute("toggle"));
assert!(called.get());
}
}