1//! Utility: Capture Screenshots of Running Zed Windows
2//!
3//! This utility finds running Zed windows and captures screenshots of them.
4//! It can be used for debugging, documentation, or visual testing.
5//!
6//! Usage:
7//! cargo run -p gpui --example capture_zed
8//!
9//! Options (via environment variables):
10//! CAPTURE_OUTPUT_DIR - Directory to save screenshots (default: current directory)
11//! CAPTURE_WINDOW_INDEX - Which Zed window to capture, 0-indexed (default: all)
12//!
13//! Note: This requires macOS and Screen Recording permissions.
14//! The first time you run this, macOS will prompt you to grant permission.
15
16use std::path::PathBuf;
17
18fn main() {
19 #[cfg(target_os = "macos")]
20 {
21 macos::run();
22 }
23
24 #[cfg(not(target_os = "macos"))]
25 {
26 eprintln!("This utility only works on macOS");
27 std::process::exit(1);
28 }
29}
30
31#[cfg(target_os = "macos")]
32mod macos {
33 use std::path::PathBuf;
34
35 // FFI declarations for CoreGraphics window list
36 #[link(name = "CoreGraphics", kind = "framework")]
37 unsafe extern "C" {
38 fn CGWindowListCopyWindowInfo(option: u32, relativeToWindow: u32) -> CFArrayRef;
39 fn CGWindowListCreateImage(
40 rect: CGRect,
41 list_option: u32,
42 window_id: u32,
43 image_option: u32,
44 ) -> CGImageRef;
45 fn CGImageGetWidth(image: CGImageRef) -> usize;
46 fn CGImageGetHeight(image: CGImageRef) -> usize;
47 fn CGImageGetBytesPerRow(image: CGImageRef) -> usize;
48 fn CGImageGetDataProvider(image: CGImageRef) -> CGDataProviderRef;
49 fn CGImageRelease(image: CGImageRef);
50 fn CGDataProviderCopyData(provider: CGDataProviderRef) -> CFDataRef;
51 }
52
53 #[link(name = "CoreFoundation", kind = "framework")]
54 unsafe extern "C" {
55 fn CFArrayGetCount(array: CFArrayRef) -> isize;
56 fn CFArrayGetValueAtIndex(array: CFArrayRef, idx: isize) -> *const std::ffi::c_void;
57 fn CFDictionaryGetValue(
58 dict: CFDictionaryRef,
59 key: *const std::ffi::c_void,
60 ) -> *const std::ffi::c_void;
61 fn CFStringCreateWithCString(
62 alloc: *const std::ffi::c_void,
63 cstr: *const i8,
64 encoding: u32,
65 ) -> CFStringRef;
66 fn CFStringGetCStringPtr(string: CFStringRef, encoding: u32) -> *const i8;
67 fn CFNumberGetValue(
68 number: CFNumberRef,
69 theType: i32,
70 valuePtr: *mut std::ffi::c_void,
71 ) -> bool;
72 fn CFDataGetLength(data: CFDataRef) -> isize;
73 fn CFDataGetBytePtr(data: CFDataRef) -> *const u8;
74 fn CFRelease(cf: *const std::ffi::c_void);
75 }
76
77 type CFArrayRef = *const std::ffi::c_void;
78 type CFDictionaryRef = *const std::ffi::c_void;
79 type CFStringRef = *const std::ffi::c_void;
80 type CFNumberRef = *const std::ffi::c_void;
81 type CGImageRef = *mut std::ffi::c_void;
82 type CGDataProviderRef = *mut std::ffi::c_void;
83 type CFDataRef = *mut std::ffi::c_void;
84
85 #[repr(C)]
86 #[derive(Copy, Clone)]
87 struct CGPoint {
88 x: f64,
89 y: f64,
90 }
91
92 #[repr(C)]
93 #[derive(Copy, Clone)]
94 struct CGSize {
95 width: f64,
96 height: f64,
97 }
98
99 #[repr(C)]
100 #[derive(Copy, Clone)]
101 struct CGRect {
102 origin: CGPoint,
103 size: CGSize,
104 }
105
106 impl CGRect {
107 fn null() -> Self {
108 CGRect {
109 origin: CGPoint {
110 x: f64::INFINITY,
111 y: f64::INFINITY,
112 },
113 size: CGSize {
114 width: 0.0,
115 height: 0.0,
116 },
117 }
118 }
119 }
120
121 // Constants
122 #[allow(non_upper_case_globals)]
123 const kCGWindowListOptionOnScreenOnly: u32 = 1 << 0;
124 #[allow(non_upper_case_globals)]
125 const kCGWindowListExcludeDesktopElements: u32 = 1 << 4;
126 #[allow(non_upper_case_globals)]
127 const kCGWindowListOptionIncludingWindow: u32 = 1 << 3;
128 #[allow(non_upper_case_globals)]
129 const kCGWindowImageBoundsIgnoreFraming: u32 = 1 << 0;
130 #[allow(non_upper_case_globals)]
131 const kCFStringEncodingUTF8: u32 = 0x08000100;
132 #[allow(non_upper_case_globals)]
133 const kCFNumberSInt32Type: i32 = 3;
134
135 #[derive(Debug)]
136 struct WindowInfo {
137 window_id: u32,
138 owner_name: String,
139 window_name: String,
140 bounds: (f64, f64, f64, f64), // x, y, width, height
141 }
142
143 fn get_cf_string(key: &str) -> CFStringRef {
144 unsafe {
145 let cstr = std::ffi::CString::new(key).unwrap();
146 CFStringCreateWithCString(std::ptr::null(), cstr.as_ptr(), kCFStringEncodingUTF8)
147 }
148 }
149
150 fn cf_string_to_rust(cf_string: CFStringRef) -> Option<String> {
151 if cf_string.is_null() {
152 return None;
153 }
154 unsafe {
155 let ptr = CFStringGetCStringPtr(cf_string, kCFStringEncodingUTF8);
156 if ptr.is_null() {
157 return None;
158 }
159 Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned())
160 }
161 }
162
163 fn cf_number_to_i32(cf_number: CFNumberRef) -> Option<i32> {
164 if cf_number.is_null() {
165 return None;
166 }
167 unsafe {
168 let mut value: i32 = 0;
169 if CFNumberGetValue(
170 cf_number,
171 kCFNumberSInt32Type,
172 &mut value as *mut i32 as *mut std::ffi::c_void,
173 ) {
174 Some(value)
175 } else {
176 None
177 }
178 }
179 }
180
181 fn get_zed_windows() -> Vec<WindowInfo> {
182 let mut windows = Vec::new();
183
184 unsafe {
185 let window_list = CGWindowListCopyWindowInfo(
186 kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
187 0,
188 );
189
190 if window_list.is_null() {
191 return windows;
192 }
193
194 let count = CFArrayGetCount(window_list);
195
196 let key_owner_name = get_cf_string("kCGWindowOwnerName");
197 let key_window_name = get_cf_string("kCGWindowName");
198 let key_window_number = get_cf_string("kCGWindowNumber");
199 let key_bounds = get_cf_string("kCGWindowBounds");
200 let key_x = get_cf_string("X");
201 let key_y = get_cf_string("Y");
202 let key_width = get_cf_string("Width");
203 let key_height = get_cf_string("Height");
204
205 for i in 0..count {
206 let dict = CFArrayGetValueAtIndex(window_list, i) as CFDictionaryRef;
207 if dict.is_null() {
208 continue;
209 }
210
211 // Get owner name
212 let owner_name_cf = CFDictionaryGetValue(dict, key_owner_name) as CFStringRef;
213 let owner_name = cf_string_to_rust(owner_name_cf).unwrap_or_default();
214
215 // Check if this is a Zed window
216 if !owner_name.contains("Zed") {
217 continue;
218 }
219
220 // Get window name
221 let window_name_cf = CFDictionaryGetValue(dict, key_window_name) as CFStringRef;
222 let window_name = cf_string_to_rust(window_name_cf).unwrap_or_default();
223
224 // Get window ID
225 let window_number_cf = CFDictionaryGetValue(dict, key_window_number) as CFNumberRef;
226 let window_id = cf_number_to_i32(window_number_cf).unwrap_or(0) as u32;
227
228 // Get bounds
229 let bounds_dict = CFDictionaryGetValue(dict, key_bounds) as CFDictionaryRef;
230 let (x, y, width, height) = if !bounds_dict.is_null() {
231 let x_cf = CFDictionaryGetValue(bounds_dict, key_x) as CFNumberRef;
232 let y_cf = CFDictionaryGetValue(bounds_dict, key_y) as CFNumberRef;
233 let w_cf = CFDictionaryGetValue(bounds_dict, key_width) as CFNumberRef;
234 let h_cf = CFDictionaryGetValue(bounds_dict, key_height) as CFNumberRef;
235
236 (
237 cf_number_to_i32(x_cf).unwrap_or(0) as f64,
238 cf_number_to_i32(y_cf).unwrap_or(0) as f64,
239 cf_number_to_i32(w_cf).unwrap_or(0) as f64,
240 cf_number_to_i32(h_cf).unwrap_or(0) as f64,
241 )
242 } else {
243 (0.0, 0.0, 0.0, 0.0)
244 };
245
246 // Skip windows with zero size (like menu bar items)
247 if width < 100.0 || height < 100.0 {
248 continue;
249 }
250
251 windows.push(WindowInfo {
252 window_id,
253 owner_name,
254 window_name,
255 bounds: (x, y, width, height),
256 });
257 }
258
259 // Clean up CF strings
260 CFRelease(key_owner_name);
261 CFRelease(key_window_name);
262 CFRelease(key_window_number);
263 CFRelease(key_bounds);
264 CFRelease(key_x);
265 CFRelease(key_y);
266 CFRelease(key_width);
267 CFRelease(key_height);
268 CFRelease(window_list);
269 }
270
271 windows
272 }
273
274 fn capture_window_to_png(
275 window_id: u32,
276 output_path: &std::path::Path,
277 ) -> Result<(usize, usize), Box<dyn std::error::Error>> {
278 use std::fs::File;
279 use std::io::BufWriter;
280
281 // Capture the window
282 let image = unsafe {
283 CGWindowListCreateImage(
284 CGRect::null(),
285 kCGWindowListOptionIncludingWindow,
286 window_id,
287 kCGWindowImageBoundsIgnoreFraming,
288 )
289 };
290
291 if image.is_null() {
292 return Err("Failed to capture window - image is null. \
293 Make sure Screen Recording permission is granted in \
294 System Preferences > Privacy & Security > Screen Recording."
295 .into());
296 }
297
298 // Get image dimensions
299 let width = unsafe { CGImageGetWidth(image) };
300 let height = unsafe { CGImageGetHeight(image) };
301
302 if width == 0 || height == 0 {
303 unsafe { CGImageRelease(image) };
304 return Err("Captured image has zero dimensions".into());
305 }
306
307 // Get the image data
308 let data_provider = unsafe { CGImageGetDataProvider(image) };
309 if data_provider.is_null() {
310 unsafe { CGImageRelease(image) };
311 return Err("Failed to get image data provider".into());
312 }
313
314 let data = unsafe { CGDataProviderCopyData(data_provider) };
315 if data.is_null() {
316 unsafe { CGImageRelease(image) };
317 return Err("Failed to copy image data".into());
318 }
319
320 let length = unsafe { CFDataGetLength(data) } as usize;
321 let ptr = unsafe { CFDataGetBytePtr(data) };
322 let bytes = unsafe { std::slice::from_raw_parts(ptr, length) };
323 let bytes_per_row = unsafe { CGImageGetBytesPerRow(image) };
324
325 // The image is in BGRA format with potential row padding, convert to RGBA for PNG
326 let mut rgba_bytes = Vec::with_capacity(width * height * 4);
327 for row in 0..height {
328 let row_start = row * bytes_per_row;
329 for col in 0..width {
330 let pixel_start = row_start + col * 4;
331 if pixel_start + 3 < length {
332 rgba_bytes.push(bytes[pixel_start + 2]); // R (was B)
333 rgba_bytes.push(bytes[pixel_start + 1]); // G
334 rgba_bytes.push(bytes[pixel_start]); // B (was R)
335 rgba_bytes.push(bytes[pixel_start + 3]); // A
336 }
337 }
338 }
339
340 // Write PNG file
341 let file = File::create(output_path)?;
342 let w = BufWriter::new(file);
343 let mut encoder = png::Encoder::new(w, width as u32, height as u32);
344 encoder.set_color(png::ColorType::Rgba);
345 encoder.set_depth(png::BitDepth::Eight);
346 let mut writer = encoder.write_header()?;
347 writer.write_image_data(&rgba_bytes)?;
348
349 // Cleanup
350 unsafe {
351 CFRelease(data as *const _);
352 CGImageRelease(image);
353 }
354
355 Ok((width, height))
356 }
357
358 pub fn run() {
359 println!("Looking for Zed windows...\n");
360
361 let windows = get_zed_windows();
362
363 if windows.is_empty() {
364 eprintln!("No Zed windows found!");
365 eprintln!("\nMake sure Zed is running and visible on screen.");
366 eprintln!("Note: Minimized windows cannot be captured.");
367 std::process::exit(1);
368 }
369
370 println!("Found {} Zed window(s):\n", windows.len());
371 for (i, window) in windows.iter().enumerate() {
372 println!(
373 " [{}] Window ID: {}, Title: \"{}\", Size: {}x{}",
374 i, window.window_id, window.window_name, window.bounds.2, window.bounds.3
375 );
376 }
377 println!();
378
379 // Get output directory
380 let output_dir = std::env::var("CAPTURE_OUTPUT_DIR")
381 .map(PathBuf::from)
382 .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
383
384 // Get window index filter
385 let window_index_filter: Option<usize> = std::env::var("CAPTURE_WINDOW_INDEX")
386 .ok()
387 .and_then(|s| s.parse().ok());
388
389 // Capture windows
390 let windows_to_capture: Vec<_> = match window_index_filter {
391 Some(idx) => {
392 if idx < windows.len() {
393 vec![&windows[idx]]
394 } else {
395 eprintln!(
396 "Window index {} is out of range (0-{})",
397 idx,
398 windows.len() - 1
399 );
400 std::process::exit(1);
401 }
402 }
403 None => windows.iter().collect(),
404 };
405
406 println!("Capturing {} window(s)...\n", windows_to_capture.len());
407
408 for (i, window) in windows_to_capture.iter().enumerate() {
409 let filename = if window.window_name.is_empty() {
410 format!("zed_window_{}.png", i)
411 } else {
412 // Sanitize window name for filename
413 let safe_name: String = window
414 .window_name
415 .chars()
416 .map(|c| {
417 if c.is_alphanumeric() || c == '-' || c == '_' {
418 c
419 } else {
420 '_'
421 }
422 })
423 .collect();
424 format!("zed_{}.png", safe_name)
425 };
426
427 let output_path = output_dir.join(&filename);
428
429 match capture_window_to_png(window.window_id, &output_path) {
430 Ok((width, height)) => {
431 println!(
432 "✓ Captured \"{}\" -> {} ({}x{})",
433 window.window_name,
434 output_path.display(),
435 width,
436 height
437 );
438 }
439 Err(e) => {
440 eprintln!("✗ Failed to capture \"{}\": {}", window.window_name, e);
441 }
442 }
443 }
444
445 println!("\nDone!");
446 }
447}