Package: keyszer
keyszer - a smart key remapper for Linux/X11Keyszer is a smart key remapper for Linux (and X11) written in Python. It's similar toxmodmapbut allows far more flexible remappings.  Keyszer was forked fromxkeysnailwhich no longer seems actively maintained.How does it work?Keyszer works at quite a low-level.  It grabs input directly from the kernel'sevdevinput devices (/dev/input/event*) and then creates an emulateduinputdevice to inject those inputs back into the kernel.  During this process the input stream is transformed on the fly as necessary to remap keys.Upgrading from xkeysnailSome small configuration changes will be needed.A few command line arguments have changed.For xkeysnail 0.4.0 seeUPGRADING_FROM_XKEYSNAIL.md.For xkeysnail (Kinto variety) seeUSING_WITH_KINTO.mdandUsing with Kinto v1.2-13.Key HighlightsLow-level library usage (evdevanduinput) allows remapping to work from the console all the way into X11.High-level and incredibly flexible remapping mechanisms:per-application keybindings- bindings that change depending on the active X11 application or windowmultiple stroke keybindings-Ctrl+x Ctrl+ccould map toCtrl+qvery flexible output-Ctrl-scould type out:save, and then hit enterstateful key combos- build Emacs style combos with shift/markmultipurpose bindings- a regular key can become a modifier when heldarbitrary functions- a key combo can run custom Python functionNew Features (since xkeysnail 0.4.0)simpler and more flexible configuration scripting APIsbetter debugging toolsconfigurableEMERGENCY EJECThotkeyconfigurableDIAGNOSTIChotkeyfully supports running as semi-privleged user (usingrootis now deprecated)addsMeta, CommandandCmd` aliases for Super/Meta modifiersupports custom modifiers viaadd_modifier(such asHyper)supportsFnas a potential modifier (on hardware where it works)addsbindhelper to support persistent holds across multiple combosmost frequently used for persistent Mac OS styleCmd-tabapp switcher panelsadds--checkfor checking the config file for issuesaddswm_namecontext for all conditionals (PR #40)addsdevice_namecontext for all conditionals (including keymaps)(fix)xmodmapcannot be used until some keys are first pressed on the emulated output(fix) ability to avoid unintentional Alt/Super false keypresses in many setups(fix) fixes multi-combo nested keymaps (vs Kinto's xkeysnail)(fix) properly cleans up pressed keys before terminationInstallationRequiresPython 3.Over time we should add individual instructions for various distros here.From sourceJust download the source and install.git clone https://github.com/joshgoebel/keyszer.git
cd keyszer
pip3 install --user --upgrade .For testing/hacking/contributingUsing a Pythonvenvmight be the simplest way to get started: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_fileSystem RequirementsKeyszer requires read/write access to:/dev/input/event*- to grab input from yourevdevinput devices/dev/uinput- to provide an emulated keyboard to the kernelRunning as a semi-privleged userIt's best to create an entirely isolated user to run the keymapper.  Group or ACL based permissions can be used to provide this user access to the necessary devices.  You'll need only a fewudevrules to ensure that the input devices are all given correct permissions.ACL based permissions (narrow, more secure)First, lets make a new user:sudo useradd keymapper...then use udev and ACL to grant our new user access:/etc/udev/rules.d/90-keymapper-acl.rulesKERNEL=="event*", SUBSYSTEM=="input", RUN+="/usr/bin/setfacl -m user:keymapper:rw /dev/input/%k"
KERNEL=="uinput", SUBSYSTEM=="misc", RUN+="/usr/bin/setfacl -m user:keymapper:rw /dev/uinput"Group based permissions (slightly wider, less secure)Many distros already have an input group; if not, you can create one.  Next, add a new user that's a member of that group:sudo useradd keymapper -G input...then use udev to grant our new user access (via theinputgroup):/etc/udev/rules.d/90-keymapper-input.rulesSUBSYSTEM=="input", GROUP="input"
KERNEL=="uinput", SUBSYSTEM=="misc", GROUP="input"systemdFor a sample systemd service file for running Keyszer as a service please seekeyszer.service.Running as the Active Logged in UserThis may be appropriate in some limited development scenarios, but is not recommended.  Giving the active, logged in user access toevdevanduinputpotentially allows all keystrokes to be logged and could allow a malicious program to take over (or destroy) your machine by injecting input into a Terminal session or other application.It would be better to open a terminal,suto a dedicatedkeymapperuser and then run Keyszer inside that context, as shown earlier.Running asrootDon't do this, it's dangerous, and not unnecessary.A semi-priveleged user with access to only the necessary input devices is a far better choice.UsagekeyszerA successful startup should resemble:keyszer v0.5.0
(--) CONFIG: /home/jgoebel/.config/keyszer/config.py
(+K) Grabbing Apple, Inc Apple Keyboard (/dev/input/event3)
(--) Ready to process input.Limiting DevicesLimit remapping to specify devices with--devices:keyszer --devices /dev/input/event3 'Topre Corporation HHKB Professional'The full path or complete device name may be used.  Device name is usually better to avoid USB device numbering jumping around after a reboot, etc...Other Options:-c,--config- location of the configuration file-w,--watch- watch for new keyboard devices to hot-plug-v- increase verbosity greatly (to help with debugging)--list-devices- list out all available input devicesConfigurationBy default we look for the configuration in~/.config/keyszer/config.py. You can override this location using the-c/--configswitch.  The configuration file is written in Python.
For an example configuration please seeexample/config.py.The configuration API:timeouts(multipurpose, suspend)wm_class_match(re_str)not_wm_class_match(re_str)add_modifier(name, aliases, key/keys)modmap(name, map, when_conditional)multipurpose_modmap(name, map, when_conditional)keymap(name, map, when_conditional)conditional(condition_fn, map)- used to wrap maps, applying them conditionallydump_diagnostics_key(key)emergency_eject_key(key)timeouts(...)Configures the timing behavior of various aspects of the keymapper.multipurpose- The number of seconds before a held multi-purpose key is assumed to be a modifier (evne in the absense of other keys).suspend- The number of seconds modifiers are "suspended" and withheld from the output waiting to see whether if they are part of a combo or if they may be the actual intended output.Defaults:timeouts(multipurpose=1,suspend=1,)dump_diagnostics_key(key)Configures a key that when hit will dump additional diagnostic information to STDOUT.dump_diagnostics_key(Key.F15)# defaultemergency_eject_key(key)Configures a key that when hit will immediately terminate keyszer; useful for development, recovering from bugs, or badly broken configurations.emergency_eject_key(Key.F16)# defaultadd_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)Note:Just addingHYPERdoesn't necessarily make it work with your software, you may still need to configure X11 setup to accept the key you choose as the "Hyper" key.wm_class_match(re_str)Helper to make matching conditionals (and caching the compiled regex) just a tiny bit simpler.keymap("Firefox",{# ... keymap here},when=wm_class_match("^Firefox$"))not_wm_class_match(re_str)The negation ofwm_class_match, matches only when the regex does NOT match.modmap(name, mappings, when_conditional = None)Maps a single physical key to a different key.  A default modmap will always be overruled by any conditional modmaps that apply.when_conditionalcan be passed to make the modmap conditional.modmap("default",{# mapping caps lock to left controlKey.CAPSLOCK:Key.LEFT_CTRL})If you don't create a default (non-conditional) modmap a blank one is created for you.  Formodmapsides of the pairing will beKeyliterals (not combos).multipurpose_modmap(name, mappings)Used to bestow a key with multiple-purposes, both for regular use and for use as a modifier.multipurpose_modmap("default",# Enter is enter if pressed and immediately released...# ...but Right Control if held down and paired with other keys.{Key.ENTER:[Key.ENTER,Key.RIGHT_CTRL]})keymap(name, mappings)Defines a keymap of input combos mapped to output equivalents.keymap("Firefox",{# when Cmd-S is input instead send Ctrl-S to the ouputC("Cmd-s"):C("Ctrl-s"),},when=lambdactx:ctx.wm_class=="Firefox")Because of thewhenconditional this keymap will only apply for Firefox.The argumentmappingsis a dictionary in the form of{ combo: command, ...}wherecomboandcommandtake following forms:combo: Combo to map, specified byK(combo_str)For the syntax of combo specifications, seeCombo Specifications.command: one of the followingK(combo_str): Type a specific key combo to the output.[command1, command2, ...]: Execute multiple commands sequentially.{ ... }: Sub-keymap. Used to defineMultiple Stroke Keys.escape_next_key: Escape the next key pressed.ignore_key: Ignore the key that is pressed next. (often used to disable native combos)bind: Bind an input and ouput modifier together such that the output is not lifted until the input is.arbitrary function: The function is executed and the returned value (if any) is used as a command.The argumentnamespecifies the keymap name. Every keymap has a name - usingdefaultis suggested for a non-conditional keymap.conditional(fn, map)Applies a map conditionally, only when thefnfunction evaluatesTrue.  The below example is a modmap that is only active when the currentWM_CLASSisTerminal.conditional(lambdactx:ctx.wm_class=="Terminal",modmap({# ...}))Thecontextobject passed to thefnfunction has several attributes:wm_class- the WM_CLASS of the [input] focused X11 windowwm_name- the WM_NAME of the [input] focused X11 windowdevice_name- name of the device where an input originatedNote:The same conditionalfncan always be passed directly tomodmapusing thewhenargument.MarksTODO: need docs (See issue #8)Combo SpecificationsThe Combo specification in a keymap is written in the form ofC("(<Modifier>-)*<Key>").<Modifier>is one of the following:CorCtrl-> Control keyAlt-> Alt keyShift-> Shift keySuper,Win,Command,Cmd,Meta-> Super/Windows/Command keyFn-> Function key (on supported keyboards)You can specify left/right modifiers by adding the prefixesLorR.<Key>is any key whose name is defined inkey.py.Some combo examples:C("LC-Alt-j"): left Control, Alt,jC("Ctrl-m"): Left or Right Control,mC("Win-o"): Cmd/Windows,oC("Alt-Shift-comma"): Alt, Left or Right Shift, commaMultiple Stroke KeysTo use multiple stroke keys, simply define a nested keymap. For example, the
following example remapsC-x C-ctoC-q.keymap("multi stroke",{C("C-x"):{C("C-c"):C("C-q"),}})Finding out the properKey.NAMEliteral for a key on your keyboardFrom a terminal session runevtestand select your keyboard's input device.  Now hit the key in question.Event: time 1655723568.594844, type 1 (EV_KEY), code 69 (KEY_NUMLOCK), value 1
Event: time 1655723568.594844, -------------- SYN_REPORT ------------Above I've just pressed "clear" on my numpad and seecode 69 (KEY_NUMLOCK)in the ouput. For Keyszer this would translate toKey.NUMLOCK.  You can also browse thefull list of key namesin the source.Finding an Application'sWM_CLASSandWM_NAMEusingxpropUse thexpropcommand from a terminal:xprop WM_CLASS WM_NAME...then click an application window.  Let's try it with Google Chrome:WM_CLASS(STRING) = "google-chrome", "Google-chrome"
WM_NAME(UTF8_STRING) = "README - Google Chrome"Use the secondWM_CLASSvalue (in this caseGoogle-chrome) when matchingcontext.wm_class.Example of Case Insensitive Matchingterminals=["gnome-terminal","konsole","io.elementary.terminal","sakura"]terminals=[term.casefold()forterminterminals]USING_TERMINAL_RE=re.compile("|".join(terminals),re.IGNORECASE)modmap("not in terminal",{Key.LEFT_ALT:Key.RIGHT_CTRL,# ...},when=lambdactx:ctx.wm_class.casefold()notinterminals)modmap("terminals",{Key.RIGHT_ALT:Key.RIGHT_CTRL,# ...},when=lambdactx:USING_TERMINAL_RE.search(ctx.wm_class))FAQCan I remap the keyboard'sFnkey?It depends.Most laptops do not allow this as theFnkeypress events are notdirectlyexposed to the operating system.  On some keyboards, it's just another key.  To find out you can runevtest.  Point it to your keyboard device and then hit a few keys; then tryFn.  If you get output, then you can mapFn.  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 ------------Does Keyszer support FreeBSD/NetBSD or other BSDs?Not at the moment, perhaps never.  If you're an expert on the BSD kernel's input layers pleasejoin the discussion.  I'm at the very least open to the discussion to find out if this is possible, a good idea, etc...Does this work with Wayland?Not yet.This is desires but seems impossible at the moment until there is a standardized system toquickly and easilydetermine the app/window that has input focus on Wayland, just like we do so easily on X11.Is keyszer compatible withKinto.sh?That is certainly the plan.The major reason Kinto.sh required it's own forkhas been resolved.  Kinto.sh should simply "just work" withkeyszer(with a few tiny config changes).  In fact, hopefully it works better than before since many quirks with the Kinto fork should be resolved. (such as nested combos not working, etc)Reference:Kinto GitHub issueregarding the transition.Instructions on altering yourkinto.pyconfig slightly. SeeUSING_WITH_KINTO.md.How can I help or contribute?Please open an issue to discuss how you'd like to get involved or respond on one of the existing issues. Also feel free to open new issues for feature requests.  Many issues are taggedgood first issueorhelp welcome.Licensekeyszeris distributed under GPL3.  SeeLICENSE.