Skip to main content

tilepad_manifest/
plugin.rs

1//! # Plugin
2//!
3//! Manifest definition for plugins
4
5use crate::{
6    ManifestError,
7    system::{Arch, OperatingSystem, platform_arch, platform_os},
8    validation::{validate_color, validate_id, validate_name},
9};
10use garde::Validate;
11use indexmap::IndexMap;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use serde_with::skip_serializing_none;
15use std::{fmt::Display, str::FromStr};
16
17/// Unique ID for a plugin
18///
19/// Uses reverse domain syntax (i.e com.example.my-plugin)
20#[derive(
21    Debug, Clone, Serialize, Deserialize, Validate, Hash, PartialEq, Eq, PartialOrd, Ord, JsonSchema,
22)]
23#[garde(transparent)]
24#[serde(transparent)]
25pub struct PluginId(#[garde(custom(validate_id))] pub String);
26
27impl TryFrom<String> for PluginId {
28    type Error = garde::Report;
29    fn try_from(value: String) -> Result<Self, Self::Error> {
30        Self::from_str(&value)
31    }
32}
33
34impl FromStr for PluginId {
35    type Err = garde::Report;
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        let value = PluginId(s.to_string());
38        value.validate()?;
39        Ok(value)
40    }
41}
42
43impl Display for PluginId {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        self.0.fmt(f)
46    }
47}
48
49impl PluginId {
50    pub fn as_str(&self) -> &str {
51        self.0.as_str()
52    }
53}
54
55impl AsRef<str> for PluginId {
56    fn as_ref(&self) -> &str {
57        self.as_str()
58    }
59}
60
61/// Version range of a node runtime, it's recommended that you leave
62/// this as the default unless you explicitly need a specific version
63/// and its features
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
65#[serde(transparent)]
66#[schemars(with = "String", example = "=22.18.0")]
67pub struct BinaryNodeVersion(pub node_semver::Range);
68
69impl AsRef<node_semver::Range> for BinaryNodeVersion {
70    fn as_ref(&self) -> &node_semver::Range {
71        &self.0
72    }
73}
74
75/// Manifest file format for plugins
76#[skip_serializing_none]
77#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
78pub struct PluginManifest {
79    /// Details about the plugin itself
80    #[garde(dive)]
81    pub plugin: MPlugin,
82
83    /// Details for running the plugin
84    /// (Option not specified for internal plugins)
85    #[garde(dive)]
86    pub bin: Option<MBin>,
87
88    /// Category for the manifest actions
89    #[garde(dive)]
90    pub category: MCategory,
91
92    /// Map of available plugin actions
93    #[garde(dive)]
94    pub actions: ActionMap,
95}
96
97impl TryFrom<&str> for PluginManifest {
98    type Error = ManifestError;
99
100    fn try_from(value: &str) -> Result<Self, Self::Error> {
101        let manifest: PluginManifest = serde_json::from_str(value)?;
102        manifest.validate()?;
103        Ok(manifest)
104    }
105}
106
107impl TryFrom<&[u8]> for PluginManifest {
108    type Error = ManifestError;
109    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
110        let manifest: PluginManifest = serde_json::from_slice(value)?;
111        manifest.validate()?;
112        Ok(manifest)
113    }
114}
115
116impl PluginManifest {
117    /// Parse a plugin manifest from a string
118    #[inline]
119    pub fn parse(value: &str) -> Result<PluginManifest, ManifestError> {
120        Self::try_from(value)
121    }
122}
123
124/// Plugin details section of the manifest
125#[skip_serializing_none]
126#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
127pub struct MPlugin {
128    /// Unique ID of the plugin (e.g com.jacobtread.tilepad.obs)
129    #[garde(dive)]
130    #[schemars(example = "com.jacobtread.tilepad.obs")]
131    pub id: PluginId,
132    /// Name of the plugin
133    #[garde(length(min = 1))]
134    #[schemars(example = "Example Plugin")]
135    pub name: String,
136    /// Current version of the plugin, semver compatible version number
137    #[garde(length(min = 1))]
138    #[schemars(example = "0.1.0")]
139    pub version: String,
140    /// List of authors for the plugin
141    #[garde(inner(length(min = 1)))]
142    #[schemars(example = ["Example Author 1", "Example Author 2"])]
143    pub authors: Vec<String>,
144    /// Description of what the plugin does
145    #[garde(skip)]
146    #[schemars(example = "My plugin that performs my actions")]
147    pub description: Option<String>,
148    /// Icon for the plugin
149    #[garde(skip)]
150    #[schemars(example = "images/icon.svg")]
151    pub icon: Option<String>,
152    /// Internal Field - Determines whether the plugin is "internal" to tilepad and
153    /// bundled (Prevents uninstalling and some other features)
154    #[garde(skip)]
155    #[schemars(skip)]
156    pub internal: Option<bool>,
157}
158
159/// Ordered map of actions defined within the plugin
160///
161/// Keys must be unique to each action following
162/// the [a-zA-Z_-] format (i.e example_action, my-action, MyAction)
163#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
164pub struct ActionMap(pub IndexMap<ActionId, ManifestAction>);
165
166impl AsRef<IndexMap<ActionId, ManifestAction>> for ActionMap {
167    fn as_ref(&self) -> &IndexMap<ActionId, ManifestAction> {
168        &self.0
169    }
170}
171
172/// Definition of the category to place the plugin actions within
173#[skip_serializing_none]
174#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
175pub struct MCategory {
176    /// Label for the category in the actions sidebar
177    #[garde(length(min = 1))]
178    #[schemars(example = "My Category")]
179    pub label: String,
180    /// Icon to show in the actions sidebar
181    #[garde(skip)]
182    #[schemars(example = "images/icon.svg")]
183    pub icon: Option<String>,
184}
185
186/// Name of an action
187///
188/// Must be [a-zA-Z_-] (i.e example_action, my-action, MyAction)
189#[derive(
190    Debug, Clone, Serialize, Deserialize, Validate, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema,
191)]
192#[garde(transparent)]
193#[serde(transparent)]
194#[schemars(example = &"example_action")]
195pub struct ActionId(#[garde(custom(validate_name))] pub String);
196
197impl ActionId {
198    pub fn as_str(&self) -> &str {
199        self.0.as_str()
200    }
201}
202
203impl TryFrom<String> for ActionId {
204    type Error = garde::Report;
205    fn try_from(value: String) -> Result<Self, Self::Error> {
206        Self::from_str(&value)
207    }
208}
209
210impl FromStr for ActionId {
211    type Err = garde::Report;
212    fn from_str(s: &str) -> Result<Self, Self::Err> {
213        let value = ActionId(s.to_string());
214        value.validate()?;
215        Ok(value)
216    }
217}
218
219impl AsRef<str> for ActionId {
220    fn as_ref(&self) -> &str {
221        self.as_str()
222    }
223}
224
225impl Display for ActionId {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        self.0.fmt(f)
228    }
229}
230
231/// Manifest action definition
232#[skip_serializing_none]
233#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
234pub struct ManifestAction {
235    /// Label for the action, shown in the sidebar
236    #[garde(length(min = 1))]
237    #[schemars(example = "My Action")]
238    pub label: String,
239
240    /// Icon for the action, shown in the sidebar and
241    /// used as the default icon when added to the grid
242    #[garde(skip)]
243    #[schemars(example = "images/icon.svg")]
244    pub icon: Option<String>,
245
246    /// Path to a "display" HTML file that can be used to make
247    /// the tile into a display tile
248    #[garde(skip)]
249    #[schemars(example = "display/my-display.display.html")]
250    pub display: Option<String>,
251
252    /// Default options for the icon when added to the grid
253    /// as a tile
254    #[garde(dive)]
255    pub icon_options: Option<ManifestActionIconOptions>,
256
257    /// Description for the action, shown as a tooltip when hovering
258    /// the action
259    #[garde(skip)]
260    #[schemars(example = "My action")]
261    pub description: Option<String>,
262
263    /// Path to the inspector HTML file to use for configuring the action
264    #[garde(skip)]
265    #[schemars(example = "inspector/index.html")]
266    pub inspector: Option<String>,
267}
268
269/// Default options for an action icon
270#[skip_serializing_none]
271#[derive(Default, Debug, Clone, Serialize, Deserialize, Validate, JsonSchema)]
272#[serde(default)]
273pub struct ManifestActionIconOptions {
274    /// Padding in pixels to pad the icon with
275    #[garde(skip)]
276    pub padding: Option<u32>,
277
278    /// Color for the tile background behind the icon
279    #[garde(inner(custom(validate_color)))]
280    #[schemars(example = "#ffffff")]
281    pub background_color: Option<String>,
282
283    /// Color of the tile border
284    #[garde(inner(custom(validate_color)))]
285    #[schemars(example = "#ffffff")]
286    pub border_color: Option<String>,
287}
288
289/// Type of binary the plugin program is using
290#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
291#[serde(untagged)]
292pub enum MBin {
293    /// Program uses the node runtime
294    Node {
295        #[garde(dive)]
296        node: MBinNode,
297    },
298
299    /// Program uses a native binary
300    Native {
301        #[garde(dive)]
302        native: Vec<MBinNative>,
303    },
304}
305
306/// Node "binary" which uses a node runtime to execute the js script
307/// at the provided `entrypoint`
308#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
309pub struct MBinNode {
310    /// Entrypoint for the program
311    ///
312    /// e.g bin/index.js
313    #[garde(length(min = 1))]
314    #[schemars(example = "bin/index.js")]
315    pub entrypoint: String,
316
317    /// Version of node the program should run using.
318    ///
319    /// It's recommended that you leave
320    /// this as the default unless you explicitly need a specific version
321    /// and its features
322    #[garde(skip)]
323    #[serde(default = "default_node_version")]
324    pub version: BinaryNodeVersion,
325}
326
327/// Default node version to use
328fn default_node_version() -> BinaryNodeVersion {
329    BinaryNodeVersion(
330        node_semver::Range::parse("=22.18.0").expect("hardcoded range should be valid"),
331    )
332}
333
334/// Native binary for a specific os + arch combo, contains a
335/// path to the binary
336#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
337pub struct MBinNative {
338    // Target OS this binary should be used for
339    #[garde(skip)]
340    pub os: OperatingSystem,
341
342    /// Target Arch this binary should be used for
343    #[garde(skip)]
344    pub arch: Arch,
345
346    /// Path to the executable file
347    #[garde(length(min = 1))]
348    #[schemars(example = "bin/example.exe")]
349    pub path: String,
350}
351
352impl MBinNative {
353    // Check if the binary is usable on the provided OS and Arch combination
354    pub fn is_usable(&self, os: &OperatingSystem, arch: &Arch) -> bool {
355        self.os.eq(os) && self.arch.eq(arch)
356    }
357
358    // Find a binary thats usable on the provided OS and Arch combination
359    pub fn find_usable<'a>(
360        options: &'a [MBinNative],
361        os: &OperatingSystem,
362        arch: &Arch,
363    ) -> Option<&'a Self> {
364        options.iter().find(|bin| bin.is_usable(os, arch))
365    }
366
367    // Find a binary compatible with the current OS and Arch
368    pub fn find_current(options: &[MBinNative]) -> Option<&Self> {
369        let os = platform_os();
370        let arch = platform_arch();
371        Self::find_usable(options, &os, &arch)
372    }
373}
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_is_usable_true_when_matches() {
380        let bin = MBinNative {
381            os: OperatingSystem::Linux,
382            arch: Arch::X64,
383            path: "bin/linux-x64".to_string(),
384        };
385        assert!(bin.is_usable(&OperatingSystem::Linux, &Arch::X64));
386    }
387
388    #[test]
389    fn test_is_usable_false_when_os_mismatch() {
390        let bin = MBinNative {
391            os: OperatingSystem::Linux,
392            arch: Arch::X64,
393            path: "bin/linux-x64".to_string(),
394        };
395        assert!(!bin.is_usable(&OperatingSystem::Windows, &Arch::X64));
396    }
397
398    #[test]
399    fn test_is_usable_false_when_arch_mismatch() {
400        let bin = MBinNative {
401            os: OperatingSystem::Linux,
402            arch: Arch::X64,
403            path: "bin/linux-x64".to_string(),
404        };
405        assert!(!bin.is_usable(&OperatingSystem::Linux, &Arch::X86));
406    }
407
408    #[test]
409    fn test_find_usable_finds_correct_bin() {
410        let bins = vec![
411            MBinNative {
412                os: OperatingSystem::Windows,
413                arch: Arch::X64,
414                path: "bin/win-x64".to_string(),
415            },
416            MBinNative {
417                os: OperatingSystem::Linux,
418                arch: Arch::X64,
419                path: "bin/linux-x64".to_string(),
420            },
421        ];
422        let result = MBinNative::find_usable(&bins, &OperatingSystem::Linux, &Arch::X64);
423        assert!(result.is_some());
424        assert_eq!(result.unwrap().path, "bin/linux-x64");
425    }
426
427    #[test]
428    fn test_find_usable_returns_none_if_no_match() {
429        let bins = vec![
430            MBinNative {
431                os: OperatingSystem::Windows,
432                arch: Arch::X64,
433                path: "bin/win-x64".to_string(),
434            },
435            MBinNative {
436                os: OperatingSystem::MacOs,
437                arch: Arch::Arm64,
438                path: "bin/macos-arm64".to_string(),
439            },
440        ];
441        let result = MBinNative::find_usable(&bins, &OperatingSystem::Linux, &Arch::X64);
442        assert!(result.is_none());
443    }
444
445    #[test]
446    fn test_find_usable_returns_first_match() {
447        let bins = vec![
448            MBinNative {
449                os: OperatingSystem::Linux,
450                arch: Arch::X64,
451                path: "bin/linux-x64-v1".to_string(),
452            },
453            MBinNative {
454                os: OperatingSystem::Linux,
455                arch: Arch::X64,
456                path: "bin/linux-x64-v2".to_string(),
457            },
458        ];
459        let result = MBinNative::find_usable(&bins, &OperatingSystem::Linux, &Arch::X64);
460        assert!(result.is_some());
461        assert_eq!(result.unwrap().path, "bin/linux-x64-v1");
462    }
463}