Skip to main content

lingxia_shell/
manager.rs

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}