Compare commits

..

No commits in common. "xcb-rewrite" and "main" have entirely different histories.

11 changed files with 1087 additions and 383 deletions

39
Cargo.lock generated
View file

@ -2,12 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "anyhow"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -74,9 +68,8 @@ dependencies = [
name = "easymacros" name = "easymacros"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow",
"clap", "clap",
"xcb", "x11",
] ]
[[package]] [[package]]
@ -116,12 +109,6 @@ version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.12.0" version = "1.12.0"
@ -134,6 +121,12 @@ version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
[[package]]
name = "pkg-config"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "1.0.4" version = "1.0.4"
@ -167,15 +160,6 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.18" version = "1.0.18"
@ -261,12 +245,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "xcb" name = "x11"
version = "1.1.1" version = "2.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b127bf5bfe9dbb39118d6567e3773d4bbc795411a8e1ef7b7e056bccac0011a9" checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a"
dependencies = [ dependencies = [
"bitflags",
"libc", "libc",
"quick-xml", "pkg-config",
] ]

View file

@ -6,6 +6,5 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1.0.65" x11 = { version = "2.19.1", features = ["xlib", "xtest"] }
xcb = { version = "1.1.1", features = [ "xtest", "record" ]}
clap = { version = "3.2.4", features = ["derive"] } clap = { version = "3.2.4", features = ["derive"] }

View file

@ -27,17 +27,18 @@ This program is inspired by [**xmacro**](https://github.com/Ortega-Dan/xmacroInc
- [ ] Rebrand? - [ ] Rebrand?
- [ ] new name - [ ] new name
- [ ] logo - [ ] logo
- [ ] Listening/remapping - [ ] macro language (easymacros daemon?)
- [ ] Modes - [ ] basic interpreter/compiler to fast intermediate lang
- [ ] Way to show current mode - [ ] stdlib
- [ ] mode change notifications? - [ ] xlib stuff (get window handles/ids)
- [ ] small gui/popups? - [ ] easy xtst/xrecord etc abstractions
- [ ] allow passing keys through in some modes - [ ] number/text inputs
- [ ] make modes listen for numbers/amounts of repetitions - [ ] clipboard
- [ ] make shortcuts be able to listen for other shortcuts and inputs - [ ] basic gui features
- [ ] rofi integration - [ ] filesystem stuff
- [ ] autorun stuff on some windows? - [ ] shell stuff
- [ ] Proper, safe xlib wrapper - [ ] find image/track image/wait for image...
- [ ] event listeners
# #

146
src/bin/easymacroplay.rs Normal file
View file

@ -0,0 +1,146 @@
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<std::path::PathBuf>,
/// Display to run the macro on. This uses the $DISPLAY environment variable by default.
#[clap(short = 'D', long)]
display: Option<String>,
/// Delay for events to be sent.
#[clap(short, long)]
delay: Option<u64>,
}
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<String>) -> 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); }
}

189
src/bin/easymacrorec.rs Normal file
View file

@ -0,0 +1,189 @@
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<std::path::PathBuf>,
/// Display to run the macro on. This uses the $DISPLAY environment variable by default.
#[clap(short = 'D', long)]
display: Option<String>,
/// Max Delay in milliseconds for macro delays
#[clap(short, long)]
max_delay: Option<u64>,
/// 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<Time>,
) {
let protocol_ranges = unsafe { XRecordAllocRange() };
let pointer_pos: Position<i16> = Position::from(xdpy.query_pointer_pos());
if pointer_pos != Position(-1, -1) {
writer.write(Instructions::MotionNotify(pointer_pos))
}
let ctx = recdpy.create_record_context(protocol_ranges);
let ev_cb_data = EvCallbackData::new(writer, xdpy, recdpy, ctx, stop_key, pointer_pos, max_delay);
if !recdpy.enable_context_async(ctx, Some(ev_callback), addr_of!(ev_cb_data) as *mut c_char) {
panic!("Failed to enable record context")
}
while ev_cb_data.working {
recdpy.process_replies();
}
xdpy.disable_context(ctx);
xdpy.free_context(ctx);
unsafe { XFree(protocol_ranges as *mut c_void) };
}
unsafe extern "C" fn ev_callback(closure: *mut c_char, intercept_data: *mut XRecordInterceptData) {
let data = &mut *(closure as *mut EvCallbackData);
let intercept_data = &mut *intercept_data;
if intercept_data.category == XRecordStartOfData {
println!("Got start of data!");
data.last_event = intercept_data.server_time;
XRecordFreeData(intercept_data);
return;
} else if intercept_data.category == XRecordEndOfData {
println!("Got end of data!");
XRecordFreeData(intercept_data);
return;
}
let ev_type = *(intercept_data.data as *const u8);
if data.pos.0 == 0 || data.pos.1 == -1 {
if ev_type == MOTIONNOTIFY_U8 {
data.update_pos(intercept_data);
data.write_pos();
} else {
println!("Move your cursor so the macro can start with a fixed cursor position!
Skipping event...");
}
} else if data.no_keypress_yet && ev_type == KEYRELEASE_U8 {
println!("Skipping KeyRelease without recorded KeyPress...");
} else {
match ev_type {
MOTIONNOTIFY_U8 => {
data.update_pos(intercept_data);
if !data.moving {
data.moving = true;
}
}
KEYPRESS_U8 | KEYRELEASE_U8 => {
let kc: u8 = *((intercept_data.data as usize + 1) as *const u8);
let keyname = data.xdpy.keycode_to_string(kc as u32);
if ev_type == KEYPRESS_U8 && kc == data.stop_key as u8 {
data.working = false;
} else {
if ev_type == KEYPRESS_U8 {
data.no_keypress_yet = false;
}
data.maybe_write_delay(intercept_data.server_time);
if data.ptr_is_moving() { data.write_pos(); }
data.writer.write(if ev_type == KEYPRESS_U8 {
Instructions::KeyStrPress(keyname)
} else {
Instructions::KeyStrRelease(keyname)
});
}
}
BUTTONPRESS_U8 | BUTTONRELEASE_U8 => {
let bc: u8 = *((intercept_data.data as usize + 1) as *const u8);
data.maybe_write_delay(intercept_data.server_time);
if data.ptr_is_moving() { data.write_pos(); }
data.writer.write(if ev_type == BUTTONPRESS_U8 {
Instructions::ButtonPress(bc)
} else {
Instructions::ButtonRelease(bc)
});
}
_ => eprintln!("Unknown event type: {:?}", ev_type)
}
}
data.ev_nr += 2;
XRecordFreeData(intercept_data)
}

View file

@ -1,47 +0,0 @@
use clap::Parser;
use anyhow::Result;
use xcb::x;
/// 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<std::path::PathBuf>,
/// Display to run the macro on. This uses the $DISPLAY environment variable by default.
#[clap(short = 'D', long)]
display: Option<String>,
/// Max Delay in milliseconds for macro delays
#[clap(short, long)]
max_delay: Option<u64>,
/// 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,
}
pub fn main() -> Result<()> {
let args = Args::parse();
let (connection, screen_nr) = xcb::Connection::connect(args.display.as_deref())?;
Ok(())
}
fn get_stop_key(connection: xcb::Connection, screen_nr: i32) -> Result<x::Keycode> {
let setup = connection.get_setup();
let screen = setup.roots().nth(screen_nr as usize).unwrap();
let grab_cookie = connection.send_request(&x::GrabKeyboard {
owner_events: true, // get all pointer events specified by the following mask
grab_window: screen.root(), // grab the root window
time: x::CURRENT_TIME,
pointer_mode: x::GrabMode::Async, // process events as normal, do not require sync
keyboard_mode: x::GrabMode::Async,
});
let grab_reply = connection.wait_for_reply(grab_cookie);
assert!(grab_reply.status() == x::GrabStatus::Success, "GrabKeyboard did not succeed");
todo!()
}

86
src/ev_callback_data.rs Normal file
View file

@ -0,0 +1,86 @@
use std::mem::size_of;
use std::time::{SystemTime, UNIX_EPOCH};
use x11::xlib::Time;
use x11::xrecord::{XRecordContext, XRecordInterceptData};
use crate::{Instructions, Keycode, Position};
use crate::macro_writer::MacroWriter;
use crate::x11_safe_wrapper::XDisplay;
#[repr(C)]
pub struct EvCallbackData {
pub writer: MacroWriter,
pub xdpy: XDisplay,
pub recdpy: XDisplay,
pub ctx: XRecordContext,
pub working: bool,
pub last_event: Time,
pub pos: Position<i16>,
pub stop_key: Keycode,
pub ev_nr: u32,
pub max_delay: Option<Time>,
pub no_keypress_yet: bool,
pub moving: bool,
}
impl EvCallbackData {
pub fn new(
writer: MacroWriter,
xdpy: XDisplay,
recdpy: XDisplay,
ctx: XRecordContext,
stop_key: Keycode,
pos: Position<i16>,
max_delay: Option<Time>,
) -> Self {
EvCallbackData {
writer,
xdpy,
recdpy,
ctx,
stop_key,
ev_nr: 0,
working: true,
pos,
max_delay,
no_keypress_yet: true,
last_event: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as Time,
moving: false,
}
}
pub fn ptr_is_moving(&self) -> bool { self.moving }
pub unsafe fn update_pos(&mut self, intercept_data: &mut XRecordInterceptData) -> Position<i16> {
self.pos.0 = *((intercept_data.data as usize + size_of::<i16>() * 10) as *const i16);
self.pos.1 = *((intercept_data.data as usize + size_of::<i16>() * 11) as *const i16);
self.pos
}
pub fn write_pos(&mut self) {
self.writer.write(Instructions::MotionNotify(self.pos));
self.moving = false;
}
pub fn maybe_write_delay(&mut self, server_time: Time) {
if server_time - self.last_event > 1 {
self.writer.write(Instructions::Delay(calculate_delay(server_time, self.last_event, self.max_delay)));
self.last_event = server_time;
}
}
}
fn calculate_delay(server_time: Time, last_event: Time, max_delay: Option<Time>) -> Time {
if let Some(max) = max_delay {
let max = max as u64;
let delay = server_time - last_event;
if delay > max {
max
} else {
delay
}
} else {
server_time - last_event
}
}

View file

@ -3,15 +3,45 @@ extern crate core;
use std::ffi::CString; use std::ffi::CString;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use xcb::x::{Keycode, Keysym, Timestamp}; use x11::xlib::{ButtonPress, ButtonRelease, KeyPress, KeyRelease, MotionNotify, Time};
use crate::x11_safe_wrapper::{Keycode, Keysym};
pub mod x11_safe_wrapper;
pub mod chartbl; pub mod chartbl;
pub mod macro_writer;
pub mod ev_callback_data;
pub const KEYPRESS_U8: u8 = KeyPress as u8;
pub const KEYRELEASE_U8: u8 = KeyRelease as u8;
pub const BUTTONPRESS_U8: u8 = ButtonPress as u8;
pub const BUTTONRELEASE_U8: u8 = ButtonRelease as u8;
pub const MOTIONNOTIFY_U8: u8 = MotionNotify as u8;
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub struct Position<T> (pub T, pub T); pub struct Position<T> (pub T, pub T);
pub enum Instruction<'a> { impl From<Position<i32>> for Position<i16> {
Delay(Timestamp), fn from(pos: Position<i32>) -> Self {
Self (pos.0 as i16, pos.1 as i16)
}
}
impl From<(i32, i32)> for Position<i32> {
fn from(v: (i32, i32)) -> Self {
Position(v.0, v.1)
}
}
impl From<(i32, i32)> for Position<i16> {
fn from(v: (i32, i32)) -> Position<i16> {
let p1: Position<i32> = Position::from(v);
Position::from(p1)
}
}
pub enum Instructions<'a> {
Delay(Time),
ButtonPress(u8), ButtonPress(u8),
ButtonRelease(u8), ButtonRelease(u8),
MotionNotify(Position<i16>), MotionNotify(Position<i16>),
@ -28,29 +58,25 @@ pub enum Instruction<'a> {
ExecNoBlock(&'a str), ExecNoBlock(&'a str),
} }
impl Display for Instruction<'_> { impl Display for Instructions<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!( write!(f, "{}",
f,
"{}",
match self { match self {
Instruction::Delay(d) => format!("Delay {}", d), Instructions::Delay(d) => format!("Delay {}", d),
Instruction::ButtonPress(b) => format!("ButtonPress {}", b), Instructions::ButtonPress(b) => format!("ButtonPress {}", b),
Instruction::ButtonRelease(b) => format!("ButtonRelease {}", b), Instructions::ButtonRelease(b) => format!("ButtonRelease {}", b),
Instruction::MotionNotify(pos) => format!("MotionNotify {} {}", pos.0, pos.1), Instructions::MotionNotify(pos) => format!("MotionNotify {} {}", pos.0, pos.1),
Instruction::KeyCodePress(kc) => format!("KeyCodePress {}", kc), Instructions::KeyCodePress(kc) => format!("KeyCodePress {}", kc),
Instruction::KeyCodeRelease(kc) => format!("KeyCodeRelease {}", kc), Instructions::KeyCodeRelease(kc) => format!("KeyCodeRelease {}", kc),
Instruction::KeySymPress(ks) => format!("KeySymPress {}", ks), Instructions::KeySymPress(ks) => format!("KeySymPress {}", ks),
Instruction::KeySymRelease(ks) => format!("KeySymRelease {}", ks), Instructions::KeySymRelease(ks) => format!("KeySymRelease {}", ks),
Instruction::KeySym(ks) => format!("KeySym {}", ks), Instructions::KeySym(ks) => format!("KeySym {}", ks),
Instruction::KeyStrPress(kstr) => Instructions::KeyStrPress(kstr) => format!("KeyStrPress {}", kstr.to_str().unwrap()),
format!("KeyStrPress {}", kstr.to_str().unwrap()), Instructions::KeyStrRelease(kstr) => format!("KeyStrRelease {}", kstr.to_str().unwrap()),
Instruction::KeyStrRelease(kstr) => Instructions::KeyStr(kstr) => format!("KeyStr {}", kstr.to_str().unwrap()),
format!("KeyStrRelease {}", kstr.to_str().unwrap()), Instructions::String(str) => format!("String {}", str),
Instruction::KeyStr(kstr) => format!("KeyStr {}", kstr.to_str().unwrap()), Instructions::ExecBlock(cmd) => format!("ExecBlock {}", cmd),
Instruction::String(str) => format!("String {}", str), Instructions::ExecNoBlock(cmd) => format!("ExecNoBlock {}", cmd),
Instruction::ExecBlock(cmd) => format!("ExecBlock {}", cmd),
Instruction::ExecNoBlock(cmd) => format!("ExecNoBlock {}", cmd),
}, },
) )
} }

33
src/macro_writer.rs Normal file
View file

@ -0,0 +1,33 @@
use std::fs::File;
use std::io;
use std::io::Write;
use crate::Instructions;
pub struct MacroWriter {
outfile: Box<dyn Write>,
ignore_delay_capturing: bool,
}
impl MacroWriter {
pub fn new(outfile: Option<std::path::PathBuf>, ignore_delay_capturing: bool) -> Self {
Self {
outfile: if let Some(outfile) = outfile {
Box::new(File::create(outfile).expect("Failed to create output file"))
} else {
Box::new(io::stdout())
},
ignore_delay_capturing,
}
}
pub fn write(&mut self, instruction: Instructions) {
if self.ignore_delay_capturing {
if let Instructions::Delay(_) = instruction {
return;
}
}
writeln!(&mut self.outfile, "{}", instruction).expect("Failed to write instruction to outfile");
}
}

288
src/x11_safe_wrapper.rs Normal file
View file

@ -0,0 +1,288 @@
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, 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,
};
#[derive(Debug, Clone, Copy)]
pub struct XDisplay {
ptr: *mut Display,
}
pub type Keysym = c_ulong;
pub type Keycode = c_uint;
const FALSE_C: c_int = 0;
const TRUE_C: c_int = 1;
impl XDisplay {
pub fn open(display_name: Option<String>) -> Self {
let name = CString::new(if let Some(name) = display_name {
name
} else {
env::var("DISPLAY").expect("DISPLAY is not set")
}).unwrap();
let name_ptr = name.as_bytes().as_ptr();
let display_ptr = unsafe { XOpenDisplay(name_ptr as *const i8) };
Self { ptr: display_ptr }
}
pub fn close(self) {
unsafe { XCloseDisplay(self.ptr) };
}
pub fn sync(&self) {
unsafe {
XSync(self.ptr, c_int::from(false));
}
}
pub fn flush(&self) {
unsafe {
XFlush(self.ptr);
}
}
pub fn query_pointer_pos(&self) -> (i32, i32) {
let mut r: (i32, i32) = (-1, -1);
let mut unneeded_ints: (i32, i32) = (0, 0);
let mut unneeded_wins: (u64, u64) = (0, 0);
let mut unneeded_mask: u32 = 0;
unsafe {
XQueryPointer(
self.ptr,
self.get_root_window(self.get_default_screen()),
&mut unneeded_wins.0,
&mut unneeded_wins.1,
&mut r.0,
&mut r.1,
&mut unneeded_ints.0,
&mut unneeded_ints.1,
&mut unneeded_mask,
)
};
r
}
pub fn get_default_screen(&self) -> c_int {
unsafe { XDefaultScreen(self.ptr) }
}
pub fn get_root_window(&self, screen_nr: c_int) -> Window {
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::<Keysym>(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 }
}
pub fn string_to_keycode(&self, string: &[u8]) -> Keycode {
self.keysym_to_keycode(string_to_keysym(string))
}
pub fn keycode_to_keysym(&self, keycode: Keycode) -> Keysym {
unsafe {
XKeycodeToKeysym(self.ptr, keycode as c_uchar, 0)
}
}
pub fn keycode_to_string(&self, keycode: Keycode) -> CString {
keysym_to_string(self.keycode_to_keysym(keycode))
}
// XTest stuff
pub fn has_xtest(&self) -> bool {
let mut vals: (c_int, c_int, c_int, c_int) = (0, 0, 0, 0);
let has_extension = unsafe {
XTestQueryExtension(self.ptr, &mut vals.0, &mut vals.1, &mut vals.2, &mut vals.3)
};
has_extension != 0
}
pub fn send_fake_keypress_from_string(&self, string: &[u8], delay: u64) {
self.send_fake_keypress_from_keysym(string_to_keysym(string), delay)
}
pub fn send_fake_keypress_from_keysym(&self, ks: Keysym, delay: u64) {
self.send_fake_keypress_from_code(self.keysym_to_keycode(ks), delay)
}
pub fn send_fake_keypress_from_code(&self, code: Keycode, delay: u64) {
unsafe { XTestFakeKeyEvent(self.ptr, code, TRUE_C, delay) };
self.flush();
}
pub fn send_fake_buttonpress(&self, button: u32, delay: u64) {
unsafe { XTestFakeButtonEvent(self.ptr, button, TRUE_C, delay) };
}
pub fn send_fake_buttonrelease(&self, button: u32, delay: u64) {
unsafe { XTestFakeButtonEvent(self.ptr, button, FALSE_C, delay) };
}
pub fn send_fake_keyrelease_from_string(&self, string: &[u8], delay: u64) {
self.send_fake_keyrelease_from_keysym(string_to_keysym(string), delay)
}
pub fn send_fake_keyrelease_from_keysym(&self, ks: Keysym, delay: u64) {
self.send_fake_keyrelease_from_code(self.keysym_to_keycode(ks), delay)
}
pub fn send_fake_keyrelease_from_code(&self, code: Keycode, delay: u64) {
unsafe { XTestFakeKeyEvent(self.ptr, code, FALSE_C, delay) };
self.flush();
}
pub fn send_fake_motion_event(&self, x: i32, y: i32, delay: u64) {
unsafe { XTestFakeMotionEvent(self.ptr, -1, x, y, delay) };
self.flush();
}
pub fn grab_control(&self) {
unsafe {
XTestGrabControl(self.ptr, TRUE_C);
}
}
pub fn allow_events(&self, event_mode: i32, time: Time) {
unsafe { XAllowEvents(self.ptr, event_mode, time) };
}
pub fn grab_keyboard(&self, window: u64, owner_events: bool, pointer_mode: i32, keyboard_mode: i32, time: Time) -> i32 {
unsafe {
XGrabKeyboard(
self.ptr,
window,
c_int::from(owner_events),
pointer_mode,
keyboard_mode,
time,
)
}
}
pub fn ungrab_keyboard(&self, time: Time) {
unsafe { XUngrabKeyboard(self.ptr, time) };
}
pub fn ungrab_pointer(&self, time: Time) {
unsafe { XUngrabPointer(self.ptr, time) };
}
pub fn window_event(&self, window: Window, event_mask: i64) -> XEvent {
// maybe dirty hack to initialize the event var?? idk how else to do this
let mut r: XEvent = XEvent { type_: GenericEvent };
unsafe { XWindowEvent(self.ptr, window, event_mask, &mut r); }
r
}
// XRecord stuff
pub fn has_xrecord(&self) -> bool {
let mut xrecord_version: (c_int, c_int) = (0, 0);
let xrec_res = unsafe { XRecordQueryVersion(self.ptr, &mut xrecord_version.0, &mut xrecord_version.1) };
xrec_res == 0
}
pub fn create_record_context(&self, mut protocol_ranges: *mut XRecordRange) -> XRecordContext {
unsafe {
(*protocol_ranges).device_events.first = KeyPress as c_uchar;
(*protocol_ranges).device_events.last = MotionNotify as c_uchar;
}
let mut clients: XRecordClientSpec = XRecordAllClients;
let ctx: XRecordContext = unsafe {
XRecordCreateContext(
self.ptr,
0,
&mut clients,
1,
&mut protocol_ranges,
1,
)
};
ctx
}
pub fn enable_context(&self,
ctx: XRecordContext,
cb: Option<unsafe extern "C" fn(_: *mut c_char, _: *mut XRecordInterceptData)>,
closure: *mut c_char,
) -> bool {
unsafe {
XRecordEnableContext(self.ptr, ctx, cb, closure as *mut c_char) != 0
}
}
pub fn enable_context_async(&self,
ctx: XRecordContext,
cb: Option<unsafe extern "C" fn(_: *mut c_char, _: *mut XRecordInterceptData)>,
closure: *mut c_char,
) -> bool {
unsafe {
XRecordEnableContextAsync(self.ptr, ctx, cb, closure as *mut c_char) != 0
}
}
pub fn disable_context(&self, ctx: XRecordContext) -> bool {
unsafe {
XRecordDisableContext(self.ptr, ctx) != 0
}
}
pub fn free_context(&self, ctx: XRecordContext) -> bool {
unsafe {
XRecordFreeContext(self.ptr, ctx) != 0
}
}
pub fn process_replies(&self) {
unsafe { XRecordProcessReplies(self.ptr) };
}
}
/// Wrapper for XStringToKeysym. Remember to give a null terminated string!
pub fn string_to_keysym(string: &[u8]) -> Keysym {
unsafe { XStringToKeysym(string.as_ptr() as *const c_char) }
}
pub fn keysym_to_string(keysym: Keysym) -> CString {
unsafe {
let cstr = CStr::from_ptr(XKeysymToString(keysym));
CString::from(cstr)
}
}