progit_plugin_sdk/
contributions.rs1use 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}
37
38impl PluginContributions {
39 pub fn command(&self, name: &str) -> Option<&CommandContribution> {
41 self.commands.iter().find(|command| command.matches(name))
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47pub struct CommandContribution {
48 pub name: String,
50 #[serde(default)]
52 pub title: Option<String>,
53 #[serde(default)]
55 pub description: String,
56 #[serde(default = "default_command_entrypoint")]
58 pub entrypoint: String,
59 #[serde(default)]
61 pub args: CommandArgs,
62 #[serde(default)]
64 pub aliases: Vec<String>,
65 #[serde(default = "default_true")]
67 pub palette: bool,
68 #[serde(default)]
70 pub tui: CommandTuiContribution,
71}
72
73impl CommandContribution {
74 pub fn matches(&self, name: &str) -> bool {
76 self.name == name || self.aliases.iter().any(|alias| alias == name)
77 }
78
79 pub fn display_title(&self) -> &str {
81 self.title.as_deref().unwrap_or(&self.name)
82 }
83}
84
85#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
87#[serde(rename_all = "kebab-case")]
88pub enum CommandArgs {
89 #[default]
91 None,
92 Fixed,
94 Passthrough,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100pub struct CommandTuiContribution {
101 #[serde(default)]
103 pub show_output: CommandOutputMode,
104}
105
106impl Default for CommandTuiContribution {
107 fn default() -> Self {
108 Self {
109 show_output: CommandOutputMode::Modal,
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
116#[serde(rename_all = "kebab-case")]
117pub enum CommandOutputMode {
118 #[default]
120 Modal,
121 Inline,
123 Silent,
125}
126
127fn default_command_entrypoint() -> String {
128 "on_command".to_string()
129}
130
131fn default_true() -> bool {
132 true
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn parses_command_contributions() {
141 let manifest = PluginContributionManifest::from_json(
142 r#"{
143 "name": "sober-raccoon",
144 "description": "Sober cockpit",
145 "contributions": {
146 "commands": [
147 {
148 "name": "sober",
149 "title": "Sober",
150 "description": "Run Sober",
151 "args": "passthrough",
152 "aliases": ["sober-raccoon"]
153 }
154 ]
155 }
156 }"#,
157 )
158 .unwrap();
159
160 let command = manifest.contributions.command("sober-raccoon").unwrap();
161 assert_eq!(command.name, "sober");
162 assert_eq!(command.display_title(), "Sober");
163 assert_eq!(command.args, CommandArgs::Passthrough);
164 assert_eq!(command.tui.show_output, CommandOutputMode::Modal);
165 }
166
167 #[test]
168 fn ignores_root_commands() {
169 let manifest = PluginContributionManifest::from_json(
170 r#"{
171 "name": "legacy",
172 "commands": ["old"]
173 }"#,
174 )
175 .unwrap();
176
177 assert!(manifest.contributions.commands.is_empty());
178 }
179}