fix: direct typing (#493)

This commit is contained in:
johnmalek312 2025-12-31 22:19:29 +11:00 committed by GitHub
parent 13cae47ff4
commit f7e73fc602
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 201 additions and 25 deletions

View file

@ -81,11 +81,29 @@ This project is actively being developed and has some [known issues](https://git
**Wayland Support (Linux):**
- Limited or no support for Wayland display server
- On Wayland the clipboard-based paste options (`Clipboard (CTRL+V)` / `Clipboard (Shift+Insert)`) copy the transcription once, then try to run [`wtype`](https://github.com/atx/wtype) (preferred) or [`dotool`](https://sr.ht/~geb/dotool/) to fire the paste keystroke. Install one of these tools to let Handy drive the compositor-friendly paste shortcut; otherwise it falls back to Enigo-generated key events, which may not work on Wayland.
- Limited support for Wayland display server
- Requires [`wtype`](https://github.com/atx/wtype) or [`dotool`](https://sr.ht/~geb/dotool/) for text input to work correctly (see [Linux Notes](#linux-notes) below for installation)
### Linux Notes
**Text Input Tools:**
For reliable text input on Linux, install the appropriate tool for your display server:
| Display Server | Recommended Tool | Install Command |
| -------------- | ---------------- | -------------------------------------------------- |
| X11 | `xdotool` | `sudo apt install xdotool` |
| Wayland | `wtype` | `sudo apt install wtype` |
| Both | `dotool` | `sudo apt install dotool` (requires `input` group) |
- **X11**: Install `xdotool` for both direct typing and clipboard paste shortcuts
- **Wayland**: Install `wtype` (preferred) or `dotool` for text input to work correctly
- **dotool setup**: Requires adding your user to the `input` group: `sudo usermod -aG input $USER` (then log out and back in)
Without these tools, Handy falls back to enigo which may have limited compatibility, especially on Wayland.
**Other Notes:**
- The recording overlay is disabled by default on Linux (`Overlay Position: None`) because certain compositors treat it as the active window. When the overlay is visible it can steal focus, which prevents Handy from pasting back into the application that triggered transcription. If you enable the overlay anyway, be aware that clipboard-based pasting might fail or end up in the wrong window.
- If you are having trouble with the app, running with the environment variable `WEBKIT_DISABLE_DMABUF_RENDERER=1` may help
- You can manage global shortcuts outside of Handy and still control the app via signals. Sending `SIGUSR2` to the Handy process toggles recording on/off, which lets Wayland window managers or other hotkey daemons keep ownership of keybindings. Example (Sway):

View file

@ -17,30 +17,36 @@ fn paste_via_clipboard(
app_handle: &AppHandle,
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();
// Write text to clipboard first
clipboard
.write_text(text)
.map_err(|e| format!("Failed to write to clipboard: {}", e))?;
std::thread::sleep(std::time::Duration::from_millis(50));
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()),
// Send paste key combo
#[cfg(target_os = "linux")]
let key_combo_sent = try_send_key_combo_linux(paste_method)?;
#[cfg(not(target_os = "linux"))]
let key_combo_sent = false;
// Fall back to enigo if no native tool handled it
if !key_combo_sent {
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));
// Restore original clipboard content
clipboard
.write_text(&clipboard_content)
.map_err(|e| format!("Failed to restore clipboard: {}", e))?;
@ -48,17 +54,55 @@ fn paste_via_clipboard(
Ok(())
}
/// Attempts to paste using Wayland-specific tools (`wtype` or `dotool`).
/// Returns `Ok(true)` if a Wayland tool handled the paste, `Ok(false)` if not applicable,
/// or `Err` on failure from the underlying tool.
/// Attempts to send a key combination using Linux-native tools.
/// Returns `Ok(true)` if a native tool handled it, `Ok(false)` to fall back to enigo.
#[cfg(target_os = "linux")]
fn try_wayland_send_paste(paste_method: &PasteMethod) -> Result<bool, String> {
fn try_send_key_combo_linux(paste_method: &PasteMethod) -> Result<bool, String> {
if is_wayland() {
// Wayland: prefer wtype, then dotool
if is_wtype_available() {
send_paste_via_wtype(paste_method)?;
info!("Using wtype for key combo");
send_key_combo_via_wtype(paste_method)?;
return Ok(true);
} else if is_dotool_available() {
send_paste_via_dotool(paste_method)?;
}
if is_dotool_available() {
info!("Using dotool for key combo");
send_key_combo_via_dotool(paste_method)?;
return Ok(true);
}
} else {
// X11: prefer xdotool
if is_xdotool_available() {
info!("Using xdotool for key combo");
send_key_combo_via_xdotool(paste_method)?;
return Ok(true);
}
}
Ok(false)
}
/// Attempts to type text directly using Linux-native tools.
/// Returns `Ok(true)` if a native tool handled it, `Ok(false)` to fall back to enigo.
#[cfg(target_os = "linux")]
fn try_direct_typing_linux(text: &str) -> Result<bool, String> {
if is_wayland() {
// Wayland: prefer wtype, then dotool
if is_wtype_available() {
info!("Using wtype for direct text input");
type_text_via_wtype(text)?;
return Ok(true);
}
if is_dotool_available() {
info!("Using dotool for direct text input");
type_text_via_dotool(text)?;
return Ok(true);
}
} else {
// X11: prefer xdotool
if is_xdotool_available() {
info!("Using xdotool for direct text input");
type_text_via_xdotool(text)?;
return Ok(true);
}
}
@ -86,9 +130,83 @@ fn is_dotool_available() -> bool {
.unwrap_or(false)
}
/// Paste using wtype and return a friendly error on failure.
#[cfg(target_os = "linux")]
fn send_paste_via_wtype(paste_method: &PasteMethod) -> Result<(), String> {
fn is_xdotool_available() -> bool {
Command::new("which")
.arg("xdotool")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
/// Type text directly via wtype on Wayland.
#[cfg(target_os = "linux")]
fn type_text_via_wtype(text: &str) -> Result<(), String> {
let output = Command::new("wtype")
.arg("--") // Protect against text starting with -
.arg(text)
.output()
.map_err(|e| format!("Failed to execute wtype: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("wtype failed: {}", stderr));
}
Ok(())
}
/// Type text directly via xdotool on X11.
#[cfg(target_os = "linux")]
fn type_text_via_xdotool(text: &str) -> Result<(), String> {
let output = Command::new("xdotool")
.arg("type")
.arg("--clearmodifiers")
.arg("--")
.arg(text)
.output()
.map_err(|e| format!("Failed to execute xdotool: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("xdotool failed: {}", stderr));
}
Ok(())
}
/// Type text directly via dotool (works on both Wayland and X11 via uinput).
#[cfg(target_os = "linux")]
fn type_text_via_dotool(text: &str) -> Result<(), String> {
use std::io::Write;
use std::process::Stdio;
let mut child = Command::new("dotool")
.stdin(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to spawn dotool: {}", e))?;
if let Some(mut stdin) = child.stdin.take() {
// dotool uses "type <text>" command
writeln!(stdin, "type {}", text)
.map_err(|e| format!("Failed to write to dotool stdin: {}", e))?;
}
let output = child
.wait_with_output()
.map_err(|e| format!("Failed to wait for dotool: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("dotool failed: {}", stderr));
}
Ok(())
}
/// Send a key combination (e.g., Ctrl+V) via wtype on Wayland.
#[cfg(target_os = "linux")]
fn send_key_combo_via_wtype(paste_method: &PasteMethod) -> Result<(), String> {
let args: Vec<&str> = match paste_method {
PasteMethod::CtrlV => vec!["-M", "ctrl", "-k", "v"],
PasteMethod::ShiftInsert => vec!["-M", "shift", "-k", "Insert"],
@ -109,9 +227,9 @@ fn send_paste_via_wtype(paste_method: &PasteMethod) -> Result<(), String> {
Ok(())
}
/// Paste using dotool and return a friendly error on failure.
/// Send a key combination (e.g., Ctrl+V) via dotool.
#[cfg(target_os = "linux")]
fn send_paste_via_dotool(paste_method: &PasteMethod) -> Result<(), String> {
fn send_key_combo_via_dotool(paste_method: &PasteMethod) -> Result<(), String> {
let command;
match paste_method {
PasteMethod::CtrlV => command = "echo key ctrl+v | dotool",
@ -132,6 +250,44 @@ fn send_paste_via_dotool(paste_method: &PasteMethod) -> Result<(), String> {
Ok(())
}
/// Send a key combination (e.g., Ctrl+V) via xdotool on X11.
#[cfg(target_os = "linux")]
fn send_key_combo_via_xdotool(paste_method: &PasteMethod) -> Result<(), String> {
let key_combo = match paste_method {
PasteMethod::CtrlV => "ctrl+v",
PasteMethod::CtrlShiftV => "ctrl+shift+v",
PasteMethod::ShiftInsert => "shift+Insert",
_ => return Err("Unsupported paste method".into()),
};
let output = Command::new("xdotool")
.arg("key")
.arg("--clearmodifiers")
.arg(key_combo)
.output()
.map_err(|e| format!("Failed to execute xdotool: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("xdotool failed: {}", stderr));
}
Ok(())
}
/// Types text directly by simulating individual key presses.
fn paste_direct(enigo: &mut Enigo, text: &str) -> Result<(), String> {
#[cfg(target_os = "linux")]
{
if try_direct_typing_linux(text)? {
return Ok(());
}
info!("Falling back to enigo for direct text input");
}
input::paste_text_direct(enigo, text)
}
pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
let settings = get_settings(&app_handle);
let paste_method = settings.paste_method;
@ -159,7 +315,9 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
PasteMethod::None => {
info!("PasteMethod::None selected - skipping paste action");
}
PasteMethod::Direct => input::paste_text_direct(&mut enigo, &text)?,
PasteMethod::Direct => {
paste_direct(&mut enigo, &text)?;
}
PasteMethod::CtrlV | PasteMethod::CtrlShiftV | PasteMethod::ShiftInsert => {
paste_via_clipboard(&mut enigo, &text, &app_handle, &paste_method)?
}