gardesk/garshot / 03e020a

Browse files

add clipboard support and compositor bypass for selection overlay

- Screenshots auto-copy to clipboard by default (using xclip)
- Add --no-clipboard flag to skip clipboard copy
- Set _NET_WM_BYPASS_COMPOSITOR on overlay window to prevent
picom from applying blur/effects to selection UI
- Set WM_CLASS to "garshot" for window identification
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
03e020a08537a857f3d387a012243082aa3e454e
Parents
f3e5dcf
Tree
6c0c0c5

3 changed files

StatusFile+-
M Cargo.lock 61 1
M garshot/src/main.rs 78 0
M garshot/src/selection/overlay.rs 26 3
Cargo.lockmodified
@@ -88,6 +88,12 @@ version = "1.5.0"
8888
 source = "registry+https://github.com/rust-lang/crates.io-index"
8989
 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
9090
 
91
+[[package]]
92
+name = "base64"
93
+version = "0.22.1"
94
+source = "registry+https://github.com/rust-lang/crates.io-index"
95
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
96
+
9197
 [[package]]
9298
 name = "bitflags"
9399
 version = "1.3.2"
@@ -306,6 +312,12 @@ dependencies = [
306312
  "windows-sys 0.61.2",
307313
 ]
308314
 
315
+[[package]]
316
+name = "fastrand"
317
+version = "2.3.0"
318
+source = "registry+https://github.com/rust-lang/crates.io-index"
319
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
320
+
309321
 [[package]]
310322
 name = "fdeflate"
311323
 version = "0.3.7"
@@ -393,6 +405,7 @@ name = "garshot"
393405
 version = "0.1.0"
394406
 dependencies = [
395407
  "anyhow",
408
+ "base64",
396409
  "cairo-rs",
397410
  "chrono",
398411
  "clap",
@@ -404,6 +417,7 @@ dependencies = [
404417
  "png",
405418
  "serde",
406419
  "serde_json",
420
+ "tempfile",
407421
  "thiserror",
408422
  "tokio",
409423
  "toml 0.8.23",
@@ -455,6 +469,18 @@ dependencies = [
455469
  "wasi",
456470
 ]
457471
 
472
+[[package]]
473
+name = "getrandom"
474
+version = "0.3.4"
475
+source = "registry+https://github.com/rust-lang/crates.io-index"
476
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
477
+dependencies = [
478
+ "cfg-if",
479
+ "libc",
480
+ "r-efi",
481
+ "wasip2",
482
+]
483
+
458484
 [[package]]
459485
 name = "gio-sys"
460486
 version = "0.20.10"
@@ -884,6 +910,12 @@ dependencies = [
884910
  "proc-macro2",
885911
 ]
886912
 
913
+[[package]]
914
+name = "r-efi"
915
+version = "5.3.0"
916
+source = "registry+https://github.com/rust-lang/crates.io-index"
917
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
918
+
887919
 [[package]]
888920
 name = "redox_syscall"
889921
 version = "0.5.18"
@@ -899,7 +931,7 @@ version = "0.5.2"
899931
 source = "registry+https://github.com/rust-lang/crates.io-index"
900932
 checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
901933
 dependencies = [
902
- "getrandom",
934
+ "getrandom 0.2.17",
903935
  "libredox",
904936
  "thiserror",
905937
 ]
@@ -1102,6 +1134,19 @@ version = "0.13.3"
11021134
 source = "registry+https://github.com/rust-lang/crates.io-index"
11031135
 checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
11041136
 
1137
+[[package]]
1138
+name = "tempfile"
1139
+version = "3.24.0"
1140
+source = "registry+https://github.com/rust-lang/crates.io-index"
1141
+checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
1142
+dependencies = [
1143
+ "fastrand",
1144
+ "getrandom 0.3.4",
1145
+ "once_cell",
1146
+ "rustix",
1147
+ "windows-sys 0.61.2",
1148
+]
1149
+
11051150
 [[package]]
11061151
 name = "thiserror"
11071152
 version = "2.0.17"
@@ -1342,6 +1387,15 @@ version = "0.11.1+wasi-snapshot-preview1"
13421387
 source = "registry+https://github.com/rust-lang/crates.io-index"
13431388
 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
13441389
 
1390
+[[package]]
1391
+name = "wasip2"
1392
+version = "1.0.2+wasi-0.2.9"
1393
+source = "registry+https://github.com/rust-lang/crates.io-index"
1394
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
1395
+dependencies = [
1396
+ "wit-bindgen",
1397
+]
1398
+
13451399
 [[package]]
13461400
 name = "wasm-bindgen"
13471401
 version = "0.2.108"
@@ -1629,6 +1683,12 @@ version = "0.0.19"
16291683
 source = "registry+https://github.com/rust-lang/crates.io-index"
16301684
 checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
16311685
 
1686
+[[package]]
1687
+name = "wit-bindgen"
1688
+version = "0.51.0"
1689
+source = "registry+https://github.com/rust-lang/crates.io-index"
1690
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
1691
+
16321692
 [[package]]
16331693
 name = "x11rb"
16341694
 version = "0.13.2"
garshot/src/main.rsmodified
@@ -47,6 +47,10 @@ enum Command {
4747
         /// Include cursor in screenshot.
4848
         #[arg(short, long)]
4949
         cursor: bool,
50
+
51
+        /// Don't copy to clipboard (default: copy to clipboard).
52
+        #[arg(long)]
53
+        no_clipboard: bool,
5054
     },
5155
 
5256
     /// Capture a region by geometry.
@@ -66,6 +70,10 @@ enum Command {
6670
         /// Include cursor in screenshot.
6771
         #[arg(short, long)]
6872
         cursor: bool,
73
+
74
+        /// Don't copy to clipboard (default: copy to clipboard).
75
+        #[arg(long)]
76
+        no_clipboard: bool,
6977
     },
7078
 
7179
     /// Capture a window.
@@ -89,6 +97,10 @@ enum Command {
8997
         /// Include cursor in screenshot.
9098
         #[arg(short, long)]
9199
         cursor: bool,
100
+
101
+        /// Don't copy to clipboard (default: copy to clipboard).
102
+        #[arg(long)]
103
+        no_clipboard: bool,
92104
     },
93105
 
94106
     /// Interactive region selection with blur overlay.
@@ -108,6 +120,10 @@ enum Command {
108120
         /// Blur radius for overlay (default: 15).
109121
         #[arg(short, long, default_value = "15")]
110122
         blur: usize,
123
+
124
+        /// Don't copy to clipboard (default: copy to clipboard).
125
+        #[arg(long)]
126
+        no_clipboard: bool,
111127
     },
112128
 
113129
     /// List available monitors.
@@ -134,6 +150,7 @@ fn main() -> anyhow::Result<()> {
134150
             let format = &config.general.format;
135151
             let output = default_output(&config, format);
136152
             capture_screen(&output, format, None, config.general.include_cursor)?;
153
+            copy_to_clipboard(&output)?;
137154
             println!("{}", output.display());
138155
         }
139156
 
@@ -142,9 +159,13 @@ fn main() -> anyhow::Result<()> {
142159
             format,
143160
             monitor,
144161
             cursor,
162
+            no_clipboard,
145163
         }) => {
146164
             let output = output.unwrap_or_else(|| default_output(&config, &format));
147165
             capture_screen(&output, &format, monitor.as_deref(), cursor)?;
166
+            if !no_clipboard {
167
+                copy_to_clipboard(&output)?;
168
+            }
148169
             println!("{}", output.display());
149170
         }
150171
 
@@ -153,9 +174,13 @@ fn main() -> anyhow::Result<()> {
153174
             output,
154175
             format,
155176
             cursor,
177
+            no_clipboard,
156178
         }) => {
157179
             let output = output.unwrap_or_else(|| default_output(&config, &format));
158180
             capture_region_cmd(&output, &format, &geometry, cursor)?;
181
+            if !no_clipboard {
182
+                copy_to_clipboard(&output)?;
183
+            }
159184
             println!("{}", output.display());
160185
         }
161186
 
@@ -165,9 +190,13 @@ fn main() -> anyhow::Result<()> {
165190
             output,
166191
             format,
167192
             cursor,
193
+            no_clipboard,
168194
         }) => {
169195
             let output = output.unwrap_or_else(|| default_output(&config, &format));
170196
             capture_window_cmd(&output, &format, id.as_deref(), decorations, cursor)?;
197
+            if !no_clipboard {
198
+                copy_to_clipboard(&output)?;
199
+            }
171200
             println!("{}", output.display());
172201
         }
173202
 
@@ -176,9 +205,13 @@ fn main() -> anyhow::Result<()> {
176205
             format,
177206
             cursor,
178207
             blur,
208
+            no_clipboard,
179209
         }) => {
180210
             let output = output.unwrap_or_else(|| default_output(&config, &format));
181211
             capture_select_cmd(&output, &format, cursor, blur)?;
212
+            if !no_clipboard {
213
+                copy_to_clipboard(&output)?;
214
+            }
182215
             println!("{}", output.display());
183216
         }
184217
 
@@ -409,6 +442,51 @@ fn capture_select_cmd(
409442
     save_image(&result.data, result.width, result.height, output, format)
410443
 }
411444
 
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
+}
489
+
412490
 fn run_daemon() -> anyhow::Result<()> {
413491
     use std::sync::Arc;
414492
     use tokio::sync::Mutex;
garshot/src/selection/overlay.rsmodified
@@ -5,10 +5,11 @@
55
 
66
 use x11rb::connection::Connection as X11Connection;
77
 use x11rb::protocol::xproto::{
8
-    ChangeGCAux, ConnectionExt, CreateGCAux, CreateWindowAux, Cursor,
9
-    EventMask, Font, Gcontext, GrabMode, GrabStatus, ImageFormat, Pixmap, Rectangle,
8
+    AtomEnum, ChangeGCAux, ConnectionExt, CreateGCAux, CreateWindowAux, Cursor,
9
+    EventMask, Font, Gcontext, GrabMode, GrabStatus, ImageFormat, Pixmap, PropMode, Rectangle,
1010
     Window, WindowClass,
1111
 };
12
+use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
1213
 
1314
 use super::blur::blur_rgba;
1415
 use super::events::{SelectionHandler, SelectionResult};
@@ -200,6 +201,28 @@ impl Overlay {
200201
                 .cursor(cursor),
201202
         )?;
202203
 
204
+        // Tell compositor to bypass this window (no blur/effects from picom)
205
+        let bypass_atom = conn.conn.intern_atom(false, b"_NET_WM_BYPASS_COMPOSITOR")?.reply()?.atom;
206
+        WrapperConnectionExt::change_property32(
207
+            &conn.conn,
208
+            PropMode::REPLACE,
209
+            window,
210
+            bypass_atom,
211
+            AtomEnum::CARDINAL,
212
+            &[1], // 1 = bypass compositor
213
+        )?;
214
+
215
+        // Set WM_CLASS for identification
216
+        let wm_class = b"garshot\0garshot\0";
217
+        WrapperConnectionExt::change_property8(
218
+            &conn.conn,
219
+            PropMode::REPLACE,
220
+            window,
221
+            AtomEnum::WM_CLASS,
222
+            AtomEnum::STRING,
223
+            wm_class,
224
+        )?;
225
+
203226
         conn.conn.flush()?;
204227
 
205228
         Ok(Self {
@@ -285,7 +308,7 @@ impl Overlay {
285308
 
286309
         if let Some(region) = region {
287310
             if region.width > 0 && region.height > 0 {
288
-                // Copy selection region from original pixmap (single X11 request!)
311
+                // Copy selection region from original pixmap
289312
                 conn.conn.copy_area(
290313
                     self.original_pixmap,
291314
                     self.window,