From e28a69d29ed1746760199f7a91aeabbbd71d20a1 Mon Sep 17 00:00:00 2001 From: Gabriel <68819302+obsidianical@users.noreply.github.com> Date: Sun, 3 Jul 2022 16:52:53 +0200 Subject: [PATCH] Implemented "String" playing! Should now be compatible with xmacro yay --- README.md | 4 +- src/bin/easymacroplay.rs | 75 +++++++++--- src/chartbl.rs | 259 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/x11_safe_wrapper.rs | 32 ++++- 5 files changed, 347 insertions(+), 24 deletions(-) create mode 100644 src/chartbl.rs diff --git a/README.md b/README.md index a9f7045..93f3272 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ This program is inspired by [**xmacro**](https://github.com/Ortega-Dan/xmacroInc

:pen_ballpoint: TODOs :notepad_spiral:

-- [ ] Playing macros (xmacro like) +- [x] Playing macros (xmacro like) - [x] Delay support - [x] KeySym/KeyCode/KeyStr action support - [x] MotionNotify and button support - - [ ] String typing support (Not too high priority, but I'll add it some time probably) + - [x] String typing support (Not too high priority, but I'll add it some time probably) - [x] ExecBlock/ExecNoBlock support (not high priority) - [x] ExecBlock - [x] ExecNoBlock diff --git a/src/bin/easymacroplay.rs b/src/bin/easymacroplay.rs index c2c0039..430c94a 100644 --- a/src/bin/easymacroplay.rs +++ b/src/bin/easymacroplay.rs @@ -1,10 +1,12 @@ use clap::Parser; -use easymacros::x11_safe_wrapper::{Keysym, XDisplay}; +use easymacros::x11_safe_wrapper::{Keysym, string_to_keysym, XDisplay}; use std::ffi::CString; use std::process::{Command, exit}; use std::time::Duration; use std::{fs, thread}; use std::io::stdin; +use x11::keysym::XK_Shift_L; +use easymacros::chartbl::CHARTBL; /// Macro player module for easymacros. It's partially compatible with xmacro macros, with aim for full compatibility. #[derive(Parser, Debug)] @@ -64,51 +66,84 @@ fn get_remote(display_name: Option) -> XDisplay { } fn run_instruction(instruction: &str, dpy: &XDisplay) { - let instruction: Vec<&str> = instruction.split(' ').collect(); + let instruction_split: Vec<&str> = instruction.split(' ').collect(); - match instruction[0] { - "Delay" => thread::sleep(Duration::from_millis(instruction[1].parse().unwrap())), - "ButtonPress" => dpy.send_fake_buttonpress(instruction[1].parse().unwrap()), - "ButtonRelease" => dpy.send_fake_buttonrelease(instruction[1].parse().unwrap()), + match instruction_split[0] { + "Delay" => thread::sleep(Duration::from_millis(instruction_split[1].parse().unwrap())), + "ButtonPress" => dpy.send_fake_buttonpress(instruction_split[1].parse().unwrap()), + "ButtonRelease" => dpy.send_fake_buttonrelease(instruction_split[1].parse().unwrap()), "MotionNotify" => dpy - .send_fake_motion_event(instruction[1].parse().unwrap(), instruction[2].parse().unwrap()), - "KeyCodePress" => dpy.send_fake_keypress_from_code(instruction[1].parse().unwrap()), - "KeyCodeRelease" => dpy.send_fake_keyrelease_from_code(instruction[1].parse().unwrap()), - "KeySymPress" => dpy.send_fake_keypress_from_keysym(instruction[1].parse().unwrap()), + .send_fake_motion_event(instruction_split[1].parse().unwrap(), instruction_split[2].parse().unwrap()), + "KeyCodePress" => dpy.send_fake_keypress_from_code(instruction_split[1].parse().unwrap()), + "KeyCodeRelease" => dpy.send_fake_keyrelease_from_code(instruction_split[1].parse().unwrap()), + "KeySymPress" => dpy.send_fake_keypress_from_keysym(instruction_split[1].parse().unwrap()), "KeySymRelease" => { - dpy.send_fake_keyrelease_from_keysym(instruction[1].parse().unwrap()) + dpy.send_fake_keyrelease_from_keysym(instruction_split[1].parse().unwrap()) } "KeySym" => { - let key: Keysym = instruction[1].parse().unwrap(); + let key: Keysym = instruction_split[1].parse().unwrap(); dpy.send_fake_keypress_from_keysym(key); dpy.send_fake_keyrelease_from_keysym(key); } "KeyStrPress" => { - dpy.send_fake_keypress_from_string(CString::new(instruction[1]).unwrap().as_bytes()) + dpy.send_fake_keypress_from_string(CString::new(instruction_split[1]).unwrap().as_bytes()) } "KeyStrRelease" => dpy - .send_fake_keyrelease_from_string(CString::new(instruction[1]).unwrap().as_bytes()), + .send_fake_keyrelease_from_string(CString::new(instruction_split[1]).unwrap().as_bytes()), "KeyStr" => { - let keystring = CString::new(instruction[1]).unwrap(); + let keystring = CString::new(instruction_split[1]).unwrap(); dpy.send_fake_keypress_from_string(keystring.as_bytes()); dpy.send_fake_keyrelease_from_string(keystring.as_bytes()); } "String" => { - println!("Strings are currently not supported."); + for c in instruction["String".len()+1..].chars() { + send_char(dpy, c); + } } "ExecBlock" | "ExecNoBlock" => { - let mut command = Command::new(instruction[1]); - for arg in &instruction[2..] { + let mut command = Command::new(instruction_split[1]); + for arg in &instruction_split[2..] { command.arg(arg); } - if instruction[0] == "ExecBlock" { + if instruction_split[0] == "ExecBlock" { command.status(); } else { command.spawn(); } } c => { - panic!("Unknown command {:?}", instruction) + panic!("Unknown command {:?}", instruction_split) } } +} + +fn send_char(dpy: &XDisplay, c: char) { + let keysym = string_to_keysym(CHARTBL[c as usize].as_ref()); + let keycode = dpy.keysym_to_keycode(keysym); + + if keycode == 0 { + eprintln!("No keycode found for character '{}'", c); + return; + } + + let map_ks = dpy.get_keyboard_mapping(keycode, 1); + if unsafe { map_ks[0] } == 0 { + eprintln!("XGetKeyboardMapping failed (keycode: {})", keycode); + return; + } + + let (ks_lower, ks_upper) = dpy.convert_case(keysym); + + let mut shift_needed = true; + if keysym == map_ks[0] && (keysym == ks_lower && keysym == ks_upper) { + shift_needed = false; + } + if keysym == ks_lower && keysym != ks_upper { + shift_needed = false; + } + + if shift_needed { dpy.send_fake_keypress_from_keysym(XK_Shift_L as Keysym); } + dpy.send_fake_keypress_from_code(keycode); + dpy.send_fake_keyrelease_from_code(keycode); + if shift_needed { dpy.send_fake_keyrelease_from_keysym(XK_Shift_L as Keysym); } } \ No newline at end of file diff --git a/src/chartbl.rs b/src/chartbl.rs new file mode 100644 index 0000000..a624d01 --- /dev/null +++ b/src/chartbl.rs @@ -0,0 +1,259 @@ +// the list is copied from https://github.com/Ortega-Dan/xmacroIncludingDelayCapturing/blob/1767fd310227dd13ae488b7d2821cc4bbf3847e0/chartbl.h#L27 +pub const CHARTBL: [&str; 256] = [ + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "BackSpace\0", + "Tab\0", + "\0", + "\0", + "\0", + "Return\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "Escape\0", + "\0", + "\0", + "\0", + "\0", + "space\0", + "exclam\0", + "quotedbl\0", + "numbersign\0", + "dollar\0", + "percent\0", + "ampersand\0", + "apostrophe\0", + "parenleft\0", + "parenright\0", + "asterisk\0", + "plus\0", + "comma\0", + "minus\0", + "period\0", + "slash\0", + "0\0", + "1\0", + "2\0", + "3\0", + "4\0", + "5\0", + "6\0", + "7\0", + "8\0", + "9\0", + "colon\0", + "semicolon\0", + "less\0", + "equal\0", + "greater\0", + "question\0", + "at\0", + "A\0", + "B\0", + "C\0", + "D\0", + "E\0", + "F\0", + "G\0", + "H\0", + "I\0", + "J\0", + "K\0", + "L\0", + "M\0", + "N\0", + "O\0", + "P\0", + "Q\0", + "R\0", + "S\0", + "T\0", + "U\0", + "V\0", + "W\0", + "X\0", + "Y\0", + "Z\0", + "bracketleft\0", + "backslash\0", + "bracketright\0", + "asciicircum\0", + "underscore\0", + "grave\0", + "a\0", + "b\0", + "c\0", + "d\0", + "e\0", + "f\0", + "g\0", + "h\0", + "i\0", + "j\0", + "k\0", + "l\0", + "m\0", + "n\0", + "o\0", + "p\0", + "q\0", + "r\0", + "s\0", + "t\0", + "u\0", + "v\0", + "w\0", + "x\0", + "y\0", + "z\0", + "braceleft\0", + "bar\0", + "braceright\0", + "asciitilde\0", + "Delete\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "\0", + "nobreakspace\0", + "exclamdown\0", + "cent\0", + "sterling\0", + "currency\0", + "yen\0", + "brokenbar\0", + "section\0", + "diaeresis\0", + "copyright\0", + "ordfeminine\0", + "guillemotleft\0", + "notsign\0", + "hyphen\0", + "registered\0", + "macron\0", + "degree\0", + "plusminus\0", + "twosuperior\0", + "threesuperior\0", + "acute\0", + "mu\0", + "paragraph\0", + "periodcentered\0", + "cedilla\0", + "onesuperior\0", + "masculine\0", + "guillemotright\0", + "onequarter\0", + "onehalf\0", + "threequarters\0", + "questiondown\0", + "Agrave\0", + "Aacute\0", + "Acircumflex\0", + "Atilde\0", + "Adiaeresis\0", + "Aring\0", + "AE\0", + "Ccedilla\0", + "Egrave\0", + "Eacute\0", + "Ecircumflex\0", + "Ediaeresis\0", + "Igrave\0", + "Iacute\0", + "Icircumflex\0", + "Idiaeresis\0", + "ETH\0", + "Ntilde\0", + "Ograve\0", + "Oacute\0", + "Ocircumflex\0", + "Otilde\0", + "Odiaeresis\0", + "multiply\0", + "Ooblique\0", + "Ugrave\0", + "Uacute\0", + "Ucircumflex\0", + "Udiaeresis\0", + "Yacute\0", + "THORN\0", + "ssharp\0", + "agrave\0", + "aacute\0", + "acircumflex\0", + "atilde\0", + "adiaeresis\0", + "aring\0", + "ae\0", + "ccedilla\0", + "egrave\0", + "eacute\0", + "ecircumflex\0", + "ediaeresis\0", + "igrave\0", + "iacute\0", + "icircumflex\0", + "idiaeresis\0", + "eth\0", + "ntilde\0", + "ograve\0", + "oacute\0", + "ocircumflex\0", + "otilde\0", + "odiaeresis\0", + "division\0", + "oslash\0", + "ugrave\0", + "uacute\0", + "ucircumflex\0", + "udiaeresis\0", + "yacute\0", + "thorn\0", + "ydiaeresis\0", +]; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 869945a..dc7937a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ extern crate core; pub mod x11_safe_wrapper; +pub mod chartbl; pub enum XMacroInstructions { Delay, diff --git a/src/x11_safe_wrapper.rs b/src/x11_safe_wrapper.rs index 583869e..fcd89e5 100644 --- a/src/x11_safe_wrapper.rs +++ b/src/x11_safe_wrapper.rs @@ -1,12 +1,13 @@ -use std::env; +use std::{env, slice}; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_int, c_uchar, c_uint, c_ulong}; -use x11::xlib::{Display, GenericEvent, KeyPress, MotionNotify, Time, Window, XAllowEvents, XCloseDisplay, XDefaultScreen, XEvent, XFlush, XGrabKeyboard, XKeycodeToKeysym, XKeysymToKeycode, XKeysymToString, XOpenDisplay, XQueryPointer, XRootWindow, XStringToKeysym, XSync, XUngrabKeyboard, XUngrabPointer, XWindowEvent}; +use x11::xlib::{Display, GenericEvent, KeyPress, MotionNotify, Time, Window, XAllowEvents, XCloseDisplay, XConvertCase, XDefaultScreen, XEvent, XFlush, XGetKeyboardMapping, XGrabKeyboard, XKeycodeToKeysym, XKeysymToKeycode, XKeysymToString, XOpenDisplay, XQueryPointer, XRootWindow, XStringToKeysym, XSync, XUngrabKeyboard, XUngrabPointer, XWindowEvent}; use x11::xrecord::{XRecordAllClients, XRecordClientSpec, XRecordContext, XRecordCreateContext, XRecordDisableContext, XRecordEnableContext, XRecordEnableContextAsync, XRecordFreeContext, XRecordInterceptData, XRecordProcessReplies, XRecordQueryVersion, XRecordRange}; use x11::xtest::{ XTestFakeButtonEvent, XTestFakeKeyEvent, XTestFakeMotionEvent, XTestGrabControl, XTestQueryExtension, }; +use crate::XMacroInstructions::KeySym; #[derive(Debug, Clone, Copy)] pub struct XDisplay { @@ -76,6 +77,33 @@ impl XDisplay { unsafe { XRootWindow(self.ptr, screen_nr) } } + pub fn get_keyboard_mapping(&self, keycode: Keycode, keycode_count: i32) -> &[Keysym] { + let mut keysyms_per_keycode = 0; + let r = unsafe { + let ptr = XGetKeyboardMapping( + self.ptr, + keycode as c_uchar, + keycode_count, + &mut keysyms_per_keycode + ); + + slice::from_raw_parts::(ptr, keysyms_per_keycode as usize) + }; + + r + } + + pub fn convert_case(&self, keysym: Keysym) -> (Keysym, Keysym) { + let mut keysym_lower: Keysym = Keysym::default(); + let mut keysym_upper: Keysym = Keysym::default(); + + unsafe { + XConvertCase(keysym, &mut keysym_lower, &mut keysym_upper); + } + + (keysym_lower, keysym_upper) + } + pub fn keysym_to_keycode(&self, keysym: Keysym) -> Keycode { unsafe { XKeysymToKeycode(self.ptr, keysym) as Keycode } }