Introduction

What is Automaton?

Automaton is a library that allows you to execute certain operations when certain inputs are given (via input devices connected to your computer). It can be used for almost all keyboard and mouse related projects, be it manipulating said devices, or for capturing their inputs.

As of yet, it provides a number of builtin methods for listening and capturing the input from your keyboards, allowing you to create of hotkeys, hotstrings, etc.

How does it work?

It is meant to be a replacement for AutoHotkey, which does not work on Linux. It makes use of the Linux evdev interface and, as such, requires root privileges. Automaton is focused on being the perfect tool for the creation of macros and other keyboard-related gimmicks for Linux.

The Automaton Project

Automaton is divided into two parts: the library itself, which is written in Python, and the autumn language, which makes use of the library to allow simple tasks to be done with relative ease (currently not implemented). The latter is similar to the AutoHotkey language, although syntactically distinct.

The library itself can be used without the autumn language (a feature that AutoHotKey lacks).

Why should I use it?

The unavailability of AutoHotKey on Linux was indubitably the impetus for this project. Sure, there many other libraries/applications in Linux that provide similar features (notable examples include keyboard and mouse, pynput, and AutoKey), however Automaton, unlike these libraries, offers the most feature parity with AutoHotKey on Linux, being directly based on its design and structure after years of use on Windows. With Automaton, you'll be able to do everything you could with Linux in roughly the same amount of code, though their syntaxes differ.

Here's a neat table that summarizes the pros and cons of Automaton, because tables never lie:

Pros:

  • Can be used across Linux. Not just x11, or KDE, all of Linux.
  • Almost complete feature parity with AutoHotkey.
  • Supports unicode 䔄 (looking at you, keyboard)
  • Supports key suppression.

Cons:

  • Does not work on Windows.

Background

Before you can start using automaton, you'll need a bit of a lesson. Linux uses an interface for communicating between the keyboard and the kernel, known as evdev. In Linux, pretty much everything is a file, and input device interfaces are no exception.

In the directory /dev/input/, there are a number of files starting with event0.

$ ls /dev/input/
by-id/    event0  event10  event12  event2  event4  event6  event8  mice    mouse1  mouse3
by-path/  event1  event11  event13  event3  event5  event7  event9  mouse0  mouse2

Automaton uses these files to function, but you (as the user) must specify what device you want to capture inputs from. For example, my keyboard and mouse correspond to /dev/input/event5 and /dev/input/event4.

Keep in mind that these paths may change everytime the device is disconnected and reconnected (such as when powering off). To figure out which of these files represent your chosen devices, refer to the next page.

Installation

Automaton is hosted on GitHub as well as PyPI. It supports Python >= 3.8. The preferred method of installation is via pip. As Automaton requires root privileges in order to function properly, the package must be installed as root as well:

$ sudo pip install automaton-linux

Automaton has a few dependencies, but if an error occurs when building evdev, make sure that you have the Python headers installed. On Debian, this is as simple as:

$ sudo apt-get install python3-dev

To make sure that it is working properly, open a Python repl as root and run the following:

from automaton import *

for device in Automaton.find_devices():
    print(device)

This should output each device in the format, name :: path. These paths may change when a device reconnects, however its name will remain the same.

Find the path of your keyboard and mouse, then proceed to the next page.

QuickStart

A simple Automaton app might look like this:

from automaton import *

app = Automaton.new(devices = [
    '/dev/input/event5',
    '/dev/input/event6'
])

app.remap(Key.A, Key.K)

app.run()

Alright, so what's happening here?

On line 1, we import everything that we need from Automaton, which is usually everything and the kitchen sink.

On line 3, we instantiate a new Automaton object. We pass to it a keyword argument, devices. This is a list of paths to the devices that we want to monitor or control. Keep in mind, Automaton can only monitor events that the devices are capable of executing.

On line 8, we call the remap method, which takes two arguments, src and dest. All it does is remap the src key to the dest key. If you were to press the A key while this script is running, you would find that instead of a being typed, a nice fat k is typed instead.

Finally, on line 10, we call the run method, which is a blocking call. Only after this method has been called, will the monitoring (and thus the remapping)start. Conversely, controlling a device can be done at any time after the Automaton object has been instantiated.

While this script is running, if you were to in a separate repl do Automaton.find_devices(), you will find a new device, named Automaton. This is a UInput device that is created by Automaton. You don't need to care about this much, and the details of how it works are explained in the Internals chapter.

With that simple app out of the way, here are some more examples of automaton apps:

Do something when a hotkey is pressed

@app.on([Key.LCtrl, Key.LShift, Key.M])
def type_hello():
    app.type("Hello, John!")

Text replacement

from datetime import datetime

@app.on(":date")
def insert_date():
    return datetime.now().strftime("%Y-%m-%d %H:%M")

As you can see, the on() method is overloaded to either take a list of keys, or a string. This is parsed into a hotkey and hotstring, respectively.

Another important point is that we are returning a string from the function. This is the replacement text that will be written instead of the hotstring.

Note: You can return a string in a hotkey invocation as well, and it does exactly what you'd expect (it is also written). Therefore, in the hotkey example, we could have just returned the string "Hello, John!"

Do something when a hotstring is typed

import os

@app.on(":shutdown")
def shutdown():
    os.system("shutdown -P")

Remapping a key to a mouse button

app.remap(Key.Numpad4, Button.LeftButton)

Actions that only works if certain condition are true:

# Only work on Fridays the 13th

from datetime import datetime as dt
ominous = lambda: dt.now().weekday() == 4 and dt.now().day == 13

app.remap(Key.A, Key.K, when=ominous)

Both app.on and app.remap have a keyword argument called when. It takes a callable that should return a Boolean value. The action will run only when the returned value is true. In this example, all As will be replaced with Ks only on Fridays the 13th.

Actions that only works if the requirements are met by a specific device

Say you have two keyboards connected, one being your daily driver, and the other your dedicated macro keyboard. You want your second keyboard and only your second keyboard to do something different when the A key is pressed. What do you do? This!

# Assume /dev/input/event5 is your main keyboard...
# and /dev/input/event6 is your macro keyboard.

app = Automaton.new(devices = [
    '/dev/input/event5',
    '/dev/input/event6',
    # We don't care about the mouse here.
])

app.on([Key.A], from_device = '/dev/input/event6')
def macro_keyboard_only():
    print("Only available on MacroKeyboard!")

Configuring the behaviour of certain actions

If you've ever used AutoHotkey, you'll probably know that you can specify if a hotstring replaces the trigger text or not. There are many other options like that too, and Automaton has them all!

app.on(":date", options = [
    # Just one of many. See Api/Actions/HotString
    HotStringOptions.PreventAutoBackspace
])
def date():
    return "No Replacement :)"

Pretty much all actions besides Redirect have options.

Another thing you can configure in AutoHotkey would be which keys can actually trigger the hotstring after the trigger text has been typed. Guess who can do that as well:

app.on(":date", triggers = [Key.Space, Key.K])
def date():
    return "15 March 2022"

By default, the triggers keys are the constant HOTSTRING_TRIGGERS (see Api/Internals/Constants)

Recording a macro, then playing it.

macro: Macro = app.record_until(lambda: app.device.is_pressed(Key.Esc))

speed = 1.5 # Speed at which to perform the actions.
macro.play(speed)

Note that macros are currently experimental.

Api

Automaton

Actions

Remap

Redirect

HotKey

HotString

Misc

ActionString

Macro

Peripheral

Internals

ActionEmitter

Context

InputStream

Constants

Input

Testing