DUAL-FUNCTION-KEYS(1)

πŸ“¦ version 1.5.0, in arch rolling, nixos 26.05, nixpkgs unstable

πŸ’– DUAL-FUNCTION-KEYS(1) πŸ’–

okay so bestie, welcome to the User Manuals page for interception β€” the dual-function-keys situation. It’s giving β€œone key, two jobs, zero chaos” energy, and honestly? Iconic.


🌸 NAME 🌸

interception β€” dual function keys πŸ’…


πŸ¦‹ DESCRIPTION πŸ¦‹

Tap for one key, hold for another. No cap.

Great for modifier keys like: hold for ctrl, tap for delete.

A hand-saver for those with restricted finger mobility. Fr fr, this one matters.

A plugin for interception tools (https://gitlab.com/interception/linux). Slay.


⭐ QUICK START ⭐

  1. Create some mappings at /etc/interception/dual-function-keys/my-mappings.yaml. There are many examples (https://gitlab.com/interception/linux/plugins/dual-function-keys/-/tree/master/doc/examples.md) β€” go browse, bestie.
  2. Create your interception tools (https://gitlab.com/interception/linux) udevmon configuration at /etc/interception/udevmon.d/my-keyboards.yaml. You can use my configuration to get started.
  3. Enable udevmon: sudo systemctl enable udevmon
  4. (Re)start udevmon: sudo systemctl restart udevmon
  5. Check for problems: journalctl -u udevmon. No news is good news. You can safely disregard any ignoring /etc/interception/udevmon.yaml, reason: bad file: /etc/interception/udevmon.yaml messages.

πŸ”₯ FUNCTIONALITY πŸ”₯

In these examples we will use the left shift key (LS).

It is configured to tap for delete (DE) and hold for LS.

- KEY: KEY_LEFTSHIFT
  TAP: KEY_DELETE
  HOLD: KEY_LEFTSHIFT

πŸ’… Tap πŸ’…

Press and release LS within TAP_MILLIS (default 200ms) for DE.

By default, until the tap is complete, we get LS. See below for other options.

                              <---------200ms--------->     <---------200ms--------->
              keyboard:       LS↓      LS↑                  LS↓                          LS↑
              computer sees:  LS↓      LS↑ DE↓ DE↑          LS↓                          LS↑

πŸ’… Double Tap πŸ’…

Tap then press again with DOUBLE_TAP_MILLIS (default 150ms) to hold DE.

                                           <-------150ms------->
                              <---------200ms--------->
              keyboard:       LS↓         LS↑             LS↓               LS↑
              computer sees:  LS↓         LS↑ DE↓ DE↑     DE↓ ..(repeats).. DE↑

You can continue double tapping so long as it is within the DOUBLE_TAP_MILLIS window. Periodt.

πŸ’… Consumption πŸ’…

Press or release another key during the TAP_MILLIS window and the tap will not occur.

This is especially useful for modifiers, for instance a quick ctrl-C. In this example we press the a key during the window.

Double taps do not apply after consumption; you will need to tap first.

Mouse and touchpad events (EV_REL and EV_ABS) can also consume taps, however you will need to use a Multiple Devices configuration.

                                                                             <-------150ms------->
                                                               <---------200ms--------->
                                               <-------150ms------->
                              <---------200ms--------->
              keyboard:       LS↓      a↓  a↑  LS↑             LS↓          LS↑           LS↓
              computer sees:  LS↓      a↓  a↑  LS↑             LS↓          LS↑ DE↓ DE↑   DE↓ ..(repeats)..

πŸŽ€ INSTALLATION πŸŽ€

πŸ“¦ Package Manager πŸ“¦

The package-manager vibe check is a packaging-status badge β€” [IMAGE: Packaging status] β€” pointing at https://repology.org/badge/vertical-allrepos/interception-dual-function-keys.svg, and it links to https://repology.org/project/interception-dual-function-keys/versions. Cute.

🧚 From Source 🧚

See runtime dependencies (https://gitlab.com/interception/linux/tools#runtime-dependencies).

Install Interception Tools (https://gitlab.com/interception/linux/tools) first.

git clone https://gitlab.com/interception/linux/plugins/dual-function-keys.git
cd dual-function-keys
make && sudo make install

Installation prefix defaults to /usr/local. This can be overridden in config.mk. πŸ’–

πŸŽ€ CONFIGURATION πŸŽ€

Okay bestie, so there are two things you gotta configure, not one:

  1. dual-function-keys itself
  2. udevmon, which is the thing that launches dual-function-keys

There's an examples file β€” https://gitlab.com/interception/linux/plugins/dual-function-keys/-/tree/master/doc/examples.md β€” and it has both dual-function-keys and udevmon.yaml configurations in it. Go look, it's literally right there. 🫢

✨ dual-function-keys ✨

This yaml file conventionally lives at /etc/interception/dual-function-keys. Like, that's its home. 🏠

You can use raw (integer) keycodes, but honestly it's way easier to use the defined strings from input-event-codes.h, which you can find here: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h

# optional
TIMING:
  TAP_MILLISEC: <integer>
  DOUBLE_TAP_MILLISEC: <integer>
  SYNTHETIC_KEYS_PAUSE_MILLISEC: <integer>

# necessary
MAPPINGS:
  - KEY: <integer | string>
    TAP: [ <integer | string>, ... ]
    HOLD: [ <integer | string>, ... ]
    # optional
    HOLD_START: [ AFTER_PRESS | AFTER_RELEASE | BEFORE_CONSUME | BEFORE_CONSUME_OR_RELEASE ]
  - KEY: ...

And our lil example from the last section? It looks exactly like this:

TIMING:
  TAP_MILLISEC: 200
  DOUBLE_TAP_MILLISEC: 150

MAPPINGS:
  - KEY: KEY_LEFTSHIFT
    TAP: KEY_DELETE
    HOLD: KEY_LEFTSHIFT

🌸 Combo Keys 🌸

You can configure TAP as a "combo" β€” that means it presses then releases multiple keys in order. Space cadet vibes fr fr (:

MAPPINGS:
  - KEY: KEY_LEFTSHIFT
    TAP: [ KEY_LEFTSHIFT, KEY_9, ]
    HOLD: KEY_LEFTSHIFT

And you can totally do the same with HOLD β€” configure it as a "combo" that presses then releases multiple keys in order, like a hyper modifier:

MAPPINGS:
  - KEY: KEY_TAB
    TAP: KEY_TAB
    HOLD: [ KEY_LEFTCTRL, KEY_LEFTMETA, KEY_LEFTALT, ]

By default there's a pause of 20ms between keys in the "combo". Want to change it? Say less:

TIMING:
    SYNTHETIC_KEYS_PAUSE_MILLISEC: 10

πŸ¦‹ Changing the Behavior of HOLD Keys πŸ¦‹

You can optionally use HOLD_START to configure how your HOLD keys behave. It's giving customization, it's giving control. πŸ’…

HOLD_START: AFTER_PRESS

If HOLD_START is unspecified, set to AFTER_PRESS, or set to some unrecognized value, the default behaviour will apply. (So basically, unrecognized = we pretend you said nothing, no judgement.)

HOLD_START: BEFORE_CONSUME

Here's the tea: HOLD keys get pressed before KEY is consumed, and released when KEY is released. So no extra keys besides the TAP keys get sent when KEY is tapped β€” but your HOLD keys can still be used as modifiers. Iconic behaviour honestly.

MAPPINGS:
  - KEY: KEY_LEFTSHIFT
    TAP: KEY_DELETE
    HOLD: KEY_LEFTSHIFT
    HOLD_START: BEFORE_CONSUME
                            <---------200ms--------->     <---------200ms--------->
keyboard:       LS↓      LS↑                  LS↓                          LS↑
computer sees:           DE↓ DE↑

                                                                       <-------150ms------->
                                                         <---------200ms--------->
                                         <-------150ms------->
                        <---------200ms--------->
keyboard:       LS↓      a↓  a↑   LS↑             LS↓          LS↑           LS↓
computer sees:       LS↓ a↓  a↑   LS↑                          DE↓ DE↑       DE↓ ..(repeats)..

HOLD_START: BEFORE_CONSUME_OR_RELEASE

Basically the same as BEFORE_CONSUME, except β€” plot twist β€” when KEY is released and it is neither tapped nor consumed before, HOLD keys are pressed in order and then released in order. Order matters, bestie. 🎯

MAPPINGS:
  - KEY: KEY_LEFTSHIFT
    TAP: KEY_DELETE
    HOLD: KEY_LEFTSHIFT
    HOLD_START: BEFORE_CONSUME_OR_RELEASE
                            <---------200ms--------->     <---------200ms--------->
keyboard:       LS↓      LS↑                  LS↓                          LS↑
computer sees:           DE↓ DE↑                                           LS↓ LS↑

HOLD_START: AFTER_RELEASE

Hold will only start after key release if the TAP_MILLISEC time has been exceeded. And this hold start is not affected by any kind of consumption. None. Zero. Nada. πŸ™ˆ

MAPPINGS:
  - KEY: KEY_A
    TAP: KEY_A
    HOLD: [KEY_LEFTSHIFT, KEY_A]
    HOLD_START: AFTER_RELEASE
                            <---------200ms--------->     <---------200ms--------->
keyboard:       a↓       a↑                   a↓                           a↑
computer sees:           a↓  a↑                                            A↓ A↑

πŸ’€ Warning πŸ’€

Do not assign the same modifier to two keys that you intend to press at the same time, because they will interfere with each other. Like, they'll fight. It's not cute.

Use the left and right versions of the modifiers instead β€” e.g. alt-tab with space-caps:

MAPPINGS:
  - KEY: KEY_CAPSLOCK
    TAP: KEY_TAB
    HOLD: KEY_LEFTALT

  - KEY: KEY_SPACE
    TAP: KEY_SPACE
    HOLD: KEY_RIGHTALT

Alternatively (and this is the glow-up ✨) you can use HOLD_START: BEFORE_CONSUME or HOLD_START: BEFORE_CONSUME_OR_RELEASE, and then assigning the same modifier will be totally fine:

MAPPINGS:
  - KEY: KEY_CAPSLOCK
    TAP: KEY_TAB
    HOLD: KEY_LEFTALT
    HOLD_START: BEFORE_CONSUME_OR_RELEASE

  - KEY: KEY_SPACE
    TAP: KEY_SPACE
    HOLD: KEY_LEFTALT
    HOLD_START: BEFORE_CONSUME_OR_RELEASE

🧚 udevmon 🧚

Okay so udevmon needs to be informed that we desire Dual Function Keys. Like, it's not a mind reader bestie. For the full story see How It Works at https://gitlab.com/interception/linux/tools#how-it-works.

- JOB: "intercept -g $DEVNODE | dual-function-keys -c </path/to/dual-function-keys.yaml> | uinput -d $DEVNODE"
  DEVICE:
    NAME: <keyboard name>

You can figure out the name by executing:

sudo uinput -p -d /dev/input/by-id/X

...where X is the device with the name that looks like your keyboard. Make sure all EV_KEYs are present under EVENTS. And if you can't find your keyboard under /dev/input/by-id, just look at devices directly under /dev/input. It's there, promise. πŸ”

For more info on uinput -p, see Interception Tools: How It Works at https://gitlab.com/interception/linux/tools#how-it-works.

Usually the name is enough to uniquely identify the keyboard, but ngl some keyboards register many devices, like a virtual mouse for example. You can run dual-function-keys for all the devices, but I personally prefer to run it only for the actual keyboard. Main character keyboard only. πŸ‘‘

My /etc/interception/udevmon.d/my-keyboards.yaml:

- JOB: "intercept -g $DEVNODE | dual-function-keys -c /etc/interception/dual-function-keys/home-row-modifiers.yaml | uinput -d $DEVNODE"
  DEVICE:
    NAME: "Minimalist Keyboard ABC"
    EVENTS:
      EV_KEY: [ KEY_LEFTSHIFT ]
- JOB: "intercept -g $DEVNODE | dual-function-keys -c /etc/interception/dual-function-keys/thumb-cluster.yaml | uinput -d $DEVNODE"
  DEVICE:
    NAME: "Split Keyboard XYZ"
    EVENTS:
      EV_KEY: [ KEY_LEFTSHIFT ]

🌈 Multiple Devices 🌈

When you're using inputs from multiple devices (think ctrl-scroll), it may be necessary to mux those devices for dual-function-keys to work across them β€” e.g. scroll consuming ctrl. The mux docs live at https://gitlab.com/interception/linux/tools#mux.

Example udevmon configuration for a mouse and a keyboard, because why choose:

- CMD: mux -c dfk -c my-keyboard -c my-mouse
- JOB:
  - mux -i dfk | dual-function-keys -c /etc/interception/dual-function-keys/my-cfg.yaml | mux -o my-keyboard -o my-mouse
  - mux -i my-keyboard | uinput -c /etc/interception/udevmon.d/my-keyboard.yaml
  - mux -i my-mouse | uinput -c /etc/interception/udevmon.d/my-mouse.yaml
- JOB: intercept -g $DEVNODE | mux -o dfk
  DEVICE:
    NAME: AT Translated Set 2 keyboard
    EVENTS:
      EV_KEY: [ KEY_LEFTCTRL ]
- JOB: intercept -g $DEVNODE | mux -o dfk
  DEVICE:
    NAME: Razer Razer Naga Trinity
    EVENTS:
      EV_REL: [REL_WHEEL]
      EV_KEY: [BTN_LEFT]

In the above example, my-keyboard.yaml and my-mouse.yaml represent the virtual devices that udevmon will create to output events. They're generated once from the device itself, like so:

sudo uinput -p -d /dev/input/by-id/usb-my-keyboard-kbd > my-keyboard.yaml

An alternative β€” if you want to live dangerously (https://gitlab.com/interception/linux/plugins/dual-function-keys/-/issues/31#note_725722450) β€” is to generate the virtual device configuration on the fly, e.g.:

- CMD: mux -c dfk -c my-keyboard -c my-mouse
- JOB:
  - mux -i dfk | dual-function-keys -c /etc/interception/dual-function-keys/my-cfg.yaml | mux -o my-keyboard -o my-mouse
  - mux -i my-keyboard | uinput -d /dev/input/by-path/my-keyboard-event-kbd
  - mux -i my-mouse | uinput -d /dev/input/by-id/usb-my-mouse-event-mouse
- JOB: intercept -g $DEVNODE | mux -o dfk
  DEVICE:
    LINK: /dev/input/by-path/my-keyboard-event-kbd
- JOB: intercept -g $DEVNODE | mux -o dfk
  DEVICE:
    LINK: /dev/input/by-id/usb-my-mouse-event-mouse

⭐ CAVEATS ⭐

As always, there's a caveat (there's always a caveat 😭): dual-function-keys operates on raw keycodes, not keysyms, as seen by X11 or Wayland.

So if you have anything modifying the keycodeβ†’keysym mapping β€” like XKB (https://www.x.org/wiki/XKB/) or xmodmap (https://wiki.archlinux.org/index.php/Xmodmap) β€” be mindful that dual-function-keys operates before them. It's up first, it goes first. πŸ₯‡

Some common XKB usages that might be found in your X11 configuration:

            Option "XkbModel" "pc105"
            Option "XKbLayout" "us"
            Option "XkbVariant" "dvp"
            Option "XkbOptions" "caps:escape"

πŸ’¬ FAQ πŸ’¬

🌱 I have a new use case. Can you support it?

Please raise an issue. Fr fr, that's the way. 🫢

dual-function-keys has been built for my needs. I will be intrigued to hear your ideas and help you make them happen.

As usual, PRs are very welcome. πŸ‘‘


⌨️ I see you are using q.m.k HHKB mod Keyboard in your udevmon. It uses QMK Firmware (https://qmk.fm/). Why not just use Tap-Hold (https://docs.qmk.fm/#/tap_hold)?

Good catch! That does indeed provide the same functionality as dual-function-keys. Unfortunately there are some drawbacks, and honestly? They're kind of a lot:

  1. Few keyboards run QMK Firmware.
  2. There are some issues with that functionality, as noted in the documentation Tap-Hold (https://docs.qmk.fm/).
  3. It requires a fast processor in the keyboard. My unscientific testing with an Ergodox (~800 scans/sec) and HHKB (~140) revealed that the slower keyboard is mushy and unuseably inaccurate. 😭

πŸ¦‹ Why not use xcape (https://github.com/alols/xcape)?

Xcape only provides simple tap/hold functionality. It appears difficult (impossible?) to add the remaining functionality using its XTest-FakeKeyEvent mechanisms.


πŸ’… My Key Combination Isn't Working

Ensure that your window manager is not intercepting that key combination. That's it, that's the whole vibe check β€” it's almost always the WM being a main character. πŸ™ˆ


πŸŽ€ I Don't Want Double Tap Functionality

Set DOUBLE_TAP_MILLISEC to 0. See Key Combinations, No Double Tap (https://gitlab.com/interception/linux/plugins/dual-function-keys/-/blob/master/doc/examples.md#key-combinations-no-double-tap).


🌈 CONTRIBUTORS 🌈

Please fork this repo and submit a PR. 🫢

If you are making changes to the documentation, please edit the pandoc flavoured dual-function-keys.md and run make doc.

Please ensure that this README.md and the man page dual-function-keys.1 has your changes and commit all three. Yes, all three β€” no skipping, bestie. 🌸

You can test the generated man page with man -l dual-function-keys.1

As usual, please obey .editorconfig.


πŸ‘‘ LICENSE πŸ‘‘

Copyright Β© 2020 Alexander Courtis


✨ AUTHORS ✨

Alexander Courtis.


And that's a wrap, bestie! πŸŽ€βœ¨ This iconic little page is from the Dual Function Keys manual, dated 2020/12/25 β€” go forth and remap responsibly. πŸ’…πŸ¦‹

πŸ“¦ where this page comes from