Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,804 changes: 1,204 additions & 600 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions input-capture/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ tokio = { version = "1.32.0", features = [
once_cell = "1.19.0"
async-trait = "0.1.81"
tokio-util = "0.7.11"
arboard = { version = "3.4", features = ["wayland-data-control"] }


[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
Expand Down
149 changes: 149 additions & 0 deletions input-capture/src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use arboard::Clipboard;
use input_event::{ClipboardEvent, Event};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio::task::spawn_blocking;
use tokio::time::interval;

use crate::{CaptureError, CaptureEvent};

/// Clipboard monitor that watches for clipboard changes
pub struct ClipboardMonitor {
event_rx: Receiver<CaptureEvent>,
_event_tx: Sender<CaptureEvent>,
last_content: Arc<Mutex<Option<String>>>,
last_change: Arc<Mutex<Option<Instant>>>,
enabled: Arc<Mutex<bool>>,
}

impl ClipboardMonitor {
pub fn new() -> Result<Self, CaptureError> {
let (event_tx, event_rx) = mpsc::channel(16);
let last_content: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let last_change: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None));
let enabled = Arc::new(Mutex::new(true));

let last_content_clone = last_content.clone();
let last_change_clone = last_change.clone();
let enabled_clone = enabled.clone();
let event_tx_clone = event_tx.clone();

// Spawn monitoring task
tokio::spawn(async move {
let mut check_interval = interval(Duration::from_millis(500));

loop {
check_interval.tick().await;

// Check if enabled
let is_enabled = {
let enabled = enabled_clone.lock().unwrap();
*enabled
};

if !is_enabled {
continue;
}

// Read clipboard in blocking task
let last_content_clone2 = last_content_clone.clone();
let last_change_clone2 = last_change_clone.clone();
let event_tx_clone2 = event_tx_clone.clone();

let _ = spawn_blocking(move || {
// Create clipboard instance
let mut clipboard = match Clipboard::new() {
Ok(c) => c,
Err(e) => {
log::debug!("Failed to create clipboard: {}", e);
return;
}
};

// Get current clipboard text
let current_text = match clipboard.get_text() {
Ok(text) => {
log::trace!("Clipboard text read: {} bytes", text.len());
text
}
Err(e) => {
// Clipboard might be empty or contain non-text data
log::trace!("Failed to get clipboard text: {}", e);
return;
}
};

// Check if content changed
let mut last_content = last_content_clone2.lock().unwrap();
let mut last_change = last_change_clone2.lock().unwrap();

let content_changed = match last_content.as_ref() {
None => true,
Some(last) => last != &current_text,
};

if content_changed {
// Debounce: ignore changes within 200ms of last change
// This prevents infinite loops when both sides update clipboard
let should_emit = match *last_change {
None => true,
Some(instant) => instant.elapsed() > Duration::from_millis(200),
};

if should_emit {
log::info!("Clipboard changed, length: {} bytes", current_text.len());
*last_content = Some(current_text.clone());
*last_change = Some(Instant::now());

// Send event
let event = CaptureEvent::Input(Event::Clipboard(
ClipboardEvent::Text(current_text),
));
let _ = event_tx_clone2.blocking_send(event);
} else {
log::trace!("Clipboard changed but debounced (too recent)");
}
}
})
.await;
}
});

Ok(Self {
event_rx,
_event_tx: event_tx,
last_content,
last_change,
enabled,
})
}

/// Receive the next clipboard event
pub async fn recv(&mut self) -> Option<CaptureEvent> {
self.event_rx.recv().await
}

/// Enable clipboard monitoring
pub fn enable(&self) {
let mut enabled = self.enabled.lock().unwrap();
*enabled = true;
log::info!("Clipboard monitoring enabled");
}

/// Disable clipboard monitoring
pub fn disable(&self) {
let mut enabled = self.enabled.lock().unwrap();
*enabled = false;
log::info!("Clipboard monitoring disabled");
}

/// Update the last known clipboard content (called when we set the clipboard)
/// This prevents detecting our own clipboard changes as external changes
pub fn update_last_content(&self, content: String) {
let mut last_content = self.last_content.lock().unwrap();
let mut last_change = self.last_change.lock().unwrap();
*last_content = Some(content);
*last_change = Some(Instant::now());
}
}
5 changes: 3 additions & 2 deletions input-capture/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use input_event::{scancode, Event, KeyboardEvent};

pub use error::{CaptureCreationError, CaptureError, InputCaptureError};

pub mod clipboard;
pub mod error;

#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Expand All @@ -35,7 +36,7 @@ mod dummy;

pub type CaptureHandle = u64;

#[derive(Copy, Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub enum CaptureEvent {
/// capture on this capture handle is now active
Begin,
Expand Down Expand Up @@ -252,7 +253,7 @@ impl Stream for InputCapture {
swap(&mut self.position_map, &mut position_map);
{
for &id in position_map.get(&pos).expect("position") {
self.pending.push_back((id, event));
self.pending.push_back((id, event.clone()));
}
}
swap(&mut self.position_map, &mut position_map);
Expand Down
2 changes: 1 addition & 1 deletion input-capture/src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ fn create_event_tap<'a>(
res_events.iter().for_each(|e| {
// error must be ignored, since the event channel
// may already be closed when the InputCapture instance is dropped.
let _ = event_tx.blocking_send((pos, *e));
let _ = event_tx.blocking_send((pos, e.clone()));
});
// Returning Drop should stop the event from being processed
// but core fundation still returns the event
Expand Down
1 change: 1 addition & 0 deletions input-emulation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ tokio = { version = "1.32.0", features = [
"signal",
] }
once_cell = "1.19.0"
arboard = { version = "3.4", features = ["wayland-data-control"] }

[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
bitflags = "2.6.0"
Expand Down
105 changes: 105 additions & 0 deletions input-emulation/src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use arboard::Clipboard;
use input_event::ClipboardEvent;
use std::sync::{Arc, Mutex};
use thiserror::Error;
use tokio::task::spawn_blocking;

#[derive(Debug, Error)]
pub enum ClipboardError {
#[error("Failed to access clipboard: {0}")]
Access(String),
#[error("Failed to set clipboard: {0}")]
Set(String),
}

/// Clipboard emulation that sets clipboard content
#[derive(Clone)]
pub struct ClipboardEmulation {
// Use Arc<Mutex<>> to share clipboard across threads
clipboard: Arc<Mutex<Option<Clipboard>>>,
}

impl ClipboardEmulation {
pub fn new() -> Result<Self, ClipboardError> {
// Try to create initial clipboard instance
let clipboard = match Clipboard::new() {
Ok(c) => Some(c),
Err(e) => {
log::warn!("Failed to create clipboard instance: {}", e);
None
}
};

Ok(Self {
clipboard: Arc::new(Mutex::new(clipboard)),
})
}

/// Set clipboard content from a clipboard event
pub async fn set(&self, event: ClipboardEvent) -> Result<(), ClipboardError> {
match event {
ClipboardEvent::Text(text) => {
let clipboard_arc = self.clipboard.clone();

spawn_blocking(move || {
let mut clipboard_guard = clipboard_arc.lock().unwrap();

// Try to get or create clipboard
let clipboard = match clipboard_guard.as_mut() {
Some(c) => c,
None => {
// Try to create a new clipboard instance
match Clipboard::new() {
Ok(c) => {
*clipboard_guard = Some(c);
clipboard_guard.as_mut().unwrap()
}
Err(e) => {
return Err(ClipboardError::Access(format!("{}", e)));
}
}
}
};

// Set clipboard text
clipboard
.set_text(text.clone())
.map_err(|e| ClipboardError::Set(format!("{}", e)))?;

log::debug!("Clipboard set, length: {} bytes", text.len());
Ok(())
})
.await
.map_err(|e| ClipboardError::Access(format!("Task join error: {}", e)))?
}
}
}

/// Get current clipboard content (for testing/verification)
pub async fn get(&self) -> Result<String, ClipboardError> {
let clipboard_arc = self.clipboard.clone();

spawn_blocking(move || {
let mut clipboard_guard = clipboard_arc.lock().unwrap();

let clipboard = match clipboard_guard.as_mut() {
Some(c) => c,
None => match Clipboard::new() {
Ok(c) => {
*clipboard_guard = Some(c);
clipboard_guard.as_mut().unwrap()
}
Err(e) => {
return Err(ClipboardError::Access(format!("{}", e)));
}
},
};

clipboard
.get_text()
.map_err(|e| ClipboardError::Access(format!("{}", e)))
})
.await
.map_err(|e| ClipboardError::Access(format!("Task join error: {}", e)))?
}
}
1 change: 1 addition & 0 deletions input-emulation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod libei;
#[cfg(target_os = "macos")]
mod macos;

pub mod clipboard;
/// fallback input emulation (logs events)
mod dummy;
mod error;
Expand Down
4 changes: 4 additions & 0 deletions input-emulation/src/libei.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ impl Emulation for LibeiEmulation<'_> {
}
KeyboardEvent::Modifiers { .. } => {}
},
Event::Clipboard(_) => {
// Clipboard events are not supported by libei emulation
log::debug!("ignoring clipboard event in libei emulation");
}
}
self.context
.flush()
Expand Down
4 changes: 4 additions & 0 deletions input-emulation/src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ impl Emulation for MacOSEmulation {
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
match event {
Event::Clipboard(_) => {
// Clipboard events are not emulated through this backend
// They are handled directly by the clipboard emulation module
}
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
let mut mouse_location = match self.get_mouse_location() {
Expand Down
5 changes: 5 additions & 0 deletions input-emulation/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ impl Emulation for WindowsEmulation {
}
KeyboardEvent::Modifiers { .. } => {}
},
Event::Clipboard(_) => {
// Clipboard events are not emulated through this backend
// They are handled directly by the clipboard emulation module
log::debug!("ignoring clipboard event in windows emulation");
}
}
// FIXME
Ok(())
Expand Down
9 changes: 7 additions & 2 deletions input-emulation/src/wlroots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,14 @@ impl Emulation for WlrootsEmulation {
_ => {}
}
}
let event_debug = format!("{event:?}");
virtual_input
.consume_event(event)
.unwrap_or_else(|_| panic!("failed to convert event: {event:?}"));
.unwrap_or_else(|_| panic!("failed to convert event: {event_debug}"));
match self.queue.flush() {
Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
self.last_flush_failed = true;
log::warn!("can't keep up, discarding event: ({handle}) - {event:?}");
log::warn!("can't keep up, discarding event: ({handle}) - {event_debug}");
}
Err(WaylandError::Protocol(e)) => panic!("wayland protocol violation: {e}"),
Ok(()) => self.last_flush_failed = false,
Expand Down Expand Up @@ -246,6 +247,10 @@ impl VirtualInput {
.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
},
Event::Clipboard(_) => {
// Clipboard events are not supported by wlroots emulation
log::debug!("ignoring clipboard event in wlroots emulation");
}
}
Ok(())
}
Expand Down
6 changes: 5 additions & 1 deletion input-emulation/src/xdg_desktop_portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use async_trait::async_trait;

use futures::FutureExt;
use input_event::{
Event::{Keyboard, Pointer},
Event::{self, Keyboard, Pointer},
KeyboardEvent, PointerEvent,
};

Expand Down Expand Up @@ -125,6 +125,10 @@ impl Emulation for DesktopPortalEmulation<'_> {
}
}
}
Event::Clipboard(_) => {
// Clipboard events are not supported by desktop portal emulation
log::debug!("ignoring clipboard event in desktop portal emulation");
}
}
Ok(())
}
Expand Down
Loading
Loading