@@ -3,7 +3,9 @@ |
| 3 | //! This is the main entry point for the garshot daemon. It can also be used | 3 | //! This is the main entry point for the garshot daemon. It can also be used |
| 4 | //! for one-shot captures without running the daemon. | 4 | //! for one-shot captures without running the daemon. |
| 5 | | 5 | |
| | 6 | +use std::io::Write; |
| 6 | use std::path::PathBuf; | 7 | use std::path::PathBuf; |
| | 8 | +use std::time::Duration; |
| 7 | | 9 | |
| 8 | use anyhow::Context; | 10 | use anyhow::Context; |
| 9 | use clap::{Parser, Subcommand}; | 11 | use clap::{Parser, Subcommand}; |
@@ -32,7 +34,7 @@ enum Command { |
| 32 | | 34 | |
| 33 | /// Capture full screen (one-shot, no daemon required). | 35 | /// Capture full screen (one-shot, no daemon required). |
| 34 | Screen { | 36 | Screen { |
| 35 | - /// Output file path. | 37 | + /// Output file path (use "-" for stdout). |
| 36 | #[arg(short, long)] | 38 | #[arg(short, long)] |
| 37 | output: Option<PathBuf>, | 39 | output: Option<PathBuf>, |
| 38 | | 40 | |
@@ -51,6 +53,14 @@ enum Command { |
| 51 | /// Don't copy to clipboard (default: copy to clipboard). | 53 | /// Don't copy to clipboard (default: copy to clipboard). |
| 52 | #[arg(long)] | 54 | #[arg(long)] |
| 53 | no_clipboard: bool, | 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 | /// Capture a region by geometry. | 66 | /// Capture a region by geometry. |
@@ -59,7 +69,7 @@ enum Command { |
| 59 | #[arg(short, long)] | 69 | #[arg(short, long)] |
| 60 | geometry: String, | 70 | geometry: String, |
| 61 | | 71 | |
| 62 | - /// Output file path. | 72 | + /// Output file path (use "-" for stdout). |
| 63 | #[arg(short, long)] | 73 | #[arg(short, long)] |
| 64 | output: Option<PathBuf>, | 74 | output: Option<PathBuf>, |
| 65 | | 75 | |
@@ -74,6 +84,14 @@ enum Command { |
| 74 | /// Don't copy to clipboard (default: copy to clipboard). | 84 | /// Don't copy to clipboard (default: copy to clipboard). |
| 75 | #[arg(long)] | 85 | #[arg(long)] |
| 76 | no_clipboard: bool, | 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 | /// Capture a window. | 97 | /// Capture a window. |
@@ -86,7 +104,7 @@ enum Command { |
| 86 | #[arg(short, long)] | 104 | #[arg(short, long)] |
| 87 | decorations: bool, | 105 | decorations: bool, |
| 88 | | 106 | |
| 89 | - /// Output file path. | 107 | + /// Output file path (use "-" for stdout). |
| 90 | #[arg(short, long)] | 108 | #[arg(short, long)] |
| 91 | output: Option<PathBuf>, | 109 | output: Option<PathBuf>, |
| 92 | | 110 | |
@@ -101,11 +119,19 @@ enum Command { |
| 101 | /// Don't copy to clipboard (default: copy to clipboard). | 119 | /// Don't copy to clipboard (default: copy to clipboard). |
| 102 | #[arg(long)] | 120 | #[arg(long)] |
| 103 | no_clipboard: bool, | 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 | /// Interactive region selection with blur overlay. | 132 | /// Interactive region selection with blur overlay. |
| 107 | Select { | 133 | Select { |
| 108 | - /// Output file path. | 134 | + /// Output file path (use "-" for stdout). |
| 109 | #[arg(short, long)] | 135 | #[arg(short, long)] |
| 110 | output: Option<PathBuf>, | 136 | output: Option<PathBuf>, |
| 111 | | 137 | |
@@ -124,6 +150,10 @@ enum Command { |
| 124 | /// Don't copy to clipboard (default: copy to clipboard). | 150 | /// Don't copy to clipboard (default: copy to clipboard). |
| 125 | #[arg(long)] | 151 | #[arg(long)] |
| 126 | no_clipboard: bool, | 152 | no_clipboard: bool, |
| | 153 | + |
| | 154 | + /// Show desktop notification on save. |
| | 155 | + #[arg(long)] |
| | 156 | + notify: bool, |
| 127 | }, | 157 | }, |
| 128 | | 158 | |
| 129 | /// List available monitors. | 159 | /// List available monitors. |
@@ -160,13 +190,31 @@ fn main() -> anyhow::Result<()> { |
| 160 | monitor, | 190 | monitor, |
| 161 | cursor, | 191 | cursor, |
| 162 | no_clipboard, | 192 | no_clipboard, |
| | 193 | + delay, |
| | 194 | + notify, |
| 163 | }) => { | 195 | }) => { |
| 164 | - let output = output.unwrap_or_else(|| default_output(&config, &format)); | 196 | + if let Some(secs) = delay { |
| 165 | - capture_screen(&output, &format, monitor.as_deref(), cursor)?; | 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)?; |
| 166 | if !no_clipboard { | 208 | if !no_clipboard { |
| 167 | - copy_to_clipboard(&output)?; | 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 | Some(Command::Region { | 220 | Some(Command::Region { |
@@ -175,13 +223,31 @@ fn main() -> anyhow::Result<()> { |
| 175 | format, | 223 | format, |
| 176 | cursor, | 224 | cursor, |
| 177 | no_clipboard, | 225 | no_clipboard, |
| | 226 | + delay, |
| | 227 | + notify, |
| 178 | }) => { | 228 | }) => { |
| 179 | - let output = output.unwrap_or_else(|| default_output(&config, &format)); | 229 | + if let Some(secs) = delay { |
| 180 | - capture_region_cmd(&output, &format, &geometry, cursor)?; | 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)?; |
| 181 | if !no_clipboard { | 241 | if !no_clipboard { |
| 182 | - copy_to_clipboard(&output)?; | 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 | Some(Command::Window { | 253 | Some(Command::Window { |
@@ -191,13 +257,31 @@ fn main() -> anyhow::Result<()> { |
| 191 | format, | 257 | format, |
| 192 | cursor, | 258 | cursor, |
| 193 | no_clipboard, | 259 | no_clipboard, |
| | 260 | + delay, |
| | 261 | + notify, |
| 194 | }) => { | 262 | }) => { |
| 195 | - let output = output.unwrap_or_else(|| default_output(&config, &format)); | 263 | + if let Some(secs) = delay { |
| 196 | - capture_window_cmd(&output, &format, id.as_deref(), decorations, cursor)?; | 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)?; |
| 197 | if !no_clipboard { | 275 | if !no_clipboard { |
| 198 | - copy_to_clipboard(&output)?; | 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 | Some(Command::Select { | 287 | Some(Command::Select { |
@@ -206,13 +290,28 @@ fn main() -> anyhow::Result<()> { |
| 206 | cursor, | 290 | cursor, |
| 207 | blur, | 291 | blur, |
| 208 | no_clipboard, | 292 | no_clipboard, |
| | 293 | + notify, |
| 209 | }) => { | 294 | }) => { |
| 210 | - let output = output.unwrap_or_else(|| default_output(&config, &format)); | 295 | + let is_stdout = output.as_ref().map(|p| p.as_os_str() == "-").unwrap_or(false); |
| 211 | - capture_select_cmd(&output, &format, cursor, blur)?; | 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)?; |
| 212 | if !no_clipboard { | 304 | if !no_clipboard { |
| 213 | - copy_to_clipboard(&output)?; | 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 | Some(Command::Monitors) => { | 317 | Some(Command::Monitors) => { |
@@ -292,24 +391,164 @@ fn capture_screen( |
| 292 | save_image(&result.data, result.width, result.height, output, format) | 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 | output: &PathBuf, | 399 | output: &PathBuf, |
| 297 | format: &str, | 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 | geometry: &str, | 538 | geometry: &str, |
| 299 | include_cursor: bool, | 539 | include_cursor: bool, |
| 300 | -) -> anyhow::Result<()> { | 540 | +) -> anyhow::Result<(Vec<u8>, u32, u32)> { |
| 301 | let conn = Connection::new().context("Failed to connect to X11")?; | 541 | let conn = Connection::new().context("Failed to connect to X11")?; |
| 302 | let buffer_size = conn.width as usize * conn.height as usize * 4; | 542 | let buffer_size = conn.width as usize * conn.height as usize * 4; |
| 303 | let shm = ShmCapture::new(&conn, buffer_size).context("Failed to create SHM buffer")?; | 543 | let shm = ShmCapture::new(&conn, buffer_size).context("Failed to create SHM buffer")?; |
| 304 | | 544 | |
| 305 | let region = Region::from_geometry(geometry).context("Invalid geometry")?; | 545 | let region = Region::from_geometry(geometry).context("Invalid geometry")?; |
| 306 | tracing::info!( | 546 | tracing::info!( |
| 307 | - "Capturing region {}x{}+{}+{} to {}", | 547 | + "Capturing region {}x{}+{}+{}", |
| 308 | region.width, | 548 | region.width, |
| 309 | region.height, | 549 | region.height, |
| 310 | region.x, | 550 | region.x, |
| 311 | - region.y, | 551 | + region.y |
| 312 | - output.display() | | |
| 313 | ); | 552 | ); |
| 314 | | 553 | |
| 315 | let mut result = capture_region(&conn, &shm, ®ion)?; | 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( | 571 | +fn capture_window_raw( |
| 333 | - output: &PathBuf, | | |
| 334 | - format: &str, | | |
| 335 | window_id: Option<&str>, | 572 | window_id: Option<&str>, |
| 336 | decorations: bool, | 573 | decorations: bool, |
| 337 | include_cursor: bool, | 574 | include_cursor: bool, |
| 338 | -) -> anyhow::Result<()> { | 575 | +) -> anyhow::Result<(Vec<u8>, u32, u32)> { |
| 339 | use garshot::capture::{capture_active_window, capture_window, get_active_window}; | 576 | use garshot::capture::{capture_active_window, capture_window, get_active_window}; |
| 340 | | 577 | |
| 341 | let conn = Connection::new().context("Failed to connect to X11")?; | 578 | let conn = Connection::new().context("Failed to connect to X11")?; |
@@ -344,11 +581,11 @@ fn capture_window_cmd( |
| 344 | | 581 | |
| 345 | let mut result = if let Some(id_str) = window_id { | 582 | let mut result = if let Some(id_str) = window_id { |
| 346 | let id = parse_window_id(id_str)?; | 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 | capture_window(&conn, &shm, id, decorations)? | 585 | capture_window(&conn, &shm, id, decorations)? |
| 349 | } else { | 586 | } else { |
| 350 | let active = get_active_window(&conn)?; | 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 | capture_active_window(&conn, &shm, decorations)? | 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) | 604 | + Ok((result.data, result.width, result.height)) |
| 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(()) | | |
| 383 | -} | | |
| 384 | - | | |
| 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 | } | 605 | } |
| 393 | | 606 | |
| 394 | -fn capture_select_cmd( | 607 | +fn capture_select_raw( |
| 395 | - output: &PathBuf, | | |
| 396 | - format: &str, | | |
| 397 | include_cursor: bool, | 608 | include_cursor: bool, |
| 398 | blur_radius: usize, | 609 | blur_radius: usize, |
| 399 | -) -> anyhow::Result<()> { | 610 | +) -> anyhow::Result<Option<(Vec<u8>, u32, u32)>> { |
| 400 | let conn = Connection::new().context("Failed to connect to X11")?; | 611 | let conn = Connection::new().context("Failed to connect to X11")?; |
| 401 | let buffer_size = conn.width as usize * conn.height as usize * 4; | 612 | let buffer_size = conn.width as usize * conn.height as usize * 4; |
| 402 | let shm = ShmCapture::new(&conn, buffer_size).context("Failed to create SHM buffer")?; | 613 | let shm = ShmCapture::new(&conn, buffer_size).context("Failed to create SHM buffer")?; |
@@ -412,7 +623,7 @@ fn capture_select_cmd( |
| 412 | Some(region) => region, | 623 | Some(region) => region, |
| 413 | None => { | 624 | None => { |
| 414 | tracing::info!("Selection cancelled"); | 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 | region.y | 635 | region.y |
| 425 | ); | 636 | ); |
| 426 | | 637 | |
| 427 | - // Capture the selected region | | |
| 428 | let mut result = capture_region(&conn, &shm, ®ion)?; | 638 | let mut result = capture_region(&conn, &shm, ®ion)?; |
| 429 | | 639 | |
| 430 | if include_cursor { | 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) | 652 | + Ok(Some((result.data, result.width, result.height))) |
| 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 | - } | | |
| 488 | } | 653 | } |
| 489 | | 654 | |
| 490 | fn run_daemon() -> anyhow::Result<()> { | 655 | fn run_daemon() -> anyhow::Result<()> { |