DUAL-FUNCTION-KEYS(1)
π 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 β
- 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. - 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. - Enable udevmon:
sudo systemctl enable udevmon - (Re)start udevmon:
sudo systemctl restart udevmon - Check for problems:
journalctl -u udevmon. No news is good news. You can safely disregard anyignoring /etc/interception/udevmon.yaml, reason: bad file: /etc/interception/udevmon.yamlmessages.
π₯ 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:
dual-function-keysitselfudevmon, which is the thing that launchesdual-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:
- Few keyboards run QMK Firmware.
- There are some issues with that functionality, as noted in the documentation Tap-Hold (https://docs.qmk.fm/).
- 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
- arch rolling: interception-dual-function-keys 1.5.0-6 Β· MIT
- nixos 26.05: dual-function-keys 1.5.0
- nixpkgs unstable: dual-function-keys 1.5.0
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms