Initial POC

This commit is contained in:
2026-05-02 22:10:21 +02:00
commit b97976bc09
5 changed files with 475 additions and 0 deletions

105
src/main.rs Normal file
View File

@@ -0,0 +1,105 @@
mod virtual_gamepad;
use std::path::PathBuf;
use evdev::{Device, EventType, KeyCode};
use tokio::sync::mpsc::{Receiver, Sender, channel};
use virtual_gamepad::VirtualGamepad;
async fn watch_keyboard(path: PathBuf, target: KeyCode, tx: Sender<PathBuf>) {
let device = Device::open(path.clone()).unwrap();
let Ok(mut stream) = device.into_event_stream() else {
return;
};
while let Ok(event) = stream.next_event().await {
if event.event_type() == EventType::KEY
&& event.code() == target.code()
&& event.value() == 0
{
let _ = tx.send(path).await;
return;
}
}
}
fn enumerate_keyboards() -> Vec<PathBuf> {
evdev::enumerate()
.filter_map(|(path, device)| {
if device.supported_keys()?.contains(KeyCode::KEY_SPACE) {
Some(path)
} else {
None
}
})
.collect()
}
async fn find_target_keyboard(device_paths: &Vec<PathBuf>) -> Option<Device>{
let (tx, mut rx): (Sender<PathBuf>, Receiver<PathBuf>) = channel(1);
let mut handles = vec![];
for path in device_paths {
handles.push(tokio::spawn(watch_keyboard(
path.clone(),
KeyCode::KEY_SPACE,
tx.clone(),
)));
}
drop(tx);
let path = rx.recv().await?;
for handle in handles {
handle.abort();
}
Device::open(path).ok()
}
#[tokio::main(flavor = "local")]
async fn main() {
let devices_paths = enumerate_keyboards();
println!("Found the following devices");
for path in &devices_paths {
if let Ok(d) = Device::open(path){
println!("\t{} ({})", d.name().unwrap_or("null"), path.display())
}
}
let Some(kb) = find_target_keyboard(&devices_paths).await else {
return;
};
let Ok(mut kb) = kb.into_event_stream() else {
return;
};
let mut gamepad = VirtualGamepad::new("Virtual Gamepad 1");
if !kb.device_mut().grab().is_ok() {
println!("Failed to grab device");
return;
}
println!("Device is grabbed");
gamepad.handle_events(&mut kb).await;
match kb.device_mut().ungrab() {
Err(err) => println!("Failed to release device: {err}"),
_ => ()
};
println!("Device released");
}

147
src/virtual_gamepad.rs Normal file
View File

@@ -0,0 +1,147 @@
use evdev::uinput::VirtualDevice;
use evdev::{
AbsInfo, AbsoluteAxisCode, AttributeSet, EventStream, EventType, InputEvent, KeyCode, UinputAbsSetup
};
use std::collections::HashMap;
#[derive(PartialEq, Debug)]
#[derive(Clone, Copy)]
#[repr(u8)]
pub enum Direction {
Up = 0,
Down,
Left,
Right,
}
#[derive(Debug)]
pub struct VirtualGamepad {
_name: String,
ui: VirtualDevice,
btn_bindings: HashMap<u16, KeyCode>,
dpad_bindings: HashMap<u16, Direction>,
dpad_state: [i8; 4],
}
impl VirtualGamepad {
pub fn new(name: &str) -> VirtualGamepad {
let mut keys = AttributeSet::<KeyCode>::new();
keys.insert(KeyCode::BTN_SOUTH);
keys.insert(KeyCode::BTN_EAST);
keys.insert(KeyCode::BTN_NORTH);
keys.insert(KeyCode::BTN_WEST);
keys.insert(KeyCode::BTN_TL);
keys.insert(KeyCode::BTN_TR);
keys.insert(KeyCode::BTN_TL2);
keys.insert(KeyCode::BTN_TR2);
keys.insert(KeyCode::BTN_START);
keys.insert(KeyCode::BTN_SELECT);
keys.insert(KeyCode::BTN_MODE);
keys.insert(KeyCode::BTN_THUMBL);
keys.insert(KeyCode::BTN_THUMBR);
let mut btn_bindings = HashMap::new();
btn_bindings.insert(KeyCode::KEY_J.code(), KeyCode::BTN_SOUTH);
btn_bindings.insert(KeyCode::KEY_I.code(), KeyCode::BTN_EAST);
btn_bindings.insert(KeyCode::KEY_O.code(), KeyCode::BTN_NORTH);
btn_bindings.insert(KeyCode::KEY_L.code(), KeyCode::BTN_WEST);
btn_bindings.insert(KeyCode::KEY_M.code(), KeyCode::BTN_TR);
btn_bindings.insert(KeyCode::KEY_K.code(), KeyCode::BTN_THUMBL);
btn_bindings.insert(KeyCode::KEY_SEMICOLON.code(), KeyCode::BTN_TL);
btn_bindings.insert(KeyCode::KEY_ENTER.code(), KeyCode::BTN_START);
btn_bindings.insert(KeyCode::KEY_ESC.code(), KeyCode::BTN_SELECT);
let mut dpad_bindings = HashMap::new();
dpad_bindings.insert(KeyCode::KEY_SPACE.code(), Direction::Up);
dpad_bindings.insert(KeyCode::KEY_E.code(), Direction::Down);
dpad_bindings.insert(KeyCode::KEY_F.code(), Direction::Right);
dpad_bindings.insert(KeyCode::KEY_W.code(), Direction::Left);
let ui = VirtualDevice::builder()
.unwrap()
.name(&name)
.with_keys(&keys)
.unwrap()
.with_absolute_axis(&UinputAbsSetup::new(
AbsoluteAxisCode::ABS_HAT0X,
AbsInfo::new(0, -1, 1, 0, 0, 0),
))
.unwrap()
.with_absolute_axis(&UinputAbsSetup::new(
AbsoluteAxisCode::ABS_HAT0Y,
AbsInfo::new(0, -1, 1, 0, 0, 0),
))
.unwrap()
.build()
.unwrap();
VirtualGamepad {
ui,
btn_bindings,
dpad_bindings,
_name: name.to_owned(),
dpad_state: [0, 0, 0, 0],
}
}
pub fn press_btn(&mut self, btn: KeyCode, press: bool) {
let _ = self
.ui
.emit(&[InputEvent::new(EventType::KEY.0, btn.code(), press as i32)]);
}
pub fn press_dpad(&mut self, btn: Direction, press: bool) {
match btn {
Direction::Up => self.dpad_state[0] = press as i8,
Direction::Down => self.dpad_state[1] = press as i8,
Direction::Left => self.dpad_state[2] = press as i8,
Direction::Right => self.dpad_state[3] = press as i8,
};
if [Direction::Up, Direction::Down].contains(&btn) {
let _ = self.ui.emit(&[InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_HAT0Y.0,
(-self.dpad_state[0] + self.dpad_state[1]) as i32,
)]);
} else {
let _ = self.ui.emit(&[InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_HAT0X.0,
(-self.dpad_state[2] + self.dpad_state[3]) as i32,
)]);
}
}
pub async fn handle_events(&mut self, es: &mut EventStream) {
while let Ok(event) = es.next_event().await {
if event.event_type() != EventType::KEY {
continue;
}
if let Some(btn) = self.btn_bindings.get(&event.code()) {
self.press_btn(btn.clone(), event.value() > 0);
}
if let Some(dir) = self.dpad_bindings.get(&event.code()) {
self.press_dpad(dir.clone(), event.value() > 0);
}
if event.code() == KeyCode::KEY_END.code() {
return;
}
}
}
}