gardesk/garclip / 6696d4b

Browse files

debounce PRIMARY selection to prevent partial captures

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6696d4bd2be549be25c7dae1f1dd5c2e9d615836
Parents
d902306
Tree
f7e7f8b

1 changed file

StatusFile+-
M garclip/src/clipboard/manager.rs 66 14
garclip/src/clipboard/manager.rsmodified
@@ -1,4 +1,5 @@
11
 use std::sync::Arc;
2
+use std::time::{Duration, Instant};
23
 
34
 use x11rb::protocol::xfixes::SelectionNotifyEvent as XFixesSelectionNotifyEvent;
45
 use x11rb::protocol::xproto::{Atom, SelectionRequestEvent, Window};
@@ -9,6 +10,13 @@ use crate::config::Config;
910
 use crate::error::Result;
1011
 use crate::x11::{get_window_class, Atoms, SelectionManager, TransferManager};
1112
 
13
+/// Pending PRIMARY selection content awaiting debounce
14
+struct PendingPrimary {
15
+    content: ClipboardContent,
16
+    source: Option<String>,
17
+    timestamp: Instant,
18
+}
19
+
1220
 /// Main clipboard manager that coordinates X11 selection handling and history
1321
 pub struct ClipboardManager {
1422
     selection_mgr: SelectionManager,
@@ -32,6 +40,12 @@ pub struct ClipboardManager {
3240
 
3341
     /// Whether XFixes monitoring is active
3442
     xfixes_active: bool,
43
+
44
+    /// Pending PRIMARY content (for debouncing)
45
+    pending_primary: Option<PendingPrimary>,
46
+
47
+    /// Debounce duration for PRIMARY selection
48
+    primary_debounce: Duration,
3549
 }
3650
 
3751
 impl ClipboardManager {
@@ -56,6 +70,8 @@ impl ClipboardManager {
5670
             last_primary_owner: 0,
5771
             watch_primary: config.behavior.watch_primary,
5872
             xfixes_active: false,
73
+            pending_primary: None,
74
+            primary_debounce: Duration::from_millis(config.behavior.primary_debounce_ms),
5975
         })
6076
     }
6177
 
@@ -63,6 +79,7 @@ impl ClipboardManager {
6379
     pub fn reload_filter(&mut self, config: &Config) {
6480
         self.filter.reload(&config.behavior, &config.filters);
6581
         self.watch_primary = config.behavior.watch_primary;
82
+        self.primary_debounce = Duration::from_millis(config.behavior.primary_debounce_ms);
6683
     }
6784
 
6885
     /// Start watching selections via XFixes (event-driven monitoring)
@@ -153,31 +170,66 @@ impl ClipboardManager {
153170
                 return Ok(None);
154171
             }
155172
 
156
-            tracing::debug!(
157
-                "Captured {} via XFixes from {:?}: {}",
158
-                if is_clipboard { "clipboard" } else { "primary" },
159
-                source,
160
-                content.preview(50)
161
-            );
162
-
163
-            // Store in history with source
164
-            let id = self.history.push(content.clone(), source);
165
-
166
-            // Store as current content
167173
             if is_clipboard {
174
+                // CLIPBOARD: store immediately
175
+                tracing::debug!(
176
+                    "Captured clipboard via XFixes from {:?}: {}",
177
+                    source,
178
+                    content.preview(50)
179
+                );
180
+
181
+                let id = self.history.push(content.clone(), source);
168182
                 self.current_clipboard = Some(content);
169183
                 self.last_clipboard_owner = event.owner;
184
+                return Ok(id);
170185
             } else {
171
-                self.current_primary = Some(content);
186
+                // PRIMARY: debounce to avoid capturing partial selections
187
+                tracing::trace!(
188
+                    "Pending primary via XFixes from {:?}: {}",
189
+                    source,
190
+                    content.preview(50)
191
+                );
192
+
193
+                self.pending_primary = Some(PendingPrimary {
194
+                    content,
195
+                    source,
196
+                    timestamp: Instant::now(),
197
+                });
172198
                 self.last_primary_owner = event.owner;
199
+                return Ok(None);
173200
             }
174
-
175
-            return Ok(id);
176201
         }
177202
 
178203
         Ok(None)
179204
     }
180205
 
206
+    /// Commit pending PRIMARY content if debounce period has elapsed
207
+    /// Returns the entry ID if content was committed
208
+    pub fn commit_pending_primary(&mut self) -> Option<u64> {
209
+        let pending = self.pending_primary.take()?;
210
+
211
+        if pending.timestamp.elapsed() < self.primary_debounce {
212
+            // Not ready yet, put it back
213
+            self.pending_primary = Some(pending);
214
+            return None;
215
+        }
216
+
217
+        tracing::debug!(
218
+            "Committing debounced primary from {:?}: {}",
219
+            pending.source,
220
+            pending.content.preview(50)
221
+        );
222
+
223
+        let id = self.history.push(pending.content.clone(), pending.source);
224
+        self.current_primary = Some(pending.content);
225
+        id
226
+    }
227
+
228
+    /// Check if there's pending primary content
229
+    pub fn has_pending_primary(&self) -> bool {
230
+        self.pending_primary.is_some()
231
+    }
232
+
181233
     /// Get the X11 connection
182234
     pub fn conn(&self) -> &Arc<RustConnection> {
183235
         self.selection_mgr.conn()