1use crate::{
2 ActivatorCollection, PinCollection, PinMutation, ShellActivator, ShellActivatorUpdate,
3 ShellPinTarget, ShellResult, ShellStore,
4};
5use std::path::PathBuf;
6use std::sync::{Mutex, MutexGuard};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ShellSnapshot {
10 pub activators: ActivatorCollection,
11 pub pins: PinCollection,
12}
13
14pub struct ShellManager {
15 store: ShellStore,
16 state: Mutex<ShellSnapshot>,
17}
18
19impl ShellManager {
20 pub fn open(root: impl Into<PathBuf>) -> ShellResult<Self> {
21 let store = ShellStore::new(root);
22 let state = ShellSnapshot {
23 activators: ActivatorCollection::default(),
24 pins: store.load_pins_recovering(),
25 };
26 Ok(Self {
27 store,
28 state: Mutex::new(state),
29 })
30 }
31
32 pub fn snapshot(&self) -> ShellSnapshot {
33 self.lock().clone()
34 }
35
36 pub fn replace_activators(&self, items: Vec<ShellActivator>) -> ShellResult<ShellSnapshot> {
37 self.mutate_activators(|state| state.replace(items))
38 }
39
40 pub fn update_activator(
41 &self,
42 id: &str,
43 patch: ShellActivatorUpdate,
44 ) -> ShellResult<ShellSnapshot> {
45 self.mutate_activators(|state| state.update(id, patch))
46 }
47
48 pub fn remove_activator(&self, id: &str) -> ShellResult<ShellSnapshot> {
49 self.mutate_activators(|state| state.remove(id))
50 }
51
52 pub fn clear_activators(&self) -> ShellResult<ShellSnapshot> {
53 self.mutate_activators(|state| {
54 state.clear();
55 Ok(())
56 })
57 }
58
59 pub fn commit_activators(
60 &self,
61 expected_generation: u64,
62 next: ActivatorCollection,
63 ) -> ShellResult<ShellSnapshot> {
64 let mut state = self.lock();
65 let actual = state.activators.generation();
66 if actual != expected_generation {
67 return Err(crate::ShellError::ConcurrentMutation {
68 expected: expected_generation,
69 actual,
70 });
71 }
72 let mut snapshot = state.clone();
73 snapshot.activators = next;
74 *state = snapshot;
75 Ok(state.clone())
76 }
77
78 pub fn pin(&self, target: ShellPinTarget) -> ShellResult<(PinMutation, ShellSnapshot)> {
79 let mut state = self.lock();
80 let mut next = state.clone();
81 let mutation = next.pins.pin(target)?;
82 if mutation == PinMutation::Changed {
83 self.store.save_pins(&next.pins)?;
84 *state = next;
85 }
86 Ok((mutation, state.clone()))
87 }
88
89 pub fn unpin(&self, target: &ShellPinTarget) -> ShellResult<(PinMutation, ShellSnapshot)> {
90 let mut state = self.lock();
91 let mut next = state.clone();
92 let mutation = next.pins.unpin(target);
93 if mutation == PinMutation::Changed {
94 self.store.save_pins(&next.pins)?;
95 *state = next;
96 }
97 Ok((mutation, state.clone()))
98 }
99
100 pub fn commit_pins(
101 &self,
102 expected: &PinCollection,
103 next: PinCollection,
104 ) -> ShellResult<ShellSnapshot> {
105 let mut state = self.lock();
106 if state.pins != *expected {
107 return Err(crate::ShellError::ConcurrentPinMutation);
108 }
109 let mut snapshot = state.clone();
110 snapshot.pins = next;
111 self.store.save_pins(&snapshot.pins)?;
112 *state = snapshot;
113 Ok(state.clone())
114 }
115
116 fn mutate_activators(
117 &self,
118 mutate: impl FnOnce(&mut ActivatorCollection) -> ShellResult<()>,
119 ) -> ShellResult<ShellSnapshot> {
120 let current = self.snapshot();
121 let mut next = current.activators.clone();
122 mutate(&mut next)?;
123 self.commit_activators(current.activators.generation(), next)
124 }
125
126 fn lock(&self) -> MutexGuard<'_, ShellSnapshot> {
127 self.state.lock().unwrap_or_else(|error| error.into_inner())
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::ShellError;
135
136 #[test]
137 fn failed_replacement_does_not_change_memory() {
138 let dir = tempfile::tempdir().unwrap();
139 let manager = ShellManager::open(dir.path()).unwrap();
140 manager
141 .replace_activators(vec![ShellActivator {
142 id: "chat".to_string(),
143 label: "Chat".to_string(),
144 icon: "icons/chat.svg".to_string(),
145 disabled: false,
146 }])
147 .unwrap();
148 let before = manager.snapshot();
149
150 let result = manager.replace_activators(vec![ShellActivator {
151 id: "".to_string(),
152 label: "Broken".to_string(),
153 icon: "icons/broken.svg".to_string(),
154 disabled: false,
155 }]);
156
157 assert_eq!(result, Err(ShellError::EmptyActivatorId));
158 assert_eq!(manager.snapshot(), before);
159 }
160
161 #[test]
162 fn activators_are_process_local() {
163 let dir = tempfile::tempdir().unwrap();
164 let manager = ShellManager::open(dir.path()).unwrap();
165 manager
166 .replace_activators(vec![ShellActivator {
167 id: "chat".to_string(),
168 label: "Chat".to_string(),
169 icon: "icons/chat.svg".to_string(),
170 disabled: false,
171 }])
172 .unwrap();
173
174 let reopened = ShellManager::open(dir.path()).unwrap();
175 assert!(!reopened.snapshot().activators.declared());
176 assert!(reopened.snapshot().activators.items().is_empty());
177 }
178}