@@ -1,4 +1,5 @@ |
| 1 | 1 | use std::sync::Arc; |
| 2 | +use std::time::{Duration, Instant}; |
| 2 | 3 | |
| 3 | 4 | use x11rb::protocol::xfixes::SelectionNotifyEvent as XFixesSelectionNotifyEvent; |
| 4 | 5 | use x11rb::protocol::xproto::{Atom, SelectionRequestEvent, Window}; |
@@ -9,6 +10,13 @@ use crate::config::Config; |
| 9 | 10 | use crate::error::Result; |
| 10 | 11 | use crate::x11::{get_window_class, Atoms, SelectionManager, TransferManager}; |
| 11 | 12 | |
| 13 | +/// Pending PRIMARY selection content awaiting debounce |
| 14 | +struct PendingPrimary { |
| 15 | + content: ClipboardContent, |
| 16 | + source: Option<String>, |
| 17 | + timestamp: Instant, |
| 18 | +} |
| 19 | + |
| 12 | 20 | /// Main clipboard manager that coordinates X11 selection handling and history |
| 13 | 21 | pub struct ClipboardManager { |
| 14 | 22 | selection_mgr: SelectionManager, |
@@ -32,6 +40,12 @@ pub struct ClipboardManager { |
| 32 | 40 | |
| 33 | 41 | /// Whether XFixes monitoring is active |
| 34 | 42 | 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, |
| 35 | 49 | } |
| 36 | 50 | |
| 37 | 51 | impl ClipboardManager { |
@@ -56,6 +70,8 @@ impl ClipboardManager { |
| 56 | 70 | last_primary_owner: 0, |
| 57 | 71 | watch_primary: config.behavior.watch_primary, |
| 58 | 72 | xfixes_active: false, |
| 73 | + pending_primary: None, |
| 74 | + primary_debounce: Duration::from_millis(config.behavior.primary_debounce_ms), |
| 59 | 75 | }) |
| 60 | 76 | } |
| 61 | 77 | |
@@ -63,6 +79,7 @@ impl ClipboardManager { |
| 63 | 79 | pub fn reload_filter(&mut self, config: &Config) { |
| 64 | 80 | self.filter.reload(&config.behavior, &config.filters); |
| 65 | 81 | self.watch_primary = config.behavior.watch_primary; |
| 82 | + self.primary_debounce = Duration::from_millis(config.behavior.primary_debounce_ms); |
| 66 | 83 | } |
| 67 | 84 | |
| 68 | 85 | /// Start watching selections via XFixes (event-driven monitoring) |
@@ -153,31 +170,66 @@ impl ClipboardManager { |
| 153 | 170 | return Ok(None); |
| 154 | 171 | } |
| 155 | 172 | |
| 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 |
| 167 | 173 | 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); |
| 168 | 182 | self.current_clipboard = Some(content); |
| 169 | 183 | self.last_clipboard_owner = event.owner; |
| 184 | + return Ok(id); |
| 170 | 185 | } 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 | + }); |
| 172 | 198 | self.last_primary_owner = event.owner; |
| 199 | + return Ok(None); |
| 173 | 200 | } |
| 174 | | - |
| 175 | | - return Ok(id); |
| 176 | 201 | } |
| 177 | 202 | |
| 178 | 203 | Ok(None) |
| 179 | 204 | } |
| 180 | 205 | |
| 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 | + |
| 181 | 233 | /// Get the X11 connection |
| 182 | 234 | pub fn conn(&self) -> &Arc<RustConnection> { |
| 183 | 235 | self.selection_mgr.conn() |