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}