use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginContributionManifest {
#[serde(default)]
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub contributions: PluginContributions,
}
impl PluginContributionManifest {
pub fn from_json(input: &str) -> serde_json::Result<Self> {
serde_json::from_str(input)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginContributions {
#[serde(default)]
pub commands: Vec<CommandContribution>,
#[serde(default)]
pub command_aliases: Vec<CommandAlias>,
#[serde(default)]
pub event_subscriptions: Vec<EventSubscription>,
#[serde(default)]
pub activation: PluginActivation,
}
impl PluginContributions {
pub fn command(&self, name: &str) -> Option<&CommandContribution> {
self.commands.iter().find(|command| command.matches(name))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandContribution {
pub name: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub description: String,
#[serde(default = "default_command_entrypoint")]
pub entrypoint: String,
#[serde(default)]
pub args: CommandArgs,
#[serde(default)]
pub aliases: Vec<String>,
#[serde(default = "default_true")]
pub palette: bool,
#[serde(default)]
pub tui: CommandTuiContribution,
}
impl CommandContribution {
pub fn matches(&self, name: &str) -> bool {
self.name == name || self.aliases.iter().any(|alias| alias == name)
}
pub fn display_title(&self) -> &str {
self.title.as_deref().unwrap_or(&self.name)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum CommandArgs {
#[default]
None,
Fixed,
Passthrough,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandTuiContribution {
#[serde(default)]
pub show_output: CommandOutputMode,
}
impl Default for CommandTuiContribution {
fn default() -> Self {
Self {
show_output: CommandOutputMode::Modal,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum CommandOutputMode {
#[default]
Modal,
Inline,
Silent,
}
fn default_command_entrypoint() -> String {
"on_command".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandAlias {
pub alias: String,
pub description: String,
pub args: String,
pub target_command: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EventSubscription {
pub event: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginActivation {
#[serde(default)]
pub files: Vec<String>,
#[serde(default)]
pub command_prefixes: Vec<String>,
#[serde(default)]
pub remote_url_patterns: Vec<String>,
}
fn default_true() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_command_contributions() {
let manifest = PluginContributionManifest::from_json(
r#"{
"name": "sober-raccoon",
"description": "Sober cockpit",
"contributions": {
"commands": [
{
"name": "sober",
"title": "Sober",
"description": "Run Sober",
"args": "passthrough",
"aliases": ["sober-raccoon"]
}
]
}
}"#,
)
.unwrap();
let command = manifest.contributions.command("sober-raccoon").unwrap();
assert_eq!(command.name, "sober");
assert_eq!(command.display_title(), "Sober");
assert_eq!(command.args, CommandArgs::Passthrough);
assert_eq!(command.tui.show_output, CommandOutputMode::Modal);
}
#[test]
fn ignores_root_commands() {
let manifest = PluginContributionManifest::from_json(
r#"{
"name": "legacy",
"commands": ["old"]
}"#,
)
.unwrap();
assert!(manifest.contributions.commands.is_empty());
}
}