Rust · 5184 bytes Raw Blame History
1 use std::collections::HashMap;
2
3 use crate::platform::accessibility::CGWindowID;
4
5 pub type WindowId = CGWindowID;
6
7 /// Tracked state for a managed window.
8 #[derive(Debug, Clone)]
9 pub struct WindowState {
10 pub id: WindowId,
11 pub app_pid: i32,
12 pub app_name: String,
13 pub app_bundle_id: String,
14 pub title: String,
15 pub role: String,
16 pub subrole: String,
17 pub x: f64,
18 pub y: f64,
19 pub width: f64,
20 pub height: f64,
21 pub floating: bool,
22 pub minimized: bool,
23 }
24
25 /// Registry of all tracked windows.
26 #[derive(Debug)]
27 pub struct WindowRegistry {
28 windows: HashMap<WindowId, WindowState>,
29 }
30
31 impl WindowRegistry {
32 pub fn new() -> Self {
33 Self {
34 windows: HashMap::new(),
35 }
36 }
37
38 pub fn add(&mut self, window: WindowState) {
39 tracing::debug!(
40 id = window.id,
41 app = %window.app_name,
42 title = %window.title,
43 "window added"
44 );
45 self.windows.insert(window.id, window);
46 }
47
48 pub fn remove(&mut self, id: WindowId) -> Option<WindowState> {
49 let removed = self.windows.remove(&id);
50 if let Some(ref w) = removed {
51 tracing::debug!(id = w.id, app = %w.app_name, title = %w.title, "window removed");
52 }
53 removed
54 }
55
56 pub fn get(&self, id: WindowId) -> Option<&WindowState> {
57 self.windows.get(&id)
58 }
59
60 pub fn get_mut(&mut self, id: WindowId) -> Option<&mut WindowState> {
61 self.windows.get_mut(&id)
62 }
63
64 pub fn contains(&self, id: WindowId) -> bool {
65 self.windows.contains_key(&id)
66 }
67
68 pub fn count(&self) -> usize {
69 self.windows.len()
70 }
71
72 pub fn all(&self) -> impl Iterator<Item = &WindowState> {
73 self.windows.values()
74 }
75
76 /// Remove all windows belonging to a given app pid.
77 pub fn remove_by_pid(&mut self, pid: i32) -> Vec<WindowState> {
78 let ids: Vec<WindowId> = self
79 .windows
80 .iter()
81 .filter(|(_, w)| w.app_pid == pid)
82 .map(|(id, _)| *id)
83 .collect();
84
85 let mut removed = Vec::with_capacity(ids.len());
86 for id in ids {
87 if let Some(w) = self.windows.remove(&id) {
88 tracing::debug!(id = w.id, app = %w.app_name, "window removed (app terminated)");
89 removed.push(w);
90 }
91 }
92 removed
93 }
94
95 /// Update position and size for a window.
96 pub fn update_geometry(&mut self, id: WindowId, x: f64, y: f64, width: f64, height: f64) {
97 if let Some(w) = self.windows.get_mut(&id) {
98 w.x = x;
99 w.y = y;
100 w.width = width;
101 w.height = height;
102 }
103 }
104
105 /// Update title for a window.
106 pub fn update_title(&mut self, id: WindowId, title: String) {
107 if let Some(w) = self.windows.get_mut(&id) {
108 w.title = title;
109 }
110 }
111 }
112
113 impl Default for WindowRegistry {
114 fn default() -> Self {
115 Self::new()
116 }
117 }
118
119 #[cfg(test)]
120 mod tests {
121 use super::*;
122
123 fn make_window(id: WindowId, app: &str, title: &str) -> WindowState {
124 WindowState {
125 id,
126 app_pid: 100,
127 app_name: app.to_string(),
128 app_bundle_id: format!("com.test.{}", app),
129 title: title.to_string(),
130 role: "AXWindow".to_string(),
131 subrole: "AXStandardWindow".to_string(),
132 x: 0.0,
133 y: 0.0,
134 width: 800.0,
135 height: 600.0,
136 floating: false,
137 minimized: false,
138 }
139 }
140
141 #[test]
142 fn add_and_get() {
143 let mut reg = WindowRegistry::new();
144 reg.add(make_window(1, "Terminal", "bash"));
145 assert_eq!(reg.count(), 1);
146 assert!(reg.contains(1));
147 assert_eq!(reg.get(1).unwrap().title, "bash");
148 }
149
150 #[test]
151 fn remove() {
152 let mut reg = WindowRegistry::new();
153 reg.add(make_window(1, "Terminal", "bash"));
154 let removed = reg.remove(1);
155 assert!(removed.is_some());
156 assert_eq!(reg.count(), 0);
157 }
158
159 #[test]
160 fn remove_by_pid() {
161 let mut reg = WindowRegistry::new();
162 let mut w1 = make_window(1, "Terminal", "tab1");
163 w1.app_pid = 200;
164 let mut w2 = make_window(2, "Terminal", "tab2");
165 w2.app_pid = 200;
166 reg.add(w1);
167 reg.add(w2);
168 reg.add(make_window(3, "Safari", "web"));
169
170 let removed = reg.remove_by_pid(200);
171 assert_eq!(removed.len(), 2);
172 assert_eq!(reg.count(), 1);
173 }
174
175 #[test]
176 fn update_geometry() {
177 let mut reg = WindowRegistry::new();
178 reg.add(make_window(1, "Terminal", "bash"));
179 reg.update_geometry(1, 100.0, 200.0, 1000.0, 700.0);
180 let w = reg.get(1).unwrap();
181 assert_eq!(w.x, 100.0);
182 assert_eq!(w.y, 200.0);
183 assert_eq!(w.width, 1000.0);
184 assert_eq!(w.height, 700.0);
185 }
186
187 #[test]
188 fn update_title() {
189 let mut reg = WindowRegistry::new();
190 reg.add(make_window(1, "Terminal", "bash"));
191 reg.update_title(1, "zsh".to_string());
192 assert_eq!(reg.get(1).unwrap().title, "zsh");
193 }
194 }
195