diff --git a/Cargo.lock b/Cargo.lock index 130998e..502a99d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" + [[package]] name = "atty" version = "0.2.14" @@ -68,8 +74,9 @@ dependencies = [ name = "easymacros" version = "0.2.0" dependencies = [ + "anyhow", "clap", - "x11", + "xcb", ] [[package]] @@ -109,6 +116,12 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "once_cell" version = "1.12.0" @@ -121,12 +134,6 @@ version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" -[[package]] -name = "pkg-config" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -160,6 +167,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.18" @@ -245,11 +261,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "x11" -version = "2.19.1" +name = "xcb" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +checksum = "b127bf5bfe9dbb39118d6567e3773d4bbc795411a8e1ef7b7e056bccac0011a9" dependencies = [ + "bitflags", "libc", - "pkg-config", + "quick-xml", ] diff --git a/Cargo.toml b/Cargo.toml index c65e9e7..8d0f48d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -x11 = { version = "2.19.1", features = ["xlib", "xtest"] } -clap = { version = "3.2.4", features = ["derive"] } \ No newline at end of file +anyhow = "1.0.65" +xcb = { version = "1.1.1", features = [ "xtest", "record" ]} +clap = { version = "3.2.4", features = ["derive"] } diff --git a/README.md b/README.md index acf0e1f..2c4eb65 100644 --- a/README.md +++ b/README.md @@ -10,35 +10,34 @@ This program is inspired by [**xmacro**](https://github.com/Ortega-Dan/xmacroInc

:pen_ballpoint: TODOs :notepad_spiral:

- [x] Playing macros (xmacro like) - - [x] Delay support - - [x] KeySym/KeyCode/KeyStr action support - - [x] MotionNotify and button support - - [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 + - [x] Delay support + - [x] KeySym/KeyCode/KeyStr action support + - [x] MotionNotify and button support + - [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 - [x] Recording macros (xmacro like) - - [x] Delay - - [x] Keyboard actions - - [x] Mouse actions + - [x] Delay + - [x] Keyboard actions + - [x] Mouse actions - [x] Utilities for playing macros - - [ ] Ignoring delays when playing - - [x] Event delay support + - [ ] Ignoring delays when playing + - [x] Event delay support - [ ] Rebrand? - [ ] new name - [ ] logo -- [ ] macro language (easymacros daemon?) - - [ ] basic interpreter/compiler to fast intermediate lang - - [ ] stdlib - - [ ] xlib stuff (get window handles/ids) - - [ ] easy xtst/xrecord etc abstractions - - [ ] number/text inputs - - [ ] clipboard - - [ ] basic gui features - - [ ] filesystem stuff - - [ ] shell stuff - - [ ] find image/track image/wait for image... - - [ ] event listeners +- [ ] Listening/remapping + - [ ] Modes + - [ ] Way to show current mode + - [ ] mode change notifications? + - [ ] small gui/popups? + - [ ] allow passing keys through in some modes + - [ ] make modes listen for numbers/amounts of repetitions + - [ ] make shortcuts be able to listen for other shortcuts and inputs + - [ ] rofi integration + - [ ] autorun stuff on some windows? +- [ ] Proper, safe xlib wrapper # diff --git a/src/bin/easymacroplay.rs b/src/bin/easymacroplay.rs deleted file mode 100644 index ba1960a..0000000 --- a/src/bin/easymacroplay.rs +++ /dev/null @@ -1,146 +0,0 @@ -use clap::Parser; -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 compatible with xmacro macros. -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -struct Args { - /// The file that contains the macro to run. - #[clap(value_parser, value_name = "input_file", value_hint = clap::ValueHint::FilePath)] - input_file: Option, - /// Display to run the macro on. This uses the $DISPLAY environment variable by default. - #[clap(short = 'D', long)] - display: Option, - /// Delay for events to be sent. - #[clap(short, long)] - delay: Option, -} - -fn main() { - let args = Args::parse(); - - let display = get_remote(args.display); - let delay = args.delay.unwrap_or(10); - - if let Some(input_file_path) = args.input_file { - let input_file_contents = fs::read_to_string(input_file_path).expect("Couldn't read macro file"); - - for instruction in input_file_contents.lines() { - run_instruction(instruction, &display, delay); - } - } else { - println!("No input file specified, reading from stdin."); - let stdin = stdin(); - - loop { - let mut line = String::new(); - stdin.read_line(&mut line).expect("Couldn't read line from stdin"); - // Without this it crashes because apparently it doesn't properly read the next input line? - println!(); - line = line.trim().to_string(); - run_instruction(&*line, &display, delay); - } - } - - display.close(); -} - -fn get_remote(display_name: Option) -> XDisplay { - let display = XDisplay::open(display_name); - - if !display.has_xtest() { - eprintln!("XTest not supported!"); - display.close(); - exit(1) - } - - display.grab_control(); - display.sync(); - - display -} - -fn run_instruction(instruction: &str, dpy: &XDisplay, delay: u64) { - let instruction_split: Vec<&str> = instruction.split(' ').collect(); - - 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(), delay), - "ButtonRelease" => dpy.send_fake_buttonrelease(instruction_split[1].parse().unwrap(), delay), - "MotionNotify" => dpy.send_fake_motion_event(instruction_split[1].parse().unwrap(), instruction_split[2].parse().unwrap(), delay), - "KeyCodePress" => dpy.send_fake_keypress_from_code(instruction_split[1].parse().unwrap(), delay), - "KeyCodeRelease" => dpy.send_fake_keyrelease_from_code(instruction_split[1].parse().unwrap(), delay), - "KeySymPress" => dpy.send_fake_keypress_from_keysym(instruction_split[1].parse().unwrap(), delay), - "KeySymRelease" => dpy.send_fake_keyrelease_from_keysym(instruction_split[1].parse().unwrap(), delay), - - "KeySym" => { - let key: Keysym = instruction_split[1].parse().unwrap(); - dpy.send_fake_keypress_from_keysym(key, delay); - dpy.send_fake_keyrelease_from_keysym(key, delay); - } - "KeyStrPress" => dpy.send_fake_keypress_from_string(CString::new(instruction_split[1]).unwrap().as_bytes(), delay), - "KeyStrRelease" => dpy.send_fake_keyrelease_from_string(CString::new(instruction_split[1]).unwrap().as_bytes(), delay), - "KeyStr" => { - let keystring = CString::new(instruction_split[1]).unwrap(); - dpy.send_fake_keypress_from_string(keystring.as_bytes(), delay); - dpy.send_fake_keyrelease_from_string(keystring.as_bytes(), delay); - } - "String" => { - for c in instruction["String".len() + 1..].chars() { - send_char(dpy, c, delay); - } - } - "ExecBlock" | "ExecNoBlock" => { - let mut command = Command::new(instruction_split[1]); - for arg in &instruction_split[2..] { - command.arg(arg); - } - if instruction_split[0] == "ExecBlock" { - command.status().unwrap(); - } else { - command.spawn().unwrap(); - } - } - c => panic!("Unknown command {:?}", instruction_split), - } -} - -fn send_char(dpy: &XDisplay, c: char, delay: u64) { - // get keystring from character and turn it into a keysym - 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 map_ks[0] == 0 { - eprintln!("XGetKeyboardMapping failed (keycode: {})", keycode); - return; - } - - let (ks_lower, ks_upper) = dpy.convert_case(keysym); - - // check if shift has to be pressed as well - 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, delay); } - dpy.send_fake_keypress_from_code(keycode, delay); - dpy.send_fake_keyrelease_from_code(keycode, delay); - if shift_needed { dpy.send_fake_keyrelease_from_keysym(XK_Shift_L as Keysym, delay); } -} \ No newline at end of file diff --git a/src/bin/easymacrorec.rs b/src/bin/easymacrorec.rs deleted file mode 100644 index 9d91d88..0000000 --- a/src/bin/easymacrorec.rs +++ /dev/null @@ -1,189 +0,0 @@ -extern crate core; - -use std::ffi::c_void; -use std::os::raw::{c_char}; -use std::process::{exit}; -use std::ptr::addr_of; - -use clap::Parser; -use x11::xlib::{CurrentTime, GrabModeAsync, GrabModeSync, GrabSuccess, KeyPressMask, SyncPointer, Time, XFree, XKeyEvent}; -use x11::xrecord::{XRecordAllocRange, XRecordEndOfData, XRecordFreeData, XRecordInterceptData, XRecordStartOfData}; - -use easymacros::{BUTTONPRESS_U8, BUTTONRELEASE_U8, Instructions, KEYPRESS_U8, KEYRELEASE_U8, MOTIONNOTIFY_U8, Position}; -use easymacros::ev_callback_data::EvCallbackData; -use easymacros::macro_writer::MacroWriter; -use easymacros::x11_safe_wrapper::{Keycode, XDisplay}; - -/// Macro recording module for easymacros. Outputs are partially compatible with xmacro. -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -struct Args { - /// The file to record the macro to. Defaults to writing to stdout. - #[clap(value_parser, value_name = "output_file", value_hint = clap::ValueHint::FilePath)] - output_file: Option, - /// Display to run the macro on. This uses the $DISPLAY environment variable by default. - #[clap(short = 'D', long)] - display: Option, - /// Max Delay in milliseconds for macro delays - #[clap(short, long)] - max_delay: Option, - /// Allow delay capturing in recording output. If this flag is set, the program will ignore the max_delay. - #[clap(short, long)] - ignore_delay_capturing: bool, -} - -fn main() { - let args = Args::parse(); - - let display = XDisplay::open(args.display.clone()); - let recorded_display = XDisplay::open(args.display.clone()); - let stop_key = get_stop_key(display); - let writer = MacroWriter::new(args.output_file, args.ignore_delay_capturing); - - event_loop( - display, - recorded_display, - stop_key, - writer, - args.max_delay, - ); - - display.close(); -} - -fn get_stop_key(display: XDisplay) -> Keycode { - let screen = display.get_default_screen(); - - let root = display.get_root_window(screen); - let potential_err = display.grab_keyboard(root, false, GrabModeSync, GrabModeAsync, CurrentTime); - - if potential_err != GrabSuccess { - eprintln!("Couldn't grab keyboard!"); - exit(1); - } - - println!("Press the key you want to use to stop recording the macro."); - - let stop_key; - loop { - display.allow_events(SyncPointer, CurrentTime); - let ev = XKeyEvent::from(display.window_event(root, KeyPressMask)); - stop_key = ev.keycode; - break; - } - - display.ungrab_keyboard(CurrentTime); - display.ungrab_pointer(CurrentTime); - - stop_key -} - -fn event_loop( - xdpy: XDisplay, - recdpy: XDisplay, - stop_key: Keycode, - mut writer: MacroWriter, - max_delay: Option