From ad6d734d51a3e1ce1b693518af76b0bd35e7cd32 Mon Sep 17 00:00:00 2001 From: Raheman Vaiya Date: Mon, 27 Dec 2021 04:30:58 -0500 Subject: [PATCH] v2.0.0-beta keyd redux take 1. A rewrite which simplifies the config format and provides a solid foundation for incrementally introducing experimental features. Internally: - Modularized and rewrote most of the code - Added a mini testing framework (t/) Externally: - Eliminated layer inheritance in favour of simple types. (layouts are now defined with `:layout` instead of `:main`) - Macros are now repeatable. - Overload now accepts a hold threshold timeout. - Config files are now vendor/product id oriented. - SIGUSR1 now triggers a config reload. - Modifiers are layers by default and can be extended directly. - Config files now end in `.conf`. - `layert()` is now `toggle()`. - All layers are 'modifier layers' (terminological change) - Eliminated the dedicated modifer layout. - Modifiers no longer apply to key sequences defined within a layer. (Layer sequences are now always executed verbatim.) The old behaviour was unintuitive and can be emulated using nested layers if necessary. --- .gitignore | 1 + CHANGELOG.md | 52 +- Makefile | 15 +- README.md | 73 +- TODO | 5 + ..._esc_basic.cfg => capslock-esc-basic.conf} | 0 ...cfg => capslock-escape-with-vim-mode.conf} | 12 +- examples/{mac_like.cfg => macos.conf} | 7 +- keyd.1.gz | Bin 4042 -> 4205 bytes man.md | 310 ++--- src/config.c | 1023 ++++++----------- src/config.h | 103 +- src/descriptor.c | 282 +++++ src/descriptor.h | 79 ++ src/error.c | 23 + src/error.h | 9 + src/ini.c | 102 ++ src/ini.h | 52 + src/keyboard.c | 474 ++++++++ src/keyboard.h | 43 + src/{main.c => keyd.c} | 712 ++++-------- src/keyd.h | 8 + src/keys.c | 44 + src/keys.h | 10 +- src/layer.c | 103 ++ src/layer.h | 38 + t/keys.py | 477 ++++++++ t/layer.t | 17 + t/layer1.t | 9 + t/layer2.t | 11 + t/layer3.t | 9 + t/layout-mods.t | 13 + t/layout-mods2.t | 13 + t/layout-seq.t | 11 + t/layout.t | 17 + t/macro-nested.t | 15 + t/macro.t | 13 + t/mod.t | 13 + t/mod2.t | 13 + t/mod3.t | 13 + t/oneshot.t | 9 + t/oneshot10.t | 11 + t/oneshot11.t | 15 + t/oneshot2.t | 9 + t/oneshot3.t | 17 + t/oneshot4.t | 13 + t/oneshot5.t | 13 + t/oneshot6.t | 13 + t/oneshot9.t | 9 + t/oneshotn.t | 9 + t/oneshotn3.t | 13 + t/overload-timeout.t | 9 + t/overload-timeout2.t | 12 + t/overload.t | 7 + t/overload1.t | 9 + t/overload2.t | 9 + t/overload3.t | 11 + t/reset.t | 15 + t/run.sh | 22 + t/runner.py | 374 ++++++ t/swap.t | 23 + t/swap2.t | 25 + t/swap3.t | 35 + t/swap4.t | 27 + t/swap5.t | 21 + t/test.conf | 78 ++ t/toggle.t | 13 + t/toggle2.t | 25 + 68 files changed, 3608 insertions(+), 1462 deletions(-) create mode 100644 TODO rename examples/{caps_esc_basic.cfg => capslock-esc-basic.conf} (100%) rename examples/{caps_esc_with_vim_mode.cfg => capslock-escape-with-vim-mode.conf} (88%) rename examples/{mac_like.cfg => macos.conf} (95%) create mode 100644 src/descriptor.c create mode 100644 src/descriptor.h create mode 100644 src/error.c create mode 100644 src/error.h create mode 100644 src/ini.c create mode 100644 src/ini.h create mode 100644 src/keyboard.c create mode 100644 src/keyboard.h rename src/{main.c => keyd.c} (56%) create mode 100644 src/keyd.h create mode 100644 src/keys.c create mode 100644 src/layer.c create mode 100644 src/layer.h create mode 100644 t/keys.py create mode 100644 t/layer.t create mode 100644 t/layer1.t create mode 100644 t/layer2.t create mode 100644 t/layer3.t create mode 100644 t/layout-mods.t create mode 100644 t/layout-mods2.t create mode 100644 t/layout-seq.t create mode 100644 t/layout.t create mode 100644 t/macro-nested.t create mode 100644 t/macro.t create mode 100644 t/mod.t create mode 100644 t/mod2.t create mode 100644 t/mod3.t create mode 100644 t/oneshot.t create mode 100644 t/oneshot10.t create mode 100644 t/oneshot11.t create mode 100644 t/oneshot2.t create mode 100644 t/oneshot3.t create mode 100644 t/oneshot4.t create mode 100644 t/oneshot5.t create mode 100644 t/oneshot6.t create mode 100644 t/oneshot9.t create mode 100644 t/oneshotn.t create mode 100644 t/oneshotn3.t create mode 100644 t/overload-timeout.t create mode 100644 t/overload-timeout2.t create mode 100644 t/overload.t create mode 100644 t/overload1.t create mode 100644 t/overload2.t create mode 100644 t/overload3.t create mode 100644 t/reset.t create mode 100755 t/run.sh create mode 100755 t/runner.py create mode 100644 t/swap.t create mode 100644 t/swap2.t create mode 100644 t/swap3.t create mode 100644 t/swap4.t create mode 100644 t/swap5.t create mode 100644 t/test.conf create mode 100644 t/toggle.t create mode 100644 t/toggle2.t diff --git a/.gitignore b/.gitignore index 3f938f0..4c07980 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tags bin/ *.gch +__pycache__ diff --git a/CHANGELOG.md b/CHANGELOG.md index d1eea69..5caf47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,47 @@ -# v1.3.1 +# v2.0-beta -- Fixed overload behaviour (see #34) +Major version update. -# v1.3.0 +This breaks 1.x configs. The format may change slightly before leaving beta, +but once it does should remain backwards compatible for the foreseeable future. -- Adds the ability to swap layers mid-key (#48). -- Adds a panic key sequence (backspace + backslash + enter). -- Clears oneshot layers when undefined keys are pressed. -- Removes duplicate keycode names in `keyd -l`. -- Fixes accidental layer toggle bug (#44). -- Improves logging. -- Fixes support for keyboards with built in mice. -- Misc bugfixes. +A non exhaustive list of changes can be found below. It is best to forget +everything you know and read man page anew. + +- Eliminated layer inheritance in favour of simple types. +(layouts are now defined with `:layout` instead of `:main`) +- Macros are now repeatable. +- Overload now accepts a hold threshold timeout. +- Config files are now vendor/product id oriented. +- SIGUSR1 now triggers a config reload. +- Modifiers are layers by default and can be extended directly. +- Config files now end in `.conf`. +- `layert()` is now `toggle()`. +- All layers are 'modifier layers' (terminological change) +- Eliminated the dedicated modifer layout. +- Modifiers no longer apply to key sequences defined within a layer. + (Layer entries are now always executed verbatim.) + + The old behaviour was unintuitive and can be emulated using nested + layers if necessary. + +For most old configs transitioning should be a simple matter of changing +the file extension from `.cfg` to `.conf`, replacing `layert` with +`toggle`, changing `:main` to `:layout` and adding + +``` +[ids] + +* + +[main] +``` + +to the top of the file. + +More involved configs may need additional changes, but should be possible +to replicate using the new rules. If not, please file an issue on +[github](https://github.com/rvaiya/keyd/issues). # v1.1.1 diff --git a/Makefile b/Makefile index 6747de5..8bedb42 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean install uninstall +.PHONY: all clean install uninstall debug man DESTDIR= PREFIX=/usr @@ -7,21 +7,22 @@ LOCK_FILE="/var/lock/keyd.lock" LOG_FILE="/var/log/keyd.log" CONFIG_DIR="/etc/keyd" -VERSION=1.3.1 +VERSION=2.0.0-beta GIT_HASH=$(shell git describe --no-match --always --abbrev=40 --dirty) -CFLAGS=-DVERSION=\"$(VERSION)\" \ +CFLAGS+=-DVERSION=\"$(VERSION)\" \ -DGIT_COMMIT_HASH=\"$(GIT_HASH)\" \ -DCONFIG_DIR=\"$(CONFIG_DIR)\" \ -DLOG_FILE=\"$(LOG_FILE)\" \ -DLOCK_FILE=\"$(LOCK_FILE)\"\ -I/usr/local/include\ - -L/usr/local/lib + -L/usr/local/lib\ all: mkdir -p bin $(CC) $(CFLAGS) -O3 src/*.c -o bin/keyd -ludev debug: + mkdir -p bin $(CC) $(CFLAGS) -O3 src/*.c -o bin/keyd -ludev -pedantic -Wall -Wextra -g man: pandoc -s -t man man.md | gzip > keyd.1.gz @@ -40,4 +41,8 @@ uninstall: rm -f $(DESTDIR)$(PREFIX)/lib/systemd/system/keyd.service\ bin/keyd $(DESTDIR)$(PREFIX)/bin/keyd\ $(DESTDIR)$(PREFIX)/share/man/man1/keyd.1.gz - +test: all + @cd t; \ + for f in *.sh; do \ + ./$$f; \ + done diff --git a/README.md b/README.md index 66a3caa..ccc0ab6 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ # Impetus Linux lacks a good key remapping solution. In order to achieve satisfactory -results a medley of tools need to be employed (e.g. xcape, xmodmap) with the end +results a medley of tools need to be employed (e.g xcape, xmodmap) with the end result often being tethered to a specified environment (X11). keyd attempts to solve this problem by providing a flexible system wide daemon which remaps keys using kernel level input primitives (evdev, uinput). -# UPDATE +# UPDATE (v2.0.0-beta) -*Version 1.0.0 has just been released and includes some breaking changes. Please see the [changelog](CHANGELOG.md) for details.* +master is currently tracking `v2.0.0-beta`. If you are looking for something +a bit more stable you may be interested the [v1](https://github.com/rvaiya/keyd/tree/v1) +branch. + +*If you are migrating your config from v1, please see the +[changelog](CHANGELOG.md) for a list of changes.* # Features @@ -32,7 +37,7 @@ Some of the more interesting ones include: - Want to put the control and escape keys where God intended. - Would like the ability to easily generate keycodes in other languages. - Constantly fiddle with their key layout. - - Want an intuitive keyboard config format which is simple to grok. + - Want an inuitive keyboard config format which is simple to grok. - Wish to be able to switch to a VT to debug something without breaking their keymap. - Like tiny daemons that adhere to the Unix philosophy. @@ -72,9 +77,15 @@ members, no personal responsibility is taken for them. 1. Install keyd -2. Put the following in `/etc/keyd/default.cfg`: +2. Put the following in `/etc/keyd/default.conf`: ``` +[ids] + +* + +[main] + # Turns capslock into an escape key when pressed and a control key when held. capslock = overload(C, esc) @@ -87,21 +98,27 @@ esc = capslock 4. See the [man page](man.md) for a comprehensive list of config options. *Note*: It is possible to render your machine unusable with a bad config file. -In the event of a rogue configuration the key sequence `backspace+slash+enter` -should terminate keyd. It is recommended that you avoid experimenting in -default.cfg (see the man page for keyboard specific configuration) so you can -plug in another keyboard which is unaffected by the changes. +Before proceeding ensure you have some way of killing keyd if things go wrong +(e.g ssh). It is recommended that you avoid experimenting in default.cfg (see +the man page for keyboard specific configuraiton) so you can plug in another +keyboard which is unaffected by the changes. # Sample Config - leftshift = oneshot(S) - capslock = overload(symbols, esc) + [ids] + + * + + [main] - [symbols] + leftshift = oneshot(S) + capslock = overload(symbols, esc) - d = ~ - f = / - ... + [symbols] + + d = ~ + f = / + ... # Recommended config @@ -109,15 +126,21 @@ Many users will probably not be interested in taking full advantage of keyd. For those who seek simple quality of life improvements I can recommend the following config: - leftshift = oneshot(S) - leftalt = oneshot(A) - rightalt = oneshot(G) - rightshift = oneshot(A) - leftmeta = oneshot(M) - rightmeta = oneshot(M) + [ids] + + * + + [main] + + leftshift = oneshot(S) + leftalt = oneshot(A) + rightalt = oneshot(G) + rightshift = oneshot(A) + leftmeta = oneshot(M) + rightmeta = oneshot(M) - capslock = overload(C, esc) - insert = S-insert + capslock = overload(C, esc) + insert = S-insert This remaps all modifiers to 'oneshot' keys and overloads the capslock key to function as both escape (when tapped) and control (when held). Thus to produce @@ -145,7 +168,7 @@ way to get comparable features. I became aware of kmonad after having published keyd. While kmonad is a fine project with similar goals, it takes a different approach and has a different design philosophy. -Notably keyd was written entirely in C with performance and simplicity in +Notably keyd was written entirely in C with performance and simplicitly in mind and will likely never be as configurable as kmonad (which is extensible in Haskell). Having said that, it supplies (in the author's opinion) the most valuable features in less than 2000 lines of C while providing @@ -155,7 +178,7 @@ a simple language agnostic config format. If you feel something is missing or find a bug you are welcome to file an issue on github. keyd has a minimalist (but sane) design philosophy which -intentionally omits certain features (e.g. unicode/executing arbitrary executables +intentionally omits certain features (e.g unicode/execing arbitrary executables as root). Things which already exist in custom keyboard firmware like QMK are good candidates for inclusion. diff --git a/TODO b/TODO new file mode 100644 index 0000000..c1127ab --- /dev/null +++ b/TODO @@ -0,0 +1,5 @@ + - Organize tests. + - Improve FAQ. + - Add more examples. + - Multi user support? + - Unicode support? diff --git a/examples/caps_esc_basic.cfg b/examples/capslock-esc-basic.conf similarity index 100% rename from examples/caps_esc_basic.cfg rename to examples/capslock-esc-basic.conf diff --git a/examples/caps_esc_with_vim_mode.cfg b/examples/capslock-escape-with-vim-mode.conf similarity index 88% rename from examples/caps_esc_with_vim_mode.cfg rename to examples/capslock-escape-with-vim-mode.conf index 944f91a..8f4a48c 100644 --- a/examples/caps_esc_with_vim_mode.cfg +++ b/examples/capslock-escape-with-vim-mode.conf @@ -1,4 +1,4 @@ -# NOTE: to use this, rename this file to `your keyboard name`.cfg and put in /etc/keyd/ +# NOTE: to use this, rename this file to default.conf and put in /etc/keyd/ # Advanced use of capslock as a triple function key: # @@ -10,14 +10,24 @@ # - when 'capslock' is held, and the 'space' key is tapped, we enter a 3rd # state for "VIM mode" where hjkl keys become arrow keys until capslock # is released. +# +[ids] +* + +[main] + capslock = overload(ctrl_vim, esc) # ctrl_vim modifier layer; inherits from 'Ctrl' modifier layer + [ctrl_vim:C] + space = swap(vim_mode) # vim_mode modifier layer; also inherits from 'Ctrl' modifier layer + [vim_mode:C] + h = left j = down k = up diff --git a/examples/mac_like.cfg b/examples/macos.conf similarity index 95% rename from examples/mac_like.cfg rename to examples/macos.conf index 3bee053..8c91f5a 100644 --- a/examples/mac_like.cfg +++ b/examples/macos.conf @@ -1,4 +1,4 @@ -# NOTE: to use this, rename this file to `your keyboard name`.cfg and put in /etc/keyd/ +# NOTE: to use this, rename this file to default.conf and put in /etc/keyd/ # Mac-Like Configuration Example # @@ -10,6 +10,11 @@ # in mutter to make it inert: # - `gsettings set org.gnome.mutter overlay-key ''` +[ids] +* + +[main] + # Create a new "Cmd" button, with various Mac OS-like features below leftalt = layer(meta_mac) diff --git a/keyd.1.gz b/keyd.1.gz index 5b8f8f0fb89062b2a6b2a251f51126390fdeff0a..7657c5a1704bb6bb92d39bcd656f580df2857369 100644 GIT binary patch literal 4205 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\+\* will force keyd to -terminate. If you are experimenting with the available options it is advisable -to do so in a keyboard specific file (see CONFIGURATION) instead of directly in -`default.cfg`, so it remains possible to plug in another keyboard unaffected by the -changes. +Becuase keyd modifies your primary input device it is possible to render your +machine unusuable with a bad config file. If you find yourself in this +situation the sequence *\+\+\* will force keyd +to terminate. # OPTIONS -**-m**: Run in monitor mode. (ensure keyd is not running to see untranslated events). +**-m**: Monitor and print keyboard events to stdout. **-l**: List all valid key names. @@ -40,32 +37,55 @@ changes. # CONFIGURATION -All configuration files are stored in */etc/keyd/*. The name of each file -should correspond to the device name to which it is to be applied followed by -.cfg (e.g "/etc/keyd/Magic Keyboard.cfg"). Configuration files are loaded upon -initialization and can be reified by reloading keyd (e.g sudo systemctl restart -keyd). +All configuration files are stored in */etc/keyd/* and are loaded upon +initialization. A reload can be triggered by restarting the daemon or by +sending SIGUSR1 to the process (e.g sudo pkill -usr1 keyd). -A list of valid key names can be produced with **-l**. The monitor flag (**-m**) can -also be used to obtain device and key names like so: +A valid config file has the extension .conf and must begin with an *ids* section that has the following form: - > sudo systemctl stop keyd + [ids] + + + + ... + +Where each \ is one of: + + - A device id of the form : (obtained with -m). + - The wildcard "*". + +A wildcard indicates that the file should apply to all keyboards +which are not explicitly listed in another configuration file and +may optionally be followed by one or more lines of the form: + + -: + +representing a device to be excluded from the matching policy. Thus the following +config will match all devices except 0123:4567: + + [ids] + * + -0123:4567 + + +The monitor flag (**-m**) can be used to interactively obtain device ids and key names like so: + + > sudo systemctl stop keyd # Avoid loopback. > sudo keyd -m - Magic Keyboard: capslock down - Magic Keyboard: capslock up + Magic Keyboard 0ade:0fac capslock down + Magic Keyboard 0ade:0fac capslock up ... -If no configuration file exists for a given keyboard *default.cfg* is used as a -fallback (if present). +Every subsequent section of the file corresponds to a layer and has the form: -Each line in a configuration file consists of a mapping of the following form: + [[:]] - = | +Where `` is either a valid modifier set (see *MODIFIERS*) or "layout". -or else represents the beginning of a new layer. E.G: +Each line within a layer is a mapping of the form: - [] + = | Where `` has the form: `[[-...]-` @@ -77,100 +97,126 @@ and each modifier is one of: \ **S** - Shift\ \ **G** - AltGr -Lines can be commented out by prepending # but inline comments are not supported. - -In addition to simple key mappings keyd can remap keys to actions which -can conditionally send keystrokes or transform the state of the keymap. +In addition to key sequences, keyd can remap keys to actions which +conditionally send keystrokes or transform the state of the keymap. It is, for instance, possible to map a key to escape when tapped and control when held by assigning it to `overload(C, esc)`. A complete list of available actions can be found in *ACTIONS*. -As a special case \ may be 'noop' which causes it to be -ignored. This can be used to simulate a modifier sequence with no -attendant termination key: - -E.G. - -`C-A-noop` will simulate the simultaneous depression and release -of the control and alt keys. - - ## Layers -Each configuration file consists of one or more layers. Each layer is a keymap -unto itself and can be transiently activated by a key mapped to the *layer* -action. +Each layer is a keymap unto itself and can be transiently activated by a key +mapped to the corresponding *layer()* action. -For example the following configuration creates a new layer called 'symbols' which +For example, the following configuration creates a new layer called 'symbols' which is activated by holding the capslock key. + [ids] + * + + [main] capslock = layer(symbols) [symbols] - f = ~ d = / + ... Pressing `capslock+f` thus produces a tilde. -Any set of valid modifiers is also a valid layer. For example the layer `M-C` -corresponds to a layer which behaves like the modifiers meta and control. These -play nicely with other modifiers and preserve existing stacking semantics. +Key sequences within a layer are fully descriptive and completely self +contained. That is, the sequence 'A-b' corresponds exactly to the combination +`+`. If any additional modifiers are active they will be deactivated +for the duration of the corresponding key stroke. -A layer may optionally have a parent from which mappings are drawn for keys -which are not explicitly mapped. By default layers do not have a parent, that -is, unmapped keys will have no effect. A parent is specified by appending -`:` to the layer name. +## Layouts -The *layout* is a special layer from which mappings are drawn if no other layers -are active. The default layout is called **main** and is the one to which -mappings are assigned if no layer heading is present. By default all keys are -defined as themselves in the main layer. Layouts should inherit from main to -avoid having to explicitly define each key. The default layout can be -changed by including `layout()` at the top of the config file. +The *layout* is a special kind of layer from which mappings are drawn if no +other layers are active. By default all keys are mapped to themselves within a +layout. Every config has at least one layout called *main*, but additional +layouts may be defined and subsequently activated using the `layout()` action. -## The Modifier Layout +Layouts also have the additional property of being affected by the active modifier +set. That is, unlike layouts, key sequences mapped within them are not +interpreted literally. -keyd distinguishes between the key layout and the modifier layout. This -allows the user to use a different letter arrangement for modifiers. It may, -for example, be desirable to use an alternative key layout like dvorak while -preserving standard qwerty modifier shortcuts. This can be achieved by passing -a second argument to the layout function like so: `layout(dvorak, main)`. The -default behaviour is to assign the modifier layout to the key layout if one -is not explicitly specified. +If you wish to use an alternative letter arrangement, this is the appropriate +place to define it. -Note that this is different from simply defining a custom layer which reassigns -each key to a modified key sequence (e.g `s = C-s`) since it applies to all -modifiers and preserves expected stacking behaviour. +E.G -## Modifier Layers + [main] -In addition to standard layers, keyd introduces the concept of 'modifier -layers' to accommodate the common use case of remapping a subset of modifier -keys. A modifier layer will behave as a set of modifiers in all instances -except when a key is explicitly mapped within it and can be defined -by creating a layer which inherits from a valid modifier set. + rightshift = layout(dvorak) -E.G.: + [dvorak:layout] - capslock = layer(custom_control) - - [custom_control:C] + rightshift = layout(main) + s = o + d = e + ... + +## Modifiers + +Unlike most other remapping tools keyd provides first class support for +modifiers. A valid modifier set may optionally be used as a layer type, +causing the layer to behave as the modifier set in all instances except +where an explicit mapping overrides the default behaviour. + +These layers play nicely with other modifiers and preserve existing stacking +semantics. + +For example: + + [main] + + leftalt = layer(myalt) + rightalt = layer(myalt) + [myalt:A] + + 1 = C-A-f1 + +Will cause the leftalt key to behave as alt in all instances except when +alt+1 is pressed, in which case the key sequence `C-A-f1` will be emitted. + +By default each modifier is mapped to an eponymously named layer. + +Thus, the above config can be shortened to: + + [A] + 1 = C-A-f1 - 2 = C-A-f2 -Will cause the capslock key to behave as control in all instances except when -`C-1` is pressed, in which case the key sequence `C-A-f1` will be emitted. This -is not possible to achieve using standard layers without breaking expected -behaviour like modifier stacking and pointer combos. +since leftalt and rightalt are already assigned to `layer(A)`. -## Summary +Additionally, any set of valid modifiers is also a valid layer. For +example, the layer `M-C` corresponds to a layer which behaves like the +modifiers meta and control, which means the following: -1. Use [mylayer] if you want to define a custom shift layer (e.g. [symbols]). -2. Use [mylayer:C] if you want a layer which behaves like a custom control key. -3. Use [mylayer:main] for defining custom key layouts (e.g. dvorak). + capslock = layer(M-A) + +will cause capslock to behave as meta and alt when held. + +### Lookup Rules + +In order to achieve this (un)holy union, the following lookup rules are used: + + - If one or more layers is active then: + - If an explicit mapping exists within the most recently active layer: + - Use the defined mapping. + - Else: + - If the layer has one or more modifiers: + Apply the corresponding modifiers to the layout. + - Else: + Do nothing. + - Else: + Use the layout mapping. + +The upshot of all this is that things should mostly just work™. The +majority of users needn't be explicitly conscious of the lookup rules +unless they are doing something unorthodox (e.g nesting hybrid layers). ## ACTIONS @@ -184,20 +230,27 @@ corresponding modifiers while held. : Activates the given layer while held. -**layert(\)** +**toggle(\)** + +: Toggles the state of the given layer. Note this is intended for layers and is +distinct from `layout()` which should be used for letter layouts. -: Toggles the state of the given layer. Note this is intended for transient -layers and is distinct from `layout()` which should be used for letter layouts. +**overload(\,\[,\])** -**overload(\,\,)** +: Activates the given layer while held and emits the given key sequence when +tapped. A timeout in milliseconds may optionally be supplied to disambiguate +between a tap and a hold. -: Activates the given layer while held and emits the given key sequence when tapped. + If a timeout is present depression of the corresponding key is only interpreted +as a layer activation in the event that it is sustained for more than +\ a milliseconds. This is useful if the overloaded key is frequently +used on its own (e.g space) and only occasionally treated as a modifier (the opposite +of the default assumption). -**layout(\[, \])** +**layout(\)** : Sets the current layout to the given layer. You will likely want to ensure -you have a way to switch layouts within the new one. A second layer may -optionally be supplied and is used as the modifier layer if present. +you have a way to switch layouts within the newly activated one. **swap(\[, \])** @@ -231,34 +284,31 @@ Examples: ## Example 1 -Set the default key layout to dvorak and the modifier layout to qwerty (main). - - layout(dvorak, main) - - [dvorak:main] - - q = apostrophe - w = comma - e = dot - # etc... - -## Example 2 - Make `esc+q/w/e` set the letter layout. - # ... + [ids] + * + + [main] esc = layer(esc) - [esc] + [dvorak:layout] + s = o + d = e + ... + [esc] q = layout(main) - w = layout(dvorak, main) - e = layout(dvorak) + w = layout(dvorak) ## Example 3 Invert the behaviour of the shift key without breaking modifier behaviour. + [ids] + * + + [main] leftshift = layer(shift) rightshift = layer(shift) @@ -274,7 +324,6 @@ Invert the behaviour of the shift key without breaking modifier behaviour. 0 = ) [shift:S] - 0 = 0 1 = 1 2 = 2 @@ -287,34 +336,35 @@ Invert the behaviour of the shift key without breaking modifier behaviour. 9 = 9 -## Example 4 +## Example 3 Tapping control once causes it to apply to the next key, tapping it twice activates it until it is pressed again, and holding it produces expected behaviour. - control = oneshot(control) + [main] + leftcontrol = oneshot(control) + rightcontrol = oneshot(control) [control:C] + toggle(control) - layert(control) +# Example 4 -# Example 5 - -Meta behaves as normal except when tab is pressed after which the alt_tab layer +Meta behaves as normal except when \` is pressed, after which the alt_tab layer is activated for the duration of the leftmeta keypress. Subsequent actuations -of \` will thus produce A-S-tab instead of M-\`. +of \` will thus produce A-tab instead of M-\`. + [main] leftmeta = layer(meta) + rightmeta = layer(meta) [meta:M] - - tab = swap(alt_tab) + ` = swap(alt_tab, A-tab) [alt_tab:A] - - tab = A-tab - ` = A-S-tab + tab = A-S-tab + ` = A-tab # AUTHOR diff --git a/src/config.c b/src/config.c index cdd5991..82af354 100644 --- a/src/config.c +++ b/src/config.c @@ -21,225 +21,69 @@ */ #include -#include -#include -#include -#include -#include -#include +#include #include #include -#include -#include -#include "config.h" +#include +#include +#include +#include +#include "ini.h" #include "keys.h" +#include "error.h" +#include "descriptor.h" +#include "layer.h" +#include "config.h" +#include -#define MAX_ARGS 10 - -struct keyboard_config *configs = NULL; - -struct layer_table_ent { - char name[256]; - char pname[256]; - - uint8_t modsonly; - struct layer *layer; -} layer_table[MAX_LAYERS]; - -int nlayers = 0; - -static int lnum = 0; -static char path[PATH_MAX]; - -static uint32_t macros[MAX_MACROS][MAX_MACRO_SIZE]; -static size_t nmacros = 0; - -static int lookup_layer(const char *name); -static int parse_modset(const char *s, uint16_t * mods); - -#define err(fmt, ...) fprintf(stderr, "%s: ERROR on line %d: "fmt"\n", path, lnum, ##__VA_ARGS__) - -static const char *modseq_to_string(uint16_t mods) -{ - static char s[32]; - int i = 0; - s[0] = '\0'; - - if (mods & MOD_CTRL) { - strcpy(s + i, "-C"); - i += 2; - } - if (mods & MOD_SHIFT) { - strcpy(s + i, "-S"); - i += 2; - } - - if (mods & MOD_SUPER) { - strcpy(s + i, "-M"); - i += 2; - } - - if (mods & MOD_ALT) { - strcpy(s + i, "-A"); - i += 2; - } - - if (mods & MOD_ALT_GR) { - strcpy(s + i, "-G"); - i += 2; - } - - if (i) - return s + 1; - else - return s; -} - -const char *keyseq_to_string(uint32_t keyseq) -{ - int i = 0; - static char s[256]; - - uint16_t mods = keyseq >> 16; - uint16_t code = keyseq & 0x00FF; - - const char *key = keycode_table[code].name; - - if (mods & MOD_CTRL) { - strcpy(s + i, "C-"); - i += 2; - } - if (mods & MOD_SHIFT) { - strcpy(s + i, "S-"); - i += 2; - } - - if (mods & MOD_SUPER) { - strcpy(s + i, "M-"); - i += 2; - } - - if (mods & MOD_ALT) { - strcpy(s + i, "A-"); - i += 2; - } - - if (mods & MOD_ALT_GR) { - strcpy(s + i, "G-"); - i += 2; - } - - if (key) - strcpy(s + i, keycode_table[code].name); - else - strcpy(s + i, "UNKNOWN"); - - return s; -} - -//Returns the position in the layer table -static struct layer_table_ent *create_layer(const char *name, - const char *pname, - uint16_t mods) -{ - struct layer_table_ent *ent = &layer_table[nlayers]; - - ent->layer = calloc(1, sizeof(struct layer)); - ent->layer->keymap = - calloc(KEY_CNT, sizeof(struct key_descriptor)); - - ent->modsonly = 0; - ent->layer->mods = mods; - - strcpy(ent->pname, pname); - strcpy(ent->name, name); - - nlayers++; - assert(nlayers <= MAX_LAYERS); - return ent; -} - -static struct layer_table_ent *create_mod_layer(uint16_t mods) -{ - struct layer_table_ent *ent = create_layer("", "", mods); - ent->modsonly = 1; - - return ent; -} +static struct config *fallback_config = NULL; +static struct config *configs = NULL; -static void keyseq_to_desc(uint32_t seq, struct key_descriptor *desc) -{ - desc->action = ACTION_KEYSEQ; - desc->arg.keyseq = seq; - - //To facilitate simplification of modifier handling convert - //all traditional modifier keys to their internal layer - //representations. - - switch (seq) { - case KEY_LEFTCTRL: - desc->action = ACTION_LAYER; - desc->arg.layer = lookup_layer("C"); - break; - case KEY_LEFTALT: - desc->action = ACTION_LAYER; - desc->arg.layer = lookup_layer("A"); - break; - case KEY_RIGHTALT: - desc->action = ACTION_LAYER; - desc->arg.layer = lookup_layer("G"); - break; - case KEY_LEFTSHIFT: - desc->action = ACTION_LAYER; - desc->arg.layer = lookup_layer("S"); - break; - case KEY_LEFTMETA: - desc->action = ACTION_LAYER; - desc->arg.layer = lookup_layer("M"); - break; - } -} +/* + * Parse a value of the form 'key = value'. The value may contain = + * and the key may itself be = as a special case. The returned + * values are pointers within the modified input string. + */ -static struct layer_table_ent *create_main_layer() +static int parse_kvp(char *s, char **key, char **value) { - int i; - struct layer_table_ent *ent = create_layer("main", "", 0); + char *last_space = NULL; + char *c = s; - for (i = 0; i < KEY_CNT; i++) - keyseq_to_desc(i, &ent->layer->keymap[i]); + if (*c == '=') //Allow the first character to be = as a special case. + c++; - return ent; -} + while (*c) { + switch (*c) { + case '=': + if (last_space) + *last_space = 0; + else + *c = 0; + c++; -//Returns the index in the layer table or -1. -static int lookup_layer(const char *name) -{ - int i; - uint16_t mods; + while (*c && *c == ' ') + c++; - for (i = 0; i < nlayers; i++) { - if (!strcmp(layer_table[i].name, name)) - return i; - } + *key = s; + *value = c; - if (!parse_modset(name, &mods)) { - //Check if a dedicated mod layer already exists. - for (i = 0; i < nlayers; i++) { - struct layer_table_ent *ent = &layer_table[i]; - if (ent->modsonly && ent->layer->mods == mods) - return i; + return 0; + case ' ': + if (!last_space) + last_space = c; + break; + default: + last_space = NULL; + break; } - //Autovivify mod layers which don't exist. - - create_mod_layer(mods); - return nlayers - 1; + c++; } return -1; } - static int parse_modset(const char *s, uint16_t * mods) { *mods = 0; @@ -277,636 +121,417 @@ static int parse_modset(const char *s, uint16_t * mods) return 0; } -static int parse_layer_heading(const char *s, char name[256], - char parent[256]) +static uint32_t lookup_keycode(const char *name) { - size_t len = strlen(s); - char *c; - - name[0] = 0; - parent[0] = 0; + int i; - if (s[0] != '[' || s[len - 1] != ']') - return -1; + for (i = 0; i < KEY_MAX; i++) { + const struct keycode_table_ent *ent = &keycode_table[i]; - c = strchr(s, ':'); - if (c) { - int sz = c - (s + 1); - memcpy(name, s + 1, sz); - name[sz] = 0; - - sz = len - 2 - sz - 1; - memcpy(parent, c + 1, sz); - parent[sz] = 0; - } else { - int sz = len - 2; - memcpy(name, s + 1, sz); - name[sz] = 0; + if (ent->name && + (!strcmp(ent->name, name) || + (ent->alt_name && !strcmp(ent->alt_name, name)))) + return i; } return 0; } -static uint32_t parse_keyseq(const char *s) +static char *read_file(const char *path) { - const char *c = s; - size_t i; - uint32_t mods = 0; - - while (*c && c[1] == '-') { - switch (*c) { - case 'C': - mods |= MOD_CTRL; - break; - case 'M': - mods |= MOD_SUPER; - break; - case 'A': - mods |= MOD_ALT; - break; - case 'S': - mods |= MOD_SHIFT; - break; - case 'G': - mods |= MOD_ALT_GR; - break; - default: - return 0; - break; - } - - c += 2; + struct stat st; + size_t sz; + int fd; + char *data; + ssize_t n = 0, nr; + + if (stat(path, &st)) { + perror("stat"); + exit(-1); } - for (i = 0; i < KEY_MAX; i++) { - const struct keycode_table_ent *ent = &keycode_table[i]; + sz = st.st_size; + data = malloc(sz + 1); - if (ent->name && !strcmp(ent->name, c)) - return (mods << 16) | i; - if (ent->alt_name && !strcmp(ent->alt_name, c)) - return (mods << 16) | i; - if (ent->shifted_name && !strcmp(ent->shifted_name, c)) - return (mods | MOD_SHIFT) << 16 | i; + fd = open(path, O_RDONLY); + while ((nr = read(fd, data + n, sz - n))) { + n += nr; } - return 0; + data[sz] = '\0'; + return data; } -uint32_t *parse_macro_fn(const char *s, size_t *szp) +static struct layer *lookup_layer(const char *name, void *_config) { - char *_s = strdup(s); - size_t len = strlen(s); - uint32_t *macro = macros[nmacros]; - - char *tok; - size_t sz = 0; + struct config *config = (struct config *) (_config); + size_t i; + uint16_t mods; - assert(nmacros < MAX_MACROS); + for (i = 0; i < config->nr_layers; i++) { + struct layer *layer = config->layers[i]; - if (strstr(s, "macro(") != s || s[len - 1] != ')') { - free(_s); - return NULL; + if (!strcmp(layer->name, name)) + return layer; } - _s[len - 1] = 0; - - for (tok = strtok(_s + 6, " "); tok; tok = strtok(NULL, " ")) { - uint32_t seq; - len = strlen(tok); - - if ((seq = parse_keyseq(tok))) { - macro[sz++] = seq; - assert(sz < MAX_MACRO_SIZE); - } else if (len > 1 && tok[len - 1] == 's' - && tok[len - 2] == 'm') { - int len = atoi(tok); - assert(len <= MAX_TIMEOUT_LEN); - - macro[sz++] = TIMEOUT_KEY(len); - } else { - char *c; - - for (c = tok; *c; c++) { - char s[2]; + /* Autovivify modifier layers as required. */ - s[0] = *c; - s[1] = 0; - - if (!(seq = parse_keyseq(s))) { - free(_s); - return NULL; - } + if (!parse_modset(name, &mods)) { + struct layer *layer = create_layer(name, mods, 0); - macro[sz++] = seq; - assert(sz < MAX_MACRO_SIZE); - } - } + config->layers[config->nr_layers++] = layer; + return layer; } - free(_s); - - *szp = sz; - nmacros++; - return macro; + return NULL; } -static int parse_kvp(char *s, char **_k, char **_v) +static int parse_header(const char *s, + char name[MAX_LAYER_NAME_LEN], + char type[MAX_LAYER_NAME_LEN]) { - char *v = NULL, *k = NULL; - - while (*s && isspace(*s)) - s++; - if (!*s) - return -1; - k = s; + char *d; + size_t typelen, namelen; + size_t len = strlen(s); - if (*s == '=') - s++; //Allow the first character to be = as a special case. + if (!(d = strchr(s, ':'))) { + if (len >= MAX_LAYER_NAME_LEN) + return -1; + else { + strcpy(name, s); + type[0] = 0; + return 0; + } + } - while (*s && !isspace(*s) && *s != '=') - s++; - if (!*s) + namelen = d - s; + typelen = len - namelen - 1; + if (namelen >= MAX_LAYER_NAME_LEN || typelen >= MAX_LAYER_NAME_LEN) return -1; - while (*s && *s != '=') { - *s = 0; - s++; - } - if (!*s) - return -1; - *s++ = 0; + memcpy(name, s, namelen); + memcpy(type, s+namelen+1, typelen); - while (*s && isspace(*s)) - *s++ = 0; - if (!*s) - return -1; - v = s; + name[namelen] = 0; + type[typelen] = 0; - *_k = k; - *_v = v; return 0; } -static int parse_fn(char *s, char **fn_name, char *args[MAX_ARGS], - size_t *nargs) +static void parse_id_section(struct config *config, struct ini_section *section) { - int openparen = 0; + size_t i; - *fn_name = s; - *nargs = 0; + for (i = 0; i < section->nr_entries; i++) { + struct ini_entry *ent = §ion->entries[i]; - while (*s && *s != ')') { - switch (*s) { - case '(': - openparen++; - *s = '\0'; - s++; + uint16_t product_id, vendor_id; - while (*s && isspace(*s)) - s++; + /* Applies to all device ids except the ones explicitly listed within the config. */ - if (!*s) - return -1; + if (!strcmp(ent->line, "*")) { + if (fallback_config) { + fprintf(stderr, "WARNING: multiple fallback configs detected: %s, %s. Ignoring %s\n", + fallback_config->name, + config->name, + config->name); - if (*s == ')') { //No args - *s = '\0'; - return 0; + return; } - args[(*nargs)++] = s; - - break; - case ',': - *s = '\0'; - s++; - while (*s && isspace(*s)) - s++; + printf("Using %s as a fallback config.\n", config->name); + fallback_config = config; + continue; + } - if (!*s) - return -1; + if (sscanf (ent->line, "-%hx:%hx", &product_id, &vendor_id) == 2) { + if (fallback_config != config) { + fprintf(stderr, "ERROR: - is not permitted in non fallback configs.\n"); + continue; + } - args[(*nargs)++] = s; - break; + config->device_ids[config->nr_device_ids++] = product_id << 16 | vendor_id; + } else if (sscanf(ent->line, "%hx:%hx", &product_id, &vendor_id) == 2) { + assert(config->nr_device_ids < MAX_DEVICE_IDS); + config->device_ids[config->nr_device_ids++] = product_id << 16 | vendor_id; + } else { + fprintf (stderr, + "ERROR %s:%zu: Invalid product id: %s\n", + config->name, ent->lnum, ent->line); } - - s++; } - - if (*s != ')') - return -1; - - *s = '\0'; - - return 0; } -static int parse_descriptor(const char *_s, struct key_descriptor *desc) -{ - uint32_t seq; - - char *s = strdup(_s); - char *fn; - char *args[MAX_ARGS]; - size_t nargs, sz; - uint32_t *macro; +/* + * Ownership of the input string is forfeit, it will eventually be freed when + * the config is destroyed. + */ - if ((seq = parse_keyseq(s))) { - keyseq_to_desc(seq, desc); +static int parse_config(const char *config_name, char *str, struct config *config) +{ + size_t i, j; - goto cleanup; - } + struct ini ini; - if ((macro = parse_macro_fn(s, &sz))) { - desc->action = ACTION_MACRO; - desc->arg.macro = macro; - desc->arg2.sz = sz; + struct ini_section *layer_sections[MAX_LAYERS]; + struct layer *layers[MAX_LAYERS]; + size_t nr_layers = 0; - goto cleanup; - } - if (parse_fn(s, &fn, args, &nargs)) { - err("%s is not a valid key sequence or action.", s); - goto fail; + if (ini_parse(str, &ini, NULL) < 0) { + fprintf(stderr, "ERROR: %s is not a valid config (missing [main]?)\n", config_name); + return -1; } - if (!strcmp(fn, "layer") && nargs == 1) { - int idx = lookup_layer(args[0]); - - if (idx < 0) { - err("%s is not a valid layer.", args[0]); - goto fail; - } + config->nr_layers = 1; + config->nr_device_ids = 0; - desc->action = ACTION_LAYER; - desc->arg.layer = idx; + config->layers[0] = create_layer("main", 0, 1); - goto cleanup; - } else if (!strcmp(fn, "layert") && nargs == 1) { - int idx = lookup_layer(args[0]); + assert(strlen(config_name) < MAX_CONFIG_NAME); + strcpy(config->name, config_name); - if (idx < 0) { - err("%s is not a valid layer.", args[0]); - goto fail; - } + /* + * First pass, create layers so they are available + * for lookup during descriptor parsing. + */ - desc->action = ACTION_LAYER_TOGGLE; - desc->arg.layer = idx; + for (i = 0; i < ini.nr_sections; i++) { + struct ini_section *section = &ini.sections[i]; + uint16_t mods; + struct layer *layer; + char name[MAX_LAYER_NAME_LEN], type[MAX_LAYER_NAME_LEN]; - goto cleanup; - } else if (!strcmp(fn, "layout") && nargs > 0) { - int idx; + if (!strcmp(section->name, "ids")) { + parse_id_section(config, section); + continue; + } - if ((idx = lookup_layer(args[0])) < 0) { - err("%s is not a valid layer.", args[0]); - goto fail; + if (parse_header(section->name, name, type) < 0) { + fprintf(stderr, + "ERROR %s:%zu: Invalid header.\n", + config_name, + section->lnum); + continue; } - desc->action = ACTION_LAYOUT; - desc->arg.layer = idx; - - if (nargs == 1) { - desc->arg2.layer = idx; - } else { - if ((idx = lookup_layer(args[1])) < 0) { - err("%s is not a valid layer.", args[1]); - goto fail; + layer = lookup_layer(name, (void*)config); + + if (!layer) { //If the layer doesn't exist, create it. + if (!strcmp(type, "layout")) { + layer = create_layer(name, 0, 1); + } else if (!parse_modset(type, &mods)) { + layer = create_layer(name, mods, 0); + } else if (strcmp(type, "")) { + fprintf(stderr, + "WARNING %s:%zu: \"%s\" is not a valid layer type " + " (must be \"layout\" or a valid modifier set).\n", + config_name, section->lnum, type); + continue; } - desc->action = ACTION_LAYOUT; - desc->arg2.layer = idx; + config->layers[config->nr_layers++] = layer; } + layers[nr_layers] = layer; + layer_sections[nr_layers++] = section; + } - goto cleanup; - } else if (!strcmp(fn, "oneshot") && nargs == 1) { - int idx; - - if ((idx = lookup_layer(args[0])) < 0) { - - err("%s is not a valid layer.", args[0]); - goto fail; - } + /* Parse each entry section entry and build the layer keymap. */ - desc->action = ACTION_ONESHOT; - desc->arg.layer = idx; + for (i = 0; i < nr_layers; i++) { + struct ini_section *section = layer_sections[i]; + struct layer *layer = layers[i]; - goto cleanup; - } else if (!strcmp(fn, "overload") && nargs == 2) { - int idx; + /* Populate the layer described by the section. */ - if ((idx = lookup_layer(args[0])) < 0) { - err("%s is not a valid layer.", args[0]); - goto fail; - } + for (j = 0; j < section->nr_entries; j++) { + struct ini_entry *ent = §ion->entries[j]; - if (!(seq = parse_keyseq(args[1]))) { - err("%s is not a valid key sequence.", args[1]); - goto fail; - } + char *k, *v; + uint16_t code; + uint16_t mod; - desc->action = ACTION_OVERLOAD; - desc->arg.keyseq = seq; - desc->arg2.layer = idx; + struct descriptor desc; - goto cleanup; - } else if (!strcmp(fn, "swap")) { - int idx; + if (parse_kvp(ent->line, &k, &v) < 0) { + fprintf(stderr, + "ERROR %s:%zu: Invalid key value pair.\n", + config_name, ent->lnum); + continue; + } - if ((idx = lookup_layer(args[0])) < 0) { - err("%s is not a valid layer.", args[0]); - goto fail; - } + code = lookup_keycode(k); - desc->action = ACTION_SWAP; - desc->arg.layer = idx; - desc->arg2.keyseq = 0; + if (!code) { + fprintf(stderr, + "ERROR %s:%zu: %s is not a valid key.\n", + config_name, ent->lnum, k); + continue; + } - if (nargs == 2) { - if (!(desc->arg2.keyseq = parse_keyseq(args[1]))) { - err("%s is not a valid key sequence.", - args[1]); - goto fail; + if (parse_descriptor(v, &desc, lookup_layer, config) < 0) { + fprintf(stderr, "ERROR %s:%zu: %s\n", config_name, + ent->lnum, errstr); + continue; } - } - goto cleanup; - } else { - err("%s is not a valid action or key.", _s); - goto fail; + layer_set_descriptor(layer, code, &desc); + } } - - cleanup: - free(s); return 0; - - fail: - free(s); - return -1; } -static void build_layer_table() +static void post_process_config(const struct config *config) { - ssize_t len; - char *line = NULL; - size_t line_sz = 0; - FILE *fh = fopen(path, "r"); - - if (!fh) { - perror("fopen"); - exit(-1); - } - - lnum = 0; - nlayers = 0; - - create_main_layer(); - while ((len = getline(&line, &line_sz, fh)) != -1) { - char name[256]; - char pname[256]; - - lnum++; - if (line[len - 1] == '\n') - line[--len] = 0; + size_t i; + uint16_t code; + + /* + * Convert all modifier keycodes into their corresponding layer + * counterparts for consistency. This allows us to avoid explicitly + * accounting for modifier layer/modifier keycode overlap within the + * remapping logic and provides the user the ability to remap stock + * modifiers using their eponymous layer names. + */ + + for (i = 0; i < config->nr_layers; i++) { + struct layer *layer = config->layers[i]; + + for (code = 0; code < KEY_MAX; code++) { + struct descriptor *d = layer_get_descriptor(layer, code); + + if (d && d->op == OP_KEYSEQ) { + uint16_t key = d->args[0].sequence.code; + struct layer *modlayer = NULL; + + switch(key) { + case KEY_RIGHTSHIFT: + case KEY_LEFTSHIFT: + modlayer = lookup_layer("S", (void*)config); + break; + case KEY_RIGHTALT: + modlayer = lookup_layer("G", (void*)config); + break; + case KEY_RIGHTCTRL: + case KEY_LEFTCTRL: + modlayer = lookup_layer("C", (void*)config); + break; + case KEY_RIGHTMETA: + case KEY_LEFTMETA: + modlayer = lookup_layer("M", (void*)config); + break; + case KEY_LEFTALT: + modlayer = lookup_layer("A", (void*)config); + break; + } - if (!parse_layer_heading(line, name, pname)) { - uint16_t mods; + if (modlayer) { + d->op = OP_LAYER; + d->args[0].layer = modlayer; + } + } - if (!parse_modset(pname, &mods)) - create_layer(name, "", mods); - else - create_layer(name, pname, 0); } } - - free(line); - fclose(fh); } -static int parse_layer_entry(char *s, uint16_t * code, - struct key_descriptor *desc) +struct config *add_config(const char *config_name, char *str) { - uint32_t seq; - char *k, *v; + struct config *config = malloc(sizeof(struct config)); - if (parse_kvp(s, &k, &v)) { - err("Invalid key value pair."); - return -1; - } - - if (!(seq = parse_keyseq(k))) { - err("'%s' is not a valid key.", k); - return -1; + if (parse_config(config_name, str, config) < 0) { + free(config); + return NULL; } - if ((seq >> 16) != 0) { - err("key cannot contain modifiers."); - return -1; - } + post_process_config(config); - if (parse_descriptor(v, desc)) - return -1; + config->next = configs; + configs = config; - *code = seq & 0xFF; - return 0; + return config; } -void post_process_config(struct keyboard_config *cfg, - const char *layout_name, - const char *modlayout_name) +void free_configs() { - cfg->default_layout = -1; - cfg->default_modlayout = -1; - - int i; - for (i = 0; i < nlayers; i++) { - struct layer_table_ent *ent = &layer_table[i]; - - if (!strcmp(ent->name, layout_name)) - cfg->default_layout = i; - - if (!strcmp(ent->name, modlayout_name)) - cfg->default_modlayout = i; - - if (ent->pname[0]) { - int j; + struct config *config = configs; - for (j = 0; j < nlayers; j++) { - struct layer_table_ent *ent2 = - &layer_table[j]; - - if (!strcmp(ent->pname, ent2->name)) { - int k; - - struct layer *dst = ent->layer; - struct layer *src = ent2->layer; + while (config) { + size_t i; + struct config *tmp; - for (k = 0; k < KEY_CNT; k++) - if (dst->keymap[k]. - action == - ACTION_UNDEFINED) - dst->keymap[k] = - src->keymap[k]; - } - } - } - } + for (i = 0; i < config->nr_layers; i++) + free_layer(config->layers[i]); - if (cfg->default_layout == -1) { - fprintf(stderr, "%s is not a valid default layout", - layout_name); - cfg->default_layout = 0; + tmp = config; + config = config->next; + free(tmp); } - if (cfg->default_modlayout == -1) { - fprintf(stderr, - "%s is not a valid default modifier layout\n", - modlayout_name); - cfg->default_modlayout = 0; - } + fallback_config = NULL; + configs = NULL; } -static void parse(struct keyboard_config *cfg) +int read_config_dir(const char *dir) { - int i; - char *line = NULL; - size_t n = 0; - ssize_t len; - struct layer *layer; - - char layout_name[256]; - char modlayout_name[256]; - - build_layer_table(); + free_configs(); - FILE *fh = fopen(path, "r"); - if (!fh) { - perror("fopen"); - exit(-1); - } - - lnum = 0; - layer = layer_table[0].layer; - - strcpy(layout_name, "main"); - strcpy(modlayout_name, "main"); - - while ((len = getline(&line, &n, fh)) != -1) { - char name[256]; - char type[256]; - - uint16_t code; - struct key_descriptor desc; - - size_t nargs; - char *fnname; - char *args[MAX_ARGS]; + DIR *dh = opendir(dir); + struct dirent *de; + if (!dh) + return -1; - lnum++; - char *s = line; + while ((de = readdir(dh))) { + size_t len = strlen(de->d_name); - while (len && isspace(s[0])) { //Trim leading whitespace - s++; - len--; + if (len > 4 && !strcmp(de->d_name+len-4, ".cfg")) { + fprintf(stderr, "NOTE: %s looks like an old (v1) config. See the man page for details on the current config format.\n", de->d_name); } - if (len && s[len - 1] == '\n') //Strip tailing newline (not present on EOF) - s[--len] = '\0'; + if (len > 5 && !strcmp(de->d_name+len-5, ".conf")) { + char path[PATH_MAX]; + char *data; - if (len == 0 || s[0] == '#') - continue; + sprintf(path, "%s/%s", dir, de->d_name); - if (!parse_layer_heading(s, name, type)) { - int idx = lookup_layer(name); - assert(idx > 0); - layer = layer_table[idx].layer; - } else if (strchr(s, '=')) { - if (!parse_layer_entry(s, &code, &desc)) - layer->keymap[code] = desc; - else - err("Invalid layer entry."); - } else if (!parse_fn(s, &fnname, args, &nargs)) { - if (!strcmp(fnname, "layout")) { - if (nargs == 1) { - strcpy(layout_name, args[0]); - strcpy(modlayout_name, args[0]); - } else if (nargs == 2) { - strcpy(layout_name, args[0]); - strcpy(modlayout_name, args[1]); - } - } - } + fprintf(stderr, "Parsing %s\n", path); + data = read_file(path); - free(line); - line = NULL; - n = 0; + add_config(path, data); + free(data); + } } - free(line); - fclose(fh); - - post_process_config(cfg, layout_name, modlayout_name); - - for (i = 0; i < nlayers; i++) - cfg->layers[i] = layer_table[i].layer; - - cfg->nlayers = nlayers; - - return; + closedir(dh); + return 0; } -void config_free() +struct config *lookup_config(uint32_t device_id) { - struct keyboard_config *cfg = configs; + struct config *config = configs; + int excluded = 0; - while (cfg) { + while (config) { size_t i; - struct keyboard_config *tmp = cfg; - cfg = cfg->next; - - for (i = 0; i < tmp->nlayers; i++) { - struct layer *layer = tmp->layers[i]; - - free(layer->keymap); - free(layer); + for (i = 0; i < config->nr_device_ids; i++) { + if (config->device_ids[i] == device_id) { + if (fallback_config == config) + excluded++; + else + return config; + } } - free(tmp); - }; -} - -void config_generate() -{ - struct dirent *ent; - DIR *dh = opendir(CONFIG_DIR); - - if (!dh) { - perror("opendir"); - exit(-1); + config = config->next; } - while ((ent = readdir(dh))) { - struct keyboard_config *cfg; - - int len = strlen(ent->d_name); - if (len <= 4 || strcmp(ent->d_name + len - 4, ".cfg")) - continue; - + if (fallback_config && !excluded) + return fallback_config; - sprintf(path, "%s/%s", CONFIG_DIR, ent->d_name); - - cfg = calloc(1, sizeof(struct keyboard_config)); - - strcpy(cfg->name, ent->d_name); - cfg->name[len - 4] = '\0'; - - parse(cfg); - - cfg->next = configs; - configs = cfg; - } - - closedir(dh); + return NULL; } diff --git a/src/config.h b/src/config.h index 9403a69..b05c460 100644 --- a/src/config.h +++ b/src/config.h @@ -1,98 +1,29 @@ -/* Copyright © 2019 Raheman Vaiya. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice (including the next - * paragraph) shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#ifndef __H_CONFIG_ -#define __H_CONFIG_ - -#ifdef __FreeBSD__ -#include -#else -#include -#endif - -#include -#include -#include "keys.h" +#ifndef CONFIG_H +#define CONFIG_H +#define MAX_DEVICE_IDS 32 +#define MAX_CONFIG_NAME 256 #define MAX_LAYERS 32 -#define MAX_LAYER_NAME_LEN 256 -#define MAX_MACRO_SIZE 256 -#define MAX_MACROS 256 - -enum action { - ACTION_UNDEFINED, - ACTION_OVERLOAD, - ACTION_MACRO, - ACTION_LAYOUT, - ACTION_KEYSEQ, - ACTION_LAYER, - ACTION_LAYER_TOGGLE, - ACTION_ONESHOT, - ACTION_SWAP, -}; - -struct key_descriptor { - enum action action; - union { - uint32_t keyseq; - uint16_t mods; - int8_t layer; - uint32_t *macro; - size_t sz; - } arg, arg2; -}; +#include "layer.h" +struct config { + char name[MAX_CONFIG_NAME]; + uint32_t device_ids[MAX_DEVICE_IDS]; - -//A layer may optionally have modifiers. If it does it is expected to behave as -//a normal modifier in all instances except when a key is explicitly defined in -//the keymap. - -struct layer { - uint16_t mods; - struct key_descriptor *keymap; - - //State - uint8_t active; - uint64_t timestamp; -}; - -struct keyboard_config { - char name[256]; - + /* The first two layers are the default main and modifier layouts. */ struct layer *layers[MAX_LAYERS]; - size_t nlayers; - int default_modlayout; - int default_layout; + size_t nr_device_ids; + size_t nr_layers; - struct keyboard_config *next; + struct config *next; }; -extern struct keyboard_config *configs; - -void config_generate(); -void config_free(); -const char *keyseq_to_string(uint32_t keyseq); + +struct config *add_config(const char *config_name, char *str); +struct config *lookup_config(uint32_t device_id); +int read_config_dir(const char *dir); +void free_configs(); #endif diff --git a/src/descriptor.c b/src/descriptor.c new file mode 100644 index 0000000..260e05e --- /dev/null +++ b/src/descriptor.c @@ -0,0 +1,282 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include + +#include "descriptor.h" +#include "layer.h" +#include "keys.h" +#include "error.h" + +#define MAX_ARGS 5 + +static struct macro macros[MAX_MACROS]; +static size_t nr_macros = 0; + +static int parse_fn(char *s, + char **name, + char *args[MAX_ARGS], + size_t *nargs) +{ + size_t len = strlen(s); + size_t n = 0; + char *arg, *c; + + if (len == 0 || s[len-1] != ')') + return -1; + + s[len-1] = 0; + + if (!(c = strchr(s, '('))) + return -1; + + *c = '\0'; + *name = s; + s = c + 1; + + for (arg = strtok(s, ","); arg; arg = strtok(NULL, ",")) { + while (isspace(arg[0])) + arg++; + + if (n >= MAX_ARGS) + return 0; + + args[n++] = arg; + } + + *nargs = n; + + return 0; +} + +static int parse_key_sequence(const char *s, struct key_sequence *seq) +{ + const char *c = s; + uint16_t code; + + if (!*s) + return -1; + + seq->mods = 0; + + while (c[1] == '-') { + switch (*c) { + case 'C': + seq->mods |= MOD_CTRL; + break; + case 'M': + seq->mods|= MOD_SUPER; + break; + case 'A': + seq->mods |= MOD_ALT; + break; + case 'S': + seq->mods |= MOD_SHIFT; + break; + case 'G': + seq->mods |= MOD_ALT_GR; + break; + default: + return -1; + break; + } + + c += 2; + } + + for (code = 0; code < KEY_MAX; code++) { + const struct keycode_table_ent *ent = &keycode_table[code]; + + if (ent->name) { + if (ent->shifted_name && + !strcmp(ent->shifted_name, c)) { + + seq->mods |= MOD_SHIFT; + seq->code = code; + + return 0; + } else if (!strcmp(ent->name, c) || + (ent->alt_name && !strcmp(ent->alt_name, c))) { + + seq->code = code; + + return 0; + } + } + } + + return -1; +} + +static struct macro *parse_macro_fn(char *s) +{ + size_t len = strlen(s); + struct macro *macro; + + char *tok; + size_t sz = 0; + + assert(nr_macros < MAX_MACROS); + macro = ¯os[nr_macros]; + + if (strstr(s, "macro(") != s || s[len-1] != ')') + return NULL; + + s[len-1] = 0; + + for (tok = strtok(s + 6, " "); tok; tok = strtok(NULL, " ")) { + struct macro_entry ent; + len = strlen(tok); + + if (!parse_key_sequence(tok, &ent.data.sequence)) { + assert(sz < MAX_MACRO_SIZE); + + ent.type = MACRO_KEYSEQUENCE; + macro->entries[macro->sz++] = ent; + } else if (len > 1 && tok[len-2] == 'm' && tok[len-1] == 's') { + int len = atoi(tok); + + ent.type = MACRO_TIMEOUT; + ent.data.timeout = len; + + assert(macro->sz < MAX_MACRO_SIZE); + + macro->entries[macro->sz++] = ent; + } else { + char *c; + + for (c = tok; *c; c++) { + char s[2]; + + s[0] = *c; + s[1] = 0; + + if (parse_key_sequence(s, &ent.data.sequence) < 0) + return NULL; + + ent.type = MACRO_KEYSEQUENCE; + assert(macro->sz < MAX_MACRO_SIZE); + macro->entries[macro->sz++] = ent; + } + } + } + + nr_macros++; + + return macro; +} + +/* + * Modifies the input string, consumes a function which which is used for + * resolving layer names as required. + */ +int parse_descriptor(char *s, + struct descriptor *desc, + struct layer *(*layer_lookup_fn)(const char *name, void *arg), + void *layer_lookup_fn_arg) +{ + struct key_sequence seq; + + char *fn; + char *args[MAX_ARGS]; + size_t nargs = 0; + struct macro *macro; + + if (!parse_key_sequence(s, &seq)) { + desc->op = OP_KEYSEQ; + + desc->args[0].sequence = seq; + } else if ((macro = parse_macro_fn(s))) { + desc->op = OP_MACRO; + + desc->args[0].macro = macro; + } else if (!parse_fn(s, &fn, args, &nargs)) { + struct layer *layer; + + if (!strcmp(fn, "layer")) + desc->op = OP_LAYER; + else if (!strcmp(fn, "reset")) + desc->op = OP_RESET; + else if (!strcmp(fn, "toggle")) + desc->op = OP_TOGGLE; + else if (!strcmp(fn, "layout")) + desc->op = OP_LAYOUT; + else if (!strcmp(fn, "oneshot")) + desc->op = OP_ONESHOT; + else if (!strcmp(fn, "overload")) + desc->op = OP_OVERLOAD; + else if (!strcmp(fn, "swap")) + desc->op = OP_SWAP; + else { + err("\"%s\" is not a valid action or key sequence.", s); + return -1; + } + + if (desc->op == OP_RESET) + return 0; + + if (nargs == 0) { + err("%s requires one or more arguments.", fn); + return -1; + } + + layer = layer_lookup_fn(args[0], layer_lookup_fn_arg); + + if (!layer) { + err("\"%s\" is not a valid layer", args[0]); + return -1; + } + + if (desc->op == OP_LAYOUT && !layer->is_layout) { + err("\"%s\" must be a valid layout.", args[0]); + return -1; + } + + desc->args[0].layer = layer; + desc->args[1].sequence = (struct key_sequence){0}; + + if (nargs > 1) { + int ret; + ret = parse_key_sequence(args[1], &seq); + + if (ret < 0) { + err("\"%s\" is not a valid key sequence", args[1]); + return -1; + } + + desc->args[1].sequence = seq; + } + + if (desc->op == OP_OVERLOAD && nargs == 3) { + desc->op = OP_OVERLOAD_TIMEOUT; + desc->args[2].timeout = atoi(args[2]); + } + } else { + err("\"%s\" is not a valid key sequence or action.", s); + return -1; + } + + return 0; +} diff --git a/src/descriptor.h b/src/descriptor.h new file mode 100644 index 0000000..d85e25d --- /dev/null +++ b/src/descriptor.h @@ -0,0 +1,79 @@ +#ifndef DESCRIPTOR_H +#define DESCRIPTOR_H + +#include +#include + +#define MAX_MACROS 256 +#define MAX_MACRO_SIZE 32 + +enum op { + OP_KEYSEQ, + + OP_ONESHOT, + OP_SWAP, + OP_LAYER, + OP_OVERLOAD, + OP_OVERLOAD_TIMEOUT, + OP_TOGGLE, + OP_RESET, + + OP_LAYOUT, + + OP_MACRO, +}; + +struct key_sequence { + uint16_t mods; + uint16_t code; +}; + +struct macro_entry { + enum { + MACRO_KEYSEQUENCE, + MACRO_TIMEOUT + } type; + + union { + struct key_sequence sequence; + uint32_t timeout; + } data; +}; + +/* + * A series of key sequences optionally punctuated by + * timeouts + */ +struct macro { + struct macro_entry entries[MAX_MACRO_SIZE]; + size_t sz; +}; + +/* Describes the intended purpose of a key. */ + +struct descriptor { + enum op op; + + union { + struct key_sequence sequence; + struct layer *layer; + struct macro *macro; + size_t sz; + size_t timeout; + } args[3]; +}; + +/* + * Creates a descriptor from the given string which describes a key action. A + * function which maps layer names to indices must also be supplied for layer + * lookup. The function should return -1 to indicate the absence of layer. + * Potentially modifies the input string in the process. layer_index_fn_arg + * gets passed back to layer_index_fn unaltered. + */ + +int parse_descriptor(char *s, + struct descriptor *desc, + struct layer *(*layer_lookup_fn)(const char *name, void *arg), + void *layer_lookup_fn_arg); + +#endif diff --git a/src/error.c b/src/error.c new file mode 100644 index 0000000..8eeb13f --- /dev/null +++ b/src/error.c @@ -0,0 +1,23 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +char errstr[1024]; diff --git a/src/error.h b/src/error.h new file mode 100644 index 0000000..1abeb7c --- /dev/null +++ b/src/error.h @@ -0,0 +1,9 @@ +#ifndef ERROR_H +#define ERROR_H + +#include + +extern char errstr[1024]; +#define err(fmt, ...) snprintf(errstr, sizeof(errstr), fmt, ##__VA_ARGS__); + +#endif diff --git a/src/ini.c b/src/ini.c new file mode 100644 index 0000000..4172055 --- /dev/null +++ b/src/ini.c @@ -0,0 +1,102 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +#include "ini.h" + +int ini_parse(char *s, struct ini *ini, const char *default_section_name) +{ + int ln = 0; + size_t n = 0; + + struct ini_section *section = NULL; + + while (s) { + struct ini_entry *ent; + char *line; + + ln++; + + line = s; + s = strchr(s, '\n'); + + if (s) { + *s = 0; + s++; + } + + + while (isspace(line[0])) + line++; + + if (line[0] == 0) + continue; + + switch (line[0]) { + size_t len; + case '[': + len = strlen(line); + + if (line[len-1] == ']') { + assert(n < MAX_SECTIONS); + + section = &ini->sections[n++]; + + line[len-1] = 0; + + strncpy(section->name, line+1, sizeof(section->name)); + section->nr_entries = 0; + section->lnum = ln; + + continue; + } + + break; + case '#': + continue; + } + + if (!section) { + if(default_section_name) { + section = &ini->sections[n++]; + strcpy(section->name, default_section_name); + + section->nr_entries = 0; + section->lnum = 0; + } else + return -1; + } + + assert(section->nr_entries < MAX_SECTION_ENTRIES); + + ent = §ion->entries[section->nr_entries++]; + + ent->line = line; + ent->lnum = ln; + } + + ini->nr_sections = n; + return 0; +} diff --git a/src/ini.h b/src/ini.h new file mode 100644 index 0000000..4ed0677 --- /dev/null +++ b/src/ini.h @@ -0,0 +1,52 @@ +#ifndef INI_H +#define INI_H + +#include + +#define MAX_SECTIONS 32 +#define MAX_SECTION_ENTRIES 128 + +struct ini_entry { + char *line; + size_t lnum; // The line number in the original source file. +}; + +struct ini_section { + char name[256]; + + size_t nr_entries; + size_t lnum; + + struct ini_entry entries[MAX_SECTION_ENTRIES]; +}; + +struct ini { + size_t nr_sections; + + struct ini_section sections[MAX_SECTIONS]; +}; + +/* + * Reads a file of the form: + * + * [section] + * + * # Comment + * + * entry1 + * entry2 + * + * [section2] + * ... + * + * Stripping comments and empty lines along the way. + * Each entry is a non comment, non empty line + * sripped of leading whitespace. If a default + * section name is supplied then entries not + * listed under an explicit heading will be + * returned under the named section. + */ + +int ini_parse(char *s, struct ini *ini, const char *default_section_name); + +#endif diff --git a/src/keyboard.c b/src/keyboard.c new file mode 100644 index 0000000..99dff00 --- /dev/null +++ b/src/keyboard.c @@ -0,0 +1,474 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include "keyboard.h" +#include "keyd.h" +#include "descriptor.h" +#include "layer.h" + +#define MACRO_REPEAT_TIMEOUT 400 /* In ms */ +#define MACRO_REPEAT_INTERVAL 20 /* In ms */ + +static void kbd_panic_check(struct keyboard *kbd) +{ + size_t i; + size_t n; + + n = 0; + for (i = 0; i < kbd->nr_active_keys; i++) { + switch (kbd->active_keys[i].code) { + case KEY_BACKSPACE: + case KEY_BACKSLASH: + case KEY_ENTER: + n++; + break; + } + } + + if (n == 3) { + fprintf(stderr, "Termination key sequence triggered (backspace+backslash+enter), terminating.\n"); + exit(1); + } +} + +static const struct descriptor *kbd_lookup_descriptor(struct keyboard *kbd, + uint16_t code, + int pressed, + const struct layer **layer) +{ + size_t i; + const struct descriptor *d = NULL; + const struct layer *dl = NULL; + + if (!code) + return NULL; + + /* Check if the key is active */ + for (i = 0; i < kbd->nr_active_keys; i++) { + struct active_key *ak = &kbd->active_keys[i]; + + if (ak->code == code) { + dl = ak->dl; + d = ak->d; + } + } + + if (!d) { + if (!kbd->nr_active_layers) + dl = kbd->layout; + else + dl = kbd->active_layers[kbd->nr_active_layers-1].layer; + + d = layer_get_descriptor(dl, code); + + /* + * If the most recently active layer is a modifier layer + * and the key is undefined, use the layout definition. + * If the key is undefined in a normal layer, treat it + * as undefined. + */ + if (!d && dl->mods) { + dl = kbd->layout; + d = layer_get_descriptor(dl, code); + } + + } + + if (pressed) { + struct active_key *ak = &kbd->active_keys[kbd->nr_active_keys++]; + + ak->code = code; + ak->d = d; + ak->dl = dl; + } else { + int n = 0; + + for (i = 0; i < kbd->nr_active_keys; i++) { + if (kbd->active_keys[i].code != code) + kbd->active_keys[n++] = kbd->active_keys[i]; + } + + kbd->nr_active_keys = n; + } + + *layer = dl; + return d; +} + +/* Compute the current modifier state based on the activated layers. */ +static uint16_t kbd_compute_mods(struct keyboard *kbd) +{ + size_t i; + uint16_t mods = 0; + + for (i = 0; i < kbd->nr_active_layers; i++) + mods |= kbd->active_layers[i].layer->mods; + + return mods; +} + +/* Compute and apply the current mod state to the virtual keyboard. */ +static void kbd_reify_mods(struct keyboard *kbd) +{ + set_mods(kbd_compute_mods(kbd)); +} + +static const struct active_layer *kbd_lookup_active_layer(struct keyboard *kbd, const struct layer *layer) +{ + size_t i; + + for (i = 0; i < kbd->nr_active_layers; i++) { + struct active_layer *al = &kbd->active_layers[i]; + + if (al->layer == layer) + return al; + } + + return NULL; +} + +static void kbd_deactivate_layer(struct keyboard *kbd, const struct layer *layer) +{ + int i; + int n = kbd->nr_active_layers; + + kbd->nr_active_layers = 0; + for (i = 0; i < n; i++) { + struct active_layer *al = &kbd->active_layers[i]; + + if (al->layer != layer) + kbd->active_layers[kbd->nr_active_layers++] = *al; + } + + kbd_reify_mods(kbd); +} + +static void kbd_clear_oneshots(struct keyboard *kbd) +{ + size_t i, n; + + n = 0; + for (i = 0; i < kbd->nr_active_layers; i++) { + struct active_layer *al = &kbd->active_layers[i]; + + if (!al->oneshot) + kbd->active_layers[n++] = *al; + } + + kbd->nr_active_layers = n; +} + + +static void kbd_activate_layer(struct keyboard *kbd, const struct layer *layer, int oneshot) +{ + struct active_layer *al; + size_t i; + + for (i = 0; i < kbd->nr_active_layers; i++) { + al = &kbd->active_layers[i]; + if(al->layer == layer) { + al->oneshot = oneshot; + return; + } + } + + al = &kbd->active_layers[kbd->nr_active_layers]; + + al->layer = layer; + al->oneshot = oneshot; + + kbd->nr_active_layers++; + kbd_reify_mods(kbd); +} + +static void kbd_process_keyseq(struct keyboard *kbd, + int verbatim, + const struct key_sequence *sequence, + int pressed) +{ + if (sequence->code == 0) + return; + + if (pressed) { + uint16_t mods; + + if (verbatim) /* Temporarily disable layer mods. */ + mods = sequence->mods; + else + mods = kbd_compute_mods(kbd) | sequence->mods; + + set_mods(mods); + + /* Accommodate rolling modified/unmodified keys (e.g [/{). */ + if (keystate[sequence->code]) + send_key(sequence->code, 0); + + send_key(sequence->code, 1); + } else + send_key(sequence->code, 0); +} + +static void kbd_swap_layer(struct keyboard *kbd, + const struct layer *dl, + const struct layer *new_layer, + const struct descriptor *new_descriptor) +{ + size_t i; + + /* + * Find the key activating dl and swap out its descriptor with the + * current one to deactivate the replacement layer. + */ + for (i = 0; i < kbd->nr_active_keys; i++) { + struct active_key *ak = &kbd->active_keys[i]; + + if(ak->d->op == OP_LAYER && ak->d->args[0].layer == dl) { + kbd_deactivate_layer(kbd, dl); + kbd_activate_layer(kbd, new_layer, 0); + + ak->d = new_descriptor; + return; + } + } +} + +#include + +static void kbd_execute_macro(struct keyboard *kbd, struct macro *macro) +{ + size_t i; + + for (i = 0; i < macro->sz; i++) { + struct macro_entry *ent = ¯o->entries[i]; + + if (ent->type == MACRO_KEYSEQUENCE) { + set_mods(ent->data.sequence.mods); + + send_key(ent->data.sequence.code, 1); + send_key(ent->data.sequence.code, 0); + } else + usleep(ent->data.timeout*1E3); + } + + kbd_reify_mods(kbd); +} + +static long get_time() +{ + struct timespec tv; + clock_gettime(CLOCK_MONOTONIC, &tv); + return (tv.tv_sec*1E3)+tv.tv_nsec/1E6; +} + +/* + * Here be tiny dragons. + * + * Code may be 0 in the event of a timeout. + * + * The return value corresponds to a timeout before which the next invocation + * of kbd_process_key_event must take place. A return value of 0 permits the + * main loop to call at liberty. + */ +long kbd_process_key_event(struct keyboard *kbd, + uint16_t code, + int pressed) +{ + int i; + const struct descriptor *d; + const struct layer *dl; + static int oneshot_latch = 0; + static int last_pressed_keycode = 0; + static struct macro *active_macro = NULL; + + static long overload_ts = 0; + static const struct descriptor *pending_overload = 0; + + long timeout = 0; + + kbd_panic_check(kbd); + + if (active_macro) { + if (!code) { + kbd_execute_macro(kbd, active_macro); + return MACRO_REPEAT_INTERVAL; + } else + active_macro = NULL; + } + + if (pending_overload) { + struct layer *layer = pending_overload->args[0].layer; + const struct key_sequence *sequence = &pending_overload->args[1].sequence; + size_t timeout = pending_overload->args[2].timeout; + + if ((get_time() - overload_ts) >= timeout) { + kbd_activate_layer(kbd, layer, 0); + } else { + kbd_process_keyseq(kbd, 0, sequence, 1); + kbd_process_keyseq(kbd, 0, sequence, 0); + } + + pending_overload = NULL; + } + + d = kbd_lookup_descriptor(kbd, code, pressed, &dl); + + if(!d) { + kbd_clear_oneshots(kbd); + return 0; + } + + switch (d->op) { + int verbatim; + const struct key_sequence *sequence; + const struct layer *layer; + struct macro *macro; + + case OP_KEYSEQ: + verbatim = dl != kbd->layout; + + kbd_process_keyseq(kbd, verbatim, &d->args[0].sequence, pressed); + + if (pressed) { + oneshot_latch = 0; + kbd_clear_oneshots(kbd); + } else + kbd_reify_mods(kbd); + + break; + case OP_RESET: + if (!pressed) { + kbd->nr_active_layers = 0; + kbd_reify_mods(kbd); + } + + break; + case OP_TOGGLE: + layer = d->args[0].layer; + + if (!pressed) { + const struct active_layer *al = kbd_lookup_active_layer(kbd, layer); + if (al) { + if (al->oneshot) /* Allow oneshot layers to toggle themselves. */ + kbd_activate_layer(kbd, layer, 0); + else + kbd_deactivate_layer(kbd, layer); + } else + kbd_activate_layer(kbd, layer, 0); + } + + break; + case OP_LAYOUT: + layer = d->args[0].layer; + + if (!pressed) + kbd->layout = layer; + + break; + case OP_LAYER: + layer = d->args[0].layer; + + if (pressed) + kbd_activate_layer(kbd, layer, 0); + else + kbd_deactivate_layer(kbd, layer); + + break; + case OP_ONESHOT: + layer = d->args[0].layer; + + if (pressed) { + oneshot_latch++; + kbd_activate_layer(kbd, layer, 0); + } else if (oneshot_latch) /* No interposed KEYSEQ since key down (other modifiers don't interfere with this). */ + kbd_activate_layer(kbd, layer, 1); + else + kbd_deactivate_layer(kbd, layer); + + break; + case OP_SWAP: + layer = d->args[0].layer; + sequence = &d->args[1].sequence; + + if (pressed) { + kbd_process_keyseq(kbd, 1, sequence, 1); + kbd_process_keyseq(kbd, 1, sequence, 0); + + kbd_swap_layer(kbd, dl, layer, d); + } else if (last_pressed_keycode != code) /* We only reach this from the remapped dl activate key. */ + kbd_deactivate_layer(kbd, layer); + + break; + case OP_OVERLOAD: + layer = d->args[0].layer; + sequence = &d->args[1].sequence; + + if (pressed) { + kbd_activate_layer(kbd, layer, 0); + } else if (last_pressed_keycode == code) { + kbd_deactivate_layer(kbd, layer); + + kbd_process_keyseq(kbd, 0, sequence, 1); + kbd_process_keyseq(kbd, 0, sequence, 0); + } else { + kbd_deactivate_layer(kbd, layer); + } + + break; + case OP_OVERLOAD_TIMEOUT: + layer = d->args[0].layer; + sequence = &d->args[1].sequence; + + if (pressed) { + pending_overload = d; + overload_ts = get_time(); + + timeout = d->args[2].timeout; + } else { + kbd_deactivate_layer(kbd, layer); + } + + break; + case OP_MACRO: + macro = d->args[0].macro; + + if (pressed) { + active_macro = macro; + kbd_execute_macro(kbd, macro); + + timeout = MACRO_REPEAT_TIMEOUT; + } else + active_macro = NULL; + + break; + default: + printf("Unrecognized op: %d, ignoring...\n", d->op); + break; + } + + if (pressed) + last_pressed_keycode = code; + + return timeout; +} diff --git a/src/keyboard.h b/src/keyboard.h new file mode 100644 index 0000000..ab0a146 --- /dev/null +++ b/src/keyboard.h @@ -0,0 +1,43 @@ +#ifndef KEYBOARD_H +#define KEYBOARD_H + +#include "config.h" +#include "layer.h" + +#define MAX_ACTIVE_KEYS 32 + +struct active_layer { + const struct layer *layer; + int oneshot; +}; + +/* Represents a currently depressed key */ +struct active_key { + uint16_t code; + + const struct descriptor *d; + const struct layer *dl; /* The layer from which the descriptor was drawn. */ +}; + +/* Active keyboard state. */ +struct keyboard { + int fd; + char devnode[256]; + uint32_t id; + + struct active_layer active_layers[MAX_LAYERS]; + size_t nr_active_layers; + + const struct layer *layout; + + struct active_key active_keys[MAX_ACTIVE_KEYS]; + size_t nr_active_keys; + + uint16_t last_pressed_keycode; + + struct keyboard *next; +}; + +long kbd_process_key_event(struct keyboard *kbd, uint16_t code, int pressed); + +#endif diff --git a/src/main.c b/src/keyd.c similarity index 56% rename from src/main.c rename to src/keyd.c index b6e15aa..a6d0f7e 100644 --- a/src/main.c +++ b/src/keyd.c @@ -49,6 +49,8 @@ #include #include "keys.h" #include "config.h" +#include "keyboard.h" +#include "keyd.h" #define VIRTUAL_KEYBOARD_NAME "keyd virtual keyboard" #define VIRTUAL_POINTER_NAME "keyd virtual pointer" @@ -65,27 +67,8 @@ static int vptr = -1; static struct udev *udev; static struct udev_monitor *udevmon; -static uint8_t keystate[KEY_CNT] = { 0 }; - -//Active keyboard state. -struct keyboard { - int fd; - char devnode[256]; - - struct layer **layers; - size_t nlayers; - - //The layer to which modifiers are applied, - //this may be distinct from the main layout - struct layer *modlayout; - struct layer *layout; - - struct key_descriptor *dcache[KEY_CNT]; - uint16_t mcache[KEY_CNT]; - uint64_t pressed_timestamps[KEY_CNT]; - - struct keyboard *next; -}; +uint8_t keystate[KEY_CNT] = { 0 }; +static int sigfds[2]; static struct keyboard *keyboards = NULL; @@ -112,13 +95,6 @@ static void _die(char *fmt, ...) #define die(fmt, ...) _die("%s:%d: "fmt, __FILE__, __LINE__, ## __VA_ARGS__) -static uint64_t get_time() -{ - struct timespec tv; - clock_gettime(CLOCK_MONOTONIC, &tv); - return (tv.tv_sec * 1E9) + tv.tv_nsec; -} - static void udev_type(struct udev_device *dev, int *iskbd, int *ismouse) { if (iskbd) @@ -128,7 +104,7 @@ static void udev_type(struct udev_device *dev, int *iskbd, int *ismouse) const char *path = udev_device_get_devnode(dev); - if (!path || !strstr(path, "event")) //Filter out non evdev devices. + if (!path || !strstr(path, "event")) /* Filter out non evdev devices. */ return; struct udev_list_entry *prop; @@ -151,6 +127,26 @@ static void udev_type(struct udev_device *dev, int *iskbd, int *ismouse) } } +static uint32_t evdev_device_id(const char *devnode) +{ + static char name[256]; + struct input_id info; + + int fd = open(devnode, O_RDONLY); + if (fd < 0) { + perror("open"); + exit(-1); + } + + if (ioctl(fd, EVIOCGID, &info) == -1) { + perror("ioctl"); + exit(-1); + } + + close(fd); + return info.vendor << 16 | info.product; +} + static const char *evdev_device_name(const char *devnode) { static char name[256]; @@ -192,9 +188,9 @@ static void get_keyboard_nodes(char *nodes[MAX_KEYBOARDS], int *sz) *sz = 0; udev_list_entry_foreach(ent, devices) { int iskbd, ismouse; + const char *name = udev_list_entry_get_name(ent);; - struct udev_device *dev = - udev_device_new_from_syspath(udev, name); + struct udev_device *dev = udev_device_new_from_syspath(udev, name); const char *path = udev_device_get_devnode(dev); udev_type(dev, &iskbd, &ismouse); @@ -202,13 +198,13 @@ static void get_keyboard_nodes(char *nodes[MAX_KEYBOARDS], int *sz) if (iskbd) { dbg("Detected keyboard node %s (%s) ismouse: %d", name, evdev_device_name(path), ismouse); + nodes[*sz] = malloc(strlen(path) + 1); strcpy(nodes[*sz], path); (*sz)++; assert(*sz <= MAX_KEYBOARDS); } else if (path) { - dbg("Ignoring %s (%s)", evdev_device_name(path), - path); + dbg("Ignoring %s (%s)", evdev_device_name(path), path); } udev_device_unref(dev); @@ -309,7 +305,7 @@ static void send_repetitions() .time.tv_usec = 0 }; - //Inefficient, but still reasonably fast (<100us) + /* Inefficient, but still reasonably fast (<100us) */ for (i = 0; i < sizeof keystate / sizeof keystate[0]; i++) { if (keystate[i]) { ev.code = i; @@ -319,17 +315,16 @@ static void send_repetitions() } } -static void send_key(uint16_t code, int is_pressed) + +void send_key(int code, int pressed) { - if (code == KEY_NOOP) - return; + keystate[code] = pressed; - keystate[code] = is_pressed; struct input_event ev; ev.type = EV_KEY; ev.code = code; - ev.value = is_pressed; + ev.value = pressed; ev.time.tv_sec = 0; ev.time.tv_usec = 0; @@ -338,360 +333,28 @@ static void send_key(uint16_t code, int is_pressed) syn(vkbd); } -static void setmods(uint16_t mods) -{ - if (!!(mods & MOD_CTRL) != keystate[KEY_LEFTCTRL]) - send_key(KEY_LEFTCTRL, !keystate[KEY_LEFTCTRL]); - if (!!(mods & MOD_SHIFT) != keystate[KEY_LEFTSHIFT]) - send_key(KEY_LEFTSHIFT, !keystate[KEY_LEFTSHIFT]); - if (!!(mods & MOD_SUPER) != keystate[KEY_LEFTMETA]) - send_key(KEY_LEFTMETA, !keystate[KEY_LEFTMETA]); - if (!!(mods & MOD_ALT) != keystate[KEY_LEFTALT]) - send_key(KEY_LEFTALT, !keystate[KEY_LEFTALT]); - if (!!(mods & MOD_ALT_GR) != keystate[KEY_RIGHTALT]) - send_key(KEY_RIGHTALT, !keystate[KEY_RIGHTALT]); -} - -static void reify_layer_mods(struct keyboard *kbd) -{ - uint16_t mods = 0; - size_t i; - - for (i = 0; i < kbd->nlayers; i++) { - struct layer *layer = kbd->layers[i]; - - if (layer->active) - mods |= layer->mods; - } - - setmods(mods); -} - -static void kbd_panic_check(struct keyboard *kbd) -{ - if (kbd->dcache[KEY_BACKSPACE] && - kbd->dcache[KEY_BACKSLASH] && kbd->dcache[KEY_ENTER]) { - info("Termination key sequence triggered (backspace backslash enter), terminating."); - exit(1); +#define SETMOD(mods, mod, key, key2) \ + if (mods & mod) { \ + if (!keystate[key] && !keystate[key2]) \ + send_key(key, 1); \ + } else { \ + if (keystate[key]) \ + send_key(key, 0); \ +\ + if (keystate[key2]) \ + send_key(key2, 0); \ } -} -static struct key_descriptor *kbd_lookup_descriptor(struct keyboard *kbd, - uint16_t code, - int pressed, - uint16_t * modsp) +void set_mods(uint16_t mods) { - size_t i; - struct key_descriptor *desc = NULL; - struct layer *layer = NULL; - uint16_t mods = 0; - size_t nactive = 0; - - //Cache the descriptor to ensure consistency upon up/down event pairs since layers can change midkey. - if (!pressed) { - *modsp = kbd->mcache[code]; - desc = kbd->dcache[code]; - - kbd->dcache[code] = NULL; - kbd->mcache[code] = 0; - - return desc; - } - //Pick the most recently activated layer in which a mapping is defined. - - for (i = 0; i < kbd->nlayers; i++) { - struct layer *l = kbd->layers[i]; - struct key_descriptor *d = &l->keymap[code]; - - if (l->active) { - if (d->action - && (!desc - || (l->timestamp > layer->timestamp))) { - desc = d; - layer = l; - } - - nactive++; - } - } - - //Calculate the modifier union of active layers, excluding the mods for - //the layer in which the mapping is defined. - - mods = 0; - for (i = 0; i < kbd->nlayers; i++) { - struct layer *l = kbd->layers[i]; - - if (l->active && l != layer) - mods |= l->mods; - } - - if (!desc) { - //If any modifier layers are active and do not explicitly - //define a mapping, obtain the target from modlayout. - - if (mods) { - if (mods == MOD_SHIFT || mods == MOD_ALT_GR) - desc = &kbd->layout->keymap[code]; - else - desc = &kbd->modlayout->keymap[code]; - } else if (!nactive) //If no layers are active use the default layout - desc = &kbd->layout->keymap[code]; - else - return NULL; - } - - kbd->pressed_timestamps[code] = get_time(); - kbd->dcache[code] = desc; - kbd->mcache[code] = mods; - - *modsp = mods; - return desc; -} - -static void do_keyseq(uint32_t seq) -{ - if (!seq) - return; - - uint16_t mods = seq >> 16; - uint16_t key = seq & 0xFFFF; - - if (mods & MOD_TIMEOUT) { - usleep(GET_TIMEOUT(seq) * 1000); - } else { - setmods(mods); - send_key(key, 1); - send_key(key, 0); - } -} - -//Where the magic happens -static void process_key_event(struct keyboard *kbd, uint16_t code, - int pressed) -{ - size_t i; - - struct key_descriptor *d; - - static struct key_descriptor *lastd = NULL; - static uint8_t oneshot_layers[MAX_LAYERS] = { 0 }; - static uint64_t last_keyseq_timestamp = 0; - static uint16_t swapped_keycode = 0; - static uint16_t last_keydown = 0; - - if (pressed) - last_keydown = code; - - uint16_t mods = 0; - - d = kbd_lookup_descriptor(kbd, code, pressed, &mods); - if (!d) - goto keyseq_cleanup; - - kbd_panic_check(kbd); - - switch (d->action) { - struct layer *layer; - uint32_t keyseq; - uint16_t keycode; - - case ACTION_OVERLOAD: - keyseq = d->arg.keyseq; - layer = kbd->layers[d->arg2.layer]; - - if (pressed) { - layer->active = 1; - layer->timestamp = get_time(); - } else { - layer->active = 0; - - if (last_keydown == code) { //If tapped - uint16_t key = keyseq & 0xFFFF; - mods |= keyseq >> 16; - - setmods(mods); - send_key(key, 1); - send_key(key, 0); - - last_keyseq_timestamp = get_time(); - goto keyseq_cleanup; - } - } - - reify_layer_mods(kbd); - break; - case ACTION_LAYOUT: - kbd->layout = kbd->layers[d->arg.layer]; - kbd->modlayout = kbd->layers[d->arg2.layer]; - - dbg("layer: %d, modlayout: %d", kbd->layout, - kbd->modlayout); - break; - case ACTION_ONESHOT: - layer = kbd->layers[d->arg.layer]; - - if (pressed) { - layer->active = 1; - oneshot_layers[d->arg.layer] = 0; - layer->timestamp = get_time(); - } else if (kbd->pressed_timestamps[code] < - last_keyseq_timestamp) { - layer->active = 0; - } else //Tapped - oneshot_layers[d->arg.layer] = 1; - - reify_layer_mods(kbd); - break; - case ACTION_LAYER_TOGGLE: - if (!pressed) { - layer = kbd->layers[d->arg.layer]; - - if (oneshot_layers[d->arg.layer]) { - oneshot_layers[d->arg.layer] = 0; - } else { - layer->active = !layer->active; - } - reify_layer_mods(kbd); - goto keyseq_cleanup; - } - break; - case ACTION_LAYER: - layer = kbd->layers[d->arg.layer]; - - if (pressed) { - layer->active = 1; - layer->timestamp = get_time(); - } else { - layer->active = 0; - } - - reify_layer_mods(kbd); - break; - case ACTION_KEYSEQ: - mods |= d->arg.keyseq >> 16; - keycode = d->arg.keyseq & 0xFFFF; - if (pressed) { - setmods(mods); - - //Account for the possibility that a version of the key - //with a different modifier set is already depressed (e.g [/{) - if (keystate[keycode]) - send_key(keycode, 0); - - send_key(keycode, 1); - } else { - reify_layer_mods(kbd); - send_key(keycode, 0); - } - - goto keyseq_cleanup; - break; - case ACTION_MACRO: - if (pressed) { - uint32_t *macro = d->arg.macro; - size_t sz = d->arg2.sz; - - for (i = 0; i < sz; i++) - do_keyseq(macro[i]); - - reify_layer_mods(kbd); - goto keyseq_cleanup; - - } - break; - case ACTION_SWAP: - layer = kbd->layers[d->arg.layer]; - - if (pressed && !swapped_keycode) { //For simplicity only allow one swapped layer at a time. - struct layer *old_layer; - uint16_t code = 0; - uint64_t maxts = 0; - - //Find a currently depressed keycode corresponding to the last active - //layer. - for (i = 0; - i < - sizeof(kbd->dcache) / sizeof(kbd->dcache[0]); - i++) { - struct key_descriptor *d = kbd->dcache[i]; - - if (d) { - struct layer *l; - - switch (d->action) { - //If it is layer key. - case ACTION_ONESHOT: - case ACTION_LAYER: - l = kbd->layers[d->arg. - layer]; - break; - case ACTION_OVERLOAD: - l = kbd->layers[d->arg2. - layer]; - break; - default: - continue; - } - - if (l->timestamp > maxts) { - code = i; - old_layer = l; - } - } - } - - //If a layer activation key is being held... - if (code) { - old_layer->active = 0; - - layer->active = 1; - layer->timestamp = get_time(); - - //Replace the keycode's descriptor with the present one so key up - //deactivates the appropriate layer. - kbd->dcache[code] = d; - - swapped_keycode = code; - - do_keyseq(d->arg2.keyseq); - reify_layer_mods(kbd); - } - } - //Key up corresponding to the swapped key. - if (!pressed && swapped_keycode == code) { - layer->active = 0; - - swapped_keycode = 0; - - reify_layer_mods(kbd); - } - - break; - case ACTION_UNDEFINED: - goto keyseq_cleanup; - break; - } - - lastd = d; - return; - - keyseq_cleanup: - lastd = d; - - if (pressed) - last_keyseq_timestamp = get_time(); - - //Clear active oneshot layers. - for (i = 0; i < kbd->nlayers; i++) { - if (oneshot_layers[i]) { - kbd->layers[i]->active = 0; - oneshot_layers[i] = 0; - } - } + SETMOD(mods, MOD_CTRL, KEY_LEFTCTRL, KEY_RIGHTCTRL) + SETMOD(mods, MOD_SHIFT, KEY_LEFTSHIFT, KEY_RIGHTSHIFT) + SETMOD(mods, MOD_SUPER, KEY_LEFTMETA, KEY_RIGHTMETA) + SETMOD(mods, MOD_ALT, KEY_LEFTALT, 0) + SETMOD(mods, MOD_ALT_GR, KEY_RIGHTALT, 0) } -//Block on the given keyboard nodes until no keys are depressed. +/* Block on the given keyboard nodes until no keys are depressed. */ static void await_keyboard_neutrality(char **devs, int n) { int fds[MAX_KEYBOARDS]; @@ -707,14 +370,15 @@ static void await_keyboard_neutrality(char **devs, int n) maxfd = fds[i]; } - //There is a race condition here since it is possible for a key down - //event to be generated before keyd is launched, in that case we hope a - //repeat event is generated within the first 300ms. If we miss the - //keydown event and the repeat event is not generated within the first - //300ms it is possible for this to yield a false positive. In practice - //this seems to work fine. Given the stateless nature of evdev I am not - //aware of a better way to achieve this. - + /* + * There is a race condition here since it is possible for a key down + * event to be generated before keyd is launched, in that case we hope a + * repeat event is generated within the first 300ms. If we miss the + * keydown event and the repeat event is not generated within the first + * 300ms it is possible for this to yield a false positive. In practice + * this seems to work fine. Given the stateless nature of evdev I am not + * aware of a better way to achieve this. + */ while (1) { struct timeval tv = { .tv_usec = 300000 @@ -762,37 +426,30 @@ static int manage_keyboard(const char *devnode) int fd; const char *name = evdev_device_name(devnode); struct keyboard *kbd; - struct keyboard_config *cfg = NULL; - struct keyboard_config *default_cfg = NULL; + struct config *config = NULL; + uint32_t id; + uint16_t vendor_id, product_id; - if (!strcmp(name, VIRTUAL_KEYBOARD_NAME) || !strcmp(name, VIRTUAL_POINTER_NAME)) //Don't manage virtual devices. - return 0; + /* Don't manage keyd's devices. */ + if (!strcmp(name, VIRTUAL_KEYBOARD_NAME) || !strcmp(name, VIRTUAL_POINTER_NAME)) + return -1; for (kbd = keyboards; kbd; kbd = kbd->next) { if (!strcmp(kbd->devnode, devnode)) { dbg("Already managing %s.", devnode); - return 0; + return -1; } } - for (cfg = configs; cfg; cfg = cfg->next) { - if (!strcmp(cfg->name, "default")) - default_cfg = cfg; + id = evdev_device_id(devnode); + vendor_id = id >> 16; + product_id = id & 0xFFFF; - if (!strcmp(cfg->name, name)) - break; - } + config = lookup_config(id); - if (!cfg) { - if (default_cfg) { - info("No config found for %s (%s), falling back to default.cfg", name, devnode); - cfg = default_cfg; - } else { - //Don't manage keyboards for which there is no configuration. - info("No config found for %s (%s), ignoring", name, - devnode); - return 0; - } + if (!config) { + fprintf(stderr, "No config found for %s (%04x:%04x)\n", evdev_device_name(devnode), vendor_id, product_id); + return -1; } if ((fd = open(devnode, O_RDONLY | O_NONBLOCK)) < 0) { @@ -800,27 +457,62 @@ static int manage_keyboard(const char *devnode) exit(1); } + /* Grab the keyboard. */ + if (ioctl(fd, EVIOCGRAB, (void *) 1) < 0) { + info("Failed to grab %04x:%04x, ignoring...\n", vendor_id, product_id); + perror("EVIOCGRAB"); + close(fd); + return -1; + } + kbd = calloc(1, sizeof(struct keyboard)); kbd->fd = fd; - kbd->layers = cfg->layers; - kbd->nlayers = cfg->nlayers; - - kbd->modlayout = cfg->layers[cfg->default_modlayout]; - kbd->layout = cfg->layers[cfg->default_layout]; + kbd->layout = config->layers[0]; strcpy(kbd->devnode, devnode); - //Grab the keyboard. - if (ioctl(fd, EVIOCGRAB, (void *) 1) < 0) { - perror("EVIOCGRAB"); - exit(-1); - } - kbd->next = keyboards; keyboards = kbd; - info("Managing %s", evdev_device_name(devnode)); - return 1; + info("Managing %s (%04x:%04x) (%s)", evdev_device_name(devnode), vendor_id, product_id, config->name); + return 0; +} + +static void scan_keyboards(int wait) +{ + int i, n; + char *devs[MAX_KEYBOARDS]; + + get_keyboard_nodes(devs, &n); + if (wait) + await_keyboard_neutrality(devs, n); + + for (i = 0; i < n; i++) { + manage_keyboard(devs[i]); + free(devs[i]); + } +} + +/* TODO: optimize */ +static void reload_config() +{ + struct keyboard *tmp; + struct keyboard *kbd = keyboards; + + while (kbd) { + struct keyboard *tmp = kbd; + + ioctl(kbd->fd, EVIOCGRAB, (void *) 0); + close(kbd->fd); + kbd = kbd->next; + free(tmp); + } + + keyboards = NULL; + + free_configs(); + read_config_dir(CONFIG_DIR); + scan_keyboards(0); } static int destroy_keyboard(const char *devnode) @@ -833,10 +525,8 @@ static int destroy_keyboard(const char *devnode) struct keyboard *kbd = *ent; *ent = kbd->next; - //Attempt to ungrab the the keyboard (assuming it still exists) - if (ioctl(kbd->fd, EVIOCGRAB, (void *) 1) < 0) { - perror("EVIOCGRAB"); - } + /* Attempt to ungrab the the keyboard (assuming it still exists) */ + ioctl(kbd->fd, EVIOCGRAB, (void *) 1); close(kbd->fd); free(kbd); @@ -858,7 +548,7 @@ static void monitor_exit(int status) tinfo.c_lflag |= ECHO; tcsetattr(0, TCSANOW, &tinfo); - exit(0); + exit(status); } static void evdev_monitor_loop(int *fds, int sz) @@ -904,9 +594,10 @@ static void evdev_monitor_loop(int *fds, int sz) FD_ZERO(&fdset); - //Proactively monitor stdout for pipe closures instead of waiting - //for a failed write to generate SIGPIPE. - + /* + * Proactively monitor stdout for pipe closures instead of waiting + * for a failed write to generate SIGPIPE. + */ if (ispiped) FD_SET(1, &fdset); @@ -918,14 +609,14 @@ static void evdev_monitor_loop(int *fds, int sz) select(maxfd + 1, &fdset, NULL, NULL, NULL); + if (FD_ISSET(1, &fdset) && read(1, NULL, 0) == -1) { /* STDOUT closed. */ + /* Re-enable echo. */ + monitor_exit(0); + } + for (i = 0; i < sz; i++) { int fd = fds[i]; - if (FD_ISSET(1, &fdset) && read(1, NULL, 0) == -1) { //STDOUT closed. - //Re-enable echo. - monitor_exit(0); - } - if (FD_ISSET(fd, &fdset)) { while (read(fd, &ev, sizeof(ev)) > 0) { if (ev.type == EV_KEY @@ -934,20 +625,12 @@ static void evdev_monitor_loop(int *fds, int sz) keycode_table[ev.code]. name; if (name) { - printf - ("%s\t%04x:%04x\t%s %s\n", - info_table - [fd].name, - info_table - [fd]. - product_id, - info_table - [fd]. - vendor_id, - name, - ev.value == - 0 ? "up" : - "down"); + printf("%s\t%04x:%04x\t%s %s\n", + info_table[fd].name, + info_table[fd].vendor_id, + info_table[fd].product_id, + name, ev.value == 0 ? "up" : "down"); + fflush(stdout); } else info("Unrecognized keycode: %d", ev.code); @@ -970,7 +653,7 @@ static int monitor_loop() struct termios tinfo; - //Disable terminal echo so keys don't appear twice. + /* Disable terminal echo so keys don't appear twice. */ tcgetattr(0, &tinfo); tinfo.c_lflag &= ~ECHO; tcsetattr(0, TCSANOW, &tinfo); @@ -983,7 +666,7 @@ static int monitor_loop() fd = open(devnodes[i], O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open"); - exit(-1); + monitor_exit(0); } fds[nfds++] = fd; } @@ -993,21 +676,32 @@ static int monitor_loop() return 0; } +static void usr1(int status) +{ + char c = ':'; + write(sigfds[1], &c, 1); +} + +/* Relative time in ns. */ +long get_time() +{ + struct timespec tv; + clock_gettime(CLOCK_MONOTONIC, &tv); + return (tv.tv_sec*1E9)+tv.tv_nsec; +} + static void main_loop() { struct keyboard *kbd; int monfd; - int i, n; - char *devs[MAX_KEYBOARDS]; + long timeout = 0; /* in ns */ + long last_ts = 0; + struct keyboard *timeout_kbd = NULL; - get_keyboard_nodes(devs, &n); - await_keyboard_neutrality(devs, n); + nice(-20); - for (i = 0; i < n; i++) { - manage_keyboard(devs[i]); - free(devs[i]); - } + scan_keyboards(1); udev = udev_new(); udevmon = udev_monitor_new_from_netlink(udev, "udev"); @@ -1015,21 +709,27 @@ static void main_loop() if (!udev) die("Can't create udev."); - udev_monitor_filter_add_match_subsystem_devtype(udevmon, "input", - NULL); + udev_monitor_filter_add_match_subsystem_devtype(udevmon, "input", NULL); udev_monitor_enable_receiving(udevmon); monfd = udev_monitor_get_fd(udevmon); + pipe(sigfds); + signal(SIGUSR1, usr1); + while (1) { int maxfd; fd_set fds; struct udev_device *dev; + int ret; + + struct timeval tv; FD_ZERO(&fds); FD_SET(monfd, &fds); + FD_SET(sigfds[0], &fds); - maxfd = monfd; + maxfd = monfd > sigfds[0] ? monfd : sigfds[0]; for (kbd = keyboards; kbd; kbd = kbd->next) { int fd = kbd->fd; @@ -1038,12 +738,36 @@ static void main_loop() FD_SET(fd, &fds); } - if (select(maxfd + 1, &fds, NULL, NULL, NULL) > 0) { + tv.tv_sec = (timeout/1E3) / 1E6; + tv.tv_usec = (long)(timeout/1E3) % (long)1E6; + + if ((ret=select(maxfd + 1, &fds, NULL, NULL, timeout > 0 ? &tv : NULL)) >= 0) { + long time = get_time(); + long elapsed; + + elapsed = time - last_ts; + last_ts = time; + + timeout -= elapsed; + + if (timeout_kbd && timeout <= 0) { + timeout = kbd_process_key_event(timeout_kbd, 0, 0) * 1E6; + + if (timeout <= 0) + timeout_kbd = NULL; + } + + if (FD_ISSET(sigfds[0], &fds)) { + char c; + read(sigfds[0], &c, 1); + info("Received SIGUSR1, reloading config"); + reload_config(); + } + if (FD_ISSET(monfd, &fds)) { int iskbd; dev = udev_monitor_receive_device(udevmon); - const char *devnode = - udev_device_get_devnode(dev); + const char *devnode = udev_device_get_devnode(dev); udev_type(dev, &iskbd, NULL); if (devnode && iskbd) { @@ -1068,33 +792,26 @@ static void main_loop() if (FD_ISSET(fd, &fds)) { struct input_event ev; - while (read(fd, &ev, sizeof(ev)) > - 0) { - //Preprocess events. + while (read(fd, &ev, sizeof(ev)) > 0) { switch (ev.type) { case EV_KEY: - if (IS_MOUSE_BTN - (ev.code)) { - write(vptr, &ev, sizeof(ev)); //Pass mouse buttons through the virtual pointer unimpeded. + if (IS_MOUSE_BTN (ev.code)) { + write(vptr, &ev, sizeof(ev)); /* Pass mouse buttons through the virtual pointer unimpeded. */ syn(vptr); - } else if (ev. - value == - 2) { - //Wayland and X both ignore repeat events but VTs seem to require them. - send_repetitions - (); + } else if (ev.value == 2) { + /* Wayland and X both ignore repeat events but VTs seem to require them. */ + send_repetitions (); } else { - process_key_event - (kbd, - ev. - code, - ev. - value); + timeout = kbd_process_key_event(kbd, ev.code, ev.value) * 1E6; + + if (timeout > 0) + timeout_kbd = kbd; + else + timeout_kbd = NULL; } break; - case EV_REL: //Pointer motion events - write(vptr, &ev, - sizeof(ev)); + case EV_REL: /* Pointer motion events */ + write(vptr, &ev, sizeof(ev)); syn(vptr); break; case EV_SYN: @@ -1113,7 +830,7 @@ static void main_loop() static void cleanup() { struct keyboard *kbd = keyboards; - config_free(); + free_configs(); while (kbd) { struct keyboard *tmp = kbd; @@ -1183,6 +900,8 @@ int main(int argc, char *argv[]) fprintf(stderr, "keyd version: %s (%s)\n", VERSION, GIT_COMMIT_HASH); return 0; + } else if (!strcmp(argv[1], "-d")) { + ;; } else if (!strcmp(argv[1], "-m")) { return monitor_loop(); } else if (!strcmp(argv[1], "-l")) { @@ -1228,7 +947,8 @@ int main(int argc, char *argv[]) daemonize(); info("Starting keyd v%s (%s).", VERSION, GIT_COMMIT_HASH); - config_generate(); + read_config_dir(CONFIG_DIR); + vkbd = create_virtual_keyboard(); vptr = create_virtual_pointer(); diff --git a/src/keyd.h b/src/keyd.h new file mode 100644 index 0000000..e013ebf --- /dev/null +++ b/src/keyd.h @@ -0,0 +1,8 @@ +#ifndef KEYD_H +#define KEYD_H + +extern uint8_t keystate[KEY_CNT]; +void set_mods(uint16_t mods); +void send_key(int code, int pressed); + +#endif diff --git a/src/keys.c b/src/keys.c new file mode 100644 index 0000000..5377a75 --- /dev/null +++ b/src/keys.c @@ -0,0 +1,44 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include "keys.h" + +uint16_t keycode_to_mod(uint16_t code) +{ + switch (code) { + case KEY_LEFTSHIFT: + case KEY_RIGHTSHIFT: + return MOD_SHIFT; + case KEY_LEFTALT: + return MOD_ALT; + case KEY_RIGHTALT: + return MOD_ALT_GR; + case KEY_RIGHTCTRL: + return MOD_CTRL; + case KEY_LEFTMETA: + case KEY_RIGHTMETA: + return MOD_SUPER; + } + + return 0; +} diff --git a/src/keys.h b/src/keys.h index 57bccb2..dc1aa26 100644 --- a/src/keys.h +++ b/src/keys.h @@ -31,6 +31,7 @@ #endif #include +#include #define MOD_ALT_GR 0x10 #define MOD_CTRL 0x8 @@ -41,14 +42,7 @@ #define KEY_NOOP 0x27b //Used for macros, bit of a kludge. - -#define MOD_TIMEOUT 0x20 //keysequence represents a timeout - -//Reserve the last 10 bits for the timeout value in ms. -#define TIMEOUT_KEY(ms) ((ms << 22) | 0x200000) -#define GET_TIMEOUT(key) (key >> 22) -#define MAX_TIMEOUT_LEN ((1<<10)-1) - +uint16_t keycode_to_mod(uint16_t code); struct keycode_table_ent { const char *name; diff --git a/src/layer.c b/src/layer.c new file mode 100644 index 0000000..cac24d9 --- /dev/null +++ b/src/layer.c @@ -0,0 +1,103 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include "layer.h" +#include "keys.h" +#include "descriptor.h" + +struct descriptor *layer_get_descriptor(const struct layer *layer, uint16_t code) +{ + struct keymap_entry *ent; + + for (ent = layer->_keymap; ent; ent = ent->next) + if (ent->code == code) + return &ent->descriptor; + + return NULL; +} + +void layer_set_descriptor(struct layer *layer, + uint16_t code, + const struct descriptor *descriptor) +{ + struct keymap_entry *ent = layer->_keymap; + + while (ent) { + if (ent->code == code) { + ent->descriptor = *descriptor; + return; + } + + ent = ent->next; + } + + ent = malloc(sizeof(struct keymap_entry)); + + ent->code = code; + ent->descriptor = *descriptor; + + ent->next = layer->_keymap; + + layer->_keymap = ent; +} + +struct layer *create_layer(const char *name, uint16_t mods, int is_layout) +{ + uint16_t code; + struct layer *layer; + struct descriptor *descriptors; + + layer = calloc(1, sizeof(struct layer)); + layer->mods = mods; + layer->is_layout = is_layout; + strcpy(layer->name, name); + + if (is_layout) { + for (code = 0; code < KEY_MAX; code++) { + struct descriptor d; + + d.op = OP_KEYSEQ; + d.args[0].sequence.code = code; + d.args[0].sequence.mods = 0; + + layer_set_descriptor(layer, code, &d); + } + } + + return layer; +} + +void free_layer(struct layer *layer) +{ + struct keymap_entry *ent = layer->_keymap; + + while (ent) { + struct keymap_entry *tmp = ent; + ent = ent->next; + free(tmp); + } + + free(layer); +} diff --git a/src/layer.h b/src/layer.h new file mode 100644 index 0000000..ca377e2 --- /dev/null +++ b/src/layer.h @@ -0,0 +1,38 @@ +#ifndef __H_LAYER_ +#define __H_LAYER_ + +#include "keys.h" +#include "descriptor.h" + +#define MAX_LAYER_NAME_LEN 32 + +struct keymap_entry { + uint16_t code; + struct descriptor descriptor; + + struct keymap_entry *next; +}; + +/* + * A layer is a map from keycodes to descriptors. It may optionally contain one + * or more modifiers which are applied to the base layout in the event that no + * matching descriptor is found in the keymap. For consistency, modifiers are + * internally mapped to eponymously named layers consisting of the + * corresponding modifier and an empty keymap. + */ + +struct layer { + char name[MAX_LAYER_NAME_LEN]; + + int is_layout; + uint16_t mods; + struct keymap_entry *_keymap; +}; + +struct layer *create_layer(const char *name, uint16_t mods, int populate); +void free_layer(struct layer *layer); + +struct descriptor *layer_get_descriptor(const struct layer *layer, uint16_t code); +void layer_set_descriptor(struct layer *layer, uint16_t code, const struct descriptor *descriptor); + +#endif diff --git a/t/keys.py b/t/keys.py new file mode 100644 index 0000000..72ef678 --- /dev/null +++ b/t/keys.py @@ -0,0 +1,477 @@ +class Key: + def __init__(self, name, code, alt_name, shifted_name): + self.name = name + self.code = code + self.alt_name = alt_name + self.shifted_name = shifted_name + + def __str__(self): + return self.name + + +def is_mouse_button(key): + return ((key.code) >= names["btn left"].code and (key.code) <= names["btn task"].code) or\ + ((key.code) >= names["btn 0"].code and ( + key.code) <= names["btn 9"].code) + + +codes = { + 1: Key("esc", 1, "escape", ""), + 2: Key("1", 2, "", "!"), + 3: Key("2", 3, "", "@"), + 4: Key("3", 4, "", "#"), + 5: Key("4", 5, "", "$"), + 6: Key("5", 6, "", "%"), + 7: Key("6", 7, "", "^"), + 8: Key("7", 8, "", "&"), + 9: Key("8", 9, "", "*"), + 10: Key("9", 10, "", "("), + 11: Key("0", 11, "", ")"), + 12: Key("-", 12, "minus", "_"), + 13: Key("=", 13, "equal", "+"), + 14: Key("backspace", 14, "", ""), + 15: Key("tab", 15, "", ""), + 16: Key("q", 16, "", "Q"), + 17: Key("w", 17, "", "W"), + 18: Key("e", 18, "", "E"), + 19: Key("r", 19, "", "R"), + 20: Key("t", 20, "", "T"), + 21: Key("y", 21, "", "Y"), + 22: Key("u", 22, "", "U"), + 23: Key("i", 23, "", "I"), + 24: Key("o", 24, "", "O"), + 25: Key("p", 25, "", "P"), + 26: Key("[", 26, "leftbrace", "{"), + 27: Key("]", 27, "rightbrace", "}"), + 28: Key("enter", 28, "\n", ""), + 29: Key("control", 29, "leftcontrol", ""), + 30: Key("a", 30, "", "A"), + 31: Key("s", 31, "", "S"), + 32: Key("d", 32, "", "D"), + 33: Key("f", 33, "", "F"), + 34: Key("g", 34, "", "G"), + 35: Key("h", 35, "", "H"), + 36: Key("j", 36, "", "J"), + 37: Key("k", 37, "", "K"), + 38: Key("l", 38, "", "L"), + 39: Key(";", 39, "semicolon", ":"), + 40: Key("'", 40, "apostrophe", "\""), + 41: Key("`", 41, "grave", "~"), + 42: Key("shift", 42, "leftshift", ""), + 43: Key("\\", 43, "backslash", " |"), + 44: Key("z", 44, "", "Z"), + 45: Key("x", 45, "", "X"), + 46: Key("c", 46, "", "C"), + 47: Key("v", 47, "", "V"), + 48: Key("b", 48, "", "B"), + 49: Key("n", 49, "", "N"), + 50: Key("m", 50, "", "M"), + 51: Key(",", 51, "comma", "<"), + 52: Key(".", 52, "dot", ">"), + 53: Key("/", 53, "slash", "?"), + 54: Key("rightshift", 54, "", ""), + 55: Key("kpasterisk", 55, "", ""), + 56: Key("alt", 56, "leftalt", ""), + 57: Key("space", 57, " ", ""), + 58: Key("capslock", 58, "", ""), + 59: Key("f1", 59, "", ""), + 60: Key("f2", 60, "", ""), + 61: Key("f3", 61, "", ""), + 62: Key("f4", 62, "", ""), + 63: Key("f5", 63, "", ""), + 64: Key("f6", 64, "", ""), + 65: Key("f7", 65, "", ""), + 66: Key("f8", 66, "", ""), + 67: Key("f9", 67, "", ""), + 68: Key("f10", 68, "", ""), + 69: Key("numlock", 69, "", ""), + 70: Key("scrolllock", 70, "", ""), + 71: Key("kp7", 71, "", ""), + 72: Key("kp8", 72, "", ""), + 73: Key("kp9", 73, "", ""), + 74: Key("kpminus", 74, "", ""), + 75: Key("kp4", 75, "", ""), + 76: Key("kp5", 76, "", ""), + 77: Key("kp6", 77, "", ""), + 78: Key("kpplus", 78, "", ""), + 79: Key("kp1", 79, "", ""), + 80: Key("kp2", 80, "", ""), + 81: Key("kp3", 81, "", ""), + 82: Key("kp0", 82, "", ""), + 83: Key("kpdot", 83, "", ""), + 85: Key("zenkakuhankaku", 85, "", ""), + 86: Key("102nd", 86, "", ""), + 87: Key("f11", 87, "", ""), + 88: Key("f12", 88, "", ""), + 89: Key("ro", 89, "", ""), + 90: Key("katakana", 90, "", ""), + 91: Key("hiragana", 91, "", ""), + 92: Key("henkan", 92, "", ""), + 93: Key("katakanahiragana", 93, "", ""), + 94: Key("muhenkan", 94, "", ""), + 95: Key("kpjpcomma", 95, "", ""), + 96: Key("kpenter", 96, "", ""), + 97: Key("rightcontrol", 97, "", ""), + 98: Key("kpslash", 98, "", ""), + 99: Key("sysrq", 99, "", ""), + 100: Key("rightalt", 100, "", ""), + 101: Key("linefeed", 101, "", ""), + 102: Key("home", 102, "", ""), + 103: Key("up", 103, "", ""), + 104: Key("pageup", 104, "", ""), + 105: Key("left", 105, "", ""), + 106: Key("right", 106, "", ""), + 107: Key("end", 107, "", ""), + 108: Key("down", 108, "", ""), + 109: Key("pagedown", 109, "", ""), + 110: Key("insert", 110, "", ""), + 111: Key("delete", 111, "", ""), + 112: Key("macro", 112, "", ""), + 113: Key("mute", 113, "", ""), + 114: Key("volumedown", 114, "", ""), + 115: Key("volumeup", 115, "", ""), + 116: Key("power", 116, "", ""), + 117: Key("kpequal", 117, "", ""), + 118: Key("kpplusminus", 118, "", ""), + 119: Key("pause", 119, "", ""), + 120: Key("scale", 120, "", ""), + 121: Key("kpcomma", 121, "", ""), + 122: Key("hangeul", 122, "", ""), + 123: Key("hanja", 123, "", ""), + 124: Key("yen", 124, "", ""), + 125: Key("meta", 125, "leftmeta", ""), + 126: Key("rightmeta", 126, "", ""), + 127: Key("compose", 127, "", ""), + 128: Key("stop", 128, "", ""), + 129: Key("again", 129, "", ""), + 130: Key("props", 130, "", ""), + 131: Key("undo", 131, "", ""), + 132: Key("front", 132, "", ""), + 133: Key("copy", 133, "", ""), + 134: Key("open", 134, "", ""), + 135: Key("paste", 135, "", ""), + 136: Key("find", 136, "", ""), + 137: Key("cut", 137, "", ""), + 138: Key("help", 138, "", ""), + 139: Key("menu", 139, "", ""), + 140: Key("calc", 140, "", ""), + 141: Key("setup", 141, "", ""), + 142: Key("sleep", 142, "", ""), + 143: Key("wakeup", 143, "", ""), + 144: Key("file", 144, "", ""), + 145: Key("sendfile", 145, "", ""), + 146: Key("deletefile", 146, "", ""), + 147: Key("xfer", 147, "", ""), + 148: Key("prog1", 148, "", ""), + 149: Key("prog2", 149, "", ""), + 150: Key("www", 150, "", ""), + 151: Key("msdos", 151, "", ""), + 152: Key("coffee", 152, "", ""), + 153: Key("display", 153, "", ""), + 154: Key("cyclewindows", 154, "", ""), + 155: Key("mail", 155, "", ""), + 156: Key("bookmarks", 156, "", ""), + 157: Key("computer", 157, "", ""), + 158: Key("back", 158, "", ""), + 159: Key("forward", 159, "", ""), + 160: Key("closecd", 160, "", ""), + 161: Key("ejectcd", 161, "", ""), + 162: Key("ejectclosecd", 162, "", ""), + 163: Key("nextsong", 163, "", ""), + 164: Key("playpause", 164, "", ""), + 165: Key("previoussong", 165, "", ""), + 166: Key("stopcd", 166, "", ""), + 167: Key("record", 167, "", ""), + 168: Key("rewind", 168, "", ""), + 169: Key("phone", 169, "", ""), + 170: Key("iso", 170, "", ""), + 171: Key("config", 171, "", ""), + 172: Key("homepage", 172, "", ""), + 173: Key("refresh", 173, "", ""), + 174: Key("exit", 174, "", ""), + 175: Key("move", 175, "", ""), + 176: Key("edit", 176, "", ""), + 177: Key("scrollup", 177, "", ""), + 178: Key("scrolldown", 178, "", ""), + 179: Key("kpleftparen", 179, "", ""), + 180: Key("kprightparen", 180, "", ""), + 181: Key("new", 181, "", ""), + 182: Key("redo", 182, "", ""), + 183: Key("f13", 183, "", ""), + 184: Key("f14", 184, "", ""), + 185: Key("f15", 185, "", ""), + 186: Key("f16", 186, "", ""), + 187: Key("f17", 187, "", ""), + 188: Key("f18", 188, "", ""), + 189: Key("f19", 189, "", ""), + 190: Key("f20", 190, "", ""), + 191: Key("f21", 191, "", ""), + 192: Key("f22", 192, "", ""), + 193: Key("f23", 193, "", ""), + 194: Key("f24", 194, "", ""), + 200: Key("playcd", 200, "", ""), + 201: Key("pausecd", 201, "", ""), + 202: Key("prog3", 202, "", ""), + 203: Key("prog4", 203, "", ""), + 204: Key("dashboard", 204, "", ""), + 205: Key("suspend", 205, "", ""), + 206: Key("close", 206, "", ""), + 207: Key("play", 207, "", ""), + 208: Key("fastforward", 208, "", ""), + 209: Key("bassboost", 209, "", ""), + 210: Key("print", 210, "", ""), + 211: Key("hp", 211, "", ""), + 212: Key("camera", 212, "", ""), + 213: Key("sound", 213, "", ""), + 214: Key("question", 214, "", ""), + 215: Key("email", 215, "", ""), + 216: Key("chat", 216, "", ""), + 217: Key("search", 217, "", ""), + 218: Key("connect", 218, "", ""), + 219: Key("finance", 219, "", ""), + 220: Key("sport", 220, "", ""), + 221: Key("shop", 221, "", ""), + 222: Key("alterase", 222, "", ""), + 223: Key("cancel", 223, "", ""), + 224: Key("brightnessdown", 224, "", ""), + 225: Key("brightnessup", 225, "", ""), + 226: Key("media", 226, "", ""), + 227: Key("switchvideomode", 227, "", ""), + 228: Key("kbdillumtoggle", 228, "", ""), + 229: Key("kbdillumdown", 229, "", ""), + 230: Key("kbdillumup", 230, "", ""), + 231: Key("send", 231, "", ""), + 232: Key("reply", 232, "", ""), + 233: Key("forwardmail", 233, "", ""), + 234: Key("save", 234, "", ""), + 235: Key("documents", 235, "", ""), + 236: Key("battery", 236, "", ""), + 237: Key("bluetooth", 237, "", ""), + 238: Key("wlan", 238, "", ""), + 239: Key("uwb", 239, "", ""), + 240: Key("unknown", 240, "", ""), + 241: Key("next", 241, "", ""), + 242: Key("prev", 242, "", ""), + 243: Key("cycle", 243, "", ""), + 244: Key("auto", 244, "", ""), + 245: Key("off", 245, "", ""), + 246: Key("wwan", 246, "", ""), + 247: Key("rfkill", 247, "", ""), + 248: Key("micmute", 248, "", ""), + 330: Key("btn dead", 330, "", ""), + 325: Key("btn dead", 325, "", ""), + 256: Key("btn 0", 256, "", ""), + 257: Key("btn 1", 257, "", ""), + 258: Key("btn 2", 258, "", ""), + 259: Key("btn 3", 259, "", ""), + 260: Key("btn 4", 260, "", ""), + 261: Key("btn 5", 261, "", ""), + 262: Key("btn 6", 262, "", ""), + 263: Key("btn 7", 263, "", ""), + 264: Key("btn 8", 264, "", ""), + 265: Key("btn 9", 265, "", ""), + 272: Key("btn left", 272, "", ""), + 273: Key("btn right", 273, "", ""), + 274: Key("btn middle", 274, "", ""), + 275: Key("btn side", 275, "", ""), + 276: Key("btn extra", 276, "", ""), + 277: Key("btn forward", 277, "", ""), + 278: Key("btn back", 278, "", ""), + 279: Key("btn task", 279, "", ""), + 352: Key("ok", 352, "", ""), + 353: Key("select", 353, "", ""), + 354: Key("goto", 354, "", ""), + 355: Key("clear", 355, "", ""), + 356: Key("power2", 356, "", ""), + 357: Key("option", 357, "", ""), + 358: Key("info", 358, "", ""), + 359: Key("time", 359, "", ""), + 360: Key("vendor", 360, "", ""), + 361: Key("archive", 361, "", ""), + 362: Key("program", 362, "", ""), + 363: Key("channel", 363, "", ""), + 364: Key("favorites", 364, "", ""), + 365: Key("epg", 365, "", ""), + 366: Key("pvr", 366, "", ""), + 367: Key("mhp", 367, "", ""), + 368: Key("language", 368, "", ""), + 369: Key("title", 369, "", ""), + 370: Key("subtitle", 370, "", ""), + 371: Key("angle", 371, "", ""), + 372: Key("zoom", 372, "", ""), + 373: Key("mode", 373, "", ""), + 374: Key("keyboard", 374, "", ""), + 375: Key("screen", 375, "", ""), + 376: Key("pc", 376, "", ""), + 377: Key("tv", 377, "", ""), + 378: Key("tv2", 378, "", ""), + 379: Key("vcr", 379, "", ""), + 380: Key("vcr2", 380, "", ""), + 381: Key("sat", 381, "", ""), + 382: Key("sat2", 382, "", ""), + 383: Key("cd", 383, "", ""), + 384: Key("tape", 384, "", ""), + 385: Key("radio", 385, "", ""), + 386: Key("tuner", 386, "", ""), + 387: Key("player", 387, "", ""), + 388: Key("text", 388, "", ""), + 389: Key("dvd", 389, "", ""), + 390: Key("aux", 390, "", ""), + 391: Key("mp3", 391, "", ""), + 392: Key("audio", 392, "", ""), + 393: Key("video", 393, "", ""), + 394: Key("directory", 394, "", ""), + 395: Key("list", 395, "", ""), + 396: Key("memo", 396, "", ""), + 397: Key("calendar", 397, "", ""), + 398: Key("red", 398, "", ""), + 399: Key("green", 399, "", ""), + 400: Key("yellow", 400, "", ""), + 401: Key("blue", 401, "", ""), + 402: Key("channelup", 402, "", ""), + 403: Key("channeldown", 403, "", ""), + 404: Key("first", 404, "", ""), + 405: Key("last", 405, "", ""), + 406: Key("ab", 406, "", ""), + 407: Key("next", 407, "", ""), + 408: Key("restart", 408, "", ""), + 409: Key("slow", 409, "", ""), + 410: Key("shuffle", 410, "", ""), + 411: Key("break", 411, "", ""), + 412: Key("previous", 412, "", ""), + 413: Key("digits", 413, "", ""), + 414: Key("teen", 414, "", ""), + 415: Key("twen", 415, "", ""), + 416: Key("videophone", 416, "", ""), + 417: Key("games", 417, "", ""), + 418: Key("zoomin", 418, "", ""), + 419: Key("zoomout", 419, "", ""), + 420: Key("zoomreset", 420, "", ""), + 421: Key("wordprocessor", 421, "", ""), + 422: Key("editor", 422, "", ""), + 423: Key("spreadsheet", 423, "", ""), + 424: Key("graphicseditor", 424, "", ""), + 425: Key("presentation", 425, "", ""), + 426: Key("database", 426, "", ""), + 427: Key("news", 427, "", ""), + 428: Key("voicemail", 428, "", ""), + 429: Key("addressbook", 429, "", ""), + 430: Key("messenger", 430, "", ""), + 431: Key("displaytoggle", 431, "", ""), + 432: Key("spellcheck", 432, "", ""), + 433: Key("logoff", 433, "", ""), + 434: Key("dollar", 434, "", ""), + 435: Key("euro", 435, "", ""), + 436: Key("frameback", 436, "", ""), + 437: Key("frameforward", 437, "", ""), + 438: Key("context_menu", 438, "", ""), + 439: Key("repeat", 439, "", ""), + 440: Key("10channelsup", 440, "", ""), + 441: Key("10channelsdown", 441, "", ""), + 442: Key("images", 442, "", ""), + 448: Key("eol", 448, "", ""), + 449: Key("eos", 449, "", ""), + 450: Key("ins_line", 450, "", ""), + 451: Key("del_line", 451, "", ""), + 464: Key("fn", 464, "", ""), + 465: Key("fnesc", 465, "", ""), + 466: Key("f1", 466, "", ""), + 467: Key("f2", 467, "", ""), + 468: Key("f3", 468, "", ""), + 469: Key("f4", 469, "", ""), + 470: Key("f5", 470, "", ""), + 471: Key("f6", 471, "", ""), + 472: Key("f7", 472, "", ""), + 473: Key("f8", 473, "", ""), + 474: Key("f9", 474, "", ""), + 475: Key("f10", 475, "", ""), + 476: Key("f11", 476, "", ""), + 477: Key("f12", 477, "", ""), + 478: Key("fn1", 478, "", ""), + 479: Key("fn2", 479, "", ""), + 480: Key("fnd", 480, "", ""), + 481: Key("fne", 481, "", ""), + 482: Key("fnf", 482, "", ""), + 483: Key("fns", 483, "", ""), + 484: Key("fnb", 484, "", ""), + 497: Key("dot1", 497, "", ""), + 498: Key("dot2", 498, "", ""), + 499: Key("dot3", 499, "", ""), + 500: Key("dot4", 500, "", ""), + 501: Key("dot5", 501, "", ""), + 502: Key("dot6", 502, "", ""), + 503: Key("dot7", 503, "", ""), + 504: Key("dot8", 504, "", ""), + 505: Key("dot9", 505, "", ""), + 506: Key("dot10", 506, "", ""), + 512: Key("np0", 512, "", ""), + 513: Key("np1", 513, "", ""), + 514: Key("np2", 514, "", ""), + 515: Key("np3", 515, "", ""), + 516: Key("np4", 516, "", ""), + 517: Key("np5", 517, "", ""), + 518: Key("np6", 518, "", ""), + 519: Key("np7", 519, "", ""), + 520: Key("np8", 520, "", ""), + 521: Key("np9", 521, "", ""), + 522: Key("npstar", 522, "", ""), + 523: Key("nppound", 523, "", ""), + 524: Key("npa", 524, "", ""), + 525: Key("npb", 525, "", ""), + 526: Key("npc", 526, "", ""), + 527: Key("npd", 527, "", ""), + 528: Key("focus", 528, "", ""), + 529: Key("button", 529, "", ""), + 530: Key("toggle", 530, "", ""), + 531: Key("on", 531, "", ""), + 532: Key("off", 532, "", ""), + 533: Key("zoomin", 533, "", ""), + 534: Key("zoomout", 534, "", ""), + 535: Key("up", 535, "", ""), + 536: Key("down", 536, "", ""), + 537: Key("left", 537, "", ""), + 538: Key("right", 538, "", ""), + 539: Key("on", 539, "", ""), + 540: Key("off", 540, "", ""), + 541: Key("attendant_toggle", 541, "", ""), + 542: Key("lights_toggle", 542, "", ""), + 560: Key("als_toggle", 560, "", ""), + 576: Key("buttonconfig", 576, "", ""), + 577: Key("taskmanager", 577, "", ""), + 578: Key("journal", 578, "", ""), + 579: Key("controlpanel", 579, "", ""), + 580: Key("appselect", 580, "", ""), + 581: Key("screensaver", 581, "", ""), + 582: Key("voicecommand", 582, "", ""), + 592: Key("min", 592, "", ""), + 593: Key("max", 593, "", ""), + 608: Key("prev", 608, "", ""), + 609: Key("next", 609, "", ""), + 610: Key("prevgroup", 610, "", ""), + 611: Key("nextgroup", 611, "", ""), + 612: Key("accept", 612, "", ""), + 613: Key("cancel", 613, "", ""), + 614: Key("up", 614, "", ""), + 615: Key("down", 615, "", ""), + 616: Key("up", 616, "", ""), + 617: Key("down", 617, "", ""), + 618: Key("root_menu", 618, "", ""), + 619: Key("media_top_menu", 619, "", ""), + 620: Key("11", 620, "", ""), + 621: Key("12", 621, "", ""), + 622: Key("desc", 622, "", ""), + 623: Key("mode", 623, "", ""), + 624: Key("favorite", 624, "", ""), + 625: Key("stop_record", 625, "", ""), + 626: Key("pause_record", 626, "", ""), + 627: Key("vod", 627, "", ""), + 628: Key("unmute", 628, "", ""), + 629: Key("fastreverse", 629, "", ""), + 630: Key("slowreverse", 630, "", ""), + 631: Key("data", 631, "", ""), + 635: Key("noop", 635, "", ""), +} + +names = {key.name: key for key in codes.values()} +shifted_names = {key.shifted_name: key for key in codes.values()} +alt_names = {key.alt_name: key for key in codes.values()} diff --git a/t/layer.t b/t/layer.t new file mode 100644 index 0000000..72c41e4 --- /dev/null +++ b/t/layer.t @@ -0,0 +1,17 @@ +3 down +2 down +b down +b up +2 up +3 up + +control down +shift down +control up +shift up +a down +a up +control down +shift down +shift up +control up diff --git a/t/layer1.t b/t/layer1.t new file mode 100644 index 0000000..75ee379 --- /dev/null +++ b/t/layer1.t @@ -0,0 +1,9 @@ +capslock down +capslock up +a down +a up + +control down +control up +a down +a up diff --git a/t/layer2.t b/t/layer2.t new file mode 100644 index 0000000..a16b32d --- /dev/null +++ b/t/layer2.t @@ -0,0 +1,11 @@ +capslock down +j down +j up +capslock up + +control down +control up +k down +k up +control down +control up diff --git a/t/layer3.t b/t/layer3.t new file mode 100644 index 0000000..222e4b5 --- /dev/null +++ b/t/layer3.t @@ -0,0 +1,9 @@ +capslock down +i down +i up +capslock up + +control down +i down +i up +control up diff --git a/t/layout-mods.t b/t/layout-mods.t new file mode 100644 index 0000000..4a72935 --- /dev/null +++ b/t/layout-mods.t @@ -0,0 +1,13 @@ +5 down +a down +b down +b up +a up +5 up + +[ down +shift down +[ up +[ down +[ up +shift up diff --git a/t/layout-mods2.t b/t/layout-mods2.t new file mode 100644 index 0000000..859534e --- /dev/null +++ b/t/layout-mods2.t @@ -0,0 +1,13 @@ +5 down +b down +a down +b up +a up +5 up + +shift down +[ down +shift up +[ up +[ down +[ up diff --git a/t/layout-seq.t b/t/layout-seq.t new file mode 100644 index 0000000..d332d5d --- /dev/null +++ b/t/layout-seq.t @@ -0,0 +1,11 @@ +9 down +9 up + +control down +shift down +meta down +x down +x up +control up +shift up +meta up diff --git a/t/layout.t b/t/layout.t new file mode 100644 index 0000000..6400905 --- /dev/null +++ b/t/layout.t @@ -0,0 +1,17 @@ +8 down +8 up +a down +a up +8 down +8 up +9 down +9 up +a down +a up + +b down +b up +8 down +8 up +a down +a up diff --git a/t/macro-nested.t b/t/macro-nested.t new file mode 100644 index 0000000..e3be8f6 --- /dev/null +++ b/t/macro-nested.t @@ -0,0 +1,15 @@ +6 down +m down +m up +6 up + +control down +control up +m down +m up +a down +a up +c down +c up +control down +control up diff --git a/t/macro.t b/t/macro.t new file mode 100644 index 0000000..ce96466 --- /dev/null +++ b/t/macro.t @@ -0,0 +1,13 @@ +m down +m up + +control down +h down +h up +control up +o down +o up +n down +n up +e down +e up diff --git a/t/mod.t b/t/mod.t new file mode 100644 index 0000000..71a806b --- /dev/null +++ b/t/mod.t @@ -0,0 +1,13 @@ +capslock down +a down +b down +capslock up +a up +b up + +control down +a down +b down +control up +a up +b up diff --git a/t/mod2.t b/t/mod2.t new file mode 100644 index 0000000..3608708 --- /dev/null +++ b/t/mod2.t @@ -0,0 +1,13 @@ +capslock down +a down +capslock up +b down +a up +b up + +control down +a down +control up +b down +a up +b up diff --git a/t/mod3.t b/t/mod3.t new file mode 100644 index 0000000..4173326 --- /dev/null +++ b/t/mod3.t @@ -0,0 +1,13 @@ +capslock down +a down +b down +a up +b up +capslock up + +control down +a down +b down +a up +b up +control up diff --git a/t/oneshot.t b/t/oneshot.t new file mode 100644 index 0000000..e997aaa --- /dev/null +++ b/t/oneshot.t @@ -0,0 +1,9 @@ +1 down +1 up +a down +a up + +control down +a down +a up +control up diff --git a/t/oneshot10.t b/t/oneshot10.t new file mode 100644 index 0000000..812504c --- /dev/null +++ b/t/oneshot10.t @@ -0,0 +1,11 @@ +l down +o down +o up +l up +a down +a up + +control down +control up +b down +b up diff --git a/t/oneshot11.t b/t/oneshot11.t new file mode 100644 index 0000000..7292406 --- /dev/null +++ b/t/oneshot11.t @@ -0,0 +1,15 @@ +l down +o down +o up +l up +j down +j up +a down +a up + +control down +j down +j up +control up +a down +a up diff --git a/t/oneshot2.t b/t/oneshot2.t new file mode 100644 index 0000000..038a58d --- /dev/null +++ b/t/oneshot2.t @@ -0,0 +1,9 @@ +1 down +a down +a up +1 up + +control down +a down +a up +control up diff --git a/t/oneshot3.t b/t/oneshot3.t new file mode 100644 index 0000000..fafa828 --- /dev/null +++ b/t/oneshot3.t @@ -0,0 +1,17 @@ +1 down +2 down +b down +b up +2 up +1 up + +control down +shift down +control up +shift up +a down +a up +control down +shift down +shift up +control up diff --git a/t/oneshot4.t b/t/oneshot4.t new file mode 100644 index 0000000..f4bf403 --- /dev/null +++ b/t/oneshot4.t @@ -0,0 +1,13 @@ +1 down +2 down +a down +a up +2 up +1 up + +control down +shift down +a down +a up +shift up +control up diff --git a/t/oneshot5.t b/t/oneshot5.t new file mode 100644 index 0000000..0f82b6d --- /dev/null +++ b/t/oneshot5.t @@ -0,0 +1,13 @@ +2 down +1 down +2 up +1 up +a down +a up + +shift down +control down +a down +a up +control up +shift up diff --git a/t/oneshot6.t b/t/oneshot6.t new file mode 100644 index 0000000..fc2eb39 --- /dev/null +++ b/t/oneshot6.t @@ -0,0 +1,13 @@ +2 down +2 up +1 down +1 up +a down +a up + +shift down +control down +a down +a up +control up +shift up diff --git a/t/oneshot9.t b/t/oneshot9.t new file mode 100644 index 0000000..e997aaa --- /dev/null +++ b/t/oneshot9.t @@ -0,0 +1,9 @@ +1 down +1 up +a down +a up + +control down +a down +a up +control up diff --git a/t/oneshotn.t b/t/oneshotn.t new file mode 100644 index 0000000..e997aaa --- /dev/null +++ b/t/oneshotn.t @@ -0,0 +1,9 @@ +1 down +1 up +a down +a up + +control down +a down +a up +control up diff --git a/t/oneshotn3.t b/t/oneshotn3.t new file mode 100644 index 0000000..2b0d141 --- /dev/null +++ b/t/oneshotn3.t @@ -0,0 +1,13 @@ +1 down +2 down +2 up +1 up +a down +a up + +control down +shift down +a down +a up +control up +shift up diff --git a/t/overload-timeout.t b/t/overload-timeout.t new file mode 100644 index 0000000..c4c1ba9 --- /dev/null +++ b/t/overload-timeout.t @@ -0,0 +1,9 @@ +t down +a down +a up +t up + +esc down +esc up +a down +a up diff --git a/t/overload-timeout2.t b/t/overload-timeout2.t new file mode 100644 index 0000000..39bb50f --- /dev/null +++ b/t/overload-timeout2.t @@ -0,0 +1,12 @@ +t down +2ms +a down +a up +t up + +control down +control up +b down +b up +control down +control up diff --git a/t/overload.t b/t/overload.t new file mode 100644 index 0000000..695e729 --- /dev/null +++ b/t/overload.t @@ -0,0 +1,7 @@ +6 down +6 up + +control down +control up +esc down +esc up diff --git a/t/overload1.t b/t/overload1.t new file mode 100644 index 0000000..8078b24 --- /dev/null +++ b/t/overload1.t @@ -0,0 +1,9 @@ +6 down +a down +a up +6 up + +control down +a down +a up +control up diff --git a/t/overload2.t b/t/overload2.t new file mode 100644 index 0000000..817edf6 --- /dev/null +++ b/t/overload2.t @@ -0,0 +1,9 @@ +6 down +a down +6 up +a up + +control down +a down +control up +a up diff --git a/t/overload3.t b/t/overload3.t new file mode 100644 index 0000000..1c971bb --- /dev/null +++ b/t/overload3.t @@ -0,0 +1,11 @@ +j down +6 down +j up +6 up + +j down +control down +j up +control up +esc down +esc up diff --git a/t/reset.t b/t/reset.t new file mode 100644 index 0000000..e14b339 --- /dev/null +++ b/t/reset.t @@ -0,0 +1,15 @@ +q down +q up +2 down +2 up +r down +r up + +control down +meta down +alt down +shift down +control up +shift up +meta up +alt up diff --git a/t/run.sh b/t/run.sh new file mode 100755 index 0000000..01ab2d0 --- /dev/null +++ b/t/run.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# TODO: make this more robust + +sudo cp test.conf /etc/keyd +sudo pkill keyd +sleep 1s + +cd $(dirname $0) +sudo ../bin/keyd -d +if [ $? -ne 0 ]; then + echo "Failed to start keyd" + sudo systemctl restart keyd + exit -1 +fi +sleep 1s + +sudo ./runner.py -v *.t +sudo ./runner.py -ev $(seq 100|sed -e 's@.*@overload-timeout.t@') +sudo pkill keyd +sleep 1s +sudo systemctl restart keyd diff --git a/t/runner.py b/t/runner.py new file mode 100755 index 0000000..f752c8b --- /dev/null +++ b/t/runner.py @@ -0,0 +1,374 @@ +#!/usr/bin/python3 + +# Copyright © 2019 Raheman Vaiya. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import selectors +import fcntl +import glob +import time +import struct +import os +from ctypes import * +import keys +import sys +import signal +import random +import re + + +class VirtualKeyboard(): + def __init__(self, name, product_id=0x9234, vendor_id=0x567a): + EV_SYN = 0x00 + EV_KEY = 0x01 + UI_SET_EVBIT = 0x40045564 + UI_SET_KEYBIT = 0x40045565 + UI_DEV_SETUP = 0x405c5503 + UI_DEV_CREATE = 0x5501 + + BUS_USB = 0x03 + version = 0 + + self.uinp = os.open("/dev/uinput", os.O_WRONLY | os.O_NONBLOCK) + fcntl.ioctl(self.uinp, UI_SET_EVBIT, EV_KEY) + fcntl.ioctl(self.uinp, UI_SET_EVBIT, EV_SYN) + + for _, key in keys.names.items(): + if not keys.is_mouse_button(key): + fcntl.ioctl(self.uinp, UI_SET_KEYBIT, key.code) + + setup_struct = struct.pack('HHHH80bI', + BUS_USB, + vendor_id, + product_id, + version, + *([ord(c) for c in name] + + ([0] * (80 - len(name)))), + 0) + + fcntl.ioctl(self.uinp, UI_DEV_SETUP, setup_struct) + fcntl.ioctl(self.uinp, UI_DEV_CREATE) + + # Kludge to give the new device some time to propagate up the + # input stack + time.sleep(.3) + + def write_code(self, code, pressed): + EV_KEY = 0x01 + EV_SYN = 0x00 + + b = struct.pack("llHHi", 0, 0, EV_KEY, code, pressed) + os.write(self.uinp, b) + b = struct.pack("llHHi", 0, 0, EV_SYN, 0, 0) + + os.write(self.uinp, b) + + def send_key(self, name, pressed): + code = 0 + + if name in keys.names: + code = keys.names[name].code + elif name in keys.alt_names: + code = keys.alt_names[name].code + else: + raise Exception(f'Could not find corresponding key for \"{name}\"') + + self.write_code(code, pressed) + + def send_string(self, s): + for c in s: + shifted = False + code = 0 + + if c in keys.names: + code = keys.names[c].code + elif c in keys.alt_names: + code = keys.alt_names[c].code + else: + code = keys.shifted_names[c].code + shifted = True + + if shifted: + self.write_code(keys.names["shift"].code, 1) + + self.write_code(code, 1) + self.write_code(code, 0) + + if shifted: + self.write_code(keys.names["shift"].code, 0) + + +class KeyStream(): + def grab(self): + EVIOCGRAB = 0x40044590 + fcntl.ioctl(self.fh, EVIOCGRAB, 1) + + def ungrab(self): + EVIOCGRAB = 0x40044590 + fcntl.ioctl(self.fh, EVIOCGRAB, 0) + + def get_name(self, fh): + EVIOCGNAME = 0x81004506 + buf = bytes(256) + + name = fcntl.ioctl(fh, EVIOCGNAME, buf) + return c_char_p(name).value.decode('utf8') + + def get_ids(self, fh): + EVIOCGID = 0x80084502 + buf = bytes(8) + + resp = fcntl.ioctl(fh, EVIOCGID, buf) + + (_, vendor, product, _) = struct.unpack("HHHH", resp) + return (product, vendor) + + def __init__(self, product, vendor): + self.fh = None + for f in glob.glob("/dev/input/event*"): + fh = open(f, 'rb') + fh.devname = self.get_name(fh) + p, v = self.get_ids(fh) + + if p == product and v == vendor: + self.fh = fh + + if not self.fh: + raise Exception( + 'Could not find keyboard with id %04x:%04x' % (vendor, product)) + + # Collect all events currently sitting on the input stream. + def collect(self): + EV_KEY = 0x01 + + events = [] + + flags = fcntl.fcntl(self.fh, fcntl.F_GETFL) + fcntl.fcntl(self.fh, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + while True: + ev = self.fh.read(24) + if not ev: + fcntl.fcntl(self.fh, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + return events + + _, _, type, code, value = struct.unpack("llhhi", ev) + + if type == EV_KEY: + key = keys.codes[code] + + events.append((key, value)) + + # Block until the next event + def next(self): + EV_KEY = 0x01 + + while True: + ev = self.fh.read(24) + _, _, type, code, value = struct.unpack("llhhi", ev) + + if type == EV_KEY: + key = keys.codes[code] + + return key, value + + +output = '' + + +def on_timeout(a, b): + print('ERROR: test timed out') + exit(-1) + + +# If we don't terminate within 5 seconds something has gone +# horribly wrong... +signal.signal(signal.SIGALRM, on_timeout) +signal.alarm(20) + +vkbd = VirtualKeyboard('test keyboard', vendor_id=0x2fac, product_id=0x2ade) +stream = KeyStream(product=0x0fac, vendor=0x0ade) + +# Grab the virtual keyboard so we don't +# cause pandemonium. + +stream.grab() + +exit_on_fail = False +verbose_flag = False + + +def diff(output, expected): + n = max(len(expected), len(output)) + + s = '' + for i in range(n): + e = expected[i] if i < len(expected) else "" + o = output[i] if i < len(output) else "" + + if e != o: + s += '\x1b[33m%-20s\x1b[0m \x1b[31m%s\x1b[0m\n' % (e, o) + else: + s += '%-20s %s\n' % (e, o) + + return s + + +class TestElement: + def __init__(self, type, code, val): + self.type = type + self.code = code + self.value = val + + +# Busy wait to minimize imprecision +# (sleep() is inaccurate). +def sleep(ms): + us = ms * 1000 + start = time.time() + + while True: + if ((time.time() - start) * 1E6) >= us: + return + + +def run_test2(name, input, output): + def printerr(s): + print(f'{name}: \x1b[31mERROR\x1b[0m: {s}') + + if verbose_flag: + sys.stdout.write('Input:\n%s\n\n%-20s %s\n%s' % + (input, "Expected Output:", "Output:", diff(result, expected))) + # print('Input:\n%s\n\nExpected Output:\n%s\n\nGot:\n%s\n' % + # (input, '\n'.join(output), '\n'.join(actual_output))) + + elements = [] + + for line in input.strip().split('\n'): + line = line.strip() + try: + timeout = int(re.match('^([0-9]+)ms$', line).group(1)) + elements.append(TestElement('timeout', 0, timeout)) + continue + except: + pass + + key, state = line.split(' ') + depress = 0 + + if state == "down": + depress = 1 + + code = 0 + if key in keys.names: + code = keys.names[key].code + else: + code = keys.alt_names[key].code + + elements.append(TestElement('code', code, depress)) + + # Actually run the test, keep this separate from parsing to minimize + # latency. The system may still preempt the thread causing spurious time + # dependent test failures. There isn't much that can be done to mitigate + # this :/. + + for e in elements: + if e.type == 'timeout': + sleep(e.value) + continue + else: + vkbd.write_code(e.code, e.value) + + expected = output.strip().split('\n') + result = [] + + time.sleep(0.00003) + results = stream.collect() + + # Try again, timeout may have been insufficient. + if len(results) != len(expected): + print('WARNING: Insufficient output, timing out one more time...') + time.sleep(.05) + results += stream.collect() + + for k, v in results: + result.append(f'{k} {"up" if v == 0 else "down"}') + + if len(result) > len(expected): + printerr('Extraneous keys.') + return False + + if len(result) < len(expected): + printerr('Missing keys.') + return False + + for i in range(len(expected)): + if result[i] != expected[i]: + printerr( + f'mismatch: expected \033[33m{expected[i]}\033[0m got \033[31m{result[i]}\033[0m.') + return False + + print(f'{name}: \x1b[33mPASSED\x1b[0m') + return True + + +import argparse +parser = argparse.ArgumentParser() +parser.add_argument('-v', '--verbose', default=False, action='store_true') +parser.add_argument('-e', '--exit-on-fail', default=False, action='store_true') +parser.add_argument('files', nargs=argparse.REMAINDER) +args = parser.parse_args() + + +# Prevent gc from interfering with +# timeout precision. +import gc +import os + +gc.collect() +gc.disable() +os.nice(-20) + +verbose_flag = args.verbose + +tests = [] +failed = False + +for file in args.files: + name = file + input, output = open(file, 'r').read().split('\n\n') + + tests.append((name, input, output)) + + if not run_test2(name, input, output): + if args.exit_on_fail: + exit(-1) + + failed = True + +if failed: + exit(-1) + +#tests = tests * 1000 +# random.shuffle(tests) +# for name, input, output in tests: +# if not run_test2(name, input, output): +# exit(-1) diff --git a/t/swap.t b/t/swap.t new file mode 100644 index 0000000..ff9dcd5 --- /dev/null +++ b/t/swap.t @@ -0,0 +1,23 @@ +alt down +` down +` up +tab down +tab up +tab down +tab up +a down +a up +alt up + +alt down +alt up +shift down +x down +x up +shift up +shift down +x down +x up +shift up +b down +b up diff --git a/t/swap2.t b/t/swap2.t new file mode 100644 index 0000000..10d5754 --- /dev/null +++ b/t/swap2.t @@ -0,0 +1,25 @@ +alt down +` down +` up +` down +` up +tab down +tab up +tab down +tab up +a down +a up +alt up + +alt down +alt up +shift down +x down +x up +shift up +shift down +x down +x up +shift up +b down +b up diff --git a/t/swap3.t b/t/swap3.t new file mode 100644 index 0000000..9bf2ff4 --- /dev/null +++ b/t/swap3.t @@ -0,0 +1,35 @@ +1 down +alt down +` down +` up +tab down +tab up +tab down +tab up +a down +a up +x down +x up +alt up +1 up + +control down +alt down +alt up +control up +shift down +x down +x up +control down +shift up +control up +shift down +x down +x up +control down +shift up +control up +b down +b up +control down +control up diff --git a/t/swap4.t b/t/swap4.t new file mode 100644 index 0000000..e271b66 --- /dev/null +++ b/t/swap4.t @@ -0,0 +1,27 @@ +alt down +1 down +1 up +tab down +tab up +tab down +tab up +a down +a up +x down +x up +alt up + +alt down +alt up +tab down +tab up +shift down +x down +x up +shift up +shift down +x down +x up +shift up +b down +b up diff --git a/t/swap5.t b/t/swap5.t new file mode 100644 index 0000000..f510441 --- /dev/null +++ b/t/swap5.t @@ -0,0 +1,21 @@ +meta down +alt down +2 down +2 up +a down +a up +alt up +meta up + +meta down +alt down +meta up +alt up +tab down +tab up +meta down +control down +a down +a up +control up +meta up diff --git a/t/test.conf b/t/test.conf new file mode 100644 index 0000000..49dc7cf --- /dev/null +++ b/t/test.conf @@ -0,0 +1,78 @@ +#TODO: Simplify and use mnemonic bindings. + +[ids] + +2fac:2ade + +[main] + +alt = layer(myalt) +capslock = layer(capslock) +1 = oneshot(C) +2 = oneshot(customshift) +3 = layer(C) +4 = toggle(test) +5 = layer(symbols) +6 = overload(6l, esc) +8 = layout(dvorak) +9 = M-C-S-x +q = toggle(M-C-A) +r = reset() +l = layer(test) +t = overload(o, esc, 2) +m = macro(C-h one) + +[6l:C] + +m = macro(mac) + +[test] + +o = oneshot(o) +a = b +b = toggle(test) +c = reset() + +[o:C] + +a = b +x = macro(mac) + +[dvorak:layout] + +a = b +9 = layout(main) + +[myalt:A] + +` = swap(tablayer) +1 = swap(tablayer, tab) +2 = swap(tablayer2, tab) + +[symbols] + +a = [ +b = S-[ + +[tablayer2:C] + +[tablayer] + +tab = S-x +a = b + +[customshift:S] + +t = toggle(customshift) +b = a + +[capslock:C] +alt = layer(target) +j = k + +[alt:A] +capslock = layer(target) + +[target] +#w = A-w +b = A-j diff --git a/t/toggle.t b/t/toggle.t new file mode 100644 index 0000000..baafcbf --- /dev/null +++ b/t/toggle.t @@ -0,0 +1,13 @@ +4 down +4 up +a down +a up +a down +a up +b down +b up + +b down +b up +b down +b up diff --git a/t/toggle2.t b/t/toggle2.t new file mode 100644 index 0000000..063fb5a --- /dev/null +++ b/t/toggle2.t @@ -0,0 +1,25 @@ +2 down +2 up +t down +t up +b down +b up +b down +b up +t down +t up +b down +b up + +shift down +shift up +a down +a up +shift down +shift up +a down +a up +shift down +shift up +b down +b up