diff --git a/src/main.rs b/src/main.rs index 9b2fe64..f5bd9cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,20 @@ use dirs::config_dir; +use input::event::{EventTrait, PointerEvent}; use input::event::keyboard::{KeyState, KeyboardEventTrait}; -use input::event::pointer::PointerScrollEvent; -use input::event::PointerEvent; -use input::{Event, Libinput, LibinputInterface}; +use input::event::pointer::{ButtonState, PointerScrollEvent}; +use input::{Event, Device, Libinput, LibinputInterface}; use libc::{O_RDONLY, O_RDWR, O_WRONLY}; use parser::Keys; use script_manager::ScriptManager; +use std::collections::HashMap; use std::fs::{File, OpenOptions}; use std::os::unix::{fs::OpenOptionsExt, io::OwnedFd}; use std::path::Path; +use std::time::{Duration, Instant}; use std::u32; +const SCROLL_HOLD_MS: u64 = 500; // how long a scroll "press" lasts + mod parser; mod script_manager; @@ -35,6 +39,26 @@ impl LibinputInterface for WBindKeysInterface { } } +fn convert_button_to_key_state(button_state: ButtonState) -> KeyState { + match button_state { + ButtonState::Pressed => KeyState::Pressed, + ButtonState::Released => KeyState::Released, + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +enum ScrollDir { + Up, + Down, + Left, + Right, +} + +struct ScrollState { + last_time: Instant, + active: bool, +} + fn main() { let mut input = Libinput::new_with_udev(WBindKeysInterface); input.udev_assign_seat("seat0").unwrap(); @@ -55,32 +79,48 @@ fn main() { script_manager.load_script(&script).unwrap(); let mut active_keys = Vec::new(); - let mut key:u32 = 0; - let mut state: KeyState = KeyState::Released; + let mut key_states: HashMap = HashMap::new(); + let mut scroll_states: HashMap = HashMap::new(); + loop { + let mut key: u32 = 0; + let mut state: KeyState = KeyState::Released; + input.dispatch().unwrap(); + + // --- Handle libinput events --- for event in &mut input { + let d: Device = event.device(); + match event { Event::Pointer(PointerEvent::Motion(_)) => {} // If event is mouse movement do nothing Event::Pointer(PointerEvent::Button(mouse_button)) => { key = mouse_button.button(); + state = convert_button_to_key_state(mouse_button.button_state()); } Event::Pointer(PointerEvent::ScrollWheel(scroll_event)) => { - if scroll_event.has_axis(input::event::pointer::Axis::Vertical) == true { - if scroll_event.scroll_value(input::event::pointer::Axis::Vertical) > 0.0 { - println!("Scroll Down!"); - key = 0x999 - }else { - println!("Scroll Up!"); - key = 0x998 - } - } else { - if scroll_event.scroll_value(input::event::pointer::Axis::Horizontal) > 0.0 { - print!("Scroll Right!"); - key = 0x997 + if let Some((scroll_dir, virtual_key)) = detect_scroll_direction(&scroll_event) { + let now = Instant::now(); + let entry = scroll_states.entry(scroll_dir).or_insert(ScrollState { + last_time: now, + active: false, + }); + + // Only emit "Pressed" if not active or expired + if !entry.active + || now.duration_since(entry.last_time) + > Duration::from_millis(SCROLL_HOLD_MS) + { + #[cfg(debug_assertions)] + println!("Scroll {:?} => Pressed ({:#03x})", scroll_dir, virtual_key); + + entry.active = true; + entry.last_time = now; + + key = virtual_key; + state = KeyState::Pressed; } else { - println!("Scroll Left!"); - key = 0x996 + // ignore repeated scrolls in the same direction } } } @@ -103,15 +143,72 @@ fn main() { } _ => {} // Ignore all other events } - if state == KeyState::Pressed { + + // Only trigger on transition: Released → Pressed + let prev_state = key_states.get(&key).copied().unwrap_or(KeyState::Released); + key_states.insert(key, state); + + if state == KeyState::Pressed && prev_state == KeyState::Released { let total_combo = active_keys .iter() .chain(std::iter::once(&key)) .copied() .collect::>(); - script_manager.handle_action(total_combo, state); + script_manager.handle_action(total_combo, state, d.id_vendor(), d.id_product()); + } + } + + // --- Handle synthetic scroll releases --- + let now = Instant::now(); + for (dir, state_entry) in scroll_states.iter_mut() { + if state_entry.active + && now.duration_since(state_entry.last_time) > Duration::from_millis(SCROLL_HOLD_MS) + { + let release_key = scroll_dir_to_key(*dir); + let prev_state = key_states.get(&release_key).copied().unwrap_or(KeyState::Released); + + if prev_state == KeyState::Pressed { + #[cfg(debug_assertions)] + println!("Scroll {:?} => Released ({:#03x})", dir, release_key); + + key_states.insert(release_key, KeyState::Released); + state_entry.active = false; + } } } + + // small sleep to avoid busy loop (libinput often blocks anyway) + std::thread::sleep(Duration::from_millis(5)); + } +} + +fn detect_scroll_direction(scroll_event: &E) -> Option<(ScrollDir, u32)> +where +E: PointerScrollEvent, +{ + if scroll_event.has_axis(input::event::pointer::Axis::Vertical) { + if scroll_event.scroll_value(input::event::pointer::Axis::Vertical) > 0.0 { + Some((ScrollDir::Down, 0x999)) + } else { + Some((ScrollDir::Up, 0x998)) + } + } else if scroll_event.has_axis(input::event::pointer::Axis::Horizontal) { + if scroll_event.scroll_value(input::event::pointer::Axis::Horizontal) > 0.0 { + Some((ScrollDir::Right, 0x997)) + } else { + Some((ScrollDir::Left, 0x996)) + } + } else { + None + } +} + +fn scroll_dir_to_key(dir: ScrollDir) -> u32 { + match dir { + ScrollDir::Up => 0x998, + ScrollDir::Down => 0x999, + ScrollDir::Left => 0x996, + ScrollDir::Right => 0x997, } } diff --git a/src/parser.rs b/src/parser.rs index 6bbfb50..cf97d0f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -86,6 +86,11 @@ pub enum Keys { Mouse3 = 0x112, Mouse4 = 0x113, Mouse5 = 0x114, + Mouse6 = 0x115, + Mouse7 = 0x116, + Mouse8 = 0x117, + Mouse9 = 0x118, + Mouse10 = 0x119, ScrollLeft = 0x996, ScrollRight = 0x997, ScrollUp = 0x998, @@ -186,11 +191,16 @@ pub fn parse_binding(binding: &str) -> Vec { "Mouse2"=> keys.push(Keys::Mouse2 as u32), "Mouse3"=> keys.push(Keys::Mouse3 as u32), "Mouse4"=> keys.push(Keys::Mouse4 as u32), - "Mouse5"=> keys.push(Keys::Mouse5 as u32), - "ScrollLeft"=> keys.push(Keys::ScrollLeft as u32), - "ScrollRight"=> keys.push(Keys::ScrollRight as u32), - "ScrollUp"=> keys.push(Keys::ScrollUp as u32), - "ScrollDown"=> keys.push(Keys::ScrollDown as u32), + "Mouse5"=> keys.push(Keys::Mouse5 as u32), + "Mouse6"=> keys.push(Keys::Mouse6 as u32), + "Mouse7"=> keys.push(Keys::Mouse7 as u32), + "Mouse8"=> keys.push(Keys::Mouse8 as u32), + "Mouse9"=> keys.push(Keys::Mouse9 as u32), + "Mouse10"=> keys.push(Keys::Mouse10 as u32), + "ScrollLeft"=> keys.push(Keys::ScrollLeft as u32), + "ScrollRight"=> keys.push(Keys::ScrollRight as u32), + "ScrollUp"=> keys.push(Keys::ScrollUp as u32), + "ScrollDown"=> keys.push(Keys::ScrollDown as u32), _ => {} } } @@ -287,11 +297,10 @@ mod tests { ("F11", Keys::F11 as u32), ("F12", Keys::F12 as u32), ("F12", Keys::F12 as u32), - ]; for (input, expected_output) in test_cases { assert_eq!(parse_binding(input), vec![expected_output], "Failed for input: {}", input); } } -} \ No newline at end of file +} diff --git a/src/script_manager.rs b/src/script_manager.rs index 69c71c8..e59daf2 100644 --- a/src/script_manager.rs +++ b/src/script_manager.rs @@ -1,68 +1,129 @@ use input::event::keyboard::KeyState; use mlua::Lua; -use std::collections::HashMap; use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; -use std::u32; use crate::parser::parse_binding; -#[derive(Debug)] +// ---------------------------------- +// Data structures +// ---------------------------------- + +#[derive(Debug, Clone)] +struct DeviceFilter { + vid: Option, + pid: Option, +} + +#[derive(Debug, Clone)] enum Bindtype { Command(String), } +#[derive(Debug, Clone)] +struct Binding { + keys: Vec, + action: Bindtype, + filter: DeviceFilter, +} + +// ---------------------------------- +// ScriptManager definition +// ---------------------------------- + pub struct ScriptManager { lua: &'static Lua, - actions: Arc, Bindtype>>>, + actions: Arc>>, } impl ScriptManager { pub fn new() -> Self { let lua = Box::leak(Box::new(Lua::new())); - let actions = Arc::new(Mutex::new(HashMap::new())); + let actions = Arc::new(Mutex::new(Vec::new())); ScriptManager { lua, actions } } + // -------------------------------------------------------- + // Register Lua functions + // -------------------------------------------------------- + pub fn register_functions(&self) -> Result<(), mlua::Error> { - let actions_str = Arc::clone(&self.actions); - - let basic_bind = - self.lua - .create_function(move |_, (binding, target): (String, String)| { - println!("Binding key: {:?}", binding); - println!("Target: {:?}", target); - let mut actions_lock = actions_str.lock().unwrap(); - let binding = parse_binding(&binding); - let target = Bindtype::Command(target); - actions_lock.insert(binding, target); - Ok(()) - })?; - self.lua.globals().set("bind", basic_bind)?; + let actions_ref = Arc::clone(&self.actions); + + // This defines the Lua function `bind{ ... }` + let bind_func = self.lua.create_function(move |_, tbl: mlua::Table| { + // Read required fields + let keys: String = tbl.get("keys")?; + let command: String = tbl.get("command")?; + + // Read optional fields + let vid: Option = tbl.get("vid").ok(); + let pid: Option = tbl.get("pid").ok(); + + println!("new binding: {:?}->{:?} (device filter: {:?}:{:?})", keys, command, vid, pid); + + let mut actions = actions_ref.lock().unwrap(); + + // Build the binding + let binding = Binding { + keys: parse_binding(&keys), + action: Bindtype::Command(command), + filter: DeviceFilter { vid, pid }, + }; + + actions.push(binding); + Ok(()) + })?; + + // Make it available globally in Lua + self.lua.globals().set("bind", bind_func)?; Ok(()) } + // -------------------------------------------------------- + // Load and execute a Lua script (e.g. config.lua) + // -------------------------------------------------------- + pub fn load_script(&self, script: &str) -> Result<(), mlua::Error> { self.lua.load(script).exec() } - pub fn handle_action(&self, total_combo: Vec, state: KeyState) { - if let Some(action) = self.actions.lock().unwrap().get(&total_combo) { - if state == KeyState::Pressed { - println!("Action: {:?}", action); - - match action { - Bindtype::Command(command) => { - // run_command_as_user(command); - Command::new("sh") - .arg("-c") - .arg(command) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("Failed to execute command"); + // -------------------------------------------------------- + // Handle key events, considering device filters + // -------------------------------------------------------- + + pub fn handle_action(&self, combo: Vec, state: KeyState, device_vid: u32, device_pid: u32) { + if state != KeyState::Pressed { + return; + } + + let actions = self.actions.lock().unwrap(); + + for binding in actions.iter() { + if binding.keys == combo { + let f = &binding.filter; + + // Device filtering logic: + let vid_ok = f.vid.map_or(true, |v| v == device_vid); + let pid_ok = f.pid.map_or(true, |p| p == device_pid); + + if vid_ok && pid_ok { + match &binding.action { + Bindtype::Command(cmd) => { + println!( + "Executing {:?} (VID={:#06x}, PID={:#06x})", + cmd, device_vid, device_pid + ); + Command::new("sh") + .arg("-c") + .arg(cmd) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to execute command"); + } } } }