@@ -3,7 +3,9 @@ |
| 3 | 3 | //! This is the main entry point for the garshot daemon. It can also be used |
| 4 | 4 | //! for one-shot captures without running the daemon. |
| 5 | 5 | |
| 6 | +use std::io::Write; |
| 6 | 7 | use std::path::PathBuf; |
| 8 | +use std::time::Duration; |
| 7 | 9 | |
| 8 | 10 | use anyhow::Context; |
| 9 | 11 | use clap::{Parser, Subcommand}; |
@@ -32,7 +34,7 @@ enum Command { |
| 32 | 34 | |
| 33 | 35 | /// Capture full screen (one-shot, no daemon required). |
| 34 | 36 | Screen { |
| 35 | | - /// Output file path. |
| 37 | + /// Output file path (use "-" for stdout). |
| 36 | 38 | #[arg(short, long)] |
| 37 | 39 | output: Option<PathBuf>, |
| 38 | 40 | |
@@ -51,6 +53,14 @@ enum Command { |
| 51 | 53 | /// Don't copy to clipboard (default: copy to clipboard). |
| 52 | 54 | #[arg(long)] |
| 53 | 55 | no_clipboard: bool, |
| 56 | + |
| 57 | + /// Delay in seconds before capture. |
| 58 | + #[arg(long)] |
| 59 | + delay: Option<u64>, |
| 60 | + |
| 61 | + /// Show desktop notification on save. |
| 62 | + #[arg(long)] |
| 63 | + notify: bool, |
| 54 | 64 | }, |
| 55 | 65 | |
| 56 | 66 | /// Capture a region by geometry. |
@@ -59,7 +69,7 @@ enum Command { |
| 59 | 69 | #[arg(short, long)] |
| 60 | 70 | geometry: String, |
| 61 | 71 | |
| 62 | | - /// Output file path. |
| 72 | + /// Output file path (use "-" for stdout). |
| 63 | 73 | #[arg(short, long)] |
| 64 | 74 | output: Option<PathBuf>, |
| 65 | 75 | |
@@ -74,6 +84,14 @@ enum Command { |
| 74 | 84 | /// Don't copy to clipboard (default: copy to clipboard). |
| 75 | 85 | #[arg(long)] |
| 76 | 86 | no_clipboard: bool, |
| 87 | + |
| 88 | + /// Delay in seconds before capture. |
| 89 | + #[arg(long)] |
| 90 | + delay: Option<u64>, |
| 91 | + |
| 92 | + /// Show desktop notification on save. |
| 93 | + #[arg(long)] |
| 94 | + notify: bool, |
| 77 | 95 | }, |
| 78 | 96 | |
| 79 | 97 | /// Capture a window. |
@@ -86,7 +104,7 @@ enum Command { |
| 86 | 104 | #[arg(short, long)] |
| 87 | 105 | decorations: bool, |
| 88 | 106 | |
| 89 | | - /// Output file path. |
| 107 | + /// Output file path (use "-" for stdout). |
| 90 | 108 | #[arg(short, long)] |
| 91 | 109 | output: Option<PathBuf>, |
| 92 | 110 | |
@@ -101,11 +119,19 @@ enum Command { |
| 101 | 119 | /// Don't copy to clipboard (default: copy to clipboard). |
| 102 | 120 | #[arg(long)] |
| 103 | 121 | no_clipboard: bool, |
| 122 | + |
| 123 | + /// Delay in seconds before capture. |
| 124 | + #[arg(long)] |
| 125 | + delay: Option<u64>, |
| 126 | + |
| 127 | + /// Show desktop notification on save. |
| 128 | + #[arg(long)] |
| 129 | + notify: bool, |
| 104 | 130 | }, |
| 105 | 131 | |
| 106 | 132 | /// Interactive region selection with blur overlay. |
| 107 | 133 | Select { |
| 108 | | - /// Output file path. |
| 134 | + /// Output file path (use "-" for stdout). |
| 109 | 135 | #[arg(short, long)] |
| 110 | 136 | output: Option<PathBuf>, |
| 111 | 137 | |
@@ -124,6 +150,10 @@ enum Command { |
| 124 | 150 | /// Don't copy to clipboard (default: copy to clipboard). |
| 125 | 151 | #[arg(long)] |
| 126 | 152 | no_clipboard: bool, |
| 153 | + |
| 154 | + /// Show desktop notification on save. |
| 155 | + #[arg(long)] |
| 156 | + notify: bool, |
| 127 | 157 | }, |
| 128 | 158 | |
| 129 | 159 | /// List available monitors. |
@@ -160,13 +190,31 @@ fn main() -> anyhow::Result<()> { |
| 160 | 190 | monitor, |
| 161 | 191 | cursor, |
| 162 | 192 | no_clipboard, |
| 193 | + delay, |
| 194 | + notify, |
| 163 | 195 | }) => { |
| 164 | | - let output = output.unwrap_or_else(|| default_output(&config, &format)); |
| 165 | | - capture_screen(&output, &format, monitor.as_deref(), cursor)?; |
| 166 | | - if !no_clipboard { |
| 167 | | - copy_to_clipboard(&output)?; |
| 196 | + if let Some(secs) = delay { |
| 197 | + sleep_with_countdown(secs); |
| 198 | + } |
| 199 | + let is_stdout = output.as_ref().map(|p| p.as_os_str() == "-").unwrap_or(false); |
| 200 | + let output_path = if is_stdout { |
| 201 | + None |
| 202 | + } else { |
| 203 | + Some(output.unwrap_or_else(|| default_output(&config, &format))) |
| 204 | + }; |
| 205 | + let (data, width, height) = capture_screen_raw(&format, monitor.as_deref(), cursor)?; |
| 206 | + if let Some(ref path) = output_path { |
| 207 | + save_image(&data, width, height, path, &format)?; |
| 208 | + if !no_clipboard { |
| 209 | + copy_to_clipboard(path)?; |
| 210 | + } |
| 211 | + if notify { |
| 212 | + send_notification(path, width, height); |
| 213 | + } |
| 214 | + println!("{}", path.display()); |
| 215 | + } else { |
| 216 | + write_stdout(&data, width, height, &format)?; |
| 168 | 217 | } |
| 169 | | - println!("{}", output.display()); |
| 170 | 218 | } |
| 171 | 219 | |
| 172 | 220 | Some(Command::Region { |
@@ -175,13 +223,31 @@ fn main() -> anyhow::Result<()> { |
| 175 | 223 | format, |
| 176 | 224 | cursor, |
| 177 | 225 | no_clipboard, |
| 226 | + delay, |
| 227 | + notify, |
| 178 | 228 | }) => { |
| 179 | | - let output = output.unwrap_or_else(|| default_output(&config, &format)); |
| 180 | | - capture_region_cmd(&output, &format, &geometry, cursor)?; |
| 181 | | - if !no_clipboard { |
| 182 | | - copy_to_clipboard(&output)?; |
| 229 | + if let Some(secs) = delay { |
| 230 | + sleep_with_countdown(secs); |
| 231 | + } |
| 232 | + let is_stdout = output.as_ref().map(|p| p.as_os_str() == "-").unwrap_or(false); |
| 233 | + let output_path = if is_stdout { |
| 234 | + None |
| 235 | + } else { |
| 236 | + Some(output.unwrap_or_else(|| default_output(&config, &format))) |
| 237 | + }; |
| 238 | + let (data, width, height) = capture_region_raw(&geometry, cursor)?; |
| 239 | + if let Some(ref path) = output_path { |
| 240 | + save_image(&data, width, height, path, &format)?; |
| 241 | + if !no_clipboard { |
| 242 | + copy_to_clipboard(path)?; |
| 243 | + } |
| 244 | + if notify { |
| 245 | + send_notification(path, width, height); |
| 246 | + } |
| 247 | + println!("{}", path.display()); |
| 248 | + } else { |
| 249 | + write_stdout(&data, width, height, &format)?; |
| 183 | 250 | } |
| 184 | | - println!("{}", output.display()); |
| 185 | 251 | } |
| 186 | 252 | |
| 187 | 253 | Some(Command::Window { |
@@ -191,13 +257,31 @@ fn main() -> anyhow::Result<()> { |
| 191 | 257 | format, |
| 192 | 258 | cursor, |
| 193 | 259 | no_clipboard, |
| 260 | + delay, |
| 261 | + notify, |
| 194 | 262 | }) => { |
| 195 | | - let output = output.unwrap_or_else(|| default_output(&config, &format)); |
| 196 | | - capture_window_cmd(&output, &format, id.as_deref(), decorations, cursor)?; |
| 197 | | - if !no_clipboard { |
| 198 | | - copy_to_clipboard(&output)?; |
| 263 | + if let Some(secs) = delay { |
| 264 | + sleep_with_countdown(secs); |
| 265 | + } |
| 266 | + let is_stdout = output.as_ref().map(|p| p.as_os_str() == "-").unwrap_or(false); |
| 267 | + let output_path = if is_stdout { |
| 268 | + None |
| 269 | + } else { |
| 270 | + Some(output.unwrap_or_else(|| default_output(&config, &format))) |
| 271 | + }; |
| 272 | + let (data, width, height) = capture_window_raw(id.as_deref(), decorations, cursor)?; |
| 273 | + if let Some(ref path) = output_path { |
| 274 | + save_image(&data, width, height, path, &format)?; |
| 275 | + if !no_clipboard { |
| 276 | + copy_to_clipboard(path)?; |
| 277 | + } |
| 278 | + if notify { |
| 279 | + send_notification(path, width, height); |
| 280 | + } |
| 281 | + println!("{}", path.display()); |
| 282 | + } else { |
| 283 | + write_stdout(&data, width, height, &format)?; |
| 199 | 284 | } |
| 200 | | - println!("{}", output.display()); |
| 201 | 285 | } |
| 202 | 286 | |
| 203 | 287 | Some(Command::Select { |
@@ -206,13 +290,28 @@ fn main() -> anyhow::Result<()> { |
| 206 | 290 | cursor, |
| 207 | 291 | blur, |
| 208 | 292 | no_clipboard, |
| 293 | + notify, |
| 209 | 294 | }) => { |
| 210 | | - let output = output.unwrap_or_else(|| default_output(&config, &format)); |
| 211 | | - capture_select_cmd(&output, &format, cursor, blur)?; |
| 212 | | - if !no_clipboard { |
| 213 | | - copy_to_clipboard(&output)?; |
| 295 | + let is_stdout = output.as_ref().map(|p| p.as_os_str() == "-").unwrap_or(false); |
| 296 | + let output_path = if is_stdout { |
| 297 | + None |
| 298 | + } else { |
| 299 | + Some(output.unwrap_or_else(|| default_output(&config, &format))) |
| 300 | + }; |
| 301 | + if let Some((data, width, height)) = capture_select_raw(cursor, blur)? { |
| 302 | + if let Some(ref path) = output_path { |
| 303 | + save_image(&data, width, height, path, &format)?; |
| 304 | + if !no_clipboard { |
| 305 | + copy_to_clipboard(path)?; |
| 306 | + } |
| 307 | + if notify { |
| 308 | + send_notification(path, width, height); |
| 309 | + } |
| 310 | + println!("{}", path.display()); |
| 311 | + } else { |
| 312 | + write_stdout(&data, width, height, &format)?; |
| 313 | + } |
| 214 | 314 | } |
| 215 | | - println!("{}", output.display()); |
| 216 | 315 | } |
| 217 | 316 | |
| 218 | 317 | Some(Command::Monitors) => { |
@@ -292,24 +391,164 @@ fn capture_screen( |
| 292 | 391 | save_image(&result.data, result.width, result.height, output, format) |
| 293 | 392 | } |
| 294 | 393 | |
| 295 | | -fn capture_region_cmd( |
| 394 | + |
| 395 | +fn save_image( |
| 396 | + data: &[u8], |
| 397 | + width: u32, |
| 398 | + height: u32, |
| 296 | 399 | output: &PathBuf, |
| 297 | 400 | format: &str, |
| 401 | +) -> anyhow::Result<()> { |
| 402 | + match format { |
| 403 | + "png" => encode_png(data, width, height, output).context("Failed to encode PNG")?, |
| 404 | + _ => anyhow::bail!("Unsupported format: {}", format), |
| 405 | + } |
| 406 | + tracing::info!("Saved {}x{} to {}", width, height, output.display()); |
| 407 | + Ok(()) |
| 408 | +} |
| 409 | + |
| 410 | +fn parse_window_id(s: &str) -> anyhow::Result<u32> { |
| 411 | + let s = s.trim(); |
| 412 | + if s.starts_with("0x") || s.starts_with("0X") { |
| 413 | + u32::from_str_radix(&s[2..], 16).context("Invalid hex window ID") |
| 414 | + } else { |
| 415 | + s.parse().context("Invalid decimal window ID") |
| 416 | + } |
| 417 | +} |
| 418 | + |
| 419 | +fn copy_to_clipboard(path: &PathBuf) -> anyhow::Result<()> { |
| 420 | + let mime_type = match path.extension().and_then(|e| e.to_str()) { |
| 421 | + Some("png") => "image/png", |
| 422 | + Some("jpg") | Some("jpeg") => "image/jpeg", |
| 423 | + Some("webp") => "image/webp", |
| 424 | + Some("ppm") | Some("pam") => "image/x-portable-pixmap", |
| 425 | + _ => "image/png", // Default to PNG |
| 426 | + }; |
| 427 | + |
| 428 | + // Try xclip first, then xsel |
| 429 | + let result = std::process::Command::new("xclip") |
| 430 | + .args(["-selection", "clipboard", "-t", mime_type, "-i"]) |
| 431 | + .arg(path) |
| 432 | + .status(); |
| 433 | + |
| 434 | + match result { |
| 435 | + Ok(status) if status.success() => { |
| 436 | + tracing::debug!("Copied {} to clipboard via xclip", path.display()); |
| 437 | + Ok(()) |
| 438 | + } |
| 439 | + _ => { |
| 440 | + // Fallback to xsel (doesn't support MIME types as well) |
| 441 | + let result = std::process::Command::new("xsel") |
| 442 | + .args(["--clipboard", "--input"]) |
| 443 | + .stdin(std::fs::File::open(path)?) |
| 444 | + .status(); |
| 445 | + |
| 446 | + match result { |
| 447 | + Ok(status) if status.success() => { |
| 448 | + tracing::debug!("Copied {} to clipboard via xsel", path.display()); |
| 449 | + Ok(()) |
| 450 | + } |
| 451 | + Ok(status) => { |
| 452 | + tracing::warn!("xsel exited with status: {}", status); |
| 453 | + anyhow::bail!("Failed to copy to clipboard") |
| 454 | + } |
| 455 | + Err(e) => { |
| 456 | + tracing::warn!("Neither xclip nor xsel available: {}", e); |
| 457 | + anyhow::bail!("No clipboard tool available (install xclip or xsel)") |
| 458 | + } |
| 459 | + } |
| 460 | + } |
| 461 | + } |
| 462 | +} |
| 463 | + |
| 464 | +fn sleep_with_countdown(secs: u64) { |
| 465 | + for i in (1..=secs).rev() { |
| 466 | + eprint!("\rCapturing in {}... ", i); |
| 467 | + std::io::stderr().flush().ok(); |
| 468 | + std::thread::sleep(Duration::from_secs(1)); |
| 469 | + } |
| 470 | + eprintln!("\rCapturing now! "); |
| 471 | +} |
| 472 | + |
| 473 | +fn send_notification(path: &PathBuf, width: u32, height: u32) { |
| 474 | + let summary = "Screenshot saved"; |
| 475 | + let body = format!("{}x{} → {}", width, height, path.display()); |
| 476 | + |
| 477 | + // Try notify-send (most common) |
| 478 | + let result = std::process::Command::new("notify-send") |
| 479 | + .args(["-i", "camera-photo", "-a", "garshot", summary, &body]) |
| 480 | + .status(); |
| 481 | + |
| 482 | + if result.is_err() || !result.unwrap().success() { |
| 483 | + tracing::debug!("notify-send not available or failed"); |
| 484 | + } |
| 485 | +} |
| 486 | + |
| 487 | +fn write_stdout(data: &[u8], width: u32, height: u32, format: &str) -> anyhow::Result<()> { |
| 488 | + let encoded = encode_to_vec(data, width, height, format)?; |
| 489 | + std::io::stdout().write_all(&encoded)?; |
| 490 | + std::io::stdout().flush()?; |
| 491 | + Ok(()) |
| 492 | +} |
| 493 | + |
| 494 | +fn encode_to_vec(data: &[u8], width: u32, height: u32, format: &str) -> anyhow::Result<Vec<u8>> { |
| 495 | + garshot::encode::encode_to_vec(data, width, height, format, 90) |
| 496 | + .context("Failed to encode image") |
| 497 | +} |
| 498 | + |
| 499 | +fn capture_screen_raw( |
| 500 | + _format: &str, |
| 501 | + monitor: Option<&str>, |
| 502 | + include_cursor: bool, |
| 503 | +) -> anyhow::Result<(Vec<u8>, u32, u32)> { |
| 504 | + let conn = Connection::new().context("Failed to connect to X11")?; |
| 505 | + let buffer_size = conn.width as usize * conn.height as usize * 4; |
| 506 | + let shm = ShmCapture::new(&conn, buffer_size).context("Failed to create SHM buffer")?; |
| 507 | + |
| 508 | + let mut result = if let Some(monitor_name) = monitor { |
| 509 | + tracing::info!("Capturing monitor {}", monitor_name); |
| 510 | + capture_monitor(&conn, &shm, monitor_name)? |
| 511 | + } else { |
| 512 | + tracing::info!("Capturing full screen"); |
| 513 | + let r = capture_full_screen(&conn, &shm)?; |
| 514 | + garshot::capture::RegionCaptureResult { |
| 515 | + data: r.data, |
| 516 | + width: r.width, |
| 517 | + height: r.height, |
| 518 | + region: Region::new(0, 0, conn.width, conn.height), |
| 519 | + } |
| 520 | + }; |
| 521 | + |
| 522 | + if include_cursor { |
| 523 | + if let Ok(cursor) = get_cursor_image(&conn) { |
| 524 | + blend_cursor( |
| 525 | + &mut result.data, |
| 526 | + result.width, |
| 527 | + result.height, |
| 528 | + &result.region, |
| 529 | + &cursor, |
| 530 | + ); |
| 531 | + } |
| 532 | + } |
| 533 | + |
| 534 | + Ok((result.data, result.width, result.height)) |
| 535 | +} |
| 536 | + |
| 537 | +fn capture_region_raw( |
| 298 | 538 | geometry: &str, |
| 299 | 539 | include_cursor: bool, |
| 300 | | -) -> anyhow::Result<()> { |
| 540 | +) -> anyhow::Result<(Vec<u8>, u32, u32)> { |
| 301 | 541 | let conn = Connection::new().context("Failed to connect to X11")?; |
| 302 | 542 | let buffer_size = conn.width as usize * conn.height as usize * 4; |
| 303 | 543 | let shm = ShmCapture::new(&conn, buffer_size).context("Failed to create SHM buffer")?; |
| 304 | 544 | |
| 305 | 545 | let region = Region::from_geometry(geometry).context("Invalid geometry")?; |
| 306 | 546 | tracing::info!( |
| 307 | | - "Capturing region {}x{}+{}+{} to {}", |
| 547 | + "Capturing region {}x{}+{}+{}", |
| 308 | 548 | region.width, |
| 309 | 549 | region.height, |
| 310 | 550 | region.x, |
| 311 | | - region.y, |
| 312 | | - output.display() |
| 551 | + region.y |
| 313 | 552 | ); |
| 314 | 553 | |
| 315 | 554 | let mut result = capture_region(&conn, &shm, ®ion)?; |
@@ -326,16 +565,14 @@ fn capture_region_cmd( |
| 326 | 565 | } |
| 327 | 566 | } |
| 328 | 567 | |
| 329 | | - save_image(&result.data, result.width, result.height, output, format) |
| 568 | + Ok((result.data, result.width, result.height)) |
| 330 | 569 | } |
| 331 | 570 | |
| 332 | | -fn capture_window_cmd( |
| 333 | | - output: &PathBuf, |
| 334 | | - format: &str, |
| 571 | +fn capture_window_raw( |
| 335 | 572 | window_id: Option<&str>, |
| 336 | 573 | decorations: bool, |
| 337 | 574 | include_cursor: bool, |
| 338 | | -) -> anyhow::Result<()> { |
| 575 | +) -> anyhow::Result<(Vec<u8>, u32, u32)> { |
| 339 | 576 | use garshot::capture::{capture_active_window, capture_window, get_active_window}; |
| 340 | 577 | |
| 341 | 578 | let conn = Connection::new().context("Failed to connect to X11")?; |
@@ -344,11 +581,11 @@ fn capture_window_cmd( |
| 344 | 581 | |
| 345 | 582 | let mut result = if let Some(id_str) = window_id { |
| 346 | 583 | let id = parse_window_id(id_str)?; |
| 347 | | - tracing::info!("Capturing window 0x{:x} to {}", id, output.display()); |
| 584 | + tracing::info!("Capturing window 0x{:x}", id); |
| 348 | 585 | capture_window(&conn, &shm, id, decorations)? |
| 349 | 586 | } else { |
| 350 | 587 | let active = get_active_window(&conn)?; |
| 351 | | - tracing::info!("Capturing active window 0x{:x} to {}", active, output.display()); |
| 588 | + tracing::info!("Capturing active window 0x{:x}", active); |
| 352 | 589 | capture_active_window(&conn, &shm, decorations)? |
| 353 | 590 | }; |
| 354 | 591 | |
@@ -364,39 +601,13 @@ fn capture_window_cmd( |
| 364 | 601 | } |
| 365 | 602 | } |
| 366 | 603 | |
| 367 | | - save_image(&result.data, result.width, result.height, output, format) |
| 368 | | -} |
| 369 | | - |
| 370 | | -fn save_image( |
| 371 | | - data: &[u8], |
| 372 | | - width: u32, |
| 373 | | - height: u32, |
| 374 | | - output: &PathBuf, |
| 375 | | - format: &str, |
| 376 | | -) -> anyhow::Result<()> { |
| 377 | | - match format { |
| 378 | | - "png" => encode_png(data, width, height, output).context("Failed to encode PNG")?, |
| 379 | | - _ => anyhow::bail!("Unsupported format: {}", format), |
| 380 | | - } |
| 381 | | - tracing::info!("Saved {}x{} to {}", width, height, output.display()); |
| 382 | | - Ok(()) |
| 604 | + Ok((result.data, result.width, result.height)) |
| 383 | 605 | } |
| 384 | 606 | |
| 385 | | -fn parse_window_id(s: &str) -> anyhow::Result<u32> { |
| 386 | | - let s = s.trim(); |
| 387 | | - if s.starts_with("0x") || s.starts_with("0X") { |
| 388 | | - u32::from_str_radix(&s[2..], 16).context("Invalid hex window ID") |
| 389 | | - } else { |
| 390 | | - s.parse().context("Invalid decimal window ID") |
| 391 | | - } |
| 392 | | -} |
| 393 | | - |
| 394 | | -fn capture_select_cmd( |
| 395 | | - output: &PathBuf, |
| 396 | | - format: &str, |
| 607 | +fn capture_select_raw( |
| 397 | 608 | include_cursor: bool, |
| 398 | 609 | blur_radius: usize, |
| 399 | | -) -> anyhow::Result<()> { |
| 610 | +) -> anyhow::Result<Option<(Vec<u8>, u32, u32)>> { |
| 400 | 611 | let conn = Connection::new().context("Failed to connect to X11")?; |
| 401 | 612 | let buffer_size = conn.width as usize * conn.height as usize * 4; |
| 402 | 613 | let shm = ShmCapture::new(&conn, buffer_size).context("Failed to create SHM buffer")?; |
@@ -412,7 +623,7 @@ fn capture_select_cmd( |
| 412 | 623 | Some(region) => region, |
| 413 | 624 | None => { |
| 414 | 625 | tracing::info!("Selection cancelled"); |
| 415 | | - return Ok(()); |
| 626 | + return Ok(None); |
| 416 | 627 | } |
| 417 | 628 | }; |
| 418 | 629 | |
@@ -424,7 +635,6 @@ fn capture_select_cmd( |
| 424 | 635 | region.y |
| 425 | 636 | ); |
| 426 | 637 | |
| 427 | | - // Capture the selected region |
| 428 | 638 | let mut result = capture_region(&conn, &shm, ®ion)?; |
| 429 | 639 | |
| 430 | 640 | if include_cursor { |
@@ -439,52 +649,7 @@ fn capture_select_cmd( |
| 439 | 649 | } |
| 440 | 650 | } |
| 441 | 651 | |
| 442 | | - save_image(&result.data, result.width, result.height, output, format) |
| 443 | | -} |
| 444 | | - |
| 445 | | -fn copy_to_clipboard(path: &PathBuf) -> anyhow::Result<()> { |
| 446 | | - let mime_type = match path.extension().and_then(|e| e.to_str()) { |
| 447 | | - Some("png") => "image/png", |
| 448 | | - Some("jpg") | Some("jpeg") => "image/jpeg", |
| 449 | | - Some("webp") => "image/webp", |
| 450 | | - Some("ppm") | Some("pam") => "image/x-portable-pixmap", |
| 451 | | - _ => "image/png", // Default to PNG |
| 452 | | - }; |
| 453 | | - |
| 454 | | - // Try xclip first, then xsel |
| 455 | | - let result = std::process::Command::new("xclip") |
| 456 | | - .args(["-selection", "clipboard", "-t", mime_type, "-i"]) |
| 457 | | - .arg(path) |
| 458 | | - .status(); |
| 459 | | - |
| 460 | | - match result { |
| 461 | | - Ok(status) if status.success() => { |
| 462 | | - tracing::debug!("Copied {} to clipboard via xclip", path.display()); |
| 463 | | - Ok(()) |
| 464 | | - } |
| 465 | | - _ => { |
| 466 | | - // Fallback to xsel (doesn't support MIME types as well) |
| 467 | | - let result = std::process::Command::new("xsel") |
| 468 | | - .args(["--clipboard", "--input"]) |
| 469 | | - .stdin(std::fs::File::open(path)?) |
| 470 | | - .status(); |
| 471 | | - |
| 472 | | - match result { |
| 473 | | - Ok(status) if status.success() => { |
| 474 | | - tracing::debug!("Copied {} to clipboard via xsel", path.display()); |
| 475 | | - Ok(()) |
| 476 | | - } |
| 477 | | - Ok(status) => { |
| 478 | | - tracing::warn!("xsel exited with status: {}", status); |
| 479 | | - anyhow::bail!("Failed to copy to clipboard") |
| 480 | | - } |
| 481 | | - Err(e) => { |
| 482 | | - tracing::warn!("Neither xclip nor xsel available: {}", e); |
| 483 | | - anyhow::bail!("No clipboard tool available (install xclip or xsel)") |
| 484 | | - } |
| 485 | | - } |
| 486 | | - } |
| 487 | | - } |
| 652 | + Ok(Some((result.data, result.width, result.height))) |
| 488 | 653 | } |
| 489 | 654 | |
| 490 | 655 | fn run_daemon() -> anyhow::Result<()> { |