Skip to main content

pixelflux/wayland/
vkclient.rs

1//! `zwp_virtual_keyboard_v1` client for typing Unicode text into a nested
2//! compositor's socket.
3//!
4//! pixelflux is normally the compositor, but in a nested deployment (a labwc/kwin
5//! session running as a client of pixelflux) the apps live on that inner
6//! compositor's socket, and keys injected into pixelflux's own seat resolve
7//! against pixelflux's keymap — an overlay the inner compositor never sees. So
8//! text is typed here as a client of whichever compositor the apps live under —
9//! by Computer-Use actions and by selkies over the `type_text_wayland` ABI —
10//! reusing the seat's [`KeymapPolicy`] over a US base: base-reachable characters
11//! press their ordinary keycodes, everything else is overlay-bound. The client
12//! is PERSISTENT per socket: the connection, virtual-keyboard device and its
13//! uploaded keymap live across calls, so a flush re-uploads (and settles) only
14//! when the accumulated keymap actually changed, and key events ride the
15//! protocol's ordering in one batch with a single closing round-trip. Any
16//! failure drops the cached connection and one reconnect is attempted before
17//! the error propagates, so a restarted app compositor heals on the next call.
18//! Blocking, off the compositor thread, with every round-trip deadline-bounded so
19//! a wedged compositor cannot hang the caller forever.
20
21use std::collections::{HashMap, HashSet};
22use std::os::fd::AsFd;
23use std::os::unix::net::UnixStream;
24use std::sync::atomic::{AtomicU64, Ordering};
25use std::sync::{Mutex, OnceLock};
26use std::time::Duration;
27
28use wayland_client::protocol::{wl_registry, wl_seat};
29use wayland_client::{delegate_noop, Connection, Dispatch, EventQueue, QueueHandle};
30use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
31    zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1,
32    zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1,
33};
34
35use crate::wayland::keymap::{compile_rmlvo, KeymapPolicy};
36use crate::wayland::wlclient::{bounded_roundtrip, impl_sync_callback, memfd_with, SyncState};
37
38/// Overlay keycodes stay under the X11 255 ceiling so XWayland apps under the app
39/// compositor can still receive them (the seat's own overlay sits above 255).
40const OVERLAY_FIRST_XKB: u32 = 150;
41const OVERLAY_LAST_XKB: u32 = 255;
42const OVERLAY_SLOTS: usize = (OVERLAY_LAST_XKB - OVERLAY_FIRST_XKB + 1) as usize;
43/// wl_keyboard / zwp_virtual_keyboard key events carry evdev codes (xkb - 8).
44const EVDEV_OFFSET: u32 = 8;
45const KEYMAP_FORMAT_XKB_V1: u32 = 1;
46
47#[derive(Default)]
48struct Globals {
49    seat: Option<wl_seat::WlSeat>,
50    manager: Option<ZwpVirtualKeyboardManagerV1>,
51    sync_done: bool,
52}
53
54impl SyncState for Globals {
55    fn sync_done_mut(&mut self) -> &mut bool {
56        &mut self.sync_done
57    }
58}
59impl_sync_callback!(Globals);
60
61impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
62    fn event(
63        state: &mut Self,
64        registry: &wl_registry::WlRegistry,
65        event: wl_registry::Event,
66        _: &(),
67        _: &Connection,
68        qh: &QueueHandle<Self>,
69    ) {
70        if let wl_registry::Event::Global { name, interface, .. } = event {
71            // Version 1 of each suffices: the seat is only the manager argument.
72            match interface.as_str() {
73                "wl_seat" if state.seat.is_none() => {
74                    state.seat = Some(registry.bind(name, 1, qh, ()));
75                }
76                "zwp_virtual_keyboard_manager_v1" if state.manager.is_none() => {
77                    state.manager = Some(registry.bind(name, 1, qh, ()));
78                }
79                _ => {}
80            }
81        }
82    }
83}
84
85delegate_noop!(Globals: ignore wl_seat::WlSeat);
86delegate_noop!(Globals: ZwpVirtualKeyboardManagerV1);
87delegate_noop!(Globals: ZwpVirtualKeyboardV1);
88
89/// The US base keymap text, compiled once per process: selkies types per text
90/// commit, and xkbcommon compilation is the expensive part of a call.
91pub(crate) fn us_base_text() -> Option<&'static str> {
92    static CACHE: OnceLock<Option<String>> = OnceLock::new();
93    CACHE.get_or_init(|| compile_rmlvo("", "", "us", "", "")).as_deref()
94}
95
96/// The keymap policy over that base, shared across calls: rebuilding it costs a
97/// keymap compile per typed batch, and its accumulated overlay assignments are
98/// device-independent, so repeat batches reuse their slots. Locked — callers
99/// run off-thread and Computer-Use may type concurrently with selkies.
100fn shared_policy() -> Option<&'static Mutex<KeymapPolicy>> {
101    static POLICY: OnceLock<Option<Mutex<KeymapPolicy>>> = OnceLock::new();
102    POLICY
103        .get_or_init(|| {
104            let mut policy =
105                KeymapPolicy::with_overlay_range(OVERLAY_FIRST_XKB, OVERLAY_LAST_XKB);
106            policy.rebuild_base(us_base_text()?.to_string()).then(|| Mutex::new(policy))
107        })
108        .as_ref()
109}
110
111fn upload_keymap(
112    vk: &ZwpVirtualKeyboardV1,
113    queue: &mut EventQueue<Globals>,
114    text: &str,
115) -> Result<(), String> {
116    let mut data = text.as_bytes().to_vec();
117    // Compositors parse the mapping as a NUL-terminated string.
118    data.push(0);
119    let fd = memfd_with(&data)?;
120    vk.keymap(KEYMAP_FORMAT_XKB_V1, fd.as_fd(), data.len() as u32);
121    queue.flush().map_err(|e| format!("flush keymap: {e}"))
122}
123
124/// Universal keysym for a character: Latin-1 keysyms are their codepoint and
125/// everything above rides the Unicode plane — the two encodings every compositor
126/// and toolkit translate algorithmically. Editing controls map to their keys;
127/// other controls have no keysym. Fixed grammar only: which keysym spells a
128/// char in any richer sense is selkies' policy, delivered via `type_keysyms_to`.
129fn universal_keysym(c: char) -> Option<u32> {
130    match c {
131        '\n' | '\r' => Some(0xFF0D),
132        '\t' => Some(0xFF09),
133        '\x1b' => Some(0xFF1B),
134        _ => match c as u32 {
135            cp @ 0x20..=0xFF => Some(cp),
136            cp @ 0x100.. => Some(0x0100_0000 | cp),
137            _ => None,
138        },
139    }
140}
141
142/// Bumped whenever the shared policy's keymap changes; each cached typer
143/// re-uploads (and settles) only when it is behind this.
144static KEYMAP_GENERATION: AtomicU64 = AtomicU64::new(1);
145
146struct Typer {
147    conn: Connection,
148    queue: EventQueue<Globals>,
149    state: Globals,
150    vk: ZwpVirtualKeyboardV1,
151    uploaded_generation: u64,
152}
153
154fn typers() -> &'static Mutex<HashMap<String, Typer>> {
155    static TYPERS: OnceLock<Mutex<HashMap<String, Typer>>> = OnceLock::new();
156    TYPERS.get_or_init(|| Mutex::new(HashMap::new()))
157}
158
159fn connect_typer(socket_path: &str) -> Result<Typer, String> {
160    let stream =
161        UnixStream::connect(socket_path).map_err(|e| format!("connect {socket_path}: {e}"))?;
162    let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
163    let mut queue = conn.new_event_queue();
164    let qh = queue.handle();
165    let _registry = conn.display().get_registry(&qh, ());
166    let mut state = Globals::default();
167    bounded_roundtrip(&conn, &mut queue, &mut state)?;
168    let seat = state.seat.take().ok_or("app compositor advertises no wl_seat")?;
169    let manager = state
170        .manager
171        .take()
172        .ok_or("app compositor does not advertise zwp_virtual_keyboard_manager_v1")?;
173    let vk = manager.create_virtual_keyboard(&seat, &qh, ());
174    // Surfaces an "unauthorized" bind error before the first keymap upload.
175    bounded_roundtrip(&conn, &mut queue, &mut state)?;
176    Ok(Typer { conn, queue, state, vk, uploaded_generation: 0 })
177}
178
179fn flush_keysyms(
180    typer: &mut Typer,
181    policy: &mut KeymapPolicy,
182    keysyms: &[u32],
183) -> Result<(), String> {
184    let none = HashSet::new();
185    // Chunk bound: at most OVERLAY_SLOTS keysyms per bind call, so a batch can
186    // never recycle a slot it assigned earlier in the same batch.
187    for chunk in keysyms.chunks(OVERLAY_SLOTS) {
188        let (keycodes, changed) = policy.bind_many_plain(chunk, &none);
189        if changed {
190            KEYMAP_GENERATION.fetch_add(1, Ordering::Relaxed);
191        }
192        // The protocol requires a keymap before the first key event even when
193        // the whole text resolves in the base; after that, only a changed
194        // keymap costs an upload, its compositor-side compile and the settle.
195        let generation = KEYMAP_GENERATION.load(Ordering::Relaxed);
196        if typer.uploaded_generation != generation {
197            upload_keymap(&typer.vk, &mut typer.queue, &policy.keymap_text())?;
198            bounded_roundtrip(&typer.conn, &mut typer.queue, &mut typer.state)?;
199            std::thread::sleep(Duration::from_millis(10));
200            typer.uploaded_generation = generation;
201        }
202        // One protocol-ordered batch, one closing round-trip: the compositor
203        // serializes per client, so per-key pacing bought nothing but latency.
204        for &kc in &keycodes {
205            // Below the evdev offset the keysym has no bindable keycode.
206            if kc < EVDEV_OFFSET {
207                continue;
208            }
209            typer.vk.key(0, kc - EVDEV_OFFSET, 1);
210            typer.vk.key(0, kc - EVDEV_OFFSET, 0);
211        }
212        typer.queue.flush().map_err(|e| format!("flush keys: {e}"))?;
213        bounded_roundtrip(&typer.conn, &mut typer.queue, &mut typer.state)?;
214    }
215    Ok(())
216}
217
218/// Type `text` in order through the persistent client for `socket_path`.
219/// Codepoints with no keysym are skipped. Blocking; call off the compositor's
220/// calloop thread.
221pub fn type_text_to(socket_path: &str, text: &str) -> Result<(), String> {
222    let syms: Vec<u32> = text.chars().filter_map(universal_keysym).collect();
223    type_keysyms_to(socket_path, &syms)
224}
225
226/// Like [`type_text_to`], but taps the given keysyms verbatim: the caller owns
227/// which keysym spells which character; this owns delivery (base-reachable
228/// keysyms press their ordinary keycodes, the rest overlay-bind).
229pub fn type_keysyms_to(socket_path: &str, keysyms: &[u32]) -> Result<(), String> {
230    let mut policy = shared_policy()
231        .ok_or("us base keymap failed to compile")?
232        .lock()
233        .unwrap_or_else(|poisoned| poisoned.into_inner());
234    let mut typers = typers().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
235    let mut last_err = None;
236    for _ in 0..2 {
237        if !typers.contains_key(socket_path) {
238            typers.insert(socket_path.to_string(), connect_typer(socket_path)?);
239        }
240        let typer = typers.get_mut(socket_path).expect("typer just ensured");
241        match flush_keysyms(typer, &mut policy, keysyms) {
242            Ok(()) => return Ok(()),
243            Err(e) => {
244                // Dropping the typer closes the socket; the compositor tears the
245                // device down and releases anything it held. Reconnect once —
246                // a restarted app compositor reappears under the same name.
247                typers.remove(socket_path);
248                last_err = Some(e);
249            }
250        }
251    }
252    Err(last_err.unwrap_or_else(|| "virtual-keyboard flush failed".to_string()))
253}