Skip to main content

lingxia_shell/
pin.rs

1use crate::{ShellError, ShellResult};
2use serde::{Deserialize, Serialize};
3
4pub const MAX_SHELL_PINS: usize = 8;
5pub(crate) const PIN_STATE_VERSION: u32 = 1;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(tag = "kind", rename_all = "lowercase")]
9pub enum ShellPinTarget {
10    Lxapp { key: String },
11    Bookmark { key: String },
12}
13
14impl ShellPinTarget {
15    fn validate(mut self) -> ShellResult<Self> {
16        let key = match &mut self {
17            Self::Lxapp { key } | Self::Bookmark { key } => key,
18        };
19        *key = key.trim().to_string();
20        if key.is_empty() {
21            return Err(ShellError::InvalidState(
22                "shell Pin target must not be empty".to_string(),
23            ));
24        }
25        Ok(self)
26    }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[serde(transparent)]
31pub struct ShellPin(pub ShellPinTarget);
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum PinMutation {
35    Changed,
36    Unchanged,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct PinCollection {
42    pub version: u32,
43    pub items: Vec<ShellPin>,
44}
45
46impl Default for PinCollection {
47    fn default() -> Self {
48        Self {
49            version: PIN_STATE_VERSION,
50            items: Vec::new(),
51        }
52    }
53}
54
55impl PinCollection {
56    pub fn restore(mut self) -> ShellResult<Self> {
57        if self.version != PIN_STATE_VERSION {
58            return Err(ShellError::UnsupportedVersion {
59                version: self.version,
60            });
61        }
62        let mut normalized = Vec::with_capacity(self.items.len());
63        for pin in self.items {
64            let pin = ShellPin(pin.0.validate()?);
65            if normalized.contains(&pin) {
66                return Err(ShellError::InvalidState(
67                    "shell Pin store contains duplicate targets".to_string(),
68                ));
69            }
70            normalized.push(pin);
71            if normalized.len() > MAX_SHELL_PINS {
72                return Err(ShellError::LimitReached {
73                    max: MAX_SHELL_PINS,
74                });
75            }
76        }
77        self.items = normalized;
78        Ok(self)
79    }
80
81    pub fn pin(&mut self, target: ShellPinTarget) -> ShellResult<PinMutation> {
82        let pin = ShellPin(target.validate()?);
83        if self.items.contains(&pin) {
84            return Ok(PinMutation::Unchanged);
85        }
86        if self.items.len() >= MAX_SHELL_PINS {
87            return Err(ShellError::LimitReached {
88                max: MAX_SHELL_PINS,
89            });
90        }
91        self.items.push(pin);
92        Ok(PinMutation::Changed)
93    }
94
95    pub fn unpin(&mut self, target: &ShellPinTarget) -> PinMutation {
96        let before = self.items.len();
97        self.items.retain(|pin| &pin.0 != target);
98        if self.items.len() == before {
99            PinMutation::Unchanged
100        } else {
101            PinMutation::Changed
102        }
103    }
104
105    pub fn is_pinned(&self, target: &ShellPinTarget) -> bool {
106        self.items.iter().any(|pin| &pin.0 == target)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn lxapp(index: usize) -> ShellPinTarget {
115        ShellPinTarget::Lxapp {
116            key: format!("app.{index}"),
117        }
118    }
119
120    #[test]
121    fn mixed_pin_order_is_preserved() {
122        let mut pins = PinCollection::default();
123        pins.pin(lxapp(1)).unwrap();
124        pins.pin(ShellPinTarget::Bookmark {
125            key: "bookmark-a".to_string(),
126        })
127        .unwrap();
128        pins.pin(lxapp(2)).unwrap();
129
130        assert!(matches!(pins.items[0].0, ShellPinTarget::Lxapp { .. }));
131        assert!(matches!(pins.items[1].0, ShellPinTarget::Bookmark { .. }));
132        assert!(matches!(pins.items[2].0, ShellPinTarget::Lxapp { .. }));
133    }
134
135    #[test]
136    fn ninth_pin_returns_typed_limit_error() {
137        let mut pins = PinCollection::default();
138        for index in 0..MAX_SHELL_PINS {
139            pins.pin(lxapp(index)).unwrap();
140        }
141
142        assert_eq!(
143            pins.pin(lxapp(MAX_SHELL_PINS)),
144            Err(ShellError::LimitReached {
145                max: MAX_SHELL_PINS
146            })
147        );
148        assert_eq!(pins.items.len(), MAX_SHELL_PINS);
149    }
150
151    #[test]
152    fn restore_rejects_overflow_instead_of_migrating_it() {
153        let stored = PinCollection {
154            version: PIN_STATE_VERSION,
155            items: (0..=MAX_SHELL_PINS)
156                .map(|index| ShellPin(lxapp(index)))
157                .collect(),
158        };
159
160        assert_eq!(
161            stored.restore(),
162            Err(ShellError::LimitReached {
163                max: MAX_SHELL_PINS,
164            })
165        );
166    }
167}