mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-06-30 04:14:53 +03:00
fix(keyboard): wayland clipboard input prompt (#14700)
* fix(keyboard): wayland clipboard input prompt Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): clipboard input, remove unused code Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): dialog, better enableAndContinue Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog consent Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): prompt text Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): text input 1. Use `keysym` for the installed version if possible. 2. Use the clipboard if the string cannot be fully handled by `keysym`. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): dialog, title type Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): better decode_utf8_prefix() Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): better process_chr() Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): unit tests Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog, no icon Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog, Toast show the result Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog, showToast() on persist failed Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt, better dialog Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog, translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): better wayland clipboard input prompt Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): wayland clipboard, link external app Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): trivial changes Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): wayland clipboard input, dialog content Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): tranlsations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@ use super::*;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use crate::clipboard::clipboard_listener;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide};
|
||||
pub use crate::clipboard::{ClipboardContext, ClipboardSide};
|
||||
pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME};
|
||||
#[cfg(windows)]
|
||||
use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data};
|
||||
@@ -109,6 +109,62 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES: usize =
|
||||
super::input_service::WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS * 4;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn decode_utf8_prefix(bytes: &[u8]) -> Option<String> {
|
||||
let end = bytes.len().min(WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES);
|
||||
let slice = &bytes[..end];
|
||||
match std::str::from_utf8(slice) {
|
||||
Ok(text) => Some(text.to_owned()),
|
||||
Err(e) => {
|
||||
if e.error_len().is_some() {
|
||||
return None;
|
||||
}
|
||||
let valid_up_to = e.valid_up_to();
|
||||
std::str::from_utf8(&slice[..valid_up_to])
|
||||
.ok()
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn decode_text_clipboard(clipboard: &Clipboard) -> Option<String> {
|
||||
if clipboard.format.enum_value() != Ok(ClipboardFormat::Text) {
|
||||
return None;
|
||||
}
|
||||
if clipboard.compress {
|
||||
let bytes = hbb_common::compress::decompress(&clipboard.content);
|
||||
return decode_utf8_prefix(&bytes);
|
||||
}
|
||||
decode_utf8_prefix(&clipboard.content)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn should_skip_wayland_clipboard_sync(msg: &Message) -> bool {
|
||||
if crate::platform::linux::is_x11() {
|
||||
return false;
|
||||
}
|
||||
let is_recent_wayland_input = |clipboard: &Clipboard| -> bool {
|
||||
let Some(text) = decode_text_clipboard(clipboard) else {
|
||||
return false;
|
||||
};
|
||||
super::input_service::is_recent_wayland_clipboard_input(&text)
|
||||
};
|
||||
|
||||
match &msg.union {
|
||||
Some(message::Union::Clipboard(clipboard)) => is_recent_wayland_input(clipboard),
|
||||
Some(message::Union::MultiClipboards(multi_clipboards)) => multi_clipboards
|
||||
.clipboards
|
||||
.iter()
|
||||
.any(is_recent_wayland_input),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
impl Handler {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
@@ -172,7 +228,19 @@ impl Handler {
|
||||
}
|
||||
}
|
||||
|
||||
check_clipboard(&mut self.ctx, ClipboardSide::Host, false)
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let msg = crate::clipboard::peek_clipboard(&mut self.ctx, ClipboardSide::Host, false)?;
|
||||
if should_skip_wayland_clipboard_sync(&msg) {
|
||||
log::debug!("Skip clipboard sync for recent Wayland keyboard injection");
|
||||
return None;
|
||||
}
|
||||
return Some(msg);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
crate::clipboard::check_clipboard(&mut self.ctx, ClipboardSide::Host, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Read clipboard data from cm using ipc.
|
||||
@@ -272,3 +340,46 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
||||
CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(target_os = "linux")]
|
||||
mod tests {
|
||||
use super::{decode_utf8_prefix, WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES};
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_returns_text_for_valid_utf8() {
|
||||
let text = "hello-مرحبا";
|
||||
assert_eq!(decode_utf8_prefix(text.as_bytes()), Some(text.to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_returns_none_for_invalid_utf8_sequence() {
|
||||
let bytes = b"ab\xffcd";
|
||||
assert_eq!(decode_utf8_prefix(bytes), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_trims_incomplete_utf8_suffix() {
|
||||
let bytes = vec![b'a', 0xE4, 0xB8];
|
||||
assert_eq!(decode_utf8_prefix(&bytes), Some("a".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_applies_max_bytes_limit() {
|
||||
let bytes = vec![b'a'; WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES + 8];
|
||||
let result = decode_utf8_prefix(&bytes).expect("expected decoded prefix");
|
||||
assert_eq!(result.len(), WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_keeps_utf8_boundary_when_limited() {
|
||||
let mut bytes = vec![b'a'; WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES - 1];
|
||||
bytes.extend_from_slice("ا".as_bytes());
|
||||
let result = decode_utf8_prefix(&bytes).expect("expected decoded prefix");
|
||||
assert_eq!(
|
||||
result.len(),
|
||||
WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES - 1
|
||||
);
|
||||
assert!(result.chars().all(|c| c == 'a'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +457,12 @@ lazy_static::lazy_static! {
|
||||
static ref RELATIVE_MOUSE_CONNS: Arc<Mutex<std::collections::HashSet<i32>>> = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref WAYLAND_CLIPBOARD_INPUT_RECORDS: Arc<Mutex<Vec<(Instant, String)>>> =
|
||||
Default::default();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_relative_mouse_active(conn: i32, active: bool) {
|
||||
let mut lock = RELATIVE_MOUSE_CONNS.lock().unwrap();
|
||||
@@ -1594,15 +1600,28 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
|
||||
}
|
||||
|
||||
fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
|
||||
// On Wayland with uinput mode, use clipboard for character input
|
||||
// On Wayland with uinput mode:
|
||||
// - ASCII printable: input via key events (custom keyboard path, e.g. portal keysym)
|
||||
// - Non-ASCII: input via clipboard paste
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
||||
// Skip clipboard for hotkeys (Ctrl/Alt/Meta pressed)
|
||||
if !is_hotkey_modifier_pressed(en) {
|
||||
if down {
|
||||
if let Ok(c) = char::try_from(chr) {
|
||||
if let Ok(c) = char::try_from(chr) {
|
||||
if is_ascii_printable(c) {
|
||||
if down {
|
||||
en.key_down(Key::Layout(c)).ok();
|
||||
} else {
|
||||
en.key_up(Key::Layout(c));
|
||||
}
|
||||
} else if down {
|
||||
input_char_via_clipboard_server(en, c);
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Ignore invalid unicode scalar in Wayland+uinput path: {}",
|
||||
chr
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1637,11 +1656,17 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
|
||||
}
|
||||
|
||||
fn process_unicode(en: &mut Enigo, chr: u32) {
|
||||
// On Wayland with uinput mode, use clipboard for character input
|
||||
// On Wayland with uinput mode:
|
||||
// - ASCII printable: input via key sequence (custom keyboard path)
|
||||
// - Non-ASCII: input via clipboard paste
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
||||
if let Ok(c) = char::try_from(chr) {
|
||||
input_char_via_clipboard_server(en, c);
|
||||
if is_ascii_printable(c) {
|
||||
en.key_sequence(&c.to_string());
|
||||
} else {
|
||||
input_char_via_clipboard_server(en, c);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1652,10 +1677,16 @@ fn process_unicode(en: &mut Enigo, chr: u32) {
|
||||
}
|
||||
|
||||
fn process_seq(en: &mut Enigo, sequence: &str) {
|
||||
// On Wayland with uinput mode, use clipboard for text input
|
||||
// On Wayland with uinput mode:
|
||||
// - pure ASCII printable sequence: input via key sequence (custom keyboard path)
|
||||
// - any non-ASCII present: input whole sequence via clipboard to preserve order
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
||||
input_text_via_clipboard_server(en, sequence);
|
||||
if sequence.chars().all(is_ascii_printable) {
|
||||
en.key_sequence(sequence);
|
||||
} else {
|
||||
input_text_via_clipboard_server(en, sequence);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1668,40 +1699,103 @@ fn process_seq(en: &mut Enigo, sequence: &str) {
|
||||
/// this delay may be insufficient, but there is no reliable alternative mechanism.
|
||||
#[cfg(target_os = "linux")]
|
||||
const CLIPBOARD_SYNC_DELAY_MS: u64 = 50;
|
||||
#[cfg(target_os = "linux")]
|
||||
const WAYLAND_CLIPBOARD_INPUT_FILTER_WINDOW: Duration = Duration::from_secs(1);
|
||||
#[cfg(target_os = "linux")]
|
||||
const WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS: usize = 256;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) const WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS: usize = 1024;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn cleanup_wayland_clipboard_input_records(records: &mut Vec<(Instant, String)>, now: Instant) {
|
||||
records.retain(|(created_at, _)| {
|
||||
now.saturating_duration_since(*created_at) <= WAYLAND_CLIPBOARD_INPUT_FILTER_WINDOW
|
||||
});
|
||||
let len = records.len();
|
||||
if len > WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS {
|
||||
records.drain(0..(len - WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn normalize_wayland_clipboard_input_text(text: &str) -> String {
|
||||
text.chars()
|
||||
.take(WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn get_wayland_clipboard_input_normalized_text(text: &str) -> Option<String> {
|
||||
let normalized = normalize_wayland_clipboard_input_text(text);
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(normalized)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn record_wayland_clipboard_input_for_sync_filter(text: &str) -> Option<(Instant, String)> {
|
||||
if text.is_empty() || crate::platform::linux::is_x11() {
|
||||
return None;
|
||||
}
|
||||
let normalized = get_wayland_clipboard_input_normalized_text(text)?;
|
||||
let now = Instant::now();
|
||||
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
||||
cleanup_wayland_clipboard_input_records(&mut records, now);
|
||||
records.push((now, normalized.clone()));
|
||||
Some((now, normalized))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn rollback_wayland_clipboard_input_record(record: (Instant, String)) {
|
||||
let (created_at, normalized) = record;
|
||||
let now = Instant::now();
|
||||
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
||||
cleanup_wayland_clipboard_input_records(&mut records, now);
|
||||
if let Some(pos) = records
|
||||
.iter()
|
||||
.rposition(|(record_created_at, record_normalized)| {
|
||||
*record_created_at == created_at && *record_normalized == normalized
|
||||
})
|
||||
{
|
||||
records.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) fn is_recent_wayland_clipboard_input(text: &str) -> bool {
|
||||
if text.is_empty() || crate::platform::linux::is_x11() {
|
||||
return false;
|
||||
}
|
||||
let Some(normalized) = get_wayland_clipboard_input_normalized_text(text) else {
|
||||
return false;
|
||||
};
|
||||
let now = Instant::now();
|
||||
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
||||
cleanup_wayland_clipboard_input_records(&mut records, now);
|
||||
records
|
||||
.iter()
|
||||
.any(|(_, record_normalized)| record_normalized == &normalized)
|
||||
}
|
||||
|
||||
/// Internal: Set clipboard content without delay.
|
||||
/// Returns true if clipboard was set successfully.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn set_clipboard_content(text: &str) -> bool {
|
||||
use arboard::{Clipboard, LinuxClipboardKind, SetExtLinux};
|
||||
|
||||
let mut clipboard = match Clipboard::new() {
|
||||
Ok(cb) => cb,
|
||||
Err(e) => {
|
||||
log::error!("set_clipboard_content: failed to create clipboard: {:?}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Set both CLIPBOARD and PRIMARY selections
|
||||
// Terminal uses PRIMARY for Shift+Insert, GUI apps use CLIPBOARD
|
||||
if let Err(e) = clipboard
|
||||
.set()
|
||||
.clipboard(LinuxClipboardKind::Clipboard)
|
||||
.text(text.to_owned())
|
||||
{
|
||||
log::error!("set_clipboard_content: failed to set CLIPBOARD: {:?}", e);
|
||||
if let Err(e) = crate::clipboard::set_text_clipboard_with_owner_sync(
|
||||
text,
|
||||
crate::clipboard::ClipboardSide::Host,
|
||||
) {
|
||||
log::error!(
|
||||
"set_clipboard_content: failed to set clipboard with owner marker: {:?}",
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if let Err(e) = clipboard
|
||||
.set()
|
||||
.clipboard(LinuxClipboardKind::Primary)
|
||||
.text(text.to_owned())
|
||||
{
|
||||
log::warn!("set_clipboard_content: failed to set PRIMARY: {:?}", e);
|
||||
// Continue anyway, CLIPBOARD might work
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1714,7 +1808,11 @@ fn set_clipboard_content(text: &str) -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
pub(super) fn set_clipboard_for_paste_sync(text: &str) -> bool {
|
||||
let record = record_wayland_clipboard_input_for_sync_filter(text);
|
||||
if !set_clipboard_content(text) {
|
||||
if let Some(record) = record {
|
||||
rollback_wayland_clipboard_input_record(record);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(CLIPBOARD_SYNC_DELAY_MS));
|
||||
@@ -1916,49 +2014,53 @@ fn translate_process_code(code: u32, down: bool) {
|
||||
fn translate_keyboard_mode(evt: &KeyEvent) {
|
||||
match &evt.union {
|
||||
Some(key_event::Union::Seq(seq)) => {
|
||||
// On Wayland, handle character input directly in this (--server) process using clipboard.
|
||||
// This function runs in the --server process (logged-in user session), which has
|
||||
// WAYLAND_DISPLAY and XDG_RUNTIME_DIR — so clipboard operations work here.
|
||||
//
|
||||
// Why not let it go through uinput IPC:
|
||||
// 1. For uinput mode: the uinput service thread runs in the --service (root) process,
|
||||
// which typically lacks user session environment. Clipboard operations there are
|
||||
// unreliable. Handling clipboard here avoids that issue.
|
||||
// 2. For RDP input mode: Portal's notify_keyboard_keysym API interprets keysyms
|
||||
// based on its internal modifier state, which may not match our released state.
|
||||
// Using clipboard bypasses this issue entirely.
|
||||
// On Wayland:
|
||||
// - uinput mode (--service): keep clipboard handling in this process because
|
||||
// clipboard is unreliable in root service context.
|
||||
// - rdp_input mode (--server): forward sequence to custom keyboard handler so
|
||||
// ASCII can use Portal keysym and non-ASCII can use clipboard.
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() {
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
|
||||
// Check if this is a hotkey (Ctrl/Alt/Meta pressed)
|
||||
// For hotkeys, we send character-based key events via Enigo instead of
|
||||
// using the clipboard. This relies on the local keyboard layout for
|
||||
// mapping characters to physical keys.
|
||||
// This assumes client and server use the same keyboard layout (common case).
|
||||
// Note: For non-Latin keyboards (e.g., Arabic), hotkeys may not work
|
||||
// correctly if the character cannot be mapped to a key via KEY_MAP_LAYOUT.
|
||||
// This is a known limitation - most common hotkeys (Ctrl+A/C/V/Z) use Latin
|
||||
// characters which are mappable on most keyboard layouts.
|
||||
if is_hotkey_modifier_pressed(&mut en) {
|
||||
// For hotkeys, send character-based key events via Enigo.
|
||||
// This relies on the local keyboard layout mapping (KEY_MAP_LAYOUT).
|
||||
for chr in seq.chars() {
|
||||
if !is_ascii_printable(chr) {
|
||||
log::warn!(
|
||||
"Hotkey with non-ASCII character may not work correctly on non-Latin keyboard layouts"
|
||||
);
|
||||
}
|
||||
en.key_click(Key::Layout(chr));
|
||||
}
|
||||
if wayland_use_rdp_input() {
|
||||
release_shift_for_char_input(&mut en);
|
||||
en.key_sequence(seq);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal text input: release Shift and use clipboard
|
||||
release_shift_for_char_input(&mut en);
|
||||
if wayland_use_uinput() {
|
||||
// Check if this is a hotkey (Ctrl/Alt/Meta pressed)
|
||||
// For hotkeys, we send character-based key events via Enigo instead of
|
||||
// using the clipboard. This relies on the local keyboard layout for
|
||||
// mapping characters to physical keys.
|
||||
// This assumes client and server use the same keyboard layout (common case).
|
||||
// Note: For non-Latin keyboards (e.g., Arabic), hotkeys may not work
|
||||
// correctly if the character cannot be mapped to a key via KEY_MAP_LAYOUT.
|
||||
// This is a known limitation - most common hotkeys (Ctrl+A/C/V/Z) use Latin
|
||||
// characters which are mappable on most keyboard layouts.
|
||||
if is_hotkey_modifier_pressed(&mut en) {
|
||||
// For hotkeys, send character-based key events via Enigo.
|
||||
// This relies on the local keyboard layout mapping (KEY_MAP_LAYOUT).
|
||||
for chr in seq.chars() {
|
||||
if !is_ascii_printable(chr) {
|
||||
log::warn!(
|
||||
"Hotkey with non-ASCII character may not work correctly on non-Latin keyboard layouts"
|
||||
);
|
||||
}
|
||||
en.key_click(Key::Layout(chr));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
input_text_via_clipboard_server(&mut en, seq);
|
||||
return;
|
||||
// Normal text input: release Shift and use clipboard
|
||||
release_shift_for_char_input(&mut en);
|
||||
if seq.chars().all(is_ascii_printable) {
|
||||
en.key_sequence(seq);
|
||||
} else {
|
||||
input_text_via_clipboard_server(&mut en, seq);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fr -> US
|
||||
|
||||
@@ -118,6 +118,23 @@ pub mod client {
|
||||
}
|
||||
|
||||
fn key_sequence(&mut self, s: &str) {
|
||||
if s.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep ordering deterministic:
|
||||
// - pure ASCII printable: send via Portal keysym
|
||||
// - any non-ASCII present (including mixed ASCII/non-ASCII): send whole
|
||||
// sequence via clipboard as one atomic paste
|
||||
let ascii_only = s.chars().all(|c| {
|
||||
let keysym = char_to_keysym(c);
|
||||
can_input_via_keysym(c, keysym)
|
||||
});
|
||||
if !ascii_only {
|
||||
input_text_via_clipboard(s, self.conn.clone(), &self.session);
|
||||
return;
|
||||
}
|
||||
|
||||
for c in s.chars() {
|
||||
let keysym = char_to_keysym(c);
|
||||
// ASCII characters: use keysym
|
||||
@@ -128,9 +145,6 @@ pub mod client {
|
||||
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
|
||||
log::error!("Failed to send keysym up: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
// Non-ASCII: use clipboard
|
||||
input_text_via_clipboard(&c.to_string(), self.conn.clone(), &self.session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,8 +181,7 @@ pub mod client {
|
||||
// ASCII characters: send keysym up if we also sent it on key_down
|
||||
let keysym = char_to_keysym(chr);
|
||||
if can_input_via_keysym(chr, keysym) {
|
||||
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session)
|
||||
{
|
||||
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
|
||||
log::error!("Failed to send keysym up: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user