refactor enigo input (#441)

* refactor enigo input

* format
This commit is contained in:
CJ Pais 2025-12-12 01:33:49 -08:00 committed by GitHub
parent 0477c6a888
commit 0cb88994f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 167 additions and 149 deletions

View file

@ -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::<EnigoState>()
.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)?
}
}

123
src-tauri/src/input.rs Normal file
View file

@ -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<Enigo>);
impl EnigoState {
pub fn new() -> Result<Self, String> {
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::<EnigoState>()?;
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(())
}

View file

@ -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"),
);

View file

@ -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<tauri::Monitor> {
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);
}
}
}