Skip to main content

lingxia_shell/
manager.rs

1use crate::{
2    PinCollection, PinMutation, ShellPinTarget, ShellResult, ShellSidebarAction,
3    ShellSidebarActionUpdate, ShellStore, ShellWindowState, SidebarActionCollection, SidebarChrome,
4    WindowFrame,
5};
6use std::path::PathBuf;
7use std::sync::{Mutex, MutexGuard};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ShellSnapshot {
11    pub sidebar_actions: SidebarActionCollection,
12    pub pins: PinCollection,
13}
14
15pub struct ShellManager {
16    store: ShellStore,
17    state: Mutex<ShellSnapshot>,
18    window_state: Mutex<ShellWindowState>,
19}
20
21impl ShellManager {
22    pub fn open(root: impl Into<PathBuf>) -> ShellResult<Self> {
23        let store = ShellStore::new(root);
24        let state = ShellSnapshot {
25            sidebar_actions: SidebarActionCollection::default(),
26            pins: store.load_pins_recovering(),
27        };
28        let window_state = store.load_window_state();
29        Ok(Self {
30            store,
31            state: Mutex::new(state),
32            window_state: Mutex::new(window_state),
33        })
34    }
35
36    pub fn snapshot(&self) -> ShellSnapshot {
37        self.lock().clone()
38    }
39
40    pub fn sidebar_chrome(&self) -> SidebarChrome {
41        self.lock_window_state().sidebar
42    }
43
44    pub fn set_sidebar_chrome(&self, chrome: SidebarChrome) -> ShellResult<()> {
45        let mut current = self.lock_window_state();
46        let mut next = *current;
47        next.sidebar = chrome.normalized();
48        if next == *current {
49            return Ok(());
50        }
51        self.store.save_window_state(&next)?;
52        *current = next;
53        Ok(())
54    }
55
56    pub fn window_frame(&self) -> Option<WindowFrame> {
57        self.lock_window_state().window
58    }
59
60    pub fn set_window_frame(&self, frame: WindowFrame) -> ShellResult<()> {
61        if !frame.valid() {
62            return Err(crate::ShellError::InvalidState(
63                "window frame must be finite with positive dimensions".to_string(),
64            ));
65        }
66        let mut current = self.lock_window_state();
67        let mut next = *current;
68        next.window = Some(frame);
69        if next == *current {
70            return Ok(());
71        }
72        self.store.save_window_state(&next)?;
73        *current = next;
74        Ok(())
75    }
76
77    pub fn replace_sidebar_actions(
78        &self,
79        items: Vec<ShellSidebarAction>,
80    ) -> ShellResult<ShellSnapshot> {
81        self.mutate_sidebar_actions(|state| state.replace(items))
82    }
83
84    pub fn update_sidebar_action(
85        &self,
86        id: &str,
87        patch: ShellSidebarActionUpdate,
88    ) -> ShellResult<ShellSnapshot> {
89        self.mutate_sidebar_actions(|state| state.update(id, patch))
90    }
91
92    pub fn remove_sidebar_action(&self, id: &str) -> ShellResult<ShellSnapshot> {
93        self.mutate_sidebar_actions(|state| state.remove(id))
94    }
95
96    pub fn clear_sidebar_actions(&self) -> ShellResult<ShellSnapshot> {
97        self.mutate_sidebar_actions(|state| {
98            state.clear();
99            Ok(())
100        })
101    }
102
103    pub fn commit_sidebar_actions(
104        &self,
105        expected_generation: u64,
106        next: SidebarActionCollection,
107    ) -> ShellResult<ShellSnapshot> {
108        let mut state = self.lock();
109        let actual = state.sidebar_actions.generation();
110        if actual != expected_generation {
111            return Err(crate::ShellError::ConcurrentMutation {
112                expected: expected_generation,
113                actual,
114            });
115        }
116        let mut snapshot = state.clone();
117        snapshot.sidebar_actions = next;
118        *state = snapshot;
119        Ok(state.clone())
120    }
121
122    pub fn pin(&self, target: ShellPinTarget) -> ShellResult<(PinMutation, ShellSnapshot)> {
123        let mut state = self.lock();
124        let mut next = state.clone();
125        let mutation = next.pins.pin(target)?;
126        if mutation == PinMutation::Changed {
127            self.store.save_pins(&next.pins)?;
128            *state = next;
129        }
130        Ok((mutation, state.clone()))
131    }
132
133    pub fn unpin(&self, target: &ShellPinTarget) -> ShellResult<(PinMutation, ShellSnapshot)> {
134        let mut state = self.lock();
135        let mut next = state.clone();
136        let mutation = next.pins.unpin(target);
137        if mutation == PinMutation::Changed {
138            self.store.save_pins(&next.pins)?;
139            *state = next;
140        }
141        Ok((mutation, state.clone()))
142    }
143
144    pub fn commit_pins(
145        &self,
146        expected: &PinCollection,
147        next: PinCollection,
148    ) -> ShellResult<ShellSnapshot> {
149        let mut state = self.lock();
150        if state.pins != *expected {
151            return Err(crate::ShellError::ConcurrentPinMutation);
152        }
153        let mut snapshot = state.clone();
154        snapshot.pins = next;
155        self.store.save_pins(&snapshot.pins)?;
156        *state = snapshot;
157        Ok(state.clone())
158    }
159
160    fn mutate_sidebar_actions(
161        &self,
162        mutate: impl FnOnce(&mut SidebarActionCollection) -> ShellResult<()>,
163    ) -> ShellResult<ShellSnapshot> {
164        let current = self.snapshot();
165        let mut next = current.sidebar_actions.clone();
166        mutate(&mut next)?;
167        self.commit_sidebar_actions(current.sidebar_actions.generation(), next)
168    }
169
170    fn lock(&self) -> MutexGuard<'_, ShellSnapshot> {
171        self.state.lock().unwrap_or_else(|error| error.into_inner())
172    }
173
174    fn lock_window_state(&self) -> MutexGuard<'_, ShellWindowState> {
175        self.window_state
176            .lock()
177            .unwrap_or_else(|error| error.into_inner())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::ShellError;
185
186    #[test]
187    fn failed_replacement_does_not_change_memory() {
188        let dir = tempfile::tempdir().unwrap();
189        let manager = ShellManager::open(dir.path()).unwrap();
190        manager
191            .replace_sidebar_actions(vec![ShellSidebarAction {
192                id: "chat".to_string(),
193                placement: crate::SidebarActionPlacement::Footer,
194                label: "Chat".to_string(),
195                icon: "icons/chat.svg".to_string(),
196                disabled: false,
197            }])
198            .unwrap();
199        let before = manager.snapshot();
200
201        let result = manager.replace_sidebar_actions(vec![ShellSidebarAction {
202            id: "".to_string(),
203            placement: crate::SidebarActionPlacement::Footer,
204            label: "Broken".to_string(),
205            icon: "icons/broken.svg".to_string(),
206            disabled: false,
207        }]);
208
209        assert_eq!(result, Err(ShellError::EmptySidebarActionId));
210        assert_eq!(manager.snapshot(), before);
211    }
212
213    #[test]
214    fn sidebar_actions_are_process_local() {
215        let dir = tempfile::tempdir().unwrap();
216        let manager = ShellManager::open(dir.path()).unwrap();
217        manager
218            .replace_sidebar_actions(vec![ShellSidebarAction {
219                id: "chat".to_string(),
220                placement: crate::SidebarActionPlacement::Footer,
221                label: "Chat".to_string(),
222                icon: "icons/chat.svg".to_string(),
223                disabled: false,
224            }])
225            .unwrap();
226
227        let reopened = ShellManager::open(dir.path()).unwrap();
228        assert!(!reopened.snapshot().sidebar_actions.declared());
229        assert!(reopened.snapshot().sidebar_actions.items().is_empty());
230    }
231
232    #[test]
233    fn sidebar_and_window_updates_preserve_each_other() {
234        let dir = tempfile::tempdir().unwrap();
235        let manager = ShellManager::open(dir.path()).unwrap();
236        let frame = WindowFrame::new(10.0, 20.0, 1200.0, 800.0).unwrap();
237
238        manager.set_window_frame(frame).unwrap();
239        manager
240            .set_sidebar_chrome(SidebarChrome::with_expanded(false, 248.0))
241            .unwrap();
242
243        let reopened = ShellManager::open(dir.path()).unwrap();
244        assert_eq!(reopened.window_frame(), Some(frame));
245        assert_eq!(
246            reopened.sidebar_chrome(),
247            SidebarChrome::with_expanded(false, 248.0)
248        );
249    }
250}