#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, thiserror::Error, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CommandError {
#[error("unknown command `{id}`")]
Unknown { id: String },
#[error("command `{id}` is not available right now: {reason}")]
Disabled { id: String, reason: String },
#[error("invalid parameters for `{id}`: {msg}")]
BadParams { id: String, msg: String },
#[error("{msg}")]
Failed { id: String, msg: String },
#[error("internal error in `{id}` (see the crash log); the command did not complete")]
Panicked { id: String },
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum ExecError<E> {
#[error(transparent)]
Command(CommandError),
#[error("{0}")]
Host(E),
}
impl<E> ExecError<E> {
pub fn host(&self) -> Option<&E> {
match self {
Self::Host(e) => Some(e),
Self::Command(_) => None,
}
}
pub fn command(&self) -> Option<&CommandError> {
match self {
Self::Command(e) => Some(e),
Self::Host(_) => None,
}
}
}
impl<E: Into<CommandError>> ExecError<E> {
pub fn into_command(self, command: &str) -> CommandError {
match self {
Self::Command(e) => e,
Self::Host(e) => e.into().or_id(command),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ParamKind {
Bool,
Int {
min: Option<i64>,
max: Option<i64>,
},
Float {
min: Option<f64>,
max: Option<f64>,
},
String,
Enum {
values: Vec<String>,
},
Json,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParamSpec {
pub name: String,
pub kind: ParamKind,
pub required: bool,
pub about: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Params {
None,
Typed(Vec<ParamSpec>),
Text(&'static str),
Static(&'static [Param]),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CommandInfo {
pub id: String,
pub label: String,
#[serde(default)]
pub menu: Vec<String>,
#[serde(default)]
pub shortcut: Option<String>,
#[serde(default)]
pub params_schema: Value,
#[serde(default)]
pub params_text: Option<String>,
pub enabled: bool,
#[serde(default)]
pub disabled_reason: Option<String>,
#[serde(default)]
pub mutates: bool,
}
pub trait Dispatch {
fn commands(&self) -> Vec<CommandInfo>;
fn execute(&mut self, id: &str, params: Value) -> Result<Value, CommandError>;
}
mod fuzzy;
mod journal;
pub mod keymap;
mod legacy;
mod macros;
pub mod menu;
mod params;
mod registry;
pub mod shortcut;
pub mod suggest;
pub use fuzzy::{FuzzyMatch, fuzzy_match, fuzzy_rank};
pub use journal::{CommandRun, Journal, JournalEntry, RunObserver};
pub use keymap::Keymap;
pub use legacy::accepted_params;
pub use params::{Kind, Param, params_schema, validate_params};
pub use registry::{Bound, MAX_DEPTH, Registry, Validation};
pub use suggest::{closest, did_you_mean_command, params_summary};
impl CommandError {
pub fn id(&self) -> &str {
match self {
Self::Unknown { id } | Self::Disabled { id, .. } | Self::BadParams { id, .. } | Self::Failed { id, .. } | Self::Panicked { id } => id,
}
}
pub fn failed(id: impl Into<String>, msg: impl Into<String>) -> Self {
Self::Failed { id: id.into(), msg: msg.into() }
}
pub fn bad_params(id: impl Into<String>, msg: impl Into<String>) -> Self {
Self::BadParams { id: id.into(), msg: msg.into() }
}
pub fn or_id(mut self, command: &str) -> Self {
let (Self::Unknown { id } | Self::Disabled { id, .. } | Self::BadParams { id, .. } | Self::Failed { id, .. } | Self::Panicked { id }) =
&mut self;
if id.is_empty() {
command.clone_into(id);
}
self
}
}
impl ParamSpec {
pub fn new(name: impl Into<String>, kind: ParamKind) -> Self {
Self { name: name.into(), kind, required: false, about: String::new() }
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn about(mut self, about: impl Into<String>) -> Self {
self.about = about.into();
self
}
}
pub type Enabled<S> = fn(&S) -> Result<(), String>;
pub type Run<S> = fn(&mut S, &Value) -> Result<Value, <S as Host>::Error>;
pub fn always<S>(_: &S) -> Result<(), String> {
Ok(())
}
pub struct CommandSpec<S: Host> {
pub id: &'static str,
pub label: &'static str,
pub menu: &'static [&'static str],
pub shortcut: Option<&'static str>,
pub params: Params,
pub enabled: Enabled<S>,
pub run: Run<S>,
pub mutates: bool,
}
impl<S: Host> CommandSpec<S> {
pub const fn new(id: &'static str, label: &'static str, run: Run<S>) -> Self {
Self { id, label, menu: &[], shortcut: None, params: Params::None, enabled: always::<S>, run, mutates: true }
}
pub const fn menu(mut self, menu: &'static [&'static str]) -> Self {
self.menu = menu;
self
}
pub const fn shortcut(mut self, shortcut: &'static str) -> Self {
self.shortcut = Some(shortcut);
self
}
pub const fn enabled(mut self, enabled: Enabled<S>) -> Self {
self.enabled = enabled;
self
}
pub const fn query(mut self) -> Self {
self.mutates = false;
self
}
pub fn params(mut self, params: Params) -> Self {
self.params = params;
self
}
}
pub trait Host: Sized {
type Error: Into<CommandError>;
fn journal(&mut self) -> &mut Journal;
fn before(&mut self, _spec: &CommandSpec<Self>, _params: &mut Value, _depth: usize) -> Result<(), Self::Error> {
Ok(())
}
fn validate(&mut self, _spec: &CommandSpec<Self>, _params: &Value, _depth: usize) -> Result<(), CommandError> {
Ok(())
}
fn enabled_for(&mut self, spec: &CommandSpec<Self>, _params: &Value) -> Result<(), String> {
(spec.enabled)(self)
}
fn after(&mut self, _spec: &CommandSpec<Self>, _params: &Value, _result: &Result<Value, CommandError>, _depth: usize) {}
fn rejected(&mut self, _id: &str, _error: &CommandError, _depth: usize) {}
fn scrub(&self, _id: &str, _params: &mut Value) {}
}