Skip to main content

waterui_cli/workflows/
capture.rs

1//! Screen capture utilities for devices.
2//!
3//! Provides unified screenshot capture across iOS simulators and Android devices.
4
5use std::path::{Path, PathBuf};
6
7use eyre::eyre;
8use jiff::Timestamp;
9
10use crate::device::Device;
11use crate::toolchain::Host;
12use crate::{android, apple};
13
14/// The platform type of a device based on its identifier.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DevicePlatform {
17    /// iOS simulator (UDID format)
18    Ios,
19    /// Android device or emulator
20    Android,
21}
22
23/// Detect the platform of a device from its identifier.
24///
25/// iOS simulators use UUIDs (e.g., `12345678-1234-1234-1234-123456789ABC`).
26/// Android devices use serials (e.g., `emulator-5554`, `ABCD1234`).
27#[must_use]
28pub fn detect_platform(device_id: &str) -> DevicePlatform {
29    let parts: Vec<&str> = device_id.split('-').collect();
30    if parts.len() == 5
31        && parts[0].len() == 8
32        && parts[1].len() == 4
33        && parts[2].len() == 4
34        && parts[3].len() == 4
35        && parts[4].len() == 12
36        && device_id.chars().all(|c| c.is_ascii_hexdigit() || c == '-')
37    {
38        return DevicePlatform::Ios;
39    }
40
41    DevicePlatform::Android
42}
43
44/// Generate a default output filename for a screenshot.
45///
46/// Format: `screenshot_YYYY-MM-DD_HHMMSS.png`
47#[must_use]
48pub fn generate_screenshot_filename() -> PathBuf {
49    let filename = format!(
50        "screenshot_{}.png",
51        Timestamp::now().strftime("%Y-%m-%d_%H%M%S")
52    );
53    PathBuf::from(filename)
54}
55
56/// Capture a screenshot from a device.
57///
58/// # Errors
59/// Returns an error if the target device cannot be reached or the screenshot command fails.
60pub async fn screenshot(host: &Host, device_id: &str, output: &Path) -> eyre::Result<()> {
61    match detect_platform(device_id) {
62        DevicePlatform::Ios => apple::device::screenshot(host, device_id, output).await,
63        DevicePlatform::Android => android::device::screenshot(host, device_id, output).await,
64    }
65}
66
67/// Verify that a device exists and return its platform.
68///
69/// # Errors
70/// Returns an error if the device cannot be found on the inferred platform.
71pub async fn verify_device(host: &Host, device_id: &str) -> eyre::Result<DevicePlatform> {
72    let platform = detect_platform(device_id);
73
74    match platform {
75        DevicePlatform::Ios => {
76            let simulators = apple::device::AppleSimulator::scan(host).await?;
77            if simulators.iter().any(|s| s.udid == device_id) {
78                Ok(DevicePlatform::Ios)
79            } else {
80                Err(eyre!("iOS simulator with UDID '{}' not found", device_id))
81            }
82        }
83        DevicePlatform::Android => {
84            let devices = android::device::AndroidDevice::scan(host).await?;
85            if devices.iter().any(|d| d.identifier() == device_id) {
86                Ok(DevicePlatform::Android)
87            } else {
88                Err(eyre!("Android device '{}' not found", device_id))
89            }
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn detects_ios_udid() {
100        assert_eq!(
101            detect_platform("12345678-1234-1234-1234-123456789ABC"),
102            DevicePlatform::Ios
103        );
104        assert_eq!(
105            detect_platform("ABCDEF12-3456-7890-ABCD-EF1234567890"),
106            DevicePlatform::Ios
107        );
108    }
109
110    #[test]
111    fn detects_android_device() {
112        assert_eq!(detect_platform("emulator-5554"), DevicePlatform::Android);
113        assert_eq!(detect_platform("ABCD1234"), DevicePlatform::Android);
114        assert_eq!(
115            detect_platform("192.168.1.100:5555"),
116            DevicePlatform::Android
117        );
118    }
119
120    #[test]
121    fn generates_valid_filename() {
122        let filename = generate_screenshot_filename();
123        let name = filename.to_string_lossy();
124        assert!(name.starts_with("screenshot_"));
125        assert!(name.ends_with(".png"));
126    }
127}