commit 6f0d10fa2fcfd0ced397d4163a81c0b5ddad548c Author: eater <=@eater.me> Date: Mon Dec 9 02:23:58 2019 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6a5bb24 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,79 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "misclick" +version = "0.1.0" +dependencies = [ + "maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rlua 0.16.3 (registry+https://github.com/rust-lang/crates.io-index)", + "x11 2.18.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pkg-config" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rlua" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "x11" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" +"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +"checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4" +"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" +"checksum rlua 0.16.3 (registry+https://github.com/rust-lang/crates.io-index)" = "62fc0e980c94fe9ef795b1bb3874649c8c6e9bb67d3b90d48380ba24c69c23ea" +"checksum x11 2.18.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39697e3123f715483d311b5826e254b6f3cfebdd83cf7ef3358f579c3d68e235" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..65d90e6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "misclick" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +[dependencies] +rlua = "0.16.3" +maplit = "1.0.2" + +[dependencies.x11] +version = "2.18.1" +features = ["xlib", "xtest"] \ No newline at end of file diff --git a/example/misclick.lua b/example/misclick.lua new file mode 100644 index 0000000..92e165c --- /dev/null +++ b/example/misclick.lua @@ -0,0 +1,2 @@ +bind(k.x, m.n(12)) +sync(m.left, k.n(132)) \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..95424f0 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,7 @@ +use std::collections::HashMap; +use crate::types::{Bind, Handler}; + +#[derive(Default, Debug)] +pub struct Config { + pub binds: HashMap +} diff --git a/src/lua_config.rs b/src/lua_config.rs new file mode 100644 index 0000000..f8bec1d --- /dev/null +++ b/src/lua_config.rs @@ -0,0 +1,39 @@ +use rlua::{Lua, Context, Error}; +use crate::maps::init_lua; +use crate::types::{Bind, Action, Handler}; +use crate::config::Config; +use std::default::Default; + +pub fn load_config(lua_config: &str) -> Result { + let lua = Lua::new(); + + let bind: Result<_, Error> = lua.context(|c: Context| { + init_lua(c)?; + + let g = c.globals(); + + let mut config = Config::default(); + + c.scope::<_, Result<_, Error>>(|scope| { + g.set("sync", scope.create_function_mut(|_: Context, args: (Bind, Bind, Option)| { + println!("config -> sync({:?}, {:?}, {:?})", args.0, args.1, args.2); + config + .binds + .insert(args.0, Handler { + action: Action::Sync(args.1), + passthrough: args.2.unwrap_or(false), + }); + + Ok(()) + })?)?; + + c.load(lua_config).exec()?; + + Ok(()) + })?; + + Ok(config) + }); + + Ok(bind?) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..804310b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,190 @@ +mod types; +mod config; +mod maps; +mod lua_config; + +extern crate x11; + +use x11::xlib; +use x11::xtest; + +use std::mem; +use std::ffi::{CStr, CString}; +use std::ptr::null; +use x11::xlib::{AnyModifier, ButtonPress, ButtonRelease, ButtonReleaseMask, ButtonPressMask, GrabModeAsync, RevertToNone, BadCursor, XEvent, xError, XAnyEvent, xEvent, Display, XKeyEvent, InputFocus, KeyPress, KeyRelease, KeyPressMask, KeyReleaseMask, XButtonEvent, CurrentTime, Button1Mask, Button2Mask, Window}; +use std::io::Cursor; +use x11::xinput2::XINotifyDetailNone; +use crate::types::{Bind, Action, Handler}; +use std::os::raw::c_long; +use std::collections::HashMap; +use crate::types::Bind::{Key, Mouse}; + +struct XInfo { + root: u64, + display: *mut Display, +} + +struct Subscription { + on: Bind, + handler: Handler, +} + +impl Subscription { + pub fn subscribe(&self, xinfo: &XInfo) { + unsafe { + match self.on { + Bind::Key(key) => { + xlib::XGrabKey(xinfo.display, key as i32, AnyModifier, xinfo.root, 0, GrabModeAsync, GrabModeAsync); + } + + Bind::Mouse(button) => { + xlib::XGrabButton(xinfo.display, button, AnyModifier, xinfo.root, 0, (ButtonReleaseMask | ButtonPressMask) as u32, GrabModeAsync, GrabModeAsync, xinfo.root, 0); + } + + _ => {} + } + } + } + + pub fn unsubscribe(&self, xinfo: &XInfo) { + unsafe { + match self.on { + Bind::Key(key) => { + xlib::XUngrabKey(xinfo.display, key as i32, AnyModifier, xinfo.root); + } + + Bind::Mouse(button) => { + xlib::XUngrabButton(xinfo.display, button, AnyModifier, xinfo.root); + } + + _ => {} + } + } + } + + pub fn handle(&self, pressed: bool, xinfo: &XInfo, coords: Target) { + println!("Handling trigger={:?} pressed={:?} action={:?}", self.on, pressed, self.handler.action); + + match &self.handler.action { + Action::Sync(bind) => { + match bind { + Bind::Key(key) => { + unsafe { + xtest::XTestFakeKeyEvent(xinfo.display, *key, pressed as i32, CurrentTime); + } + } + + Bind::Mouse(key) => { + unsafe { + xtest::XTestFakeButtonEvent(xinfo.display, *key, pressed as i32, CurrentTime); + } + } + + _ => {} + } + } + _ => {} + } + } +} + +fn main() { + let config = lua_config::load_config("\ + + # sync up keys with others + # tables defined in maps.rs + # manual key codes can be given via k.n() or m.n() + sync(k.n(52), m.left)\ + \ + ").unwrap(); + + let display = unsafe { xlib::XOpenDisplay(null()) }; + let root = unsafe { xlib::XDefaultRootWindow(display) }; + + unsafe { xtest::XTestGrabControl(display, true as i32) }; + + let xinfo = XInfo { display, root }; + + let mut subs: HashMap = HashMap::new(); + + for (bind, handler) in config.binds.iter() { + let sub = Subscription { handler: (*handler).clone(), on: (*bind).clone() }; + sub.subscribe(&xinfo); + subs.insert((*bind).clone(), sub); + } + + let mut event: xlib::XEvent = unsafe { mem::MaybeUninit::uninit().assume_init() }; + loop { + unsafe { xlib::XNextEvent(display, &mut event); } + match event.get_type() { + xlib::KeyPress => { + let key = Key(unsafe { event.key.keycode }); + if let Some(sub) = subs.get(&key) { + sub.handle(true, &xinfo, unsafe { event.key.to_coords() }); + } + } + + xlib::KeyRelease => { + let key = Key(unsafe { event.key.keycode }); + if let Some(sub) = subs.get(&key) { + sub.handle(false, &xinfo, unsafe { event.key.to_coords() }); + } + } + + xlib::ButtonPress => { + let key = Mouse(unsafe { event.button.button }); + if let Some(sub) = subs.get(&key) { + sub.handle(true, &xinfo, unsafe { event.button.to_coords() }); + } + } + + xlib::ButtonRelease => { + let key = Mouse(unsafe { event.button.button }); + if let Some(sub) = subs.get(&key) { + sub.handle(false, &xinfo, unsafe { event.button.to_coords() }); + } + } + + _ => {} + } + } +} + +struct Target { + x: i32, + y: i32, + x_root: i32, + y_root: i32, + window: Window, + subwindow: Window, +} + +trait ToCoords { + fn to_coords(&self) -> Target; +} + +impl ToCoords for XKeyEvent { + fn to_coords(&self) -> Target { + Target { + x: self.x, + y: self.y, + x_root: self.x_root, + y_root: self.y_root, + window: self.window, + subwindow: self.subwindow, + } + } +} + +impl ToCoords for XButtonEvent { + fn to_coords(&self) -> Target { + Target { + x: self.x, + y: self.y, + x_root: self.x_root, + y_root: self.y_root, + window: self.window, + subwindow: self.subwindow, + } + } +} \ No newline at end of file diff --git a/src/maps.rs b/src/maps.rs new file mode 100644 index 0000000..a818ea5 --- /dev/null +++ b/src/maps.rs @@ -0,0 +1,32 @@ +use rlua::{Result, Context}; +use crate::types::Bind; + +pub fn init_lua(lua: Context) -> Result<()> { + create_button_map(lua)?; + + Ok(()) +} + +fn create_button_map(lua: Context) -> Result<()> { + let globals = lua.globals(); + + let mouse_table = lua.create_table()?; + + mouse_table.set("n", lua.create_function(|_, n: u32| Ok(Bind::Mouse(n)))?)?; + mouse_table.set("left", Bind::Mouse(1))?; + mouse_table.set("middle", Bind::Mouse(2))?; + mouse_table.set("right", Bind::Mouse(3))?; + mouse_table.set("scroll_up", Bind::Mouse(4))?; + mouse_table.set("scroll_down", Bind::Mouse(5))?; + + globals.set("mouse", mouse_table.clone())?; + globals.set("m", mouse_table)?; + + let key_table = lua.create_table()?; + key_table.set("n", lua.create_function(|_, n: u32| Ok(Bind::Key(n)))?)?; + + globals.set("key", key_table.clone())?; + globals.set("k", key_table.clone())?; + + return Ok(()); +} \ No newline at end of file diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..53a8b60 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,90 @@ +use rlua::{FromLua, Context, Value, Error, ToLua}; +use std::convert::TryFrom; + +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone)] +pub enum Bind { + Mouse(u32), + Key(u32), + Ping(String), +} + +#[derive(Debug, Clone)] +pub struct Handler { + pub passthrough: bool, + pub action: Action, +} + +#[derive(Debug, Clone)] +pub enum Action { + Sync(Bind), + Press(Bind), + Release(Bind), + Sleep(u32), + List(Vec), +} + +impl<'lua> FromLua<'lua> for Bind { + fn from_lua(lua_value: Value<'lua>, _: Context<'lua>) -> Result { + if let Value::Integer(n) = lua_value { + return Ok(Bind::Key(u32::try_from(n).map_err(|e| Error::external(e))?)); + } + + if let Value::String(c) = lua_value { + return Ok(Bind::Ping(String::from(c.to_str()?))); + } + + if let Value::Table(table) = lua_value { + let typ: String = table.get("type")?; + let bind = match typ.as_str() { + "mouse" => Bind::Mouse(table.get("code")?), + "key" => Bind::Key(table.get("code")?), + "ping" => Bind::Ping(table.get("code")?), + _ => return Err(Error::RuntimeError("Type should be one of mouse, key or ping".to_string())) + }; + + return Ok(bind); + } + + Err(Error::RuntimeError("Can't create Bind from non-table".to_string())) + } +} + +impl<'lua> ToLua<'lua> for Bind { + fn to_lua(self, lua: Context<'lua>) -> Result, Error> { + let table = lua.create_table()?; + table.set("type", match self { + Bind::Key(_) => "key", + Bind::Ping(_) => "ping", + Bind::Mouse(_) => "mouse", + })?; + + match self { + Bind::Key(n) => table.set("code", n)?, + Bind::Mouse(n) => table.set("code", n)?, + Bind::Ping(c) => table.set("code", c)?, + } + + Ok(table.to_lua(lua)?) + } +} + +impl<'lua> FromLua<'lua> for Action { + fn from_lua(lua_value: Value<'lua>, _: Context<'lua>) -> Result { + let table = if let Value::Table(table) = lua_value { + table + } else { + return Err(Error::RuntimeError("Action should be a table".to_string())); + }; + + let res = match table.get::<&str, String>("type")?.as_str() { + "sync" => Action::Sync(table.get("bind")?), + "press" => Action::Press(table.get("bind")?), + "release" => Action::Release(table.get("bind")?), + "sleep" => Action::Sleep(table.get("time")?), + "list" => Action::List(table.get("items")?), + _ => return Err(Error::RuntimeError("Action type is invalid, should be one of sync, release, press, sleep, list".to_string())) + }; + + Ok(res) + } +}