feat(host/input): headless KDE input via org_kde_kwin_fake_input
Desktop-mode (KWin) streaming had no input: the path was libei via the RemoteDesktop portal, which (a) isn't reachable from the host service env and (b) requires a human to approve "Allow remote control?" — a non-starter on a headless box. KWin's own headless RDP server (krdpserver) solves this with org_kde_kwin_fake_input, authorized by the exact same .desktop X-KDE-Wayland-Interfaces grant we already ship (org_kde_kwin_fake_input is listed alongside zkde_screencast_unstable_v1). Add a fake_input injector: vendor the protocol XML, bind the global as an ordinary Wayland client, authenticate (auto-accepted for an interface-authorized client — no dialog), and translate pointer (rel/abs), button, scroll, keyboard (raw evdev keycodes resolved by KWin's own keymap) and touch. Select it for KWin (compositor=="kwin" or XDG_CURRENT_DESKTOP KDE); GNOME stays on libei (it has neither fake_input nor the wlr protocols). PUNKTFUNK_INPUT_BACKEND=kwin forces it. cargo check + clippy + fmt green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="fake_input">
|
||||||
|
<copyright>
|
||||||
|
SPDX-FileCopyrightText: 2015 Martin Gräßlin
|
||||||
|
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
</copyright>
|
||||||
|
<interface name="org_kde_kwin_fake_input" version="4">
|
||||||
|
<description summary="Fake input manager">
|
||||||
|
This interface allows other processes to provide fake input events.
|
||||||
|
Purpose is on the one hand side to provide testing facilities like XTest
|
||||||
|
on X11, but also to support use cases like remote control (a remote
|
||||||
|
desktop server). The compositor gates the interface: it is only exposed
|
||||||
|
to clients authorized through their .desktop X-KDE-Wayland-Interfaces, so
|
||||||
|
binding it is the authorization — no per-event confirmation dialog.
|
||||||
|
</description>
|
||||||
|
<request name="authenticate">
|
||||||
|
<description summary="Information about the application requesting fake input">
|
||||||
|
A FakeInput is required to authenticate itself by providing the
|
||||||
|
application name and the reason for fake input. The compositor may use
|
||||||
|
this information to decide whether to allow or deny the request.
|
||||||
|
</description>
|
||||||
|
<arg name="application" type="string" summary="user visible name of the application requesting fake input"/>
|
||||||
|
<arg name="reason" type="string" summary="reason of why fake input is requested"/>
|
||||||
|
</request>
|
||||||
|
<request name="pointer_motion">
|
||||||
|
<description summary="pointer motion event"/>
|
||||||
|
<arg name="delta_x" type="fixed" summary="X delta of the relative pointer motion"/>
|
||||||
|
<arg name="delta_y" type="fixed" summary="Y delta of the relative pointer motion"/>
|
||||||
|
</request>
|
||||||
|
<request name="button">
|
||||||
|
<description summary="pointer button event"/>
|
||||||
|
<arg name="button" type="uint" summary="evdev button code"/>
|
||||||
|
<arg name="state" type="uint" summary="button state, 0 released, 1 pressed"/>
|
||||||
|
</request>
|
||||||
|
<request name="axis">
|
||||||
|
<description summary="pointer axis (scroll) event"/>
|
||||||
|
<arg name="axis" type="uint" summary="wl_pointer.axis (0 vertical, 1 horizontal)"/>
|
||||||
|
<arg name="value" type="fixed" summary="axis value"/>
|
||||||
|
</request>
|
||||||
|
<request name="touch_down" since="2">
|
||||||
|
<description summary="touch down event"/>
|
||||||
|
<arg name="id" type="uint" summary="unique id of this touch point; must not be reused until up"/>
|
||||||
|
<arg name="x" type="fixed" summary="x coordinate in global compositor space"/>
|
||||||
|
<arg name="y" type="fixed" summary="y coordinate in global compositor space"/>
|
||||||
|
</request>
|
||||||
|
<request name="touch_motion" since="2">
|
||||||
|
<description summary="touch motion event"/>
|
||||||
|
<arg name="id" type="uint" summary="unique id of an existing touch point"/>
|
||||||
|
<arg name="x" type="fixed" summary="x coordinate in global compositor space"/>
|
||||||
|
<arg name="y" type="fixed" summary="y coordinate in global compositor space"/>
|
||||||
|
</request>
|
||||||
|
<request name="touch_up" since="2">
|
||||||
|
<description summary="touch up event"/>
|
||||||
|
<arg name="id" type="uint" summary="unique id of an existing touch point"/>
|
||||||
|
</request>
|
||||||
|
<request name="touch_cancel" since="2">
|
||||||
|
<description summary="cancel all current touch points"/>
|
||||||
|
</request>
|
||||||
|
<request name="touch_frame" since="2">
|
||||||
|
<description summary="end a set of touch events (atomic frame)"/>
|
||||||
|
</request>
|
||||||
|
<request name="pointer_motion_absolute" since="3">
|
||||||
|
<description summary="absolute pointer motion event"/>
|
||||||
|
<arg name="x" type="fixed" summary="x coordinate in global compositor space"/>
|
||||||
|
<arg name="y" type="fixed" summary="y coordinate in global compositor space"/>
|
||||||
|
</request>
|
||||||
|
<request name="keyboard_key" since="4">
|
||||||
|
<description summary="keyboard key event"/>
|
||||||
|
<arg name="button" type="uint" summary="evdev key code"/>
|
||||||
|
<arg name="state" type="uint" summary="key state, 0 released, 1 pressed"/>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
@@ -24,6 +24,9 @@ pub trait InputInjector {
|
|||||||
pub enum Backend {
|
pub enum Backend {
|
||||||
/// wlroots virtual pointer + keyboard Wayland protocols — the headless-Sway path.
|
/// wlroots virtual pointer + keyboard Wayland protocols — the headless-Sway path.
|
||||||
WlrVirtual,
|
WlrVirtual,
|
||||||
|
/// KWin `org_kde_kwin_fake_input` — direct injection, no RemoteDesktop portal / approval dialog
|
||||||
|
/// (authorized by the host's `.desktop`). The headless KDE-Desktop path; what krdpserver uses.
|
||||||
|
KwinFakeInput,
|
||||||
/// libei via `reis` — Wayland-native (RemoteDesktop portal). Not yet implemented.
|
/// libei via `reis` — Wayland-native (RemoteDesktop portal). Not yet implemented.
|
||||||
Libei,
|
Libei,
|
||||||
/// libei directly against gamescope's own EIS socket (no portal): input lands in the
|
/// libei directly against gamescope's own EIS socket (no portal): input lands in the
|
||||||
@@ -47,6 +50,16 @@ pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
|
|||||||
anyhow::bail!("wlroots virtual input requires Linux + a Wayland compositor")
|
anyhow::bail!("wlroots virtual input requires Linux + a Wayland compositor")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Backend::KwinFakeInput => {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
Ok(Box::new(kwin_fake_input::KwinFakeInjector::open()?))
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
anyhow::bail!("KWin fake_input requires Linux + a KWin Wayland session")
|
||||||
|
}
|
||||||
|
}
|
||||||
Backend::Libei => {
|
Backend::Libei => {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
@@ -90,12 +103,18 @@ pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
|
|||||||
/// Pick the injection backend for the current session. gamescope hosts its own EIS server (no
|
/// Pick the injection backend for the current session. gamescope hosts its own EIS server (no
|
||||||
/// portal), so a gamescope session injects directly into it. wlroots/Sway only implements the
|
/// portal), so a gamescope session injects directly into it. wlroots/Sway only implements the
|
||||||
/// ScreenCast portal (no RemoteDesktop), so libei can't run there — use the wlr virtual-input
|
/// ScreenCast portal (no RemoteDesktop), so libei can't run there — use the wlr virtual-input
|
||||||
/// protocols. KWin and GNOME implement RemoteDesktop but not the wlr protocols, so use libei.
|
/// protocols. **KWin** exposes `org_kde_kwin_fake_input` (direct injection, no portal / approval
|
||||||
/// `PUNKTFUNK_INPUT_BACKEND=wlr|libei|gamescope|uinput` overrides the auto-detection.
|
/// dialog — the only headless-capable path; what krdpserver uses), so prefer it there. **GNOME**
|
||||||
|
/// has neither fake_input nor the wlr protocols, so it uses libei via the RemoteDesktop portal
|
||||||
|
/// (which needs a user to approve, or a pre-seeded grant — not truly headless).
|
||||||
|
/// `PUNKTFUNK_INPUT_BACKEND=wlr|kwin|libei|gamescope|uinput` overrides the auto-detection.
|
||||||
pub fn default_backend() -> Backend {
|
pub fn default_backend() -> Backend {
|
||||||
if let Ok(v) = std::env::var("PUNKTFUNK_INPUT_BACKEND") {
|
if let Ok(v) = std::env::var("PUNKTFUNK_INPUT_BACKEND") {
|
||||||
match v.trim().to_ascii_lowercase().as_str() {
|
match v.trim().to_ascii_lowercase().as_str() {
|
||||||
"wlr" | "wlroots" | "wlrvirtual" => return Backend::WlrVirtual,
|
"wlr" | "wlroots" | "wlrvirtual" => return Backend::WlrVirtual,
|
||||||
|
"kwin" | "fakeinput" | "fake_input" | "kwin-fake-input" => {
|
||||||
|
return Backend::KwinFakeInput
|
||||||
|
}
|
||||||
"libei" | "ei" | "portal" => return Backend::Libei,
|
"libei" | "ei" | "portal" => return Backend::Libei,
|
||||||
"gamescope" | "gamescope-ei" => return Backend::GamescopeEi,
|
"gamescope" | "gamescope-ei" => return Backend::GamescopeEi,
|
||||||
"uinput" => return Backend::Uinput,
|
"uinput" => return Backend::Uinput,
|
||||||
@@ -112,16 +131,26 @@ pub fn default_backend() -> Backend {
|
|||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
if crate::config::config()
|
// An explicit compositor pick (set per connect / mid-stream) is the strongest signal.
|
||||||
.compositor
|
let compositor = crate::config::config().compositor.clone();
|
||||||
.as_deref()
|
if let Some(c) = compositor.as_deref() {
|
||||||
.is_some_and(|v| v.trim().eq_ignore_ascii_case("gamescope"))
|
let c = c.trim();
|
||||||
{
|
if c.eq_ignore_ascii_case("gamescope") {
|
||||||
return Backend::GamescopeEi;
|
return Backend::GamescopeEi;
|
||||||
|
}
|
||||||
|
if c.eq_ignore_ascii_case("kwin") {
|
||||||
|
return Backend::KwinFakeInput;
|
||||||
|
}
|
||||||
|
if c.eq_ignore_ascii_case("wlroots") || c.eq_ignore_ascii_case("sway") {
|
||||||
|
return Backend::WlrVirtual;
|
||||||
|
}
|
||||||
|
// mutter (GNOME) falls through to the XDG_CURRENT_DESKTOP check below.
|
||||||
}
|
}
|
||||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
|
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
|
||||||
let d = desktop.to_ascii_uppercase();
|
let d = desktop.to_ascii_uppercase();
|
||||||
if d.contains("KDE") || d.contains("GNOME") {
|
if d.contains("KDE") {
|
||||||
|
Backend::KwinFakeInput
|
||||||
|
} else if d.contains("GNOME") {
|
||||||
Backend::Libei
|
Backend::Libei
|
||||||
} else {
|
} else {
|
||||||
Backend::WlrVirtual
|
Backend::WlrVirtual
|
||||||
@@ -478,6 +507,9 @@ pub mod gamepad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
#[path = "inject/linux/kwin_fake_input.rs"]
|
||||||
|
mod kwin_fake_input;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
#[path = "inject/linux/libei.rs"]
|
#[path = "inject/linux/libei.rs"]
|
||||||
mod libei;
|
mod libei;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
//! Headless input injection on KWin via the privileged `org_kde_kwin_fake_input` protocol — the
|
||||||
|
//! exact path KDE's own headless RDP server (`krdpserver`) uses. KWin advertises this restricted
|
||||||
|
//! global only to a client authorized through its installed `.desktop` `X-KDE-Wayland-Interfaces`
|
||||||
|
//! (we ship `io.unom.Punktfunk.Host.desktop`, which lists `org_kde_kwin_fake_input` alongside
|
||||||
|
//! `zkde_screencast_unstable_v1`). Binding the global IS the authorization, so injection needs **no
|
||||||
|
//! RemoteDesktop portal and no "Allow remote control?" dialog** — it works with no user present,
|
||||||
|
//! which the libei/portal path cannot. We connect as an ordinary Wayland client on the KWin session's
|
||||||
|
//! `$WAYLAND_DISPLAY` and translate events into fake-input requests; keyboard keys are raw Linux
|
||||||
|
//! evdev codes that KWin resolves through the session's own keymap (no keymap upload, unlike the wlr
|
||||||
|
//! virtual-keyboard path), and absolute pointer/touch coordinates are global compositor space — which
|
||||||
|
//! on a headless box (single per-session virtual output at the origin, scale 1) equals the streamed
|
||||||
|
//! output's pixels.
|
||||||
|
|
||||||
|
#![allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)]
|
||||||
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
use super::{gs_button_to_evdev, vk_to_evdev, InputEvent, InputInjector};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use punktfunk_core::input::InputKind;
|
||||||
|
use wayland_client::protocol::wl_registry::{self, WlRegistry};
|
||||||
|
use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle};
|
||||||
|
|
||||||
|
// Generate the client bindings for the vendored protocol XML inline (no build.rs), exactly like the
|
||||||
|
// KWin virtual-output backend. Path is relative to CARGO_MANIFEST_DIR.
|
||||||
|
#[allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)]
|
||||||
|
pub mod fake {
|
||||||
|
use wayland_client;
|
||||||
|
use wayland_client::protocol::*;
|
||||||
|
|
||||||
|
pub mod __interfaces {
|
||||||
|
use wayland_client::protocol::__interfaces::*;
|
||||||
|
wayland_scanner::generate_interfaces!("protocols/fake-input.xml");
|
||||||
|
}
|
||||||
|
use self::__interfaces::*;
|
||||||
|
|
||||||
|
wayland_scanner::generate_client_code!("protocols/fake-input.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
use fake::org_kde_kwin_fake_input::OrgKdeKwinFakeInput as FakeInput;
|
||||||
|
|
||||||
|
/// Highest interface version we drive. `keyboard_key` arrived at v4; KWin advertises ≥4.
|
||||||
|
const MAX_VERSION: u32 = 4;
|
||||||
|
|
||||||
|
/// `wl_pointer.axis` values used by `axis`.
|
||||||
|
const AXIS_VERTICAL: u32 = 0;
|
||||||
|
const AXIS_HORIZONTAL: u32 = 1;
|
||||||
|
/// `code` value marking a horizontal scroll event (mirrors `gamestream::input` / the wlr backend).
|
||||||
|
const SCROLL_HORIZONTAL: u32 = 1;
|
||||||
|
|
||||||
|
/// Registry-bound globals (the Wayland dispatch state).
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
fake: Option<FakeInput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<WlRegistry, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
registry: &WlRegistry,
|
||||||
|
event: wl_registry::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let wl_registry::Event::Global {
|
||||||
|
name,
|
||||||
|
interface,
|
||||||
|
version,
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
if interface == "org_kde_kwin_fake_input" {
|
||||||
|
state.fake = Some(registry.bind(name, version.min(MAX_VERSION), qh, ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fake_input emits no events.
|
||||||
|
impl Dispatch<FakeInput, ()> for State {
|
||||||
|
fn event(
|
||||||
|
_: &mut Self,
|
||||||
|
_: &FakeInput,
|
||||||
|
_: <FakeInput as Proxy>::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct KwinFakeInjector {
|
||||||
|
conn: Connection,
|
||||||
|
queue: EventQueue<State>,
|
||||||
|
state: State,
|
||||||
|
fake: FakeInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KwinFakeInjector {
|
||||||
|
pub fn open() -> Result<Self> {
|
||||||
|
let conn = Connection::connect_to_env()
|
||||||
|
.context("connect to KWin Wayland (is WAYLAND_DISPLAY set to the KWin socket?)")?;
|
||||||
|
let mut queue = conn.new_event_queue();
|
||||||
|
let qh = queue.handle();
|
||||||
|
let _registry = conn.display().get_registry(&qh, ());
|
||||||
|
let mut state = State::default();
|
||||||
|
queue
|
||||||
|
.roundtrip(&mut state)
|
||||||
|
.context("Wayland registry roundtrip")?;
|
||||||
|
|
||||||
|
let fake = state.fake.clone().context(
|
||||||
|
"KWin does not expose org_kde_kwin_fake_input to this client — install the host's \
|
||||||
|
.desktop (io.unom.Punktfunk.Host.desktop, X-KDE-Wayland-Interfaces) and re-login so \
|
||||||
|
KWin authorizes it (the grant is cached per-exe on first connect), or this is not a \
|
||||||
|
KWin session",
|
||||||
|
)?;
|
||||||
|
// Authenticate (the legacy handshake; for an interface-authorized client KWin accepts it
|
||||||
|
// without a dialog — same as krdpserver/krfb headless).
|
||||||
|
fake.authenticate("punktfunk".into(), "remote streaming input".into());
|
||||||
|
queue
|
||||||
|
.roundtrip(&mut state)
|
||||||
|
.context("fake_input authenticate roundtrip")?;
|
||||||
|
conn.flush().ok();
|
||||||
|
|
||||||
|
tracing::info!("KWin fake_input ready (headless keyboard/mouse/touch — no portal)");
|
||||||
|
Ok(Self {
|
||||||
|
conn,
|
||||||
|
queue,
|
||||||
|
state,
|
||||||
|
fake,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputInjector for KwinFakeInjector {
|
||||||
|
fn inject(&mut self, event: &InputEvent) -> Result<()> {
|
||||||
|
match event.kind {
|
||||||
|
InputKind::MouseMove => {
|
||||||
|
self.fake.pointer_motion(event.x as f64, event.y as f64);
|
||||||
|
}
|
||||||
|
InputKind::MouseMoveAbs => {
|
||||||
|
let w = (event.flags >> 16) & 0xffff;
|
||||||
|
let h = event.flags & 0xffff;
|
||||||
|
if w > 0 && h > 0 {
|
||||||
|
let x = event.x.clamp(0, w as i32) as f64;
|
||||||
|
let y = event.y.clamp(0, h as i32) as f64;
|
||||||
|
self.fake.pointer_motion_absolute(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputKind::MouseButtonDown | InputKind::MouseButtonUp => {
|
||||||
|
if let Some(btn) = gs_button_to_evdev(event.code) {
|
||||||
|
let st = u32::from(event.kind == InputKind::MouseButtonDown);
|
||||||
|
self.fake.button(btn, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputKind::MouseScroll => {
|
||||||
|
// GameStream sends WHEEL_DELTA(120)-scaled units; a notch ≈ 15px. Vertical flips
|
||||||
|
// sign on the Wayland axis, horizontal passes through — same as the wlr backend.
|
||||||
|
let horizontal = event.code == SCROLL_HORIZONTAL;
|
||||||
|
let axis = if horizontal {
|
||||||
|
AXIS_HORIZONTAL
|
||||||
|
} else {
|
||||||
|
AXIS_VERTICAL
|
||||||
|
};
|
||||||
|
let notches = event.x as f64 / 120.0;
|
||||||
|
let sign = if horizontal { 1.0 } else { -1.0 };
|
||||||
|
self.fake.axis(axis, sign * notches * 15.0);
|
||||||
|
}
|
||||||
|
InputKind::KeyDown | InputKind::KeyUp => {
|
||||||
|
// Raw evdev keycode; KWin resolves it through the session's own keymap (and tracks
|
||||||
|
// modifier state itself, so no separate modifiers request is needed).
|
||||||
|
if let Some(evdev) = vk_to_evdev(event.code as u8) {
|
||||||
|
let st = u32::from(event.kind == InputKind::KeyDown);
|
||||||
|
self.fake.keyboard_key(evdev as u32, st);
|
||||||
|
} else {
|
||||||
|
tracing::debug!(vk = event.code, "unmapped VK keycode — dropped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Touch: id = event.code, coords in the client surface w×h packed into flags (same
|
||||||
|
// absolute mapping as MouseMoveAbs). Each event is its own frame.
|
||||||
|
InputKind::TouchDown | InputKind::TouchMove => {
|
||||||
|
let w = (event.flags >> 16) & 0xffff;
|
||||||
|
let h = event.flags & 0xffff;
|
||||||
|
if w > 0 && h > 0 {
|
||||||
|
let x = event.x.clamp(0, w as i32) as f64;
|
||||||
|
let y = event.y.clamp(0, h as i32) as f64;
|
||||||
|
if event.kind == InputKind::TouchDown {
|
||||||
|
self.fake.touch_down(event.code, x, y);
|
||||||
|
} else {
|
||||||
|
self.fake.touch_motion(event.code, x, y);
|
||||||
|
}
|
||||||
|
self.fake.touch_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputKind::TouchUp => {
|
||||||
|
self.fake.touch_up(event.code);
|
||||||
|
self.fake.touch_frame();
|
||||||
|
}
|
||||||
|
// Gamepads are injected through uinput, not the compositor.
|
||||||
|
InputKind::GamepadButton | InputKind::GamepadAxis => {}
|
||||||
|
}
|
||||||
|
// Surface protocol errors / disconnects, then push the batch to the compositor.
|
||||||
|
self.queue
|
||||||
|
.dispatch_pending(&mut self.state)
|
||||||
|
.context("wayland dispatch")?;
|
||||||
|
self.conn.flush().context("wayland flush")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user