Skip to main content

lore/
model.rs

1//! Types describing a command library.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7/// One command definition file.
8#[derive(Debug, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct Library {
11    pub version: u32,
12
13    #[serde(default)]
14    pub commands: Vec<Entry>,
15
16    /// Glob patterns matched against entry ids, used to hide entries from
17    /// earlier layers without redefining them.
18    #[serde(default)]
19    pub disabled: Vec<String>,
20}
21
22/// A single command a user can pick.
23#[derive(Debug, Clone, PartialEq, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct Entry {
26    pub id: String,
27    pub cmd: CommandBody,
28    pub desc: String,
29
30    #[serde(default)]
31    pub tags: Vec<String>,
32
33    #[serde(default)]
34    pub params: BTreeMap<String, ParamSpec>,
35
36    /// Marks a destructive command so the picker can warn before it is inserted.
37    #[serde(default)]
38    pub danger: bool,
39
40    #[serde(skip)]
41    pub layer: Layer,
42}
43
44/// A command string, optionally specialised per shell family.
45#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
46#[serde(untagged)]
47pub enum CommandBody {
48    Shared(String),
49    PerShell(BTreeMap<ShellFamily, String>),
50}
51
52/// Optional metadata for a placeholder used in a command.
53#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
54#[serde(deny_unknown_fields)]
55pub struct ParamSpec {
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub desc: Option<String>,
58
59    /// Command whose output supplies selectable values for this placeholder.
60    /// Reserved by the schema; the current runtime ignores it and falls back to
61    /// a free text prompt.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub from: Option<String>,
64}
65
66/// Where an entry came from. Later layers shadow earlier ones by id.
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
68pub enum Layer {
69    #[default]
70    Builtin,
71    Project,
72    User,
73}
74
75/// Shells that share a command dialect.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
77#[serde(rename_all = "lowercase")]
78pub enum ShellFamily {
79    Posix,
80    PowerShell,
81}
82
83impl Entry {
84    /// The command text for `family`, or `None` when this entry has no variant
85    /// for it and should be hidden from that shell.
86    pub fn cmd_for(&self, family: ShellFamily) -> Option<&str> {
87        match &self.cmd {
88            CommandBody::Shared(cmd) => Some(cmd),
89            CommandBody::PerShell(variants) => variants.get(&family).map(String::as_str),
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    fn entry(cmd: CommandBody) -> Entry {
99        Entry {
100            id: "test".into(),
101            cmd,
102            desc: "test".into(),
103            tags: Vec::new(),
104            params: BTreeMap::new(),
105            danger: false,
106            layer: Layer::Builtin,
107        }
108    }
109
110    #[test]
111    fn shared_command_serves_every_family() {
112        let e = entry(CommandBody::Shared("git status".into()));
113        assert_eq!(e.cmd_for(ShellFamily::Posix), Some("git status"));
114        assert_eq!(e.cmd_for(ShellFamily::PowerShell), Some("git status"));
115    }
116
117    #[test]
118    fn missing_variant_hides_entry_from_that_shell() {
119        let e = entry(CommandBody::PerShell(BTreeMap::from([(
120            ShellFamily::Posix,
121            "ss -tulpn".into(),
122        )])));
123        assert_eq!(e.cmd_for(ShellFamily::Posix), Some("ss -tulpn"));
124        assert_eq!(e.cmd_for(ShellFamily::PowerShell), None);
125    }
126
127    #[test]
128    fn layers_order_from_weakest_to_strongest() {
129        assert!(Layer::Builtin < Layer::Project);
130        assert!(Layer::Project < Layer::User);
131    }
132}