1pub use par_term_config::snapshot_types::TabSnapshot;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use uuid::Uuid;
20
21pub type ArrangementId = Uuid;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct MonitorInfo {
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub name: Option<String>,
30
31 #[serde(default)]
33 pub index: usize,
34
35 #[serde(default)]
37 pub position: (i32, i32),
38
39 #[serde(default)]
41 pub size: (u32, u32),
42
43 #[serde(default = "default_scale_factor", skip_serializing_if = "is_one")]
46 pub scale_factor: f64,
47}
48
49fn default_scale_factor() -> f64 {
50 1.0
51}
52
53fn is_one(v: &f64) -> bool {
54 (*v - 1.0).abs() < f64::EPSILON
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct WindowSnapshot {
60 pub monitor: MonitorInfo,
62
63 pub position_relative: (i32, i32),
65
66 pub size: (u32, u32),
68
69 pub tabs: Vec<TabSnapshot>,
71
72 #[serde(default)]
74 pub active_tab_index: usize,
75
76 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub tmux_session_name: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct WindowArrangement {
85 pub id: ArrangementId,
87
88 pub name: String,
90
91 pub monitor_layout: Vec<MonitorInfo>,
93
94 pub windows: Vec<WindowSnapshot>,
96
97 #[serde(default)]
99 pub created_at: String,
100
101 #[serde(default)]
103 pub order: usize,
104}
105
106#[derive(Debug, Clone, Default)]
108pub struct ArrangementManager {
109 arrangements: HashMap<ArrangementId, WindowArrangement>,
111
112 order: Vec<ArrangementId>,
114}
115
116impl ArrangementManager {
117 pub fn new() -> Self {
119 Self {
120 arrangements: HashMap::new(),
121 order: Vec::new(),
122 }
123 }
124
125 pub fn from_arrangements(arrangements: Vec<WindowArrangement>) -> Self {
127 let mut manager = Self::new();
128 for arrangement in arrangements {
129 manager.add(arrangement);
130 }
131 manager.sort_by_order();
132 manager
133 }
134
135 pub fn add(&mut self, arrangement: WindowArrangement) {
137 let id = arrangement.id;
138 if !self.order.contains(&id) {
139 self.order.push(id);
140 }
141 self.arrangements.insert(id, arrangement);
142 }
143
144 pub fn get(&self, id: &ArrangementId) -> Option<&WindowArrangement> {
146 self.arrangements.get(id)
147 }
148
149 pub fn get_mut(&mut self, id: &ArrangementId) -> Option<&mut WindowArrangement> {
151 self.arrangements.get_mut(id)
152 }
153
154 pub fn update(&mut self, arrangement: WindowArrangement) {
156 let id = arrangement.id;
157 if self.arrangements.contains_key(&id) {
158 self.arrangements.insert(id, arrangement);
159 }
160 }
161
162 pub fn remove(&mut self, id: &ArrangementId) -> Option<WindowArrangement> {
164 self.order.retain(|aid| aid != id);
165 self.arrangements.remove(id)
166 }
167
168 pub fn arrangements_ordered(&self) -> Vec<&WindowArrangement> {
170 self.order
171 .iter()
172 .filter_map(|id| self.arrangements.get(id))
173 .collect()
174 }
175
176 pub fn to_vec(&self) -> Vec<WindowArrangement> {
178 self.arrangements_ordered().into_iter().cloned().collect()
179 }
180
181 pub fn len(&self) -> usize {
183 self.arrangements.len()
184 }
185
186 pub fn is_empty(&self) -> bool {
188 self.arrangements.is_empty()
189 }
190
191 pub fn find_by_name(&self, name: &str) -> Option<&WindowArrangement> {
193 let lower = name.to_lowercase();
194 self.arrangements
195 .values()
196 .find(|a| a.name.to_lowercase() == lower)
197 }
198
199 pub fn move_up(&mut self, id: &ArrangementId) {
201 if let Some(pos) = self.order.iter().position(|aid| aid == id)
202 && pos > 0
203 {
204 self.order.swap(pos, pos - 1);
205 self.update_orders();
206 }
207 }
208
209 pub fn move_down(&mut self, id: &ArrangementId) {
211 if let Some(pos) = self.order.iter().position(|aid| aid == id)
212 && pos < self.order.len() - 1
213 {
214 self.order.swap(pos, pos + 1);
215 self.update_orders();
216 }
217 }
218
219 fn sort_by_order(&mut self) {
221 self.order.sort_by_key(|id| {
222 self.arrangements
223 .get(id)
224 .map(|a| a.order)
225 .unwrap_or(usize::MAX)
226 });
227 }
228
229 fn update_orders(&mut self) {
231 for (i, id) in self.order.iter().enumerate() {
232 if let Some(arrangement) = self.arrangements.get_mut(id) {
233 arrangement.order = i;
234 }
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn make_arrangement(name: &str, order: usize) -> WindowArrangement {
244 WindowArrangement {
245 id: Uuid::new_v4(),
246 name: name.to_string(),
247 monitor_layout: Vec::new(),
248 windows: Vec::new(),
249 created_at: String::new(),
250 order,
251 }
252 }
253
254 #[test]
255 fn test_manager_basic_operations() {
256 let mut manager = ArrangementManager::new();
257 assert!(manager.is_empty());
258
259 let arr = make_arrangement("Test", 0);
260 let id = arr.id;
261 manager.add(arr);
262
263 assert_eq!(manager.len(), 1);
264 assert!(manager.get(&id).is_some());
265 assert_eq!(manager.get(&id).unwrap().name, "Test");
266
267 let removed = manager.remove(&id);
268 assert!(removed.is_some());
269 assert!(manager.is_empty());
270 }
271
272 #[test]
273 fn test_manager_ordering() {
274 let mut manager = ArrangementManager::new();
275
276 let a1 = make_arrangement("First", 0);
277 let a2 = make_arrangement("Second", 1);
278 let a3 = make_arrangement("Third", 2);
279
280 let id1 = a1.id;
281 let id2 = a2.id;
282 let id3 = a3.id;
283
284 manager.add(a1);
285 manager.add(a2);
286 manager.add(a3);
287
288 let ordered = manager.arrangements_ordered();
289 assert_eq!(ordered.len(), 3);
290 assert_eq!(ordered[0].id, id1);
291 assert_eq!(ordered[1].id, id2);
292 assert_eq!(ordered[2].id, id3);
293
294 manager.move_up(&id2);
296 let ordered = manager.arrangements_ordered();
297 assert_eq!(ordered[0].id, id2);
298 assert_eq!(ordered[1].id, id1);
299
300 manager.move_down(&id2);
302 let ordered = manager.arrangements_ordered();
303 assert_eq!(ordered[0].id, id1);
304 assert_eq!(ordered[1].id, id2);
305 }
306
307 #[test]
308 fn test_find_by_name() {
309 let mut manager = ArrangementManager::new();
310 manager.add(make_arrangement("Work Setup", 0));
311 manager.add(make_arrangement("Home Setup", 1));
312
313 assert!(manager.find_by_name("work setup").is_some());
314 assert!(manager.find_by_name("HOME SETUP").is_some());
315 assert!(manager.find_by_name("nonexistent").is_none());
316 }
317
318 #[test]
319 fn test_serialization() {
320 let arr = WindowArrangement {
321 id: Uuid::new_v4(),
322 name: "Test".to_string(),
323 monitor_layout: vec![MonitorInfo {
324 name: Some("DELL U2720Q".to_string()),
325 index: 0,
326 position: (0, 0),
327 size: (2560, 1440),
328 scale_factor: 1.0,
329 }],
330 windows: vec![WindowSnapshot {
331 monitor: MonitorInfo {
332 name: Some("DELL U2720Q".to_string()),
333 index: 0,
334 position: (0, 0),
335 size: (2560, 1440),
336 scale_factor: 1.0,
337 },
338 position_relative: (100, 200),
339 size: (800, 600),
340 tabs: vec![TabSnapshot {
341 cwd: Some("/home/user".to_string()),
342 title: "bash".to_string(),
343 custom_color: None,
344 user_title: None,
345 custom_icon: None,
346 }],
347 active_tab_index: 0,
348 tmux_session_name: None,
349 }],
350 created_at: "2024-01-01T00:00:00Z".to_string(),
351 order: 0,
352 };
353
354 let yaml = serde_yaml_ng::to_string(&arr).unwrap();
355 let deserialized: WindowArrangement = serde_yaml_ng::from_str(&yaml).unwrap();
356
357 assert_eq!(deserialized.id, arr.id);
358 assert_eq!(deserialized.name, arr.name);
359 assert_eq!(deserialized.windows.len(), 1);
360 assert_eq!(deserialized.windows[0].tabs.len(), 1);
361 assert_eq!(
362 deserialized.windows[0].tabs[0].cwd,
363 Some("/home/user".to_string())
364 );
365 }
366}