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}