diff --git a/src-tauri/src/clipboard.rs b/src-tauri/src/clipboard.rs index fe5aaad..64e4e1f 100644 --- a/src-tauri/src/clipboard.rs +++ b/src-tauri/src/clipboard.rs @@ -1,10 +1,8 @@ +use crate::input::{self, EnigoState}; use crate::settings::{get_settings, ClipboardHandling, PasteMethod}; use enigo::Enigo; -use enigo::Key; -use enigo::Keyboard; -use enigo::Settings; use log::info; -use tauri::AppHandle; +use tauri::{AppHandle, Manager}; use tauri_plugin_clipboard_manager::ClipboardExt; #[cfg(target_os = "linux")] @@ -12,136 +10,19 @@ use crate::utils::is_wayland; #[cfg(target_os = "linux")] use std::process::Command; -/// Sends a Ctrl+V or Cmd+V paste command using platform-specific virtual key codes. -/// This ensures the paste works regardless of keyboard layout (e.g., Russian, AZERTY, DVORAK). -fn send_paste_ctrl_v() -> Result<(), String> { - #[cfg(target_os = "linux")] - if try_wayland_send_paste(&PasteMethod::CtrlV)? { - return Ok(()); - } - - // Platform-specific key definitions - #[cfg(target_os = "macos")] - let (modifier_key, v_key_code) = (Key::Meta, Key::Other(9)); - #[cfg(target_os = "windows")] - let (modifier_key, v_key_code) = (Key::Control, Key::Other(0x56)); // VK_V - #[cfg(target_os = "linux")] - let (modifier_key, v_key_code) = (Key::Control, Key::Unicode('v')); - - let mut enigo = Enigo::new(&Settings::default()) - .map_err(|e| format!("Failed to initialize Enigo: {}", e))?; - - // Press modifier + V - enigo - .key(modifier_key, enigo::Direction::Press) - .map_err(|e| format!("Failed to press modifier key: {}", e))?; - enigo - .key(v_key_code, enigo::Direction::Click) - .map_err(|e| format!("Failed to click V key: {}", e))?; - - std::thread::sleep(std::time::Duration::from_millis(100)); - - enigo - .key(modifier_key, enigo::Direction::Release) - .map_err(|e| format!("Failed to release modifier key: {}", e))?; - - Ok(()) -} - -/// Sends a Ctrl+Shift+V paste command. -/// This is commonly used in terminal applications on Linux to paste without formatting. -fn send_paste_ctrl_shift_v() -> Result<(), String> { - #[cfg(target_os = "linux")] - if try_wayland_send_paste(&PasteMethod::CtrlShiftV)? { - return Ok(()); - } - - // Platform-specific key definitions - #[cfg(target_os = "macos")] - let (modifier_key, v_key_code) = (Key::Meta, Key::Other(9)); // Cmd+Shift+V on macOS - #[cfg(target_os = "windows")] - let (modifier_key, v_key_code) = (Key::Control, Key::Other(0x56)); // VK_V - #[cfg(target_os = "linux")] - let (modifier_key, v_key_code) = (Key::Control, Key::Unicode('v')); - - let mut enigo = Enigo::new(&Settings::default()) - .map_err(|e| format!("Failed to initialize Enigo: {}", e))?; - - // Press Ctrl/Cmd + Shift + V - enigo - .key(modifier_key, enigo::Direction::Press) - .map_err(|e| format!("Failed to press modifier key: {}", e))?; - enigo - .key(Key::Shift, enigo::Direction::Press) - .map_err(|e| format!("Failed to press Shift key: {}", e))?; - enigo - .key(v_key_code, enigo::Direction::Click) - .map_err(|e| format!("Failed to click V key: {}", e))?; - - std::thread::sleep(std::time::Duration::from_millis(100)); - - enigo - .key(Key::Shift, enigo::Direction::Release) - .map_err(|e| format!("Failed to release Shift key: {}", e))?; - enigo - .key(modifier_key, enigo::Direction::Release) - .map_err(|e| format!("Failed to release modifier key: {}", e))?; - - Ok(()) -} - -/// Sends a Shift+Insert paste command (Windows and Linux only). -/// This is more universal for terminal applications and legacy software. -fn send_paste_shift_insert() -> Result<(), String> { - #[cfg(target_os = "linux")] - if try_wayland_send_paste(&PasteMethod::ShiftInsert)? { - return Ok(()); - } - - #[cfg(target_os = "windows")] - let insert_key_code = Key::Other(0x2D); // VK_INSERT - #[cfg(not(target_os = "windows"))] - let insert_key_code = Key::Other(0x76); // XK_Insert (keycode 118 / 0x76, also used as fallback) - - let mut enigo = Enigo::new(&Settings::default()) - .map_err(|e| format!("Failed to initialize Enigo: {}", e))?; - - // Press Shift + Insert - enigo - .key(Key::Shift, enigo::Direction::Press) - .map_err(|e| format!("Failed to press Shift key: {}", e))?; - enigo - .key(insert_key_code, enigo::Direction::Click) - .map_err(|e| format!("Failed to click Insert key: {}", e))?; - - std::thread::sleep(std::time::Duration::from_millis(100)); - - enigo - .key(Key::Shift, enigo::Direction::Release) - .map_err(|e| format!("Failed to release Shift key: {}", e))?; - - Ok(()) -} - -/// Pastes text directly using the enigo text method. -/// This tries to use system input methods if possible, otherwise simulates keystrokes one by one. -fn paste_via_direct_input(text: &str) -> Result<(), String> { - let mut enigo = Enigo::new(&Settings::default()) - .map_err(|e| format!("Failed to initialize Enigo: {}", e))?; - - enigo - .text(text) - .map_err(|e| format!("Failed to send text directly: {}", e))?; - - Ok(()) -} - /// Pastes text using the clipboard: saves current content, writes text, sends paste keystroke, restores clipboard. fn paste_via_clipboard( + enigo: &mut Enigo, text: &str, app_handle: &AppHandle, - send_paste: fn() -> Result<(), String>, + paste_method: &PasteMethod, ) -> Result<(), String> { + // Check for Wayland first + #[cfg(target_os = "linux")] + if try_wayland_send_paste(paste_method)? { + return Ok(()); + } + let clipboard = app_handle.clipboard(); let clipboard_content = clipboard.read_text().unwrap_or_default(); @@ -150,7 +31,14 @@ fn paste_via_clipboard( .map_err(|e| format!("Failed to write to clipboard: {}", e))?; std::thread::sleep(std::time::Duration::from_millis(50)); - send_paste()?; + + match paste_method { + PasteMethod::CtrlV => input::send_paste_ctrl_v(enigo)?, + PasteMethod::CtrlShiftV => input::send_paste_ctrl_shift_v(enigo)?, + PasteMethod::ShiftInsert => input::send_paste_shift_insert(enigo)?, + _ => return Err("Invalid paste method for clipboard paste".into()), + } + std::thread::sleep(std::time::Duration::from_millis(50)); clipboard @@ -257,18 +145,23 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> { info!("Using paste method: {:?}", paste_method); + // Get the managed Enigo instance + let enigo_state = app_handle + .try_state::() + .ok_or("Enigo state not initialized")?; + let mut enigo = enigo_state + .0 + .lock() + .map_err(|e| format!("Failed to lock Enigo: {}", e))?; + // Perform the paste operation match paste_method { PasteMethod::None => { info!("PasteMethod::None selected - skipping paste action"); } - PasteMethod::Direct => paste_via_direct_input(&text)?, - PasteMethod::CtrlV => paste_via_clipboard(&text, &app_handle, send_paste_ctrl_v)?, - PasteMethod::CtrlShiftV => { - paste_via_clipboard(&text, &app_handle, send_paste_ctrl_shift_v)? - } - PasteMethod::ShiftInsert => { - paste_via_clipboard(&text, &app_handle, send_paste_shift_insert)? + PasteMethod::Direct => input::paste_text_direct(&mut enigo, &text)?, + PasteMethod::CtrlV | PasteMethod::CtrlShiftV | PasteMethod::ShiftInsert => { + paste_via_clipboard(&mut enigo, &text, &app_handle, &paste_method)? } } diff --git a/src-tauri/src/input.rs b/src-tauri/src/input.rs new file mode 100644 index 0000000..ee33317 --- /dev/null +++ b/src-tauri/src/input.rs @@ -0,0 +1,123 @@ +use enigo::{Enigo, Key, Keyboard, Mouse, Settings}; +use std::sync::Mutex; +use tauri::{AppHandle, Manager}; + +/// Wrapper for Enigo to store in Tauri's managed state. +/// Enigo is wrapped in a Mutex since it requires mutable access. +pub struct EnigoState(pub Mutex); + +impl EnigoState { + pub fn new() -> Result { + let enigo = Enigo::new(&Settings::default()) + .map_err(|e| format!("Failed to initialize Enigo: {}", e))?; + Ok(Self(Mutex::new(enigo))) + } +} + +/// Get the current mouse cursor position using the managed Enigo instance. +/// Returns None if the state is not available or if getting the location fails. +pub fn get_cursor_position(app_handle: &AppHandle) -> Option<(i32, i32)> { + let enigo_state = app_handle.try_state::()?; + let enigo = enigo_state.0.lock().ok()?; + enigo.location().ok() +} + +/// Sends a Ctrl+V or Cmd+V paste command using platform-specific virtual key codes. +/// This ensures the paste works regardless of keyboard layout (e.g., Russian, AZERTY, DVORAK). +/// Note: On Wayland, this may not work - callers should check for Wayland and use alternative methods. +pub fn send_paste_ctrl_v(enigo: &mut Enigo) -> Result<(), String> { + // Platform-specific key definitions + #[cfg(target_os = "macos")] + let (modifier_key, v_key_code) = (Key::Meta, Key::Other(9)); + #[cfg(target_os = "windows")] + let (modifier_key, v_key_code) = (Key::Control, Key::Other(0x56)); // VK_V + #[cfg(target_os = "linux")] + let (modifier_key, v_key_code) = (Key::Control, Key::Unicode('v')); + + // Press modifier + V + enigo + .key(modifier_key, enigo::Direction::Press) + .map_err(|e| format!("Failed to press modifier key: {}", e))?; + enigo + .key(v_key_code, enigo::Direction::Click) + .map_err(|e| format!("Failed to click V key: {}", e))?; + + std::thread::sleep(std::time::Duration::from_millis(100)); + + enigo + .key(modifier_key, enigo::Direction::Release) + .map_err(|e| format!("Failed to release modifier key: {}", e))?; + + Ok(()) +} + +/// Sends a Ctrl+Shift+V paste command. +/// This is commonly used in terminal applications on Linux to paste without formatting. +/// Note: On Wayland, this may not work - callers should check for Wayland and use alternative methods. +pub fn send_paste_ctrl_shift_v(enigo: &mut Enigo) -> Result<(), String> { + // Platform-specific key definitions + #[cfg(target_os = "macos")] + let (modifier_key, v_key_code) = (Key::Meta, Key::Other(9)); // Cmd+Shift+V on macOS + #[cfg(target_os = "windows")] + let (modifier_key, v_key_code) = (Key::Control, Key::Other(0x56)); // VK_V + #[cfg(target_os = "linux")] + let (modifier_key, v_key_code) = (Key::Control, Key::Unicode('v')); + + // Press Ctrl/Cmd + Shift + V + enigo + .key(modifier_key, enigo::Direction::Press) + .map_err(|e| format!("Failed to press modifier key: {}", e))?; + enigo + .key(Key::Shift, enigo::Direction::Press) + .map_err(|e| format!("Failed to press Shift key: {}", e))?; + enigo + .key(v_key_code, enigo::Direction::Click) + .map_err(|e| format!("Failed to click V key: {}", e))?; + + std::thread::sleep(std::time::Duration::from_millis(100)); + + enigo + .key(Key::Shift, enigo::Direction::Release) + .map_err(|e| format!("Failed to release Shift key: {}", e))?; + enigo + .key(modifier_key, enigo::Direction::Release) + .map_err(|e| format!("Failed to release modifier key: {}", e))?; + + Ok(()) +} + +/// Sends a Shift+Insert paste command (Windows and Linux only). +/// This is more universal for terminal applications and legacy software. +/// Note: On Wayland, this may not work - callers should check for Wayland and use alternative methods. +pub fn send_paste_shift_insert(enigo: &mut Enigo) -> Result<(), String> { + #[cfg(target_os = "windows")] + let insert_key_code = Key::Other(0x2D); // VK_INSERT + #[cfg(not(target_os = "windows"))] + let insert_key_code = Key::Other(0x76); // XK_Insert (keycode 118 / 0x76, also used as fallback) + + // Press Shift + Insert + enigo + .key(Key::Shift, enigo::Direction::Press) + .map_err(|e| format!("Failed to press Shift key: {}", e))?; + enigo + .key(insert_key_code, enigo::Direction::Click) + .map_err(|e| format!("Failed to click Insert key: {}", e))?; + + std::thread::sleep(std::time::Duration::from_millis(100)); + + enigo + .key(Key::Shift, enigo::Direction::Release) + .map_err(|e| format!("Failed to release Shift key: {}", e))?; + + Ok(()) +} + +/// Pastes text directly using the enigo text method. +/// This tries to use system input methods if possible, otherwise simulates keystrokes one by one. +pub fn paste_text_direct(enigo: &mut Enigo, text: &str) -> Result<(), String> { + enigo + .text(text) + .map_err(|e| format!("Failed to send text directly: {}", e))?; + + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 882c112..ea9f85c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,6 +6,7 @@ pub mod audio_toolkit; mod clipboard; mod commands; mod helpers; +mod input; mod llm_client; mod managers; mod overlay; @@ -108,7 +109,11 @@ fn show_main_window(app: &AppHandle) { } fn initialize_core_logic(app_handle: &AppHandle) { - // First, initialize the managers + // Initialize the input state (Enigo singleton for keyboard/mouse simulation) + let enigo_state = input::EnigoState::new().expect("Failed to initialize input state (Enigo)"); + app_handle.manage(enigo_state); + + // Initialize the managers let recording_manager = Arc::new( AudioRecordingManager::new(app_handle).expect("Failed to initialize recording manager"), ); diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index e5bc6a0..4d328a4 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -1,6 +1,6 @@ +use crate::input; use crate::settings; use crate::settings::OverlayPosition; -use enigo::{Enigo, Mouse}; use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, PhysicalSize}; #[cfg(not(target_os = "macos"))] @@ -70,16 +70,13 @@ fn force_overlay_topmost(overlay_window: &tauri::webview::WebviewWindow) { } fn get_monitor_with_cursor(app_handle: &AppHandle) -> Option { - let enigo = Enigo::new(&Default::default()); - if let Ok(enigo) = enigo { - if let Ok(mouse_location) = enigo.location() { - if let Ok(monitors) = app_handle.available_monitors() { - for monitor in monitors { - let is_within = - is_mouse_within_monitor(mouse_location, monitor.position(), monitor.size()); - if is_within { - return Some(monitor); - } + if let Some(mouse_location) = input::get_cursor_position(app_handle) { + if let Ok(monitors) = app_handle.available_monitors() { + for monitor in monitors { + let is_within = + is_mouse_within_monitor(mouse_location, monitor.position(), monitor.size()); + if is_within { + return Some(monitor); } } }