gardesk/garshot / c985aa3

Browse files

add delay timer, desktop notifications, and stdout output

- --delay <secs> flag for countdown before capture
- --notify flag for desktop notification on save
- -o - for stdout output mode (piping to other tools)
- Updated README with new options and libnotify dependency
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c985aa337aeee938e6f4c13ae7e350f8163a36dc
Parents
754344f
Tree
cd1ae40

2 changed files

StatusFile+-
M README.md 10 4
M garshot/src/main.rs 278 113
README.mdmodified
@@ -22,20 +22,21 @@ Build:
22
 Runtime:
22
 Runtime:
23
 - X11
23
 - X11
24
 - xclip (for clipboard)
24
 - xclip (for clipboard)
25
+- libnotify (for --notify, optional)
25
 
26
 
26
 ### Fedora/RHEL
27
 ### Fedora/RHEL
27
 ```sh
28
 ```sh
28
-sudo dnf install libxcb-devel libX11-devel cairo-devel xclip
29
+sudo dnf install libxcb-devel libX11-devel cairo-devel xclip libnotify
29
 ```
30
 ```
30
 
31
 
31
 ### Debian/Ubuntu
32
 ### Debian/Ubuntu
32
 ```sh
33
 ```sh
33
-sudo apt install libxcb1-dev libx11-dev libcairo2-dev xclip
34
+sudo apt install libxcb1-dev libx11-dev libcairo2-dev xclip libnotify-bin
34
 ```
35
 ```
35
 
36
 
36
 ### Arch
37
 ### Arch
37
 ```sh
38
 ```sh
38
-sudo pacman -S libxcb libx11 cairo xclip
39
+sudo pacman -S libxcb libx11 cairo xclip libnotify
39
 ```
40
 ```
40
 
41
 
41
 ## Build
42
 ## Build
@@ -65,6 +66,9 @@ garshot screen # All monitors
65
 garshot screen -m DP-1             # Specific monitor
66
 garshot screen -m DP-1             # Specific monitor
66
 garshot screen -c                  # Include cursor
67
 garshot screen -c                  # Include cursor
67
 garshot screen -f jpeg -o shot.jpg # JPEG output
68
 garshot screen -f jpeg -o shot.jpg # JPEG output
69
+garshot screen --delay 3           # 3 second countdown
70
+garshot screen --notify            # Desktop notification on save
71
+garshot screen -o - | feh -        # Pipe to viewer
68
 ```
72
 ```
69
 
73
 
70
 ### Interactive selection
74
 ### Interactive selection
@@ -99,10 +103,12 @@ garshot monitors
99
 
103
 
100
 | Option | Description |
104
 | Option | Description |
101
 |--------|-------------|
105
 |--------|-------------|
102
-| `-o, --output <PATH>` | Output file path |
106
+| `-o, --output <PATH>` | Output file path (use `-` for stdout) |
103
 | `-f, --format <FMT>` | png, jpeg, webp (default: png) |
107
 | `-f, --format <FMT>` | png, jpeg, webp (default: png) |
104
 | `-c, --cursor` | Include cursor in capture |
108
 | `-c, --cursor` | Include cursor in capture |
105
 | `--no-clipboard` | Don't copy to clipboard |
109
 | `--no-clipboard` | Don't copy to clipboard |
110
+| `--delay <SECS>` | Delay before capture (screen/region/window) |
111
+| `--notify` | Show desktop notification on save |
106
 
112
 
107
 ## Configuration
113
 ## Configuration
108
 
114
 
garshot/src/main.rsmodified
@@ -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, &region)?;
554
     let mut result = capture_region(&conn, &shm, &region)?;
@@ -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, &region)?;
638
     let mut result = capture_region(&conn, &shm, &region)?;
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<()> {