Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 30 additions & 18 deletions crates/rds-desktop/src/input/x11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
//! live in `input/portal` and `input/wlr`.

use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;

mod keymap;
mod repeat;
use keymap::KeyMap;
use x11rb::protocol::xkb::ConnectionExt as _;

Expand All @@ -17,8 +19,6 @@ use x11rb::rust_connection::RustConnection;

use crate::{DesktopError, InputSink};

const KEY_PRESS: u8 = 2;
const KEY_RELEASE: u8 = 3;
const BUTTON_PRESS: u8 = 4;
const BUTTON_RELEASE: u8 = 5;
const MOTION_NOTIFY: u8 = 6;
Expand All @@ -27,13 +27,14 @@ const MAX_SCROLL_CLICKS: f64 = 32.0;
/// XTEST input bound to one X screen, resolving evdev through actual XKB key names.
/// Extended evdev keys outside core X11's 8-bit keycodes are refused.
pub struct XtestInput {
conn: RustConnection,
conn: Arc<RustConnection>,
root: x11rb::protocol::xproto::Window,
screen: u32,
width: u16,
height: u16,
keymap: KeyMap,
keys: BTreeMap<u32, u8>,
server: String,
keys: BTreeMap<u32, repeat::Hold>,
buttons: BTreeSet<u8>,
scroll_x: f64,
scroll_y: f64,
Expand Down Expand Up @@ -77,17 +78,26 @@ impl XtestInput {
));
}
let keymap = load_keymap(&conn)?;
let display =
std::env::var("DISPLAY").map_err(|_| error("X11 display name is unavailable"))?;
// X screens share the same core keyboard. Normalize the screen suffix
// so parallel controllers retain one per-key repeat/hold reference.
let server = display
.rsplit_once('.')
.filter(|(_, suffix)| suffix.parse::<u32>().is_ok())
.map_or(display.clone(), |(server, _)| server.to_owned());
tracing::info!(
mapped_keys = keymap.len(),
"X11 physical keyboard map ready"
);
Ok(Self {
conn,
conn: Arc::new(conn),
root,
screen,
width,
height,
keymap,
server,
keys: BTreeMap::new(),
buttons: BTreeSet::new(),
scroll_x: 0.0,
Expand Down Expand Up @@ -160,8 +170,8 @@ impl XtestInput {
self.keymap = load_keymap(&self.conn)?;
}
}
let key = if let Some(key) = self.keys.get(&code) {
*key
let key = if let Some(hold) = self.keys.get(&code) {
hold.key
} else {
let key = self.keymap.resolve(code)?;
if !pressed {
Expand All @@ -171,12 +181,18 @@ impl XtestInput {
};
if pressed {
self.keyboard_on_screen()?;
}
self.fake(if pressed { KEY_PRESS } else { KEY_RELEASE }, key, 0, 0)?;
if pressed {
self.keys.insert(code, key);
} else {
self.keys.remove(&code);
let modifier = matches!(
code,
29 | 42 | 54 | 56 | 97 | 100 | 125 | 126 | 58 | 69 | 70
);
if let Some(hold) = self.keys.get(&code) {
return hold.repeat(modifier);
}
let hold =
repeat::Hold::press(self.conn.clone(), self.root, &self.server, key, modifier)?;
self.keys.insert(code, hold);
} else if let Some(mut hold) = self.keys.remove(&code) {
hold.release()?;
}
Ok(())
}
Expand Down Expand Up @@ -318,11 +334,7 @@ impl Drop for XtestInput {
fn drop(&mut self) {
// Best-effort release of only this sink's injected holds. The worker
// drops the sink after its last in-flight call, outside Tokio workers.
for key in self.keys.values() {
let _ = self
.conn
.xtest_fake_input(KEY_RELEASE, *key, 0, self.root, 0, 0, 0);
}
self.keys.clear();
for button in &self.buttons {
let _ = self
.conn
Expand Down
126 changes: 126 additions & 0 deletions crates/rds-desktop/src/input/x11/repeat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Client-paced repeats with real native holds, shared by keyboard controllers.
use crate::DesktopError;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use x11rb::protocol::xproto::{AutoRepeatMode, ChangeKeyboardControlAux, ConnectionExt as _};
use x11rb::protocol::xtest::ConnectionExt as _;
use x11rb::rust_connection::RustConnection;

struct State {
owners: u32,
repeat: bool,
}
static HOLDS: Mutex<BTreeMap<(String, u8), State>> = Mutex::new(BTreeMap::new());

pub(super) struct Hold {
conn: Arc<RustConnection>,
root: u32,
server: String,
pub(super) key: u8,
released: bool,
}

fn error() -> DesktopError {
DesktopError::Input("X11 keyboard repeat state failed".into())
}
fn mode(conn: &RustConnection, key: u8, on: bool) -> Result<(), DesktopError> {
conn.change_keyboard_control(
&ChangeKeyboardControlAux::new()
.key(u32::from(key))
.auto_repeat_mode(if on {
AutoRepeatMode::ON
} else {
AutoRepeatMode::OFF
}),
)
.map_err(|_| error())?
.check()
.map_err(|_| error())
}
fn fake(conn: &RustConnection, root: u32, key: u8, press: bool) -> Result<(), DesktopError> {
conn.xtest_fake_input(if press { 2 } else { 3 }, key, 0, root, 0, 0, 0)
.map_err(|_| error())?
.check()
.map_err(|_| error())
}

impl Hold {
pub(super) fn press(
conn: Arc<RustConnection>,
root: u32,
server: &str,
key: u8,
modifier: bool,
) -> Result<Self, DesktopError> {
let mut holds = HOLDS.lock().map_err(|_| error())?;
let id = (server.to_owned(), key);
if let Some(state) = holds.get_mut(&id) {
let next = state.owners.checked_add(1).ok_or_else(error)?;
if !modifier {
fake(&conn, root, key, false)?;
fake(&conn, root, key, true)?;
}
state.owners = next;
} else {
let keyboard = conn
.get_keyboard_control()
.map_err(|_| error())?
.reply()
.map_err(|_| error())?;
let repeat = keyboard.auto_repeats[usize::from(key) / 8] & (1 << (key % 8)) != 0;
mode(&conn, key, false)?;
if let Err(e) = fake(&conn, root, key, true) {
let _ = mode(&conn, key, repeat);
return Err(e);
}
holds.insert(id, State { owners: 1, repeat });
}
Ok(Self {
conn,
root,
server: server.to_owned(),
key,
released: false,
})
}

pub(super) fn repeat(&self, modifier: bool) -> Result<(), DesktopError> {
if modifier {
return Ok(());
}
// A repeated client KeyDown is an intentional native repeat. Core X
// suppresses duplicate presses with typematic disabled, so pulse once.
let _holds = HOLDS.lock().map_err(|_| error())?;
fake(&self.conn, self.root, self.key, false)?;
fake(&self.conn, self.root, self.key, true)
}

pub(super) fn release(&mut self) -> Result<(), DesktopError> {
if self.released {
return Ok(());
}
let mut holds = HOLDS.lock().map_err(|_| error())?;
let id = (self.server.clone(), self.key);
let Some(state) = holds.get_mut(&id) else {
self.released = true;
return Ok(());
};
if state.owners > 1 {
state.owners -= 1;
self.released = true;
return Ok(());
}
let release = fake(&self.conn, self.root, self.key, false);
let restore = mode(&self.conn, self.key, state.repeat);
holds.remove(&id);
self.released = true;
release.and(restore)
}
}
impl Drop for Hold {
fn drop(&mut self) {
if self.release().is_err() {
tracing::warn!("X11 keyboard hold cleanup failed");
}
}
}
1 change: 1 addition & 0 deletions crates/rds-desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod decode_work;
mod delivery_rate;
pub mod input;
pub mod mailbox;
mod media_repair;
mod order;
pub mod render;
#[cfg(any(all(target_os = "linux", feature = "x11"), test))]
Expand Down
Loading
Loading