Coverage Report

Created: 2026-07-21 00:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
super-stt-daemon/src/output/keyboard/mod.rs
Line
Count
Source
1
// SPDX-License-Identifier: GPL-3.0-only
2
3
mod enigo_backend;
4
mod xdg_portal_backend;
5
mod ydotool_backend;
6
7
use anyhow::Result;
8
use log::{debug, warn};
9
use super_stt_shared::models::write_method::WriteMethod;
10
11
use enigo_backend::EnigoBackend;
12
use xdg_portal_backend::XdgPortalBackend;
13
use ydotool_backend::YdotoolBackend;
14
15
/// Keyboard simulation backend.
16
///
17
/// # Safety
18
///
19
/// `Simulator` is `Send + Sync` because it is only ever accessed by one
20
/// recording session at a time (guarded by `busy`). The `!Send`
21
/// inner type (`Enigo` with raw xkbcommon pointers) is never used
22
/// concurrently.
23
pub enum Simulator {
24
    WaylandProtocol(Box<EnigoBackend>),
25
    Ydotool(YdotoolBackend),
26
    XdgPortal(XdgPortalBackend),
27
}
28
29
// SAFETY: see Simulator doc comment — single-writer access enforced by daemon.
30
unsafe impl Send for Simulator {}
31
unsafe impl Sync for Simulator {}
32
33
impl Simulator {
34
    /// Create a simulator for the requested write method.
35
    ///
36
    /// # Errors
37
    /// Returns an error only when a *specific* method is requested and fails.
38
    /// `Auto` always falls back to Wayland protocol.
39
0
    pub async fn new(method: WriteMethod) -> Result<Self> {
40
0
        let sim = match method {
41
0
            WriteMethod::Auto => Self::auto().await?,
42
            WriteMethod::XdgDesktopPortal => {
43
0
                let backend = XdgPortalBackend::new().await?;
44
0
                Self::XdgPortal(backend)
45
            }
46
            WriteMethod::Ydotool => {
47
0
                anyhow::ensure!(YdotoolBackend::is_available(), "ydotool is not available");
48
0
                Self::Ydotool(YdotoolBackend::new())
49
            }
50
0
            WriteMethod::WaylandProtocol => Self::WaylandProtocol(Box::new(EnigoBackend::new()?)),
51
        };
52
0
        Ok(sim)
53
0
    }
54
55
    /// Auto-detect: XDG Portal → ydotool → Wayland protocol.
56
0
    async fn auto() -> Result<Self> {
57
0
        debug!("Auto-detecting write method...");
58
59
0
        let portal_available = XdgPortalBackend::is_available().await;
60
0
        debug!("XDG Desktop Portal available: {portal_available}");
61
0
        if portal_available {
62
0
            match XdgPortalBackend::new().await {
63
0
                Ok(backend) => return Ok(Self::XdgPortal(backend)),
64
0
                Err(e) => warn!("XDG Portal available but session failed: {e}"),
65
            }
66
0
        }
67
68
0
        let ydotool_available = YdotoolBackend::is_available();
69
0
        debug!("ydotool available: {ydotool_available}");
70
0
        if ydotool_available {
71
0
            return Ok(Self::Ydotool(YdotoolBackend::new()));
72
0
        }
73
74
0
        debug!("Falling back to Wayland protocol");
75
0
        Ok(Self::WaylandProtocol(Box::new(EnigoBackend::new()?)))
76
0
    }
77
78
    /// Human-readable name for logging.
79
    #[must_use]
80
0
    pub fn name(&self) -> &'static str {
81
0
        match self {
82
0
            Self::XdgPortal(_) => "XDG Desktop Portal",
83
0
            Self::Ydotool(_) => "ydotool",
84
0
            Self::WaylandProtocol(_) => "Wayland protocol",
85
        }
86
0
    }
87
88
    /// Type text using the active backend. Async so the portal backend awaits
89
    /// its D-Bus calls directly and the blocking backends yield the worker
90
    /// (audit Tier 3 #35).
91
    ///
92
    /// # Errors
93
    /// Returns an error if the backend fails to simulate key input.
94
0
    pub async fn type_text(&mut self, text: &str) -> Result<()> {
95
0
        match self {
96
            // The enigo/ydotool backends are synchronous and `!Send`; run them
97
            // under `block_in_place` so their handle never crosses an await and
98
            // the runtime spins up a replacement worker rather than stalling. The
99
            // portal backend is genuinely async — await it directly.
100
0
            Self::WaylandProtocol(b) => tokio::task::block_in_place(|| b.type_text(text)),
101
0
            Self::Ydotool(_) => tokio::task::block_in_place(|| YdotoolBackend::type_text(text)),
102
0
            Self::XdgPortal(b) => b.type_text(text).await,
103
        }
104
0
    }
105
106
    /// Backspace N characters using the active backend.
107
    ///
108
    /// # Errors
109
    /// Returns an error if the backend fails to simulate key input.
110
0
    pub async fn backspace_n(&mut self, n: usize) -> Result<()> {
111
0
        match self {
112
0
            Self::WaylandProtocol(b) => {
113
0
                tokio::task::block_in_place(|| b.backspace_n(n));
114
0
                Ok(())
115
            }
116
0
            Self::Ydotool(_) => tokio::task::block_in_place(|| YdotoolBackend::backspace_n(n)),
117
0
            Self::XdgPortal(b) => b.backspace_n(n).await,
118
        }
119
0
    }
120
}