Skip to main content

A smart, flexible keymapper for Linux.

Project description

keyszer - a smart key remapper for Linux

work in progress discord

So this is a fork of xkeysnail?

Yes, this is a fork/reboot of the popular xkeysnail project. The xkeysnail project seems largely unmaintained since it's last release in Fall 2020; by unmaintained I mean that the author no longer seems involved (no commits, no releases, no comms on issues, no response to emails, no GitHub activity, etc).

Is it ready for me to use/test?

Certainly. I've been using it myself full time with a 99% stock Kinto.sh config file. If you're comfortable running from source and sending detailed bug reports we'd love to have your help. If you're comfortable hacking on the source, even better!

See UPGRADING_FROM_XKEYSNAIL.md to get started with your upgrade.

Is this compatible with Kinto.sh?

That is the plan. The major reason that Kinto.sh is using a fork has been resolved. Kinto.sh should simply "just work" with keyszer. In fact it should work better than before since many quirks with the Kinto version of xkeysnail are resolved. (such as nested combos not working, etc)

Note: If you want to get ahead of the curve you will need to alter your kinto.py config file just slightly. USING_WITH_KINTO.md

What features/fixes does it already have or have plans for in the near future?

  • Slightly simpler configuration API
  • more debugging logging
  • initial tests framework
  • more tests, tests, tests
  • entirely rewritten multi-modmap support
  • better conditional support (keymaps can now be conditional based on device name)
  • #10 No more running as root root
  • #9 Alt/Super wrongly trigger other non-combos when used as part of a combo
  • #7 Support for Hyper as a modifier
  • #2 Support for WM_NAME conditionals
  • #11 Support "sticky" Command-TAB to proper support Kinto.sh

Can I help/contribute?

Sure. Just open an issue to discuss how you'd like to get involved or respond on one of the existing issues. Or feel free to open new issues for feature requests.


keyszer - a smart key remapper for Linux

latest version license

discord open issues help welcome issues good first issue

keyszer is a keyboard remapping tool for the X environment written in Python. It's similar xmodmap but allows more flexible remappings.

Features

  • High-level and flexible remapping mechanisms:
    • per-application keybindings
    • multiple stroke keybindings such as Ctrl+x Ctrl+c to Ctrl+q
    • multipurpose bindings a regular key can become a modifier when held
  • Uses low-level libraries (evdev and uinput), making remapping work almost everywhere

This project was forked from keyszer which itself was based on the older pykeymacs.

Installation

Requires Python 3.

From source

git clone https://github.com/joshgoebel/keyszer.git
cd keyszer
sudo pip3 install --upgrade .

For testing/hacking/contributing

git clone https://github.com/joshgoebel/keyszer.git
cd keyszer
python -m venv .venv
source .venv/bin/activate
pip3 install -e .
./bin/keyszer -c config_file

Setup Requirements

We will need read/write access to:

  • /dev/input/event* - to capture input from actual hardware input devices
  • /dev/uinput - to present a pretend keyboard to the kernel

Running as a user in the input group (most secure)

Some distros already have an input group, or you can create one. You'll just need a few udev rules to make sure that the input devices are all given read/write access to that group.

/etc/udev/rules.d/90-custom-input.rules:

SUBSYSTEM=="input", GROUP="input"
KERNEL=="uinput", SUBSYSTEM=="misc", GROUP="input"

...and a new user that is a member of that group.

sudo useradd keymapper -G input

systemd

For a sample systemd service file please see keyszer.service.

Running as Your Logged in User

Caveats / Security Concerns

  • any running programs can potentially log all your keystrokes (including your passwords!) simply by monitoring the input devices

udev rules:

/etc/udev/rules.d/90-custom-input.rules:

SUBSYSTEM=="input", GROUP="input"
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"

With Systemd

Would it make sense to use systemd here also?

With a graphical display manager?

HOW?

With .xinitrc

If you're using a minimal setup you can simply add us to your .xinitrc. For example to start us up and then start Awesome WM.

keyszer &
exec awesome

Running as root (most insecure)

Caveats / Security Concerns

Don't do this, it's bad, dangerous, and wholly unnecessary.

Usage

keyszer 

A successful startup will look a bit like:

keyszer v0.4.99
(--) CONFIG: /home/jgoebel/.config/keyszer/config.py
(+K) Grabbing Apple, Inc Apple Keyboard (/dev/input/event3)
(--) Ready to process input.

Limiting Devices

Limit remapping to specify devices with --devices:

keyszer --devices /dev/input/event3 'Topre Corporation HHKB Professional'

The path or full device name can be used.

Other Options:

  • -c, --config - location of the configuration file
  • -w, --watch - watch for new keyboard devices to hot-plug
  • -v - much increased verbosity to help with debugging
  • --list-devices - list all available input devices

Configuration

By default we will look for the configuration in ~/.config/keyszer/config.py but you can override this location with the -c/--config switch. The configuration file is a Python script that defines modmaps, keymaps, and other configuration details. For an example configuration please see example/config.py.

The configuration API:

  • timeout(s)
  • add_modifier(name, aliases, key/keys)
  • keymap(name, map, when)
  • modmap(name, map)
  • multipurpose_modmap(name, map)
  • conditional(condition_fn, map) - used to wrap maps and only apply them conditionally

timeout(s)

Sets the number of seconds before multi-purpose modmaps timeout... ie, how long you have to press and release a key before it's instead assuming it's part of a combo.

add_modifier(name, aliases, key/keys)

Allows you to add custom modifiers and then map them to actual keys.

add_modifier("HYPER", aliases = ["Hyper"], key = Key.F24)

wm_class_match(re_str)

Helper to make matching conditionals a tiny bit simpler.

conditional(wm_class_match("^Firefox$"),
    keymap("Firefox",{
        # ... keymap here
    }))

not_wm_class_match(re_str)

The opposite of wm_class_match, matches only when the regex is NOT a match.

modmap(name, mappings, when)

Entirely maps one key to a different key, in all contexts. Note that the default modmap will be overruled by any conditional modmaps that apply. when can optionally be passed to make the modmap conditional.

modmap("default", {
    # mapping caps lock to left control
    Key.CAPSLOCK: Key.LEFT_CTRL
})

multipurpose_modmap(name, mappings)

Used to map a key with multiple-purposes, both for regular usage and use as a modifier (when held down).

multipurpose_modmap("default",
    # Enter is enter when pressed and released. Control when held down.
    {Key.ENTER: [Key.ENTER, Key.RIGHT_CTRL]}
)

keymap(name, mappings)

Defines a keymap consisting of mappings of the input combos mapped to output equivalents.

keymap("Firefox", {
    # when Cmd-S is hit instead send Ctrl-S
    K("Cmd-s"): K("Ctrl-s"),
}, when = lambda ctx: ctx.wm_class == "Firefox")

Argument mappings is a dictionary in the form of {key: command, key2: command2, ...} where key and command take following forms:

  • key: Key to override specified by K("YYY")
  • command: one of the followings
    • K("YYY"): Dispatch custom key to the application.
    • [command1, command2, ...]: Execute commands sequentially.
    • { ... }: Sub-keymap. Used to define multiple stroke keybindings. See multiple stroke keys for details.
    • escape_next_key: Escape next key.
    • arbitrary function: The function is executed and the returned value is used as a command.

Argument name specifies the keymap name. Every keymap should have a name. default is suggested for non-conditional keymaps.

conditional(fn, map)

Applies a map conditionally only when the fn function evaluates True. The below example is a modmap that is only active when the current WM_CLASS is Terminal.

conditional(
    lambda ctx: ctx.wm_class == "Terminal",
    modmap({
        # ...
    })
)

The context object passed to the fn function has several attributes:

  • wm_class - the WM_CLASS of the currently focused X11 window
  • device_name - the name of the device an input event originated on

Key Specification

Key specification in a keymap is in a form of K("(<Modifier>-)*<Key>") where

<Modifier> is one of the following:

  • C or Ctrl -> Control key
  • Alt -> Alt key
  • Shift -> Shift key
  • Super or Win or Cmd -> Super/Windows/Command key

You can specify left/right modifiers by adding any one of prefixes L/R.

<Key> is a key whose name is defined in key.py.

Here is a list of key specification examples:

  • K("LC-Alt-j"): Left Ctrl + Alt + j
  • K("Ctrl-m"): Ctrl + m
  • K("Win-o"): Super/Windows + o
  • K("Alt-Shift-comma"): Alt + Shift + comma

Multiple Stroke Keys

When you needs multiple stroke keys, define a nested keymap. For example, the following example remaps C-x C-c to C-q.

keymap(None, {
    K("C-x"): {
      K("C-c"): K("C-q"),
    }
})

Finding an Application's WM_CLASS with xprop

To check WM_CLASS of the application you want to have custom keymap, use xprop command:

xprop WM_CLASS

and then click the application. xprop tells WM_CLASS of the application as follows.

WM_CLASS(STRING) = "Navigator", "Firefox"

Use the second value (in this case Firefox) when matching context.wm_class when using a conditional.

Example of Case Insensitivity Matching

terminals = ["gnome-terminal","konsole","io.elementary.terminal","sakura"]
terminals = [term.casefold() for term in terminals]
termStr = "|".join(str(x) for x in terminals)

conditional(
    lambda ctx: ctx.wm_class.casefold() not in terminals,
    modmap({
        Key.LEFT_ALT: Key.RIGHT_CTRL,   # WinMac
        # ... 
    }))

conditional(
    lambda ctx: re.compile(termStr, re.IGNORECASE).search(ctx.wm_class),
    modmap("default", {
        Key.LEFT_ALT: Key.RIGHT_CTRL,   # WinMac
        # ... 
    }))

FAQ

Can I remap the Fn key?

Most laptops do not allow this as the Fn key is not directly exposed to the operating system. On some keyboards it's just another key. To find out you can run evtest. Point it to your keyboard device and then hit a few keys; then try Fn. If you get output, then you can map Fn. If not, you can't.

Here is an example from a full size Apple keyboard I have:

Event: time 1654948033.572989, type 1 (EV_KEY), code 464 (KEY_FN), value 1
Event: time 1654948033.572989, -------------- SYN_REPORT ------------
Event: time 1654948033.636611, type 1 (EV_KEY), code 464 (KEY_FN), value 0
Event: time 1654948033.636611, -------------- SYN_REPORT ------------

License

keyszer is distributed under GPL. See our LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

keyszer-0.5.0.tar.gz (42.9 kB view hashes)

Uploaded Source

Built Distribution

keyszer-0.5.0-py3-none-any.whl (35.5 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page