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 tracing::info!("take_screenshot: hiding window");
62 let _ = window.hide();
63 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 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 let id = Uuid::new_v4();
155 store.insert(id, Arc::clone(&image));
156
157 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 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 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}