Auto hotkey in Rust

2021-01-16

And Now for Something Completely Different

Lately I've been spending my evenings playing Barotrauma with a few old friends of mine. The game is pretty mouse-heavy, so when things go south (as they often do), your hand can quickly get tired from all the pointing and clicking.

Aware of the fact that carpal tunnel syndrome is no joke, I went looking for a solution for key/mouse input automation - especially drag-and-drop and double-click. My search yielded the following:

  • java.awt.Robot. It works, but the last time I used it was in 2013, when I bought a Leap Motion unit and wanted to create my own pointing app.
  • AutoHotkey - great for windowed applications, didn't emit click events in Barotrauma.
  • Node.js/JS - my inner Bachelor of Engineering in Computer Science shivers at the thought. A Node-based solution would necessarily rely on a library written in a compiled language and use FFI. Wrong tool for the job.

...in Rust!

Then there's Rust - a language I've been on-and-off trying to learn since 2015, and thanks to Rustlings finally got the hang of late last year. My greatest concern was that despite being around since 2010, it's still fairly immature in terms of availability of libraries, so a solution might have not existed. Devising one would be a great project in and of itself but that wasn't my goal here.

Turns out it wasn't that bad and after some digging I found the neccesary tools, namely:

  • Enigo - a cross-platform input simulation solution.
  • device_query - a library to query mouse and keyobard inputs globally. Also cross-platform.

Below is a minimal application showcasing what these two can do in combination:

use enigo::{*};
use std::thread;
use std::time::Duration;

use device_query::{DeviceQuery, DeviceState, MouseState, Keycode};

fn main() {
    let wait_time = Duration::from_millis(50); // wait between commands is essential.

    let mut enigo = Enigo::new();
    let device_state = DeviceState::new();

    loop {
      let keys: Vec<Keycode> = device_state.get_keys();

      // On Alt+Z, drag and drop from the current mouse position 450px to the right.
      if keys.contains(&Keycode::LAlt) && keys.contains(&Keycode::Z) {
          let mouse: MouseState = device_state.get_mouse();
          let (x,y) = mouse.coords;
          enigo.mouse_down(MouseButton::Left);
          thread::sleep(wait_time);

          enigo.mouse_move_to(x + 450, y);
          thread::sleep(wait_time);

          enigo.mouse_up(MouseButton::Left);
          thread::sleep(wait_time);

          // Move back to the next slot in the crate/cabinet you're inspecting.
          enigo.mouse_move_to(x + 70, y);
      }

      // On Alt+X, double click.
      if keys.contains(&Keycode::LAlt) && keys.contains(&Keycode::X) {
          enigo.mouse_down(MouseButton::Left);
          thread::sleep(wait_time);

          enigo.mouse_up(MouseButton::Left);
          thread::sleep(wait_time);

          enigo.mouse_down(MouseButton::Left);
          thread::sleep(wait_time);

          enigo.mouse_up(MouseButton::Left);
      }

      thread::sleep(wait_time); // Curb your busy waiting.
    }
}

Caveats

  • Drag and drop may malfunction if the gesture is too fast. The snippet above works well in Barotrauma, but trying to use it to move windows in Windows will likely fail. You may want to use mouse_move_relative instead and through trial and error find a combination of delay and (X,Y) offsets that works. In my case Windows would refuse to move more than 4 pixels in any given direction or faster than every 50 milliseconds.
  • Same goes for double click, but here you can expect a rate limiter instead, which appears to queue the clicks, so your hotkey will work, but slower than expected.
  • I'm currently, for better or worse, on Windows, so this code was only tested in this OS.