capture_zed.rs

  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}