Skip to main content

fotos_lib/commands/
capture.rs

1use crate::capture::ImageStore;
2use base64::prelude::*;
3use serde::Serialize;
4use std::io::Cursor;
5use std::sync::Arc;
6use tauri::Emitter;
7use uuid::Uuid;
8use xcap::{Monitor, Window};
9
10#[derive(Serialize, Clone)]
11pub struct ScreenshotResponse {
12    pub id: String,
13    pub width: u32,
14    pub height: u32,
15    pub data_url: String,
16}
17
18#[derive(Serialize, Clone)]
19struct ScreenshotReadyEvent {
20    id: String,
21    width: u32,
22    height: u32,
23}
24
25#[derive(Serialize)]
26pub struct MonitorInfo {
27    pub id: u32,
28    pub name: String,
29    pub x: i32,
30    pub y: i32,
31    pub width: u32,
32    pub height: u32,
33    pub is_primary: bool,
34}
35
36#[derive(Serialize)]
37pub struct WindowInfo {
38    pub id: u32,
39    pub title: String,
40    pub app_name: String,
41    pub x: i32,
42    pub y: i32,
43    pub width: u32,
44    pub height: u32,
45}
46
47#[tauri::command]
48pub async fn take_screenshot(
49    mode: String,
50    monitor: Option<u32>,
51    window_id: Option<u32>,
52    store: tauri::State<'_, ImageStore>,
53    app: tauri::AppHandle,
54    window: tauri::WebviewWindow,
55) -> Result<ScreenshotResponse, String> {
56    tracing::info!("take_screenshot: mode={mode}");
57
58    let image = match mode.as_str() {
59        "fullscreen" => {
60            // Hide the app window so it doesn't appear in the screenshot.
61            tracing::info!("take_screenshot: hiding window");
62            let _ = window.hide();
63            // Give the compositor a moment to actually hide the window.
64            tokio::time::sleep(std::time::Duration::from_millis(200)).await;
65
66            #[cfg(target_os = "linux")]
67            let in_flatpak = std::env::var("FLATPAK_ID").is_ok();
68            #[cfg(target_os = "linux")]
69            tracing::info!("take_screenshot: in_flatpak={in_flatpak}");
70            #[cfg(target_os = "linux")]
71            let result = if in_flatpak {
72                tracing::info!("take_screenshot: routing to portal backend");
73                crate::capture::portal::capture_via_portal()
74                    .await
75                    .map_err(|e| {
76                        tracing::error!("take_screenshot: portal failed: {e}");
77                        format!("Portal capture failed: {}", e)
78                    })
79            } else {
80                tracing::info!("take_screenshot: routing to xcap backend");
81                crate::capture::xcap_backend::capture_fullscreen()
82                    .await
83                    .map_err(|e| {
84                        tracing::error!("take_screenshot: xcap failed: {e}");
85                        format!("Capture failed: {}", e)
86                    })
87            };
88            #[cfg(not(target_os = "linux"))]
89            let result = {
90                tracing::info!("take_screenshot: routing to xcap backend");
91                crate::capture::xcap_backend::capture_fullscreen()
92                    .await
93                    .map_err(|e| {
94                        tracing::error!("take_screenshot: xcap failed: {e}");
95                        format!("Capture failed: {}", e)
96                    })
97            };
98
99            // Always restore the window before returning.
100            tracing::info!("take_screenshot: restoring window");
101            let _ = window.show();
102            let _ = window.set_focus();
103
104            result?
105        }
106        "monitor" => {
107            let index = monitor.ok_or("monitor index required for mode 'monitor'")?;
108            tracing::info!("take_screenshot: monitor index={index}");
109
110            let _ = window.hide();
111            tokio::time::sleep(std::time::Duration::from_millis(200)).await;
112
113            let result = crate::capture::xcap_backend::capture_monitor(index)
114                .await
115                .map_err(|e| {
116                    tracing::error!("take_screenshot: monitor capture failed: {e}");
117                    format!("Monitor capture failed: {}", e)
118                });
119
120            let _ = window.show();
121            let _ = window.set_focus();
122
123            result?
124        }
125        "window" => {
126            let wid = window_id.ok_or("window_id required for mode 'window'")?;
127            tracing::info!("take_screenshot: window_id={wid}");
128
129            crate::capture::xcap_backend::capture_window(wid)
130                .await
131                .map_err(|e| {
132                    tracing::error!("take_screenshot: window capture failed: {e}");
133                    format!("Window capture failed: {}", e)
134                })?
135        }
136        "region" => {
137            return Err("Region capture is handled in-app; use fullscreen + crop_image".into());
138        }
139        other => {
140            return Err(format!("Unknown capture mode '{}'", other));
141        }
142    };
143    tracing::info!(
144        "take_screenshot: image captured ({}x{})",
145        image.width(),
146        image.height()
147    );
148
149    let image = Arc::new(image);
150    let width = image.width();
151    let height = image.height();
152
153    // Generate UUID and store image (Arc clone, no pixel copy)
154    let id = Uuid::new_v4();
155    store.insert(id, Arc::clone(&image));
156
157    // Convert to base64 PNG data URL
158    let mut png_data = Vec::new();
159    image
160        .write_to(&mut Cursor::new(&mut png_data), image::ImageFormat::Png)
161        .map_err(|e| format!("PNG encoding failed: {}", e))?;
162
163    let base64_data = BASE64_STANDARD.encode(&png_data);
164    let data_url = format!("data:image/png;base64,{}", base64_data);
165
166    // Emit screenshot-ready event
167    let event = ScreenshotReadyEvent {
168        id: id.to_string(),
169        width,
170        height,
171    };
172    app.emit("screenshot-ready", event)
173        .map_err(|e| format!("Failed to emit event: {}", e))?;
174
175    Ok(ScreenshotResponse {
176        id: id.to_string(),
177        width,
178        height,
179        data_url,
180    })
181}
182
183#[tauri::command]
184pub fn crop_image(
185    image_id: String,
186    x: u32,
187    y: u32,
188    width: u32,
189    height: u32,
190    store: tauri::State<'_, ImageStore>,
191) -> Result<ScreenshotResponse, String> {
192    let id = Uuid::parse_str(&image_id).map_err(|_| format!("Invalid image ID: {image_id}"))?;
193    let base = store
194        .get(&id)
195        .ok_or_else(|| format!("No image found for ID: {image_id}"))?;
196
197    // Clamp to image bounds to avoid panic
198    let img_w = base.width();
199    let img_h = base.height();
200    let x = x.min(img_w.saturating_sub(1));
201    let y = y.min(img_h.saturating_sub(1));
202    let width = width.min(img_w - x);
203    let height = height.min(img_h - y);
204
205    let cropped = Arc::new(base.crop_imm(x, y, width, height));
206    let new_id = Uuid::new_v4();
207    store.insert(new_id, Arc::clone(&cropped));
208
209    let mut png_data = Vec::new();
210    cropped
211        .write_to(&mut Cursor::new(&mut png_data), image::ImageFormat::Png)
212        .map_err(|e| format!("PNG encoding failed: {e}"))?;
213    let data_url = format!(
214        "data:image/png;base64,{}",
215        BASE64_STANDARD.encode(&png_data)
216    );
217
218    Ok(ScreenshotResponse {
219        id: new_id.to_string(),
220        width,
221        height,
222        data_url,
223    })
224}
225
226#[tauri::command]
227pub async fn list_monitors() -> Result<Vec<MonitorInfo>, String> {
228    tokio::task::spawn_blocking(|| {
229        let monitors = Monitor::all().map_err(|e| format!("Failed to enumerate monitors: {e}"))?;
230        monitors
231            .into_iter()
232            .map(|m| {
233                Ok(MonitorInfo {
234                    id: m.id().map_err(|e| format!("monitor.id: {e}"))?,
235                    name: m.name().map_err(|e| format!("monitor.name: {e}"))?,
236                    x: m.x().map_err(|e| format!("monitor.x: {e}"))?,
237                    y: m.y().map_err(|e| format!("monitor.y: {e}"))?,
238                    width: m.width().map_err(|e| format!("monitor.width: {e}"))?,
239                    height: m.height().map_err(|e| format!("monitor.height: {e}"))?,
240                    is_primary: m
241                        .is_primary()
242                        .map_err(|e| format!("monitor.is_primary: {e}"))?,
243                })
244            })
245            .collect()
246    })
247    .await
248    .map_err(|e| format!("Task join error: {e}"))?
249}
250
251#[tauri::command]
252pub async fn list_windows() -> Result<Vec<WindowInfo>, String> {
253    tokio::task::spawn_blocking(|| {
254        let windows = Window::all().map_err(|e| format!("Failed to enumerate windows: {e}"))?;
255        windows
256            .into_iter()
257            .map(|w| {
258                Ok(WindowInfo {
259                    id: w.id().map_err(|e| format!("window.id: {e}"))?,
260                    title: w.title().map_err(|e| format!("window.title: {e}"))?,
261                    app_name: w.app_name().map_err(|e| format!("window.app_name: {e}"))?,
262                    x: w.x().map_err(|e| format!("window.x: {e}"))?,
263                    y: w.y().map_err(|e| format!("window.y: {e}"))?,
264                    width: w.width().map_err(|e| format!("window.width: {e}"))?,
265                    height: w.height().map_err(|e| format!("window.height: {e}"))?,
266                })
267            })
268            .collect()
269    })
270    .await
271    .map_err(|e| format!("Task join error: {e}"))?
272}