feat(linux): add KDE Plasma 6 Wayland support via kwtype (#676)

* feat(linux): add KDE Plasma 6 Wayland support via kwtype

On KDE Plasma 6 Wayland, existing input tools have limitations:
- ydotool type: cannot handle German umlauts (ö, ä, ü, ß)
- wtype: doesn't work (KDE lacks zwp_virtual_keyboard_manager_v1)
- ydotool key: key combos don't reach native Wayland apps like Kate

This adds support for kwtype, which uses the KDE Fake Input protocol:
- Properly handles Unicode characters including German umlauts
- Works with native Wayland applications (Kate, Dolphin, etc.)
- Requires kwtype to be installed: https://github.com/Sporif/KWtype

Changes:
- Add is_kde_plasma() and is_kde_wayland() detection in utils.rs
- Add kwtype integration for direct text typing on KDE Wayland
- Add wl-copy integration for clipboard operations on Wayland
- Skip wtype on KDE (known not to work)

Recommended: Use PasteMethod::Direct on KDE Wayland for best results.

* add -- to make sure input is good

---------

Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
Datenfalke 2026-02-05 12:11:33 +01:00 committed by GitHub
parent 7703d10f5f
commit 0afd5ec01f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 107 additions and 9 deletions

View file

@ -7,7 +7,7 @@ use tauri::{AppHandle, Manager};
use tauri_plugin_clipboard_manager::ClipboardExt;
#[cfg(target_os = "linux")]
use crate::utils::is_wayland;
use crate::utils::{is_kde_wayland, is_wayland};
#[cfg(target_os = "linux")]
use std::process::Command;
@ -23,9 +23,23 @@ fn paste_via_clipboard(
let clipboard_content = clipboard.read_text().unwrap_or_default();
// Write text to clipboard first
clipboard
// On Wayland, prefer wl-copy for better compatibility (especially with umlauts)
#[cfg(target_os = "linux")]
let write_result = if is_wayland() && is_wl_copy_available() {
info!("Using wl-copy for clipboard write on Wayland");
write_clipboard_via_wl_copy(text)
} else {
clipboard
.write_text(text)
.map_err(|e| format!("Failed to write to clipboard: {}", e))
};
#[cfg(not(target_os = "linux"))]
let write_result = clipboard
.write_text(text)
.map_err(|e| format!("Failed to write to clipboard: {}", e))?;
.map_err(|e| format!("Failed to write to clipboard: {}", e));
write_result?;
std::thread::sleep(Duration::from_millis(paste_delay_ms));
@ -49,9 +63,16 @@ fn paste_via_clipboard(
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))?;
// On Wayland, prefer wl-copy for better compatibility
#[cfg(target_os = "linux")]
if is_wayland() && is_wl_copy_available() {
let _ = write_clipboard_via_wl_copy(&clipboard_content);
} else {
let _ = clipboard.write_text(&clipboard_content);
}
#[cfg(not(target_os = "linux"))]
let _ = clipboard.write_text(&clipboard_content);
Ok(())
}
@ -61,8 +82,9 @@ fn paste_via_clipboard(
#[cfg(target_os = "linux")]
fn try_send_key_combo_linux(paste_method: &PasteMethod) -> Result<bool, String> {
if is_wayland() {
// Wayland: prefer wtype, then dotool, then ydotool
if is_wtype_available() {
// Wayland: prefer wtype (but not on KDE), then dotool, then ydotool
// Note: wtype doesn't work on KDE (no zwp_virtual_keyboard_manager_v1 support)
if !is_kde_wayland() && is_wtype_available() {
info!("Using wtype for key combo");
send_key_combo_via_wtype(paste_method)?;
return Ok(true);
@ -99,8 +121,15 @@ fn try_send_key_combo_linux(paste_method: &PasteMethod) -> Result<bool, String>
#[cfg(target_os = "linux")]
fn try_direct_typing_linux(text: &str) -> Result<bool, String> {
if is_wayland() {
// KDE Wayland: prefer kwtype (uses KDE Fake Input protocol, supports umlauts)
if is_kde_wayland() && is_kwtype_available() {
info!("Using kwtype for direct text input on KDE Wayland");
type_text_via_kwtype(text)?;
return Ok(true);
}
// Wayland: prefer wtype, then dotool, then ydotool
if is_wtype_available() {
// Note: wtype doesn't work on KDE (no zwp_virtual_keyboard_manager_v1 support)
if !is_kde_wayland() && is_wtype_available() {
info!("Using wtype for direct text input");
type_text_via_wtype(text)?;
return Ok(true);
@ -171,6 +200,26 @@ fn is_xdotool_available() -> bool {
.unwrap_or(false)
}
/// Check if kwtype is available (KDE Wayland virtual keyboard input tool)
#[cfg(target_os = "linux")]
fn is_kwtype_available() -> bool {
Command::new("which")
.arg("kwtype")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
/// Check if wl-copy is available (Wayland clipboard tool)
#[cfg(target_os = "linux")]
fn is_wl_copy_available() -> bool {
Command::new("which")
.arg("wl-copy")
.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> {
@ -254,6 +303,40 @@ fn type_text_via_ydotool(text: &str) -> Result<(), String> {
Ok(())
}
/// Type text directly via kwtype (KDE Wayland virtual keyboard, uses KDE Fake Input protocol).
#[cfg(target_os = "linux")]
fn type_text_via_kwtype(text: &str) -> Result<(), String> {
let output = Command::new("kwtype")
.arg("--")
.arg(text)
.output()
.map_err(|e| format!("Failed to execute kwtype: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("kwtype failed: {}", stderr));
}
Ok(())
}
/// Write text to clipboard via wl-copy (Wayland clipboard tool).
#[cfg(target_os = "linux")]
fn write_clipboard_via_wl_copy(text: &str) -> Result<(), String> {
let output = Command::new("wl-copy")
.arg("--")
.arg(text)
.output()
.map_err(|e| format!("Failed to execute wl-copy: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("wl-copy 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> {

View file

@ -52,3 +52,18 @@ pub fn is_wayland() -> bool {
.map(|v| v.to_lowercase() == "wayland")
.unwrap_or(false)
}
/// Check if running on KDE Plasma desktop environment
#[cfg(target_os = "linux")]
pub fn is_kde_plasma() -> bool {
std::env::var("XDG_CURRENT_DESKTOP")
.map(|v| v.to_uppercase().contains("KDE"))
.unwrap_or(false)
|| std::env::var("KDE_SESSION_VERSION").is_ok()
}
/// Check if running on KDE Plasma with Wayland
#[cfg(target_os = "linux")]
pub fn is_kde_wayland() -> bool {
is_wayland() && is_kde_plasma()
}