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