Skip to main content

teksilo_platform/
key_report.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Telling a screen reader about each key, where it cannot see them itself.
5//!
6//! A screen reader has to know the key behind every change it hears about:
7//! Orca decides whether a caret move is read as a character, a word or a line
8//! from the last key it saw (`_presentTextAtNewCaretPosition` in
9//! `orca/scripts/default.py`), echoes typed keys, and runs its own commands
10//! (Orca+T, which says the time, and the rest) from keys the application must
11//! then never see.
12//!
13//! On Linux under Wayland it cannot read the keyboard. libatspi 2.52 gives it
14//! the *legacy* keyboard device there, which learns of a key only when the
15//! application that received it reports it to the AT-SPI registry: a call to
16//! `org.a11y.atspi.DeviceEventController.NotifyListenersSync` on the
17//! accessibility bus, whose `bool` answer says whether a listener (Orca,
18//! running one of its commands) took the key. GTK 3's bridge
19//! (`spi_atk_bridge_key_listener` in at-spi2-core's `atk-adaptor/event.c`)
20//! and Qt's (`QSpiApplicationAdaptor::eventFilter`) make that call for every
21//! key they handle, and neither delivers a key a listener took.
22//! `accesskit_unix` makes no such call, so before this module a Teksilo
23//! window in a Wayland session left Orca deaf to every key: no key echo, no
24//! caret speech, no Orca command.
25//!
26//! [`KeyReportGate`] is that call, placed where `teksilo-app` receives winit's
27//! `KeyboardInput`, before anything acts on the key:
28//!
29//! - **When.** Only while an assistive technology is attached, the same
30//!   condition under which `accesskit_unix` exposes the tree at all
31//!   ([`PlatformWindow::accessibility_active`](crate::PlatformWindow::accessibility_active)).
32//!   On an X11 session Orca reads the keyboard itself and registers no
33//!   listener, so the registry answers at once that nobody took the key: the
34//!   cost of what GTK and Qt already do there.
35//! - **What.** Every physical press and release, a held key's repeats as
36//!   presses (as GTK reports each autorepeat event). Not winit's *synthetic*
37//!   events, which X11 makes up for keys already held when a window gains or
38//!   loses focus: nobody pressed them, and they are delivered untouched.
39//! - **The struct.** `(uiiiisb)`: press 0 / release 1, the X keysym, the X
40//!   keycode (the evdev code + 8), the X modifier mask, a time in
41//!   milliseconds, the text the key types, and whether it types it (the
42//!   crate's `DeviceEvent`). winit reports no lock state, so Num Lock is learnt from
43//!   the keys that show it (a keypad digit: on; a keypad motion: off; the Num
44//!   Lock key toggles what is known) and carried as Mod2 on every key while
45//!   known on; until a key has shown it, it is taken as off.
46//! - **Consumed keys.** A press a listener took is not delivered, and neither
47//!   is its release: the release always follows its press, whatever the
48//!   registry says about the release itself, so the application never sees a
49//!   key go down without coming up or the other way round.
50//! - **Time.** A press waits for the registry's answer, as GTK waits. A
51//!   release does not: whether it is delivered is already decided by its
52//!   press, and the next press waits behind it anyway, since reports are sent
53//!   one at a time, in order.
54//! - **Releases are never lost.** The release of a key whose press was
55//!   reported is reported too, whatever window it comes up in (a window whose
56//!   adapter a reader has not reached yet included), and never turned away
57//!   for lack of room: libatspi's legacy device sets the Orca modifier
58//!   (Insert, Caps Lock) from the press and clears it from the release, and a
59//!   lost release leaves every later key looking like an Orca command.
60//!   Presses behind a registry that hangs are bounded (64) and dropped
61//!   unsent once a second old, rather than replayed when it wakes.
62//! - **No registry, or a slow one.** The wait is bounded (200 ms, between Qt's
63//!   100 ms and GTK 3's 500 ms), and a press that runs out of it is
64//!   delivered. Until that report is finally answered, the presses that follow
65//!   are reported without waiting. So a registry that stops answering costs
66//!   one 200 ms pause, however many keys follow; one that answers every
67//!   report, but each more than 200 ms late, costs up to 200 ms on every
68//!   press made after it has answered the one before. A missing bus or
69//!   registry is answered at once, and asked again at most every few
70//!   seconds. The D-Bus traffic runs on a thread of its own; the event loop
71//!   only ever waits on a channel.
72//! - **Secure fields.** While a secure field has focus (one that declares
73//!   `ImePurpose::Password`, as every `PasswordField` does, revealed or not),
74//!   a key that types text (a character, a dead key, Space) is reported with
75//!   no text and the keysym `VoidSymbol`, press and release; every other key
76//!   is reported in full. Its physical keycode and modifiers are still
77//!   reported, because Orca matches its own commands on them, so a process
78//!   listening to the registry can still tell which physical keys were
79//!   pressed there. GTK 3.24 (`atk_key_event_from_gdk_event_key`) and Qt 6.8
80//!   (`QSpiApplicationAdaptor::eventFilter`) report such keys in full,
81//!   character included. Orca needs none of it there: it echoes nothing in a
82//!   password field (`presentKeyboardEvent`) and obscures those keys in its
83//!   own log.
84//! - **Input methods.** A key an input method takes never reaches Teksilo as a
85//!   `KeyboardInput`, on either Linux backend: on Wayland the compositor gives
86//!   it to the input method before the application, and on X11 winit drops
87//!   every event `XFilterEvent` claims. So those keys are not reported, as
88//!   they are not by a GTK application on Wayland; the composition itself
89//!   reaches the reader as text changes.
90//!
91//! On Windows and macOS the gate reports nothing and delivers every key: their
92//! screen readers read the keyboard themselves.
93
94use std::time::Instant;
95
96use winit::event::ElementState;
97use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey};
98
99#[cfg(all(unix, not(target_os = "macos")))]
100mod atspi;
101mod keysym;
102#[cfg(test)]
103mod tests;
104#[cfg(all(unix, not(target_os = "macos")))]
105mod worker;
106
107/// Whether a key event is a press or a release, as the registry numbers them
108/// (`ATSPI_KEY_PRESSED_EVENT`, `ATSPI_KEY_RELEASED_EVENT`).
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub(crate) enum DeviceEventKind {
111    /// A key went down, or repeated while held.
112    Pressed = 0,
113    /// A key came up.
114    Released = 1,
115}
116
117/// One key event as the AT-SPI registry reads it: the `(uiiiisb)` struct of
118/// `NotifyListenersSync` (Qt's `QSpiDeviceEvent`; the registry's own
119/// introspection XML says `(uiuuisb)`, but its demarshaller reads this).
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub(crate) struct DeviceEvent {
122    /// Press or release.
123    pub kind: DeviceEventKind,
124    /// The X keysym of the key, at the shift level it was pressed at: `0x61`
125    /// for `a`, `0x41` for Shift+`a`, `0xff54` for Down.
126    pub keysym: u32,
127    /// The X keycode: the Linux evdev code plus 8. Orca matches its own
128    /// commands on this.
129    pub hw_code: u32,
130    /// The X modifier mask held with the key: Shift `1 << 0`, Caps Lock
131    /// `1 << 1`, Control `1 << 2`, Alt (Mod1) `1 << 3`, Num Lock (Mod2)
132    /// `1 << 4`, Super (Mod4) `1 << 6`.
133    pub modifiers: u32,
134    /// Milliseconds on a clock of the reporter's own.
135    pub timestamp: u32,
136    /// The text of the key when it has printable text, else empty; a reader
137    /// names a key from its keysym when this is empty.
138    pub event_string: String,
139    /// Whether the key types its text: it has some, and no Control, Alt or
140    /// Super is held.
141    pub is_text: bool,
142}
143
144impl DeviceEvent {
145    /// The event for `key`, with the keyboard in `keyboard`, Num Lock known
146    /// to be on or not, at `timestamp`. When `withhold`, a key that types
147    /// text (a secure field has focus) is reported with no text and
148    /// `VoidSymbol`: its keycode and modifiers only.
149    pub(crate) fn for_key(
150        key: &KeyInput<'_>,
151        keyboard: KeyboardState,
152        num_lock: bool,
153        withhold: bool,
154        timestamp: u32,
155    ) -> Self {
156        let modifiers = keyboard.modifiers;
157        let withheld = withhold && keysym::types_text(key.logical_key);
158        let event_string = if withheld {
159            String::new()
160        } else {
161            keysym::event_string(key.logical_key)
162        };
163        let chorded = modifiers.control_key() || modifiers.alt_key() || modifiers.super_key();
164        Self {
165            kind: match key.state {
166                ElementState::Pressed => DeviceEventKind::Pressed,
167                ElementState::Released => DeviceEventKind::Released,
168            },
169            keysym: if withheld {
170                keysym::VOID_SYMBOL
171            } else {
172                keysym::keysym(key.logical_key, key.location, modifiers.shift_key())
173            },
174            hw_code: keysym::hardware_keycode(key.physical_key),
175            modifiers: keysym::modifier_mask(keyboard, num_lock),
176            timestamp,
177            is_text: !event_string.is_empty() && !chorded,
178            event_string,
179        }
180    }
181}
182
183/// A key event as the reporter needs it: the fields of winit's `KeyEvent`
184/// (which only winit can build) that say which key it was, plus the
185/// `is_synthetic` flag of the `WindowEvent` that carried it.
186#[derive(Debug, Clone, Copy)]
187pub struct KeyInput<'a> {
188    /// The key's place on the keyboard.
189    pub physical_key: PhysicalKey,
190    /// What the key means with the current layout and shift level.
191    pub logical_key: &'a Key,
192    /// Which of two keys of the same meaning it was: left or right, main
193    /// block or keypad.
194    pub location: KeyLocation,
195    /// Pressed or released. A repeat is a press.
196    pub state: ElementState,
197    /// A press the keyboard repeated because the key is held.
198    pub repeat: bool,
199    /// winit made this event up, for a key already held when focus changed.
200    pub synthetic: bool,
201}
202
203impl<'a> KeyInput<'a> {
204    /// The key of a winit `KeyboardInput`.
205    pub fn from_winit(event: &'a winit::event::KeyEvent, is_synthetic: bool) -> Self {
206        Self {
207            physical_key: event.physical_key,
208            logical_key: &event.logical_key,
209            location: event.location,
210            state: event.state,
211            repeat: event.repeat,
212            synthetic: is_synthetic,
213        }
214    }
215}
216
217/// What else is true of the keyboard when a key event arrives.
218#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
219pub struct KeyboardState {
220    /// The modifiers held, as winit last reported them.
221    pub modifiers: ModifiersState,
222    /// Whether Caps Lock is on, as far as the application knows.
223    pub caps_lock: bool,
224}
225
226/// Where a key lands, as far as reporting it goes.
227#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
228pub struct KeyTarget {
229    /// An assistive technology is attached to the window the key came to
230    /// ([`PlatformWindow::accessibility_active`](crate::PlatformWindow::accessibility_active)).
231    pub reader_attached: bool,
232    /// The focused widget is a secure text field (it declares
233    /// `ImePurpose::Password`, as a `PasswordField` does).
234    pub secure_field: bool,
235}
236
237/// What the registry said about a reported key press.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239// Only the Linux reporter and the tests answer anything but `Unanswered`.
240#[cfg_attr(not(all(unix, not(target_os = "macos"))), allow(dead_code))]
241pub(crate) enum ReportOutcome {
242    /// A listener took the key for itself; the application must not see it.
243    Consumed,
244    /// The registry answered, and nobody took the key.
245    NotConsumed,
246    /// No answer in time, or nobody to ask. The key is the application's.
247    Unanswered,
248}
249
250/// Where key events are reported. The platform's is the AT-SPI registry on
251/// Linux; a test hands the gate one of its own.
252pub(crate) trait KeyEventReporter {
253    /// Report a key press (or repeat) and wait, briefly, for the answer.
254    fn report_press(&mut self, event: &DeviceEvent) -> ReportOutcome;
255
256    /// Report a key release. Nothing waits for its answer.
257    fn report_release(&mut self, event: &DeviceEvent);
258}
259
260/// Whether a key event goes on to the application.
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub enum KeyDisposition {
263    /// Dispatch it as usual.
264    Deliver,
265    /// An assistive technology took it: drop it.
266    Drop,
267}
268
269/// Reports each key to the platform's assistive technology before the
270/// application acts on it, and says which keys the application must not see.
271/// See the [module documentation](self).
272pub struct KeyReportGate {
273    reporter: Option<Box<dyn KeyEventReporter>>,
274    /// Builds the platform's reporter the first time one is needed.
275    start: Option<fn() -> Box<dyn KeyEventReporter>>,
276    /// The keys held down, one entry per key from its first press to its
277    /// release. A handful at most.
278    held: Vec<Hold>,
279    /// Whether Num Lock is on, once a key has shown it; `None` until then.
280    /// See [`keysym::num_lock_shown`].
281    num_lock: Option<bool>,
282    /// The origin of [`DeviceEvent::timestamp`].
283    epoch: Instant,
284}
285
286/// What the gate remembers of one key from its first press to its release.
287#[derive(Debug, Clone)]
288struct Hold {
289    key: PhysicalKey,
290    /// Whether any press of this hold (the first, or a repeat) reached the
291    /// application. Its release is delivered exactly when one did.
292    delivered: bool,
293    /// Whether any press of this hold was reported. Its release then is,
294    /// whatever window it comes up in.
295    reported: bool,
296    /// Whether a press of this hold landed in a secure field. Its release
297    /// then keeps the character to itself too, wherever focus has gone.
298    withheld: bool,
299    /// The keysym, text and `is_text` the last reported press of this hold
300    /// carried, which its release carries too: winit composes on presses
301    /// only (the key that completes a dead-key sequence is `é` down and `e`
302    /// up), and Shift can come or go during a hold, while the release is of
303    /// the key that went down. Orca pairs a release with its press by them.
304    named: Option<(u32, String, bool)>,
305}
306
307impl std::fmt::Debug for KeyReportGate {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        f.debug_struct("KeyReportGate")
310            .field("started", &self.reporter.is_some())
311            .finish_non_exhaustive()
312    }
313}
314
315impl Default for KeyReportGate {
316    fn default() -> Self {
317        Self::for_platform()
318    }
319}
320
321impl KeyReportGate {
322    /// The gate for this platform. It starts nothing until an assistive
323    /// technology is attached; on Windows and macOS it never reports.
324    pub fn for_platform() -> Self {
325        #[cfg(all(unix, not(target_os = "macos")))]
326        let start: Option<fn() -> Box<dyn KeyEventReporter>> = Some(atspi::start_reporter);
327        #[cfg(not(all(unix, not(target_os = "macos"))))]
328        let start: Option<fn() -> Box<dyn KeyEventReporter>> = None;
329        Self {
330            reporter: None,
331            start,
332            held: Vec::new(),
333            num_lock: None,
334            epoch: Instant::now(),
335        }
336    }
337
338    /// A gate that reports to `reporter`.
339    #[cfg(test)]
340    pub(crate) fn with_reporter(reporter: impl KeyEventReporter + 'static) -> Self {
341        Self {
342            reporter: Some(Box::new(reporter)),
343            start: None,
344            held: Vec::new(),
345            num_lock: None,
346            epoch: Instant::now(),
347        }
348    }
349
350    /// Start the platform's reporter now, so that it is connected before the
351    /// first key. Call it once an assistive technology is attached; it does
352    /// nothing after the first call.
353    pub fn warm_up(&mut self) {
354        self.reporter();
355    }
356
357    fn reporter(&mut self) -> Option<&mut (dyn KeyEventReporter + 'static)> {
358        if self.reporter.is_none()
359            && let Some(start) = self.start.take()
360        {
361            self.reporter = Some(start());
362        }
363        self.reporter.as_deref_mut()
364    }
365
366    /// Report `key` if an assistive technology is attached where it lands
367    /// (`target`), and say whether the application gets it.
368    ///
369    /// A press is dropped when a listener took it. A release goes where the
370    /// presses of its hold went, whatever anyone says about the release, and
371    /// whether or not a reader is still attached: it is delivered when any
372    /// press of the hold was (the first press, or a repeat), so the
373    /// application sees a key come up exactly when it saw it go down. A new
374    /// press, as opposed to a repeat, starts a new hold. A synthetic event is
375    /// neither reported nor dropped.
376    pub fn filter(
377        &mut self,
378        key: &KeyInput<'_>,
379        keyboard: KeyboardState,
380        target: KeyTarget,
381    ) -> KeyDisposition {
382        let at_active = target.reader_attached;
383        if key.synthetic {
384            return KeyDisposition::Deliver;
385        }
386        let pressed = key.state == ElementState::Pressed;
387        if pressed && let Some(on) = keysym::num_lock_shown(key.logical_key, key.location) {
388            self.num_lock = Some(on);
389        }
390        let num_lock = self.num_lock == Some(true);
391        let epoch = self.epoch;
392        // Read only for a report. Wraps after 49 days, as X server time does.
393        let event = |key: &KeyInput<'_>, withhold: bool| {
394            let timestamp = epoch.elapsed().as_millis() as u32;
395            DeviceEvent::for_key(key, keyboard, num_lock, withhold, timestamp)
396        };
397        let secure = target.secure_field;
398        match key.state {
399            ElementState::Pressed => {
400                let reporter = if at_active { self.reporter() } else { None };
401                let reported = reporter.is_some();
402                let mut named = None;
403                let outcome = reporter.map_or(ReportOutcome::Unanswered, |reporter| {
404                    let press = event(key, secure);
405                    let outcome = reporter.report_press(&press);
406                    named = Some((press.keysym, press.event_string, press.is_text));
407                    outcome
408                });
409                // The Num Lock key's own event carries the state before it.
410                if *key.logical_key == Key::Named(NamedKey::NumLock) && !key.repeat {
411                    self.num_lock = self.num_lock.map(|on| !on);
412                }
413                let delivered = outcome != ReportOutcome::Consumed;
414                match self
415                    .held
416                    .iter_mut()
417                    .find(|hold| hold.key == key.physical_key)
418                {
419                    Some(hold) if key.repeat => {
420                        hold.delivered |= delivered;
421                        hold.reported |= reported;
422                        hold.withheld |= secure;
423                        hold.named = named.or(hold.named.take());
424                    }
425                    Some(hold) => {
426                        hold.delivered = delivered;
427                        hold.reported = reported;
428                        hold.withheld = secure;
429                        hold.named = named;
430                    }
431                    None => self.held.push(Hold {
432                        key: key.physical_key,
433                        delivered,
434                        reported,
435                        withheld: secure,
436                        named,
437                    }),
438                }
439                if delivered {
440                    KeyDisposition::Deliver
441                } else {
442                    KeyDisposition::Drop
443                }
444            }
445            ElementState::Released => {
446                let hold = self
447                    .held
448                    .iter()
449                    .position(|hold| hold.key == key.physical_key)
450                    .map(|index| self.held.swap_remove(index));
451                // The registry heard the press, so it hears the release, even
452                // in a window whose adapter no reader has reached yet:
453                // libatspi's legacy device sets the Orca modifier from the one
454                // and clears it from the other.
455                if (at_active || hold.as_ref().is_some_and(|hold| hold.reported))
456                    && let Some(reporter) = self.reporter()
457                {
458                    let withhold = secure || hold.as_ref().is_some_and(|hold| hold.withheld);
459                    let mut release = event(key, withhold);
460                    if !withhold
461                        && let Some((keysym, text, is_text)) =
462                            hold.as_ref().and_then(|hold| hold.named.clone())
463                    {
464                        release.keysym = keysym;
465                        release.event_string = text;
466                        release.is_text = is_text;
467                    }
468                    reporter.report_release(&release);
469                }
470                // A release with no press seen here (the key went down in
471                // another application) is the application's.
472                if hold.is_none_or(|hold| hold.delivered) {
473                    KeyDisposition::Deliver
474                } else {
475                    KeyDisposition::Drop
476                }
477            }
478        }
479    }
480}