Skip to main content

par_term_settings_ui/
arrangements.rs

1//! Window arrangement types and manager for saving/restoring window layouts
2//!
3//! Arrangements capture the positions, sizes, and tab CWDs of all windows
4//! so they can be restored later. Monitor-aware to handle external monitor
5//! disconnect/reconnect scenarios.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11/// Unique identifier for an arrangement
12pub type ArrangementId = Uuid;
13
14/// Information about a monitor at capture time
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct MonitorInfo {
17    /// Monitor name (primary matching key, e.g. "DELL U2720Q")
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub name: Option<String>,
20
21    /// Monitor index (fallback matching)
22    #[serde(default)]
23    pub index: usize,
24
25    /// Monitor position in virtual screen coordinates
26    #[serde(default)]
27    pub position: (i32, i32),
28
29    /// Monitor size in physical pixels
30    #[serde(default)]
31    pub size: (u32, u32),
32}
33
34/// Snapshot of a single tab's state
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct TabSnapshot {
37    /// Working directory (from Tab::get_cwd())
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub cwd: Option<String>,
40
41    /// Tab title
42    #[serde(default)]
43    pub title: String,
44}
45
46/// Snapshot of a single window's state
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct WindowSnapshot {
49    /// Monitor this window was on
50    pub monitor: MonitorInfo,
51
52    /// Position relative to monitor origin (portable across setups)
53    pub position_relative: (i32, i32),
54
55    /// Outer window size in physical pixels
56    pub size: (u32, u32),
57
58    /// Tabs in this window
59    pub tabs: Vec<TabSnapshot>,
60
61    /// Index of the active tab
62    #[serde(default)]
63    pub active_tab_index: usize,
64}
65
66/// A saved window arrangement
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct WindowArrangement {
69    /// Unique identifier
70    pub id: ArrangementId,
71
72    /// Display name for the arrangement
73    pub name: String,
74
75    /// All monitors present at capture time
76    pub monitor_layout: Vec<MonitorInfo>,
77
78    /// All windows in this arrangement
79    pub windows: Vec<WindowSnapshot>,
80
81    /// ISO 8601 timestamp when the arrangement was created
82    #[serde(default)]
83    pub created_at: String,
84
85    /// Display order
86    #[serde(default)]
87    pub order: usize,
88}
89
90/// Manages a collection of saved window arrangements
91#[derive(Debug, Clone, Default)]
92pub struct ArrangementManager {
93    /// All arrangements indexed by ID
94    arrangements: HashMap<ArrangementId, WindowArrangement>,
95
96    /// Ordered list of arrangement IDs for display
97    order: Vec<ArrangementId>,
98}
99
100impl ArrangementManager {
101    /// Create a new empty arrangement manager
102    pub fn new() -> Self {
103        Self {
104            arrangements: HashMap::new(),
105            order: Vec::new(),
106        }
107    }
108
109    /// Create a manager from a list of arrangements
110    pub fn from_arrangements(arrangements: Vec<WindowArrangement>) -> Self {
111        let mut manager = Self::new();
112        for arrangement in arrangements {
113            manager.add(arrangement);
114        }
115        manager.sort_by_order();
116        manager
117    }
118
119    /// Add an arrangement to the manager
120    pub fn add(&mut self, arrangement: WindowArrangement) {
121        let id = arrangement.id;
122        if !self.order.contains(&id) {
123            self.order.push(id);
124        }
125        self.arrangements.insert(id, arrangement);
126    }
127
128    /// Get an arrangement by ID
129    pub fn get(&self, id: &ArrangementId) -> Option<&WindowArrangement> {
130        self.arrangements.get(id)
131    }
132
133    /// Get a mutable reference to an arrangement by ID
134    pub fn get_mut(&mut self, id: &ArrangementId) -> Option<&mut WindowArrangement> {
135        self.arrangements.get_mut(id)
136    }
137
138    /// Update an arrangement (replaces if exists)
139    pub fn update(&mut self, arrangement: WindowArrangement) {
140        let id = arrangement.id;
141        if self.arrangements.contains_key(&id) {
142            self.arrangements.insert(id, arrangement);
143        }
144    }
145
146    /// Remove an arrangement by ID
147    pub fn remove(&mut self, id: &ArrangementId) -> Option<WindowArrangement> {
148        self.order.retain(|aid| aid != id);
149        self.arrangements.remove(id)
150    }
151
152    /// Get all arrangements in display order
153    pub fn arrangements_ordered(&self) -> Vec<&WindowArrangement> {
154        self.order
155            .iter()
156            .filter_map(|id| self.arrangements.get(id))
157            .collect()
158    }
159
160    /// Get all arrangements as a vector (for serialization)
161    pub fn to_vec(&self) -> Vec<WindowArrangement> {
162        self.arrangements_ordered().into_iter().cloned().collect()
163    }
164
165    /// Get the number of arrangements
166    pub fn len(&self) -> usize {
167        self.arrangements.len()
168    }
169
170    /// Check if there are no arrangements
171    pub fn is_empty(&self) -> bool {
172        self.arrangements.is_empty()
173    }
174
175    /// Find an arrangement by name (case-insensitive)
176    pub fn find_by_name(&self, name: &str) -> Option<&WindowArrangement> {
177        let lower = name.to_lowercase();
178        self.arrangements
179            .values()
180            .find(|a| a.name.to_lowercase() == lower)
181    }
182
183    /// Move an arrangement earlier in the order (towards index 0)
184    pub fn move_up(&mut self, id: &ArrangementId) {
185        if let Some(pos) = self.order.iter().position(|aid| aid == id)
186            && pos > 0
187        {
188            self.order.swap(pos, pos - 1);
189            self.update_orders();
190        }
191    }
192
193    /// Move an arrangement later in the order (towards the end)
194    pub fn move_down(&mut self, id: &ArrangementId) {
195        if let Some(pos) = self.order.iter().position(|aid| aid == id)
196            && pos < self.order.len() - 1
197        {
198            self.order.swap(pos, pos + 1);
199            self.update_orders();
200        }
201    }
202
203    /// Sort arrangements by their order field
204    fn sort_by_order(&mut self) {
205        self.order.sort_by_key(|id| {
206            self.arrangements
207                .get(id)
208                .map(|a| a.order)
209                .unwrap_or(usize::MAX)
210        });
211    }
212
213    /// Update the order field of all arrangements to match their position
214    fn update_orders(&mut self) {
215        for (i, id) in self.order.iter().enumerate() {
216            if let Some(arrangement) = self.arrangements.get_mut(id) {
217                arrangement.order = i;
218            }
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    fn make_arrangement(name: &str, order: usize) -> WindowArrangement {
228        WindowArrangement {
229            id: Uuid::new_v4(),
230            name: name.to_string(),
231            monitor_layout: Vec::new(),
232            windows: Vec::new(),
233            created_at: String::new(),
234            order,
235        }
236    }
237
238    #[test]
239    fn test_manager_basic_operations() {
240        let mut manager = ArrangementManager::new();
241        assert!(manager.is_empty());
242
243        let arr = make_arrangement("Test", 0);
244        let id = arr.id;
245        manager.add(arr);
246
247        assert_eq!(manager.len(), 1);
248        assert!(manager.get(&id).is_some());
249        assert_eq!(manager.get(&id).unwrap().name, "Test");
250
251        let removed = manager.remove(&id);
252        assert!(removed.is_some());
253        assert!(manager.is_empty());
254    }
255
256    #[test]
257    fn test_manager_ordering() {
258        let mut manager = ArrangementManager::new();
259
260        let a1 = make_arrangement("First", 0);
261        let a2 = make_arrangement("Second", 1);
262        let a3 = make_arrangement("Third", 2);
263
264        let id1 = a1.id;
265        let id2 = a2.id;
266        let id3 = a3.id;
267
268        manager.add(a1);
269        manager.add(a2);
270        manager.add(a3);
271
272        let ordered = manager.arrangements_ordered();
273        assert_eq!(ordered.len(), 3);
274        assert_eq!(ordered[0].id, id1);
275        assert_eq!(ordered[1].id, id2);
276        assert_eq!(ordered[2].id, id3);
277
278        // Move second to first position
279        manager.move_up(&id2);
280        let ordered = manager.arrangements_ordered();
281        assert_eq!(ordered[0].id, id2);
282        assert_eq!(ordered[1].id, id1);
283
284        // Move second (now first) down
285        manager.move_down(&id2);
286        let ordered = manager.arrangements_ordered();
287        assert_eq!(ordered[0].id, id1);
288        assert_eq!(ordered[1].id, id2);
289    }
290
291    #[test]
292    fn test_find_by_name() {
293        let mut manager = ArrangementManager::new();
294        manager.add(make_arrangement("Work Setup", 0));
295        manager.add(make_arrangement("Home Setup", 1));
296
297        assert!(manager.find_by_name("work setup").is_some());
298        assert!(manager.find_by_name("HOME SETUP").is_some());
299        assert!(manager.find_by_name("nonexistent").is_none());
300    }
301
302    #[test]
303    fn test_serialization() {
304        let arr = WindowArrangement {
305            id: Uuid::new_v4(),
306            name: "Test".to_string(),
307            monitor_layout: vec![MonitorInfo {
308                name: Some("DELL U2720Q".to_string()),
309                index: 0,
310                position: (0, 0),
311                size: (2560, 1440),
312            }],
313            windows: vec![WindowSnapshot {
314                monitor: MonitorInfo {
315                    name: Some("DELL U2720Q".to_string()),
316                    index: 0,
317                    position: (0, 0),
318                    size: (2560, 1440),
319                },
320                position_relative: (100, 200),
321                size: (800, 600),
322                tabs: vec![TabSnapshot {
323                    cwd: Some("/home/user".to_string()),
324                    title: "bash".to_string(),
325                }],
326                active_tab_index: 0,
327            }],
328            created_at: "2024-01-01T00:00:00Z".to_string(),
329            order: 0,
330        };
331
332        let yaml = serde_yaml::to_string(&arr).unwrap();
333        let deserialized: WindowArrangement = serde_yaml::from_str(&yaml).unwrap();
334
335        assert_eq!(deserialized.id, arr.id);
336        assert_eq!(deserialized.name, arr.name);
337        assert_eq!(deserialized.windows.len(), 1);
338        assert_eq!(deserialized.windows[0].tabs.len(), 1);
339        assert_eq!(
340            deserialized.windows[0].tabs[0].cwd,
341            Some("/home/user".to_string())
342        );
343    }
344}