1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14pub struct PluginContributionManifest {
15 #[serde(default)]
16 pub name: String,
17 #[serde(default)]
18 pub description: String,
19 #[serde(default)]
20 pub contributions: PluginContributions,
21}
22
23impl PluginContributionManifest {
24 pub fn from_json(input: &str) -> serde_json::Result<Self> {
26 serde_json::from_str(input)
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32pub struct PluginContributions {
33 #[serde(default)]
35 pub commands: Vec<CommandContribution>,
36 #[serde(default)]
38 pub command_aliases: Vec<CommandAlias>,
39 #[serde(default)]
41 pub event_subscriptions: Vec<EventSubscription>,
42 #[serde(default)]
44 pub activation: PluginActivation,
45}
46
47impl PluginContributions {
48 pub fn command(&self, name: &str) -> Option<&CommandContribution> {
50 self.commands.iter().find(|command| command.matches(name))
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
56pub struct CommandContribution {
57 pub name: String,
59 #[serde(default)]
61 pub title: Option<String>,
62 #[serde(default)]
64 pub description: String,
65 #[serde(default = "default_command_entrypoint")]
67 pub entrypoint: String,
68 #[serde(default)]
70 pub args: CommandArgs,
71 #[serde(default)]
73 pub aliases: Vec<String>,
74 #[serde(default = "default_true")]
76 pub palette: bool,
77 #[serde(default)]
79 pub tui: CommandTuiContribution,
80}
81
82impl CommandContribution {
83 pub fn matches(&self, name: &str) -> bool {
85 self.name == name || self.aliases.iter().any(|alias| alias == name)
86 }
87
88 pub fn display_title(&self) -> &str {
90 self.title.as_deref().unwrap_or(&self.name)
91 }
92}
93
94#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
96#[serde(rename_all = "kebab-case")]
97pub enum CommandArgs {
98 #[default]
100 None,
101 Fixed,
103 Passthrough,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
109pub struct CommandTuiContribution {
110 #[serde(default)]
112 pub show_output: CommandOutputMode,
113}
114
115impl Default for CommandTuiContribution {
116 fn default() -> Self {
117 Self {
118 show_output: CommandOutputMode::Modal,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
125#[serde(rename_all = "kebab-case")]
126pub enum CommandOutputMode {
127 #[default]
129 Modal,
130 Inline,
132 Silent,
134}
135
136fn default_command_entrypoint() -> String {
137 "on_command".to_string()
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144pub struct CommandAlias {
145 pub alias: String,
147 pub description: String,
149 pub args: String,
151 pub target_command: String,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
157pub struct EventSubscription {
158 pub event: String,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, Default)]
176pub struct PluginActivation {
177 #[serde(default)]
181 pub files: Vec<String>,
182 #[serde(default)]
186 pub command_prefixes: Vec<String>,
187 #[serde(default)]
189 pub remote_url_patterns: Vec<String>,
190}
191
192fn default_true() -> bool {
193 true
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn parses_command_contributions() {
202 let manifest = PluginContributionManifest::from_json(
203 r#"{
204 "name": "sober-raccoon",
205 "description": "Sober cockpit",
206 "contributions": {
207 "commands": [
208 {
209 "name": "sober",
210 "title": "Sober",
211 "description": "Run Sober",
212 "args": "passthrough",
213 "aliases": ["sober-raccoon"]
214 }
215 ]
216 }
217 }"#,
218 )
219 .unwrap();
220
221 let command = manifest.contributions.command("sober-raccoon").unwrap();
222 assert_eq!(command.name, "sober");
223 assert_eq!(command.display_title(), "Sober");
224 assert_eq!(command.args, CommandArgs::Passthrough);
225 assert_eq!(command.tui.show_output, CommandOutputMode::Modal);
226 }
227
228 #[test]
229 fn ignores_root_commands() {
230 let manifest = PluginContributionManifest::from_json(
231 r#"{
232 "name": "legacy",
233 "commands": ["old"]
234 }"#,
235 )
236 .unwrap();
237
238 assert!(manifest.contributions.commands.is_empty());
239 }
240}