Full project rename, decided 2026-06-10: - Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs. - C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h, PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl. PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants). - Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1. WIRE BREAK: clients must be rebuilt from this revision. - Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / …. - Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the persistent identity is unchanged, pinned fingerprints stay valid). - Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated. - scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated. Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of "desktop but no apps/settings" over the stream: plasmashell launched without XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and rendered an empty menu. The script sets the complete KDE session env (menu prefix, KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell. Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS, zero lumen references left outside .git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
//! Input events flowing client → host (and the host-side receive callback).
|
||||
//!
|
||||
//! Input rides the same transport as video but on its own wire tag
|
||||
//! ([`INPUT_MAGIC`]), so a session can demultiplex video from input by the first byte.
|
||||
|
||||
/// Wire tag distinguishing an input datagram from a video packet.
|
||||
pub const INPUT_MAGIC: u8 = 0xC8;
|
||||
|
||||
/// Fixed serialized size of an [`InputEvent`] on the wire (tag + fields).
|
||||
pub const INPUT_WIRE_LEN: usize = 1 + 1 + 4 + 4 + 4 + 4; // = 18
|
||||
|
||||
/// Kinds of input event. `#[repr(u8)]` so it crosses the C ABI as a byte tag.
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum InputKind {
|
||||
KeyDown = 0,
|
||||
KeyUp = 1,
|
||||
/// Relative motion: `x`/`y` carry `dx`/`dy`.
|
||||
MouseMove = 2,
|
||||
/// Absolute motion: `x`/`y` carry pixel coordinates.
|
||||
MouseMoveAbs = 3,
|
||||
MouseButtonDown = 4,
|
||||
MouseButtonUp = 5,
|
||||
/// `x` carries the (signed) scroll delta.
|
||||
MouseScroll = 6,
|
||||
/// `code` = button bit ([`gamepad`] `BTN_*`), `x` ≠ 0 = pressed, `flags` = pad index.
|
||||
GamepadButton = 7,
|
||||
/// `code` = axis id ([`gamepad`] `AXIS_*`), `x` = axis value, `flags` = pad index.
|
||||
/// Sticks are i16 range (−32768..32767) in the XInput/Moonlight convention — **+y =
|
||||
/// up** (unlike mouse coordinates); triggers 0..255.
|
||||
GamepadAxis = 8,
|
||||
}
|
||||
|
||||
/// The gamepad wire contract for [`InputKind::GamepadButton`]/[`InputKind::GamepadAxis`].
|
||||
///
|
||||
/// Everything follows the GameStream/XInput conventions end to end: buttons reuse
|
||||
/// GameStream's `buttonFlags` bit positions, sticks are −32768..32767 with **+y = up**,
|
||||
/// triggers 0..255 (what Moonlight sends and what the host's virtual xpad already
|
||||
/// consumes). One event carries one transition: `code` = the bit below, `x` = 1 pressed /
|
||||
/// 0 released. Axes are sent individually; the host accumulates per-pad state and emits
|
||||
/// one evdev SYN per event.
|
||||
pub mod gamepad {
|
||||
pub const BTN_DPAD_UP: u32 = 0x0001;
|
||||
pub const BTN_DPAD_DOWN: u32 = 0x0002;
|
||||
pub const BTN_DPAD_LEFT: u32 = 0x0004;
|
||||
pub const BTN_DPAD_RIGHT: u32 = 0x0008;
|
||||
pub const BTN_START: u32 = 0x0010;
|
||||
pub const BTN_BACK: u32 = 0x0020;
|
||||
pub const BTN_LS_CLICK: u32 = 0x0040;
|
||||
pub const BTN_RS_CLICK: u32 = 0x0080;
|
||||
pub const BTN_LB: u32 = 0x0100;
|
||||
pub const BTN_RB: u32 = 0x0200;
|
||||
pub const BTN_GUIDE: u32 = 0x0400;
|
||||
pub const BTN_A: u32 = 0x1000;
|
||||
pub const BTN_B: u32 = 0x2000;
|
||||
pub const BTN_X: u32 = 0x4000;
|
||||
pub const BTN_Y: u32 = 0x8000;
|
||||
|
||||
/// Axis ids for `InputKind::GamepadAxis`.
|
||||
pub const AXIS_LS_X: u32 = 0;
|
||||
pub const AXIS_LS_Y: u32 = 1;
|
||||
pub const AXIS_RS_X: u32 = 2;
|
||||
pub const AXIS_RS_Y: u32 = 3;
|
||||
/// Triggers: value range 0..255.
|
||||
pub const AXIS_LT: u32 = 4;
|
||||
pub const AXIS_RT: u32 = 5;
|
||||
}
|
||||
|
||||
impl InputKind {
|
||||
pub fn from_u8(v: u8) -> Option<InputKind> {
|
||||
use InputKind::*;
|
||||
Some(match v {
|
||||
0 => KeyDown,
|
||||
1 => KeyUp,
|
||||
2 => MouseMove,
|
||||
3 => MouseMoveAbs,
|
||||
4 => MouseButtonDown,
|
||||
5 => MouseButtonUp,
|
||||
6 => MouseScroll,
|
||||
7 => GamepadButton,
|
||||
8 => GamepadAxis,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A single input event. `#[repr(C)]` — shared verbatim with the C ABI as
|
||||
/// `PunktfunkInputEvent`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct InputEvent {
|
||||
pub kind: InputKind,
|
||||
pub _pad: [u8; 3],
|
||||
/// keycode / button id / axis id, depending on `kind`.
|
||||
pub code: u32,
|
||||
/// x / dx / abs-x / axis-value / scroll-delta, depending on `kind`.
|
||||
pub x: i32,
|
||||
/// y / dy / abs-y, depending on `kind`.
|
||||
pub y: i32,
|
||||
/// modifier bitmask or gamepad index.
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
impl InputEvent {
|
||||
/// Serialize to the fixed wire layout (`INPUT_MAGIC` + little-endian fields).
|
||||
pub fn encode(&self) -> [u8; INPUT_WIRE_LEN] {
|
||||
let mut b = [0u8; INPUT_WIRE_LEN];
|
||||
b[0] = INPUT_MAGIC;
|
||||
b[1] = self.kind as u8;
|
||||
b[2..6].copy_from_slice(&self.code.to_le_bytes());
|
||||
b[6..10].copy_from_slice(&self.x.to_le_bytes());
|
||||
b[10..14].copy_from_slice(&self.y.to_le_bytes());
|
||||
b[14..18].copy_from_slice(&self.flags.to_le_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
/// Parse from the wire layout. Returns `None` on bad tag/length/kind.
|
||||
pub fn decode(buf: &[u8]) -> Option<InputEvent> {
|
||||
if buf.len() < INPUT_WIRE_LEN || buf[0] != INPUT_MAGIC {
|
||||
return None;
|
||||
}
|
||||
let kind = InputKind::from_u8(buf[1])?;
|
||||
Some(InputEvent {
|
||||
kind,
|
||||
_pad: [0; 3],
|
||||
code: u32::from_le_bytes(buf[2..6].try_into().unwrap()),
|
||||
x: i32::from_le_bytes(buf[6..10].try_into().unwrap()),
|
||||
y: i32::from_le_bytes(buf[10..14].try_into().unwrap()),
|
||||
flags: u32::from_le_bytes(buf[14..18].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn input_wire_roundtrip() {
|
||||
let e = InputEvent {
|
||||
kind: InputKind::MouseMove,
|
||||
_pad: [0; 3],
|
||||
code: 0,
|
||||
x: -12,
|
||||
y: 34,
|
||||
flags: 0xABCD,
|
||||
};
|
||||
assert_eq!(InputEvent::decode(&e.encode()), Some(e));
|
||||
assert!(InputEvent::decode(&[0u8; INPUT_WIRE_LEN]).is_none()); // bad magic
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user