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 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub mux_session_name: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct WindowArrangement {
92 pub id: ArrangementId,
94
95 pub name: String,
97
98 pub monitor_layout: Vec<MonitorInfo>,
100
101 pub windows: Vec<WindowSnapshot>,
103
104 #[serde(default)]
106 pub created_at: String,
107
108 #[serde(default)]
110 pub order: usize,
111}
112
113#[derive(Debug, Clone, Default)]
115pub struct ArrangementManager {
116 arrangements: HashMap<ArrangementId, WindowArrangement>,
118
119 order: Vec<ArrangementId>,
121}
122
123impl ArrangementManager {
124 pub fn new() -> Self {
126 Self {
127 arrangements: HashMap::new(),
128 order: Vec::new(),
129 }
130 }
131
132 pub fn from_arrangements(arrangements: Vec<WindowArrangement>) -> Self {
134 let mut manager = Self::new();
135 for arrangement in arrangements {
136 manager.add(arrangement);
137 }
138 manager.sort_by_order();
139 manager
140 }
141
142 pub fn add(&mut self, arrangement: WindowArrangement) {
144 let id = arrangement.id;
145 if !self.order.contains(&id) {
146 self.order.push(id);
147 }
148 self.arrangements.insert(id, arrangement);
149 }
150
151 pub fn get(&self, id: &ArrangementId) -> Option<&WindowArrangement> {
153 self.arrangements.get(id)
154 }
155
156 pub fn get_mut(&mut self, id: &ArrangementId) -> Option<&mut WindowArrangement> {
158 self.arrangements.get_mut(id)
159 }
160
161 pub fn update(&mut self, arrangement: WindowArrangement) {
163 let id = arrangement.id;
164 if self.arrangements.contains_key(&id) {
165 self.arrangements.insert(id, arrangement);
166 }
167 }
168
169 pub fn remove(&mut self, id: &ArrangementId) -> Option<WindowArrangement> {
171 self.order.retain(|aid| aid != id);
172 self.arrangements.remove(id)
173 }
174
175 pub fn arrangements_ordered(&self) -> Vec<&WindowArrangement> {
177 self.order
178 .iter()
179 .filter_map(|id| self.arrangements.get(id))
180 .collect()
181 }
182
183 pub fn to_vec(&self) -> Vec<WindowArrangement> {
185 self.arrangements_ordered().into_iter().cloned().collect()
186 }
187
188 pub fn len(&self) -> usize {
190 self.arrangements.len()
191 }
192
193 pub fn is_empty(&self) -> bool {
195 self.arrangements.is_empty()
196 }
197
198 pub fn find_by_name(&self, name: &str) -> Option<&WindowArrangement> {
200 let lower = name.to_lowercase();
201 self.arrangements
202 .values()
203 .find(|a| a.name.to_lowercase() == lower)
204 }
205
206 pub fn move_up(&mut self, id: &ArrangementId) {
208 if let Some(pos) = self.order.iter().position(|aid| aid == id)
209 && pos > 0
210 {
211 self.order.swap(pos, pos - 1);
212 self.update_orders();
213 }
214 }
215
216 pub fn move_down(&mut self, id: &ArrangementId) {
218 if let Some(pos) = self.order.iter().position(|aid| aid == id)
219 && pos < self.order.len() - 1
220 {
221 self.order.swap(pos, pos + 1);
222 self.update_orders();
223 }
224 }
225
226 fn sort_by_order(&mut self) {
228 self.order.sort_by_key(|id| {
229 self.arrangements
230 .get(id)
231 .map(|a| a.order)
232 .unwrap_or(usize::MAX)
233 });
234 }
235
236 fn update_orders(&mut self) {
238 for (i, id) in self.order.iter().enumerate() {
239 if let Some(arrangement) = self.arrangements.get_mut(id) {
240 arrangement.order = i;
241 }
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 fn make_arrangement(name: &str, order: usize) -> WindowArrangement {
251 WindowArrangement {
252 id: Uuid::new_v4(),
253 name: name.to_string(),
254 monitor_layout: Vec::new(),
255 windows: Vec::new(),
256 created_at: String::new(),
257 order,
258 }
259 }
260
261 #[test]
262 fn test_manager_basic_operations() {
263 let mut manager = ArrangementManager::new();
264 assert!(manager.is_empty());
265
266 let arr = make_arrangement("Test", 0);
267 let id = arr.id;
268 manager.add(arr);
269
270 assert_eq!(manager.len(), 1);
271 assert!(manager.get(&id).is_some());
272 assert_eq!(manager.get(&id).unwrap().name, "Test");
273
274 let removed = manager.remove(&id);
275 assert!(removed.is_some());
276 assert!(manager.is_empty());
277 }
278
279 #[test]
280 fn test_manager_ordering() {
281 let mut manager = ArrangementManager::new();
282
283 let a1 = make_arrangement("First", 0);
284 let a2 = make_arrangement("Second", 1);
285 let a3 = make_arrangement("Third", 2);
286
287 let id1 = a1.id;
288 let id2 = a2.id;
289 let id3 = a3.id;
290
291 manager.add(a1);
292 manager.add(a2);
293 manager.add(a3);
294
295 let ordered = manager.arrangements_ordered();
296 assert_eq!(ordered.len(), 3);
297 assert_eq!(ordered[0].id, id1);
298 assert_eq!(ordered[1].id, id2);
299 assert_eq!(ordered[2].id, id3);
300
301 manager.move_up(&id2);
303 let ordered = manager.arrangements_ordered();
304 assert_eq!(ordered[0].id, id2);
305 assert_eq!(ordered[1].id, id1);
306
307 manager.move_down(&id2);
309 let ordered = manager.arrangements_ordered();
310 assert_eq!(ordered[0].id, id1);
311 assert_eq!(ordered[1].id, id2);
312 }
313
314 #[test]
315 fn test_find_by_name() {
316 let mut manager = ArrangementManager::new();
317 manager.add(make_arrangement("Work Setup", 0));
318 manager.add(make_arrangement("Home Setup", 1));
319
320 assert!(manager.find_by_name("work setup").is_some());
321 assert!(manager.find_by_name("HOME SETUP").is_some());
322 assert!(manager.find_by_name("nonexistent").is_none());
323 }
324
325 #[test]
326 fn test_serialization() {
327 let arr = WindowArrangement {
328 id: Uuid::new_v4(),
329 name: "Test".to_string(),
330 monitor_layout: vec![MonitorInfo {
331 name: Some("DELL U2720Q".to_string()),
332 index: 0,
333 position: (0, 0),
334 size: (2560, 1440),
335 scale_factor: 1.0,
336 }],
337 windows: vec![WindowSnapshot {
338 monitor: MonitorInfo {
339 name: Some("DELL U2720Q".to_string()),
340 index: 0,
341 position: (0, 0),
342 size: (2560, 1440),
343 scale_factor: 1.0,
344 },
345 position_relative: (100, 200),
346 size: (800, 600),
347 tabs: vec![TabSnapshot {
348 cwd: Some("/home/user".to_string()),
349 title: "bash".to_string(),
350 custom_color: None,
351 user_title: None,
352 custom_icon: None,
353 }],
354 active_tab_index: 0,
355 tmux_session_name: None,
356 mux_session_name: None,
357 }],
358 created_at: "2024-01-01T00:00:00Z".to_string(),
359 order: 0,
360 };
361
362 let yaml = serde_yaml_ng::to_string(&arr).unwrap();
363 let deserialized: WindowArrangement = serde_yaml_ng::from_str(&yaml).unwrap();
364
365 assert_eq!(deserialized.id, arr.id);
366 assert_eq!(deserialized.name, arr.name);
367 assert_eq!(deserialized.windows.len(), 1);
368 assert_eq!(deserialized.windows[0].tabs.len(), 1);
369 assert_eq!(
370 deserialized.windows[0].tabs[0].cwd,
371 Some("/home/user".to_string())
372 );
373 }
374}