├── .busted ├── .gitignore ├── .luacheckrc ├── .luacov ├── .travis.yml ├── .vscode ├── launch.json ├── multibow.code-workspace └── tasks.json ├── LICENSE ├── README.md ├── README.md.archived ├── check.sh ├── env.sh ├── multibow.code-workspace ├── multibow.jpg ├── sdcard ├── keys.lua ├── layouts │ ├── empty.lua │ ├── kdenlive.lua │ ├── keymap-template.lua │ ├── media-player.lua │ ├── shift.lua │ └── vsc-golang.lua └── snippets │ ├── mb │ ├── keymaps.lua │ ├── keys.lua │ ├── leds.lua │ ├── morekeys.lua │ └── routehandlers.lua │ └── multibow.lua ├── setup-tests.sh └── spec ├── hwkeys.lua ├── hwkeys_spec.lua ├── integration_spec.lua ├── layouts ├── empty_spec.lua ├── kdenlive_spec.lua ├── keymap-template_spec.lua ├── shift_spec.lua └── vsc-golang_spec.lua ├── mock ├── keybow.lua └── mocked-keybow.lua ├── mocked-keybow_spec.lua └── snippets ├── keys_spec.lua ├── leds_spec.lua ├── multibow_spec.lua └── routehandlers_spec.lua /.busted: -------------------------------------------------------------------------------- 1 | -- Configuration for "busted" TDD tool to unit test Multibow 2 | 3 | --[[ 4 | Copyright 2019 Harald Albrecht 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]]-- 24 | 25 | return { 26 | default = { 27 | lpath = "./sdcard/?.lua;./spec/mock/?.lua", 28 | -- Provides an "insl" convenience replacement for busted's insulate() using 29 | -- a fixed descriptive text ... or rather, icon. Please not that "insl" 30 | -- not only rhymes with "insulation", but even more so with the German 31 | -- "insel", meaning "island". And that's exactly what it does: splendid 32 | -- isolation... 33 | e = "INSL = '[⛔]';" 34 | .. "_BUSTED = require('busted');" 35 | .. "function insl(f) _BUSTED.insulate(_INSL, f) end;" 36 | .. "function inslit(d, f) _BUSTED.insulate(_INSL, function() _BUSTED.it(d, f) end) end;" 37 | , 38 | verbose = true, 39 | recursive = true, 40 | coverage=true, 41 | } 42 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | luacov.report.out 3 | luacov.stats.out 4 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- Configuration for "luacheck"ing Multibow 2 | 3 | --[[ 4 | Copyright 2019 Harald Albrecht 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]]-- 24 | 25 | stds.multibow = { 26 | read_globals = { 27 | -- 3rd party stuff that doesn't exactly play by the new Lua module rules... 28 | "keybow", 29 | 30 | -- our unit testing setup 31 | "insl", "inslit" 32 | } 33 | } 34 | 35 | std = "max+multibow" 36 | 37 | exclude_files = { "spec/mock/keybow.lua" } 38 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | -- Configuration for "luacov"ering Multibow 2 | 3 | --[[ 4 | Copyright 2019 Harald Albrecht 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]]-- 24 | 25 | return { 26 | runreport=true, 27 | deletestats=true, 28 | exclude={ 29 | "env", 30 | "spec" 31 | } 32 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # As there is (still) no Travis Lua language support, we use the fine 2 | # https://github.com/mpeterv/hererocks local environment build to set 3 | # up a local Lua/luarocks/packages environment. 4 | dist: xenial 5 | language: python 6 | python: 3.7 7 | sudo: false 8 | 9 | branches: 10 | only: 11 | - /.*/ 12 | 13 | install: 14 | - ./env.sh # installs the local environment. 15 | 16 | script: 17 | - ./env.sh busted # runs all tests. 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "attach", 9 | "type": "lua", 10 | "request": "attach", 11 | "program": "", 12 | "stopOnEntry": false, 13 | "cwd": "${workspaceFolder}", 14 | "debugServer" : 4278 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/multibow.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | } 6 | ], 7 | "settings": { 8 | "files.associations": { 9 | ".busted": "lua", 10 | ".luacheckrc": "lua", 11 | ".luacov": "lua" 12 | }, 13 | "lua.format.lineWidth": 80, 14 | "lua.luacheckPath": "luacheck", 15 | "lua.preferLuaCheckErrors": true, 16 | "lua.targetVersion": "5.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "type": "shell", 13 | "command": "${workspaceFolder}/env.sh", 14 | "args": ["luacheck", "-q", "./sdcard", "./spec"], 15 | "presentation": { 16 | "echo": true, 17 | "reveal": "always", 18 | "focus": false, 19 | "panel": "shared", 20 | "showReuseMessage": true, 21 | "clear": true 22 | } 23 | }, 24 | { 25 | "label": "test (optionally #f focused)", 26 | "group": { 27 | "kind": "test", 28 | "isDefault": true 29 | }, 30 | "type": "shell", 31 | "command": "if [[ $(${workspaceFolder}/env.sh busted -l -t f) ]]; then ${workspaceFolder}/env.sh busted -t f; else ${workspaceFolder}/env.sh busted; fi && sed -n \"/^Total/p\" ${workspaceFolder}/luacov.report.out", 32 | "presentation": { 33 | "echo": true, 34 | "reveal": "always", 35 | "focus": false, 36 | "panel": "shared", 37 | "showReuseMessage": true, 38 | "clear": true 39 | } 40 | }, 41 | ] 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Harald Albrecht 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (Why) This Project Is Archived 2 | 3 | I've spent a lot of effort on developing advanced functionality for 4 | Pibow/Multibow, such as asynchronous key handling, key sequence chaining, 5 | timers, multiple overlays, unit tests for all this stuff, et cetera. 6 | 7 | The advanced asynchronous functionality requires changes in the base Pibow 8 | firmware. Pimoroni initially provided a low-level function timer tick on a 9 | separate code branch (without a release), on which I then built a lot of 10 | advanced functionality. 11 | 12 | However, many months later, and after several fruitless attempts to get some 13 | reaction from Pimoroni, there is no visible activity to release the low-level 14 | timer tick function I need for Multibow as part of the ordinary Pibow firmware 15 | releases. 16 | 17 | Since I don't have the resources to maintain my own fork of the Pibow software 18 | and are a believing in the principle of "upstream first", this is the end of 19 | my Multibow project. 20 | 21 | For reference: [Pibow Issue #15: Timer 22 | Support?](https://github.com/pimoroni/keybow-firmware/issues/15) -------------------------------------------------------------------------------- /README.md.archived: -------------------------------------------------------------------------------- 1 | # Multibow 2 | 3 | Find it on GitHub: 4 | [thediveo/multibow](https://github.com/thediveo/multibow). 5 | 6 | ![Multibow on Keybow](multibow.jpg) 7 | 8 | Multibow adds ease-of use support for **multiple layouts** to [Pimoroni 9 | Keybow](https://shop.pimoroni.com/products/keybow) macro keyboards. Simply 10 | switch between the installed layouts by pressing a special key combination 11 | (defaults to press-hold key #11, then tap key #5). And you can even control the key LEDs brightness (press-hold key #11, then tap key #8 to change brightness). 12 | 13 | > "_Keybows_" are solderless DIY 4x3 mechanical USB keyboards, powered by a 14 | > Raspberry Pi. And yes, these days even _keyboards_ now run Linux and script 15 | > interpreters... 16 | 17 | And yes, this is probably a New Year's project slightly gone overboard ... 18 | what sane reason is there to end up with a Lua-scripted multi-layout keyboard 19 | "operating" system and a bunch of automated unit test cases? 20 | 21 | ## Installation 22 | 23 | 1. Download the [Pibow 24 | firmware](https://github.com/pimoroni/keybow-firmware/releases) and copy 25 | all files inside its `sdcard/` subdirectory onto an empty, FAT32 formatted 26 | microSD card. Copy only the files **inside** `sdcard/`, but do **not** 27 | place them into a ~~`sdcard`~~ directory on your microSD card. 28 | 29 | 2. Download all files from the `sdcard/` subdirectory of this repository and 30 | then copy them onto the microSD card. This will overwrite but one file 31 | `key.lua`, all other files are new. 32 | - download recent stable 33 | [sdcard.zip](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/TheDiveO/multibow/tree/master/sdcard) 34 | – courtesy of Minhas Kamal's incredibly useful 35 | [DownGit](https://github.com/MinhasKamal/DownGit) service which lets 36 | users directly download GitHub repository directories as .zip files. 37 | _Please note that we're not responsible for the DownGit service and its 38 | integrity, so be cautious when downloading files._ 39 | 40 | ## Multiple Keyboard Layouts 41 | 42 | To enable one or more multibow keyboard layouts, edit `sdcard/keys.lua` 43 | accordingly in order to "`require`" them. The default configuration looks as 44 | follows: 45 | 46 | ```lua 47 | require "layouts/shift" -- for cycling between layouts. 48 | require "layouts/media-player" -- indispensable media player controls. 49 | require "layouts/vsc-golang" -- debugging Go programs in VisualStudio Code. 50 | require "layouts/kdenlive" -- editing video using Kdenlive. 51 | require "layouts/empty" -- empty, do-nothing layout. 52 | ``` 53 | 54 | > You can disable a specific keyboard layout by simply putting two dashes `--` 55 | > in front of the `require "..."`, making it look like `--require "..."`. 56 | 57 | ## Layouts 58 | 59 | The default setup activates the following macro keyboard layouts shown below. 60 | 61 | > You can switch (cycle) between them by pressing and holding key #11 62 | > (top-left key in landscape), then tapping key #5 (immediately right to #11), 63 | > and finally releasing both keys. 64 | 65 | ### Media Player Controls 66 | 67 | We start with the probably indispensable media player controls keyboard layout. 68 | 'nuff said. 69 | 70 | ```text 71 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 72 | ┊ 11 ┊ ┊ 8 ┊ ┊ 5 ┊ ┊ 2 ┊ 73 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 74 | 🔇 🔈/🔉 🔊 75 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 76 | ┊ 10 ┊ ┊ 7 ┊ ┊ 4 ┊ ┊ 1 ┊ 77 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 78 | ⏹️️ 79 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 80 | ┊ 9 ┊ ┊ 6 ┊ ┊ 3 ┊ ┊ 0 ┊ 81 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 82 | ◀️◀️ ▮▶ ▶▶ 83 | ``` 84 | 85 | ### Debug Go in VisualStudio Code 86 | 87 | Debug Go programs and packages in VisualStudio Code with its Go extension. 88 | 89 | ```text 90 | ┌╌╌╌╌┐ ╔════╗ ╔════╗ ╔════╗ 91 | ┊ 11 ┊ ║ 8 ║ ║ 5 ║ ║ 2 ║ 92 | └╌╌╌╌┘ ╚════╝ ╚════╝ ╚════╝ 93 | OUTPUT DEBUG CLOSE PANEL 94 | ╔════╗ ╔════╗ ╔════╗ ╔════╗ 95 | ║ 10 ║ ║ 7 ║ ║ 4 ║ ║ 1 ║ 96 | ╚════╝ ╚════╝ ╚════╝ ╚════╝ 97 | ▶ ⏹STOP ↺RELOAD TSTPKG 98 | ╔════╗ ╔════╗ ╔════╗ ╔════╗ 99 | ║ 9 ║ ║ 6 ║ ║ 3 ║ ║ 0 ║ 100 | ╚════╝ ╚════╝ ╚════╝ ╚════╝ 101 | ▮▶ ⮧INTO ⭢STEP ⮥OUT 102 | ``` 103 | 104 | - ▶ starts the program without debugging. 105 | - ▮▶ starts, continues, or pauses the program to be debugged. 106 | - ⮧INTO steps _into_ a function call. 107 | - ⭢STEP steps _over_ a line/function call. 108 | - ⏹STOP stops debugging 109 | - ↺RELOAD reloads the program being debugged. 110 | - ☑TSTPKG activates the command "go: test package". 111 | - OUTPUT opens/shows output panel. 112 | - DEBUG opens/shows debug panel. 113 | - CLOSE PANEL ... closes the output/debug panel. 114 | 115 | ### Kdenlive Video Editor 116 | 117 | _coming soon..._ 118 | 119 | ### SHIFT Overlay 120 | 121 | This layout provides a SHIFT key. Only when pressed and held, two additional 122 | keys become active for controlling the brightness of the Keybow LEDs and for 123 | switching between multiple keyboard layouts. 124 | 125 | Simply pressing and then immediately releasing the SHIFT key without pressing 126 | any of the other keys activates the SHIFT layer in other Multibow keyboard 127 | layouts that are SHIFT-aware. 128 | 129 | > **NOTE:** press and hold SHIFT, then use →LAYOUT and 🔆BRIGHT. The SHIFT key 130 | > is always active, regardless of keyboard layout. The other keys in this 131 | > layout become only active _while_ holding SHIFT. 132 | 133 | ```text 134 | ╔════╗ ╔╌╌╌╌╗ ╔╌╌╌╌╗ ┌╌╌╌╌┐ 135 | ║ 11 ║ ┊ 8 ┊ ┊ 5 ┊ ┊ 2 ┊ 136 | ╚════╝ ╚╌╌╌╌╝ ╚╌╌╌╌╝ └╌╌╌╌┘ 137 | ⇑SHIFT →LAYOUT 🔆BRIGHT 138 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 139 | ┊ 10 ┊ ┊ 7 ┊ ┊ 4 ┊ ┊ 1 ┊ 140 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 141 | 142 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 143 | ┊ 9 ┊ ┊ 6 ┊ ┊ 3 ┊ ┊ 0 ┊ 144 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 145 | ``` 146 | 147 | - press ⇑SHIFT, release ⇑SHIFT: if a keyboard layout has a SHIFT layer, then 148 | this activates and deactivates this ⇑SHIFT layer. 149 | - press ⇑SHIFT, tap →LAYOUT, release ⇑SHIFT: switches to next keyboard layout. 150 | - press ⇑SHIFT, tap 🔆BRIGHT, release 🔆BRIGHT: changes the keyboard LED 151 | brightness in three different brightness steps (70% → 100% → 40% → 70% → 152 | ...). 153 | 154 | ### Empty 155 | 156 | Just as its name says: an empty keyboard layout – useful if you want to have 157 | also a "no" layout with no functionality whatsoever to switch to. (_This 158 | layout by courtesy of Margaret Thatcher._) 159 | 160 | ```text 161 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 162 | ┊ 11 ┊ ┊ 8 ┊ ┊ 7 ┊ ┊ 6 ┊ 163 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 164 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 165 | ┊ 10 ┊ ┊ 7 ┊ ┊ 4 ┊ ┊ 1 ┊ 166 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 167 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 168 | ┊ 9 ┊ ┊ 6 ┊ ┊ 3 ┊ ┊ 0 ┊ 169 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 170 | ``` 171 | 172 | ## Your Own Multikey Keyboard Layout 173 | 174 | You may want to start from our template in `layouts/keymap-template.lua`. 175 | 176 | 1. copy and rename the new layout file name to something more meaningful. 177 | 178 | 2. edit your new layout file and change its name which is specified in the 179 | `kmt.name` element: 180 | 181 | ```lua 182 | km.keymap = { 183 | -- IMPORTANT: Make sure to change the keymap name to make it unique! 184 | name="my-cool-layout", 185 | -- ... 186 | } 187 | ``` 188 | 189 | 3. add key definitions for colors and handlers as necessary, see next for examples. 190 | 191 | - you can specify key handlers either "inline", as you can see from the 192 | example mapping for key #0: 193 | 194 | ```lua 195 | km.keymap = { 196 | -- ... 197 | [0] = { c={r=1, g=1, b=1}, press=function() mb.tap("a") end}, 198 | } 199 | ``` 200 | 201 | This sets the key #0's LED color to white, and emits an "a" press everytime 202 | you tap key #0. 203 | 204 | - for more complex handling, you may want to use a dedicated function instead: 205 | 206 | ```lua 207 | function km.mypress(keyno) 208 | mb.tap("a") 209 | end 210 | 211 | km.keymap = { 212 | -- ... 213 | [1] = { c={r=1, g=1, b=1}, press=km.mypress} 214 | } 215 | 216 | - you can also do things on key releases: 217 | 218 | ```lua 219 | km.keymap = { 220 | -- ... 221 | [2] = { c={r=1, g=1, b=1}, release=function() mb.tap("x") end}, 222 | } 223 | ``` 224 | 225 | For more details and examples, please have a look at the keyboard layouts in 226 | `layouts/vsc-golang.lua` and `layouts/kdenlive.lua`. 227 | 228 | ## Licenses 229 | 230 | Multibow is (c) 2019 Harald Albrecht and is licensed under the MIT license, see 231 | the [LICENSE](LICENSE) file. 232 | 233 | The file `keybow.lua` included from 234 | [pimoroni/keybow-firmware](https://github.com/pimoroni/keybow-firmware) for 235 | testing purposes is licensed under the MIT license, as declared by Pimoroni's 236 | keybow-firmware GitHub repository. 237 | 238 | ## Developing 239 | 240 | Whether you want to dry-run your own keyboard layout or to hack Multibow: use 241 | the unit tests which you can find in the `spec/` subdirectory. These tests 242 | help you in detecting syntax and logic errors early, avoiding the 243 | rinse-and-repeat cycle with copying to microSD card, starting the Keybow 244 | hardware, and then wondering what went wrong, without any real clue as to what 245 | is the cause of failure. 246 | 247 | Before your first testing, you'll need to run `./setup-tests.sh` once in order 248 | to install (on Ubuntu-based distributions) the required system distribution and 249 | LuaRocks packages. 250 | 251 | Afterwards, simply run `./check.sh` while in the `multibow` repository root 252 | directory to run all tests and linting. 253 | 254 | If you want to just test a certain file or directory, then run `busted 255 | spec/layout/kdenlive_spec.lua` to unit test a specific keyboard layout 256 | (or set of layouts) or `busted spec/layout` to check all layouts. 257 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "testing..." 3 | ./env.sh busted 4 | echo "linting..." 5 | ./env.sh luacheck -q ./sdcard ./spec 6 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Checks for a local lua/luarocks/... environment, installing it if it is 4 | # missing. If a command and arguments are specified, then this command is run 5 | # from the environment. 6 | # 7 | # Copyright 2019 Harald Albrecht 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to 11 | # deal in the Software without restriction, including without limitation the 12 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 13 | # sell copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in 17 | # all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 25 | # IN THE SOFTWARE. 26 | 27 | VENV=./env 28 | HEREROCKS=$VENV/hererocks.py 29 | 30 | # Checks for a specific luarocks package, installing it if necessary. 31 | rock() { 32 | luarocks list | grep -q $1 33 | if [ $? != 0 ]; then 34 | luarocks install $1 35 | fi 36 | } 37 | 38 | # Downloads the fine hererocks Lua/luarocks installation script if not already 39 | # done so, then ensures to install Lua 5.3 and latest luarocks. 40 | mkdir -p $VENV 41 | if [ ! -s $VENV/hererocks.py ]; then 42 | wget https://raw.githubusercontent.com/mpeterv/hererocks/latest/hererocks.py -O $HEREROCKS 43 | fi 44 | if [ ! -s $VENV/bin/activate ]; then 45 | python3 $HEREROCKS $VENV -l5.3 -rlatest 46 | fi 47 | 48 | # Activate the Lua/luarocks environment, then check for required luarocks 49 | # packages, and install the missing ones. 50 | source $VENV/bin/activate 51 | rock luasec 52 | rock busted 53 | rock luasocket 54 | rock luacheck 55 | rock cluacov 56 | 57 | # Finally check if a command with args should be run from inside the Lua 58 | # environment. 59 | if [ -n "$1" ]; then 60 | CMD=$VENV/bin/$1 61 | shift 62 | $CMD "$@" 63 | fi 64 | -------------------------------------------------------------------------------- /multibow.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "files.associations": { 9 | ".busted": "lua", 10 | ".luacheckrc": "lua" 11 | }, 12 | "lua.format.lineWidth": 80, 13 | "lua.luacheckPath": "luacheck", 14 | "lua.preferLuaCheckErrors": true 15 | } 16 | } -------------------------------------------------------------------------------- /multibow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thediveo/multibow/bddfcc9f2c10f3aa36be09b69e211d1087e9e7a5/multibow.jpg -------------------------------------------------------------------------------- /sdcard/keys.lua: -------------------------------------------------------------------------------- 1 | require "keybow" 2 | require "layouts/shift" -- for cycling between layouts. 3 | require "layouts/media-player" -- indispensable media player controls. 4 | require "layouts/vsc-golang" -- debugging Go programs in VisualStudio Code. 5 | require "layouts/kdenlive" -- editing video using Kdenlive. 6 | require "layouts/empty" -- empty, do-nothing layout. 7 | -------------------------------------------------------------------------------- /sdcard/layouts/empty.lua: -------------------------------------------------------------------------------- 1 | -- An empty Multibow layout. Useful for "switching off" any active keymaps, 2 | -- with only the permanent keymaps (SHIFT, etc) being still in place. 3 | 4 | --[[ 5 | Copyright 2019 Harald Albrecht 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | ]]-- 25 | 26 | local empty = {} -- module 27 | 28 | local mb = require("snippets/multibow") 29 | 30 | --[[ 31 | The Keybow layout is as follows when in landscape orientation, with the USB 32 | cable going off "northwards": 33 | 34 | ┋┋ 35 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 36 | ┊ 11 ┊ ┊ 8 ┊ ┊ 7 ┊ ┊ 6 ┊ 37 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 38 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 39 | ┊ 10 ┊ ┊ 7 ┊ ┊ 4 ┊ ┊ 1 ┊ 40 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 41 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 42 | ┊ 9 ┊ ┊ 6 ┊ ┊ 3 ┊ ┊ 0 ┊ 43 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 44 | 45 | ]]-- 46 | 47 | empty.keymap = { name="empty" } 48 | mb.register_keymap(empty.keymap) 49 | 50 | return empty -- module 51 | -------------------------------------------------------------------------------- /sdcard/layouts/kdenlive.lua: -------------------------------------------------------------------------------- 1 | -- A Multibow keyboard layout for the Kdenlive (https://kdenlive.org/) open 2 | -- source non-linear video editor. 3 | 4 | --[[ 5 | Copyright 2019 Harald Albrecht 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | ]]-- 25 | 26 | 27 | -- allow users to set their own configuration before req'ing this 28 | -- module, in order to control the key layout. For defaults, please see 29 | -- below. 30 | local k = _G.kdenlive or {} -- module 31 | 32 | local mb = require("snippets/multibow") 33 | 34 | -- luacheck: ignore 614 35 | --[[ 36 | The Keybow layout is as follows when in landscape orientation, with the USB 37 | cable going off "northwards": 38 | 39 | ┋┋ 40 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 41 | ┊ 11 ┊ ┊ 8 ┊ ┊ 5 ┊ ┊ 2 ┊ 42 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 43 | 44 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 45 | ┊ 10 ┊ ┊ 7 ┊ ┊ 4 ┊ ┊ 1 ┊ 46 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 47 | 48 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 49 | ┊ 9 ┊ ┊ 6 ┊ ┊ 3 ┊ ┊ 0 ┊ 50 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 51 | ⍇ ⍈ 52 | (⯬) (⯮) 53 | 54 | ]]-- 55 | 56 | k.KEY_PLAY_AROUND_MOUSE = k.KEY_PLAY_AROUND_MOUSE or 10 57 | 58 | k.KEY_ZONE_BEGIN = k.KEY_ZONE_BEGIN or 7 59 | k.KEY_ZONE_END = k.KEY_ZONE_END or 4 60 | 61 | k.KEY_CLIP_BEGIN = k.KEY_CLIP_BEGIN or 6 62 | k.KEY_CLIP_END = k.KEY_CLIP_END or 3 63 | k.KEY_PROJECT_BEGIN = k.KEY_PROJECT_BEGIN or 6 64 | k.KEY_PROJECT_END = k.KEY_PROJECT_END or 3 65 | 66 | -- (Default) key colors for unshifted and shifted keys. 67 | k.COLOR_UNSHIFTED = k.COLOR_UNSHIFTED or {r=0, g=1, b=0} 68 | k.COLOR_SHIFTED = k.COLOR_SHIFTED or {r=1, g=0, b=0} 69 | 70 | 71 | function k.play_around_mouse(...) 72 | mb.tap("p") 73 | mb.tap_times(keybow.LEFT_ARROW, 3, keybow.LEFT_SHIFT) 74 | mb.tap("i") 75 | mb.tap_times(keybow.RIGHT_ARROW, 3, keybow.LEFT_SHIFT) 76 | mb.tap("o") 77 | mb.tap(...) 78 | end 79 | 80 | 81 | -- Unshift to primary keymap. For simplification, use it with the "anykey" 82 | -- release handlers, see below. 83 | function k.unshift(_) 84 | mb.activate_keymap(k.keymap.name) 85 | end 86 | 87 | -- Helps avoiding individual color setting... 88 | function k.init_color(keymap, color) 89 | for keyno, keydef in pairs(keymap) do 90 | if type(keyno) == "number" and keyno >= 0 then 91 | if not keydef.c then 92 | keydef.c = color 93 | end 94 | end 95 | end 96 | return keymap 97 | end 98 | 99 | k.keymap = k.init_color({ 100 | name="kdenlive", 101 | [k.KEY_ZONE_BEGIN] = {press=function() mb.tap("I") end}, 102 | [k.KEY_ZONE_END] = {press=function() mb.tap("O") end}, 103 | [k.KEY_CLIP_BEGIN] = {press=function() mb.tap(keybow.HOME) end}, 104 | [k.KEY_CLIP_END] = {press=function() mb.tap(keybow.END) end}, 105 | [k.KEY_PLAY_AROUND_MOUSE] = {press=function() k.play_around_mouse(keybow.SPACE, keybow.LEFT_CTRL) end}, 106 | }, k.COLOR_UNSHIFTED) 107 | k.keymap_shifted = k.init_color({ 108 | name="kdenlive-shifted", 109 | secondary=true, 110 | [k.KEY_PROJECT_BEGIN] = {press=function() mb.tap(keybow.HOME, keybow.LEFT_CTRL) end}, 111 | [k.KEY_PROJECT_END] = {press=function() mb.tap(keybow.END, keybow.LEFT_CTRL) end}, 112 | [k.KEY_PLAY_AROUND_MOUSE] = {press=function() k.play_around_mouse(keybow.SPACE, keybow.LEFT_ALT) end}, 113 | [-1] = {release=k.unshift}, 114 | }, k.COLOR_SHIFTED) 115 | k.keymap.shift_to = k.keymap_shifted 116 | k.keymap_shifted.shift_to = k.keymap 117 | 118 | mb.register_keymap(k.keymap) 119 | mb.register_keymap(k.keymap_shifted) 120 | 121 | 122 | return k -- module 123 | -------------------------------------------------------------------------------- /sdcard/layouts/keymap-template.lua: -------------------------------------------------------------------------------- 1 | -- A Multibow template layout, useful for starting your own keymap layouts. 2 | 3 | --[[ 4 | Copyright 2019 Harald Albrecht 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]]-- 24 | 25 | local km = {} -- module 26 | 27 | local mb = require("snippets/multibow") 28 | 29 | --[[ 30 | The Keybow layout is as follows when in landscape orientation, with the USB 31 | cable going off "northwards": 32 | 33 | ┋┋ 34 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 35 | ┊ 11 ┊ ┊ 8 ┊ ┊ 5 ┊ ┊ 2 ┊ 36 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 37 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 38 | ┊ 10 ┊ ┊ 7 ┊ ┊ 4 ┊ ┊ 1 ┊ 39 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 40 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 41 | ┊ 9 ┊ ┊ 6 ┊ ┊ 3 ┊ ┊ 0 ┊ 42 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 43 | 44 | ]]-- 45 | 46 | -- Some action on a certain key press... 47 | function km.mypress(keyno) -- luacheck: ignore 212 48 | end 49 | 50 | -- Some action on a certain key release... 51 | function km.myrelease(keyno) -- luacheck: ignore 212 52 | end 53 | 54 | -- The keymap layout... 55 | km.keymap = { 56 | -- IMPORTANT: Make sure to change the keymap name to make it unique! 57 | name="keymap-template", 58 | 59 | -- The index entries below are defining keys as to their LED color and 60 | -- what key taps should be send to the USB host to which your Keybow is 61 | -- connected to. 62 | [0] = { c={r=1, g=1, b=1}, press=function() mb.tap("a") end}, 63 | [1] = { c={r=1, g=1, b=1}, press=km.mypress}, 64 | [2] = { c={r=1, g=1, b=1}, release=km.myrelease}, 65 | } 66 | mb.register_keymap(km.keymap) 67 | 68 | return km -- module 69 | -------------------------------------------------------------------------------- /sdcard/layouts/media-player.lua: -------------------------------------------------------------------------------- 1 | -- A Multibow simple media player keyboard layout. 2 | 3 | --[[ 4 | Copyright 2019 Harald Albrecht 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]]-- 24 | 25 | local mplay = _G.mplay or {} -- module 26 | 27 | local mb = require("snippets/multibow") 28 | 29 | --[[ 30 | The Keybow layout is as follows when in landscape orientation, with the USB 31 | cable going off "northwards": 32 | 33 | ┋┋ 34 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 35 | ┊ 11 ┊ ┊ 8 ┊ ┊ 5 ┊ ┊ 2 ┊ 36 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 37 | 🔇 🔈/🔉 🔊 38 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 39 | ┊ 10 ┊ ┊ 7 ┊ ┊ 4 ┊ ┊ 1 ┊ 40 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 41 | ⏹️️ 42 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 43 | ┊ 9 ┊ ┊ 6 ┊ ┊ 3 ┊ ┊ 0 ┊ 44 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 45 | ◀️◀️ ▮▶ ▶▶ 46 | 47 | ]]-- 48 | 49 | mplay.KEY_STOP = mplay.KEY_STOP or 4 50 | mplay.KEY_PLAYPAUSE = mplay.KEY_PLAYPAUSE or 3 51 | mplay.KEY_PREV = mplay.KEY_PREV or 6 52 | mplay.KEY_NEXT = mplay.KEY_NEXT or 0 53 | 54 | mplay.KEY_VOLUP = mplay.KEY_VOLUP or 2 55 | mplay.KEY_VOLDN = mplay.KEY_VOLDN or 5 56 | mplay.KEY_MUTE = mplay.KEY_MUTE or 8 57 | 58 | mplay.keymap = { 59 | name="mediaplayer", 60 | 61 | [mplay.KEY_STOP] = { c={r=1, g=0, b=0}, press=function() mb.tap(keybow.MEDIA_STOPCD) end}, 62 | [mplay.KEY_PLAYPAUSE] = { c={r=0, g=1, b=0}, press=function() mb.tap(keybow.MEDIA_PLAYPAUSE) end}, 63 | [mplay.KEY_PREV] = { c={r=0.5, g=0.5, b=1}, press=function() mb.tap(keybow.MEDIA_PREVIOUSSONG) end}, 64 | [mplay.KEY_NEXT] = { c={r=0, g=1, b=1}, press=function() mb.tap(keybow.MEDIA_NEXTSONG) end}, 65 | 66 | [mplay.KEY_MUTE] = { c={r=0.5, g=0.1, b=0.1}, press=function() mb.tap(keybow.MEDIA_MUTE) end}, 67 | [mplay.KEY_VOLDN] = { c={r=0.5, g=0.5, b=0.5}, press=function() mb.tap(keybow.MEDIA_VOLUMEDOWN) end}, 68 | [mplay.KEY_VOLUP] = { c={r=1, g=1, b=1}, press=function() mb.tap(keybow.MEDIA_VOLUMEUP) end}, 69 | } 70 | mb.register_keymap(mplay.keymap) 71 | 72 | return mplay -- module 73 | -------------------------------------------------------------------------------- /sdcard/layouts/shift.lua: -------------------------------------------------------------------------------- 1 | -- A permanent "SHIFT" Multibow keymap layout for cycling keymaps, LED 2 | -- brightness control, and adding SHIFT layers to other Multibow keyboard 3 | -- (multi) layouts. 4 | 5 | --[[ 6 | Copyright 2019 Harald Albrecht 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ]]-- 26 | 27 | -- allow users to set their own configuration before req'ing this 28 | -- module, in order to control the key layout. For defaults, please see 29 | -- below. 30 | local shift = _G.shift or {} -- module 31 | 32 | local mb = require "snippets/multibow" 33 | 34 | -- luacheck: ignore 614 35 | --[[ 36 | The Keybow layout is as follows when in landscape orientation, with the USB 37 | cable going off "northwards": 38 | 39 | ┋┋ 40 | ╔════╗ ╔╌╌╌╌╗ ╔╌╌╌╌╗ ┌╌╌╌╌┐ 41 | ║ 11 ║ ┊ 8 ┊ ┊ 5 ┊ ┊ 2 ┊ 42 | ╚════╝ ╚╌╌╌╌╝ ╚╌╌╌╌╝ └╌╌╌╌┘ 43 | SHIFT →LAYOUT 🔆BRIGHT 44 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 45 | ┊ 10 ┊ ┊ 7 ┊ ┊ 4 ┊ ┊ 1 ┊ 46 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 47 | 48 | ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ ┌╌╌╌╌┐ 49 | ┊ 9 ┊ ┊ 6 ┊ ┊ 3 ┊ ┊ 0 ┊ 50 | └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ └╌╌╌╌┘ 51 | 52 | 53 | ]]-- 54 | 55 | 56 | -- Default hardware key to function assignments, can be overriden by users 57 | shift.KEY_SHIFT = shift.KEY_SHIFT or 11 58 | shift.KEY_LAYOUT = shift.KEY_LAYOUT or 8 59 | shift.KEY_BRIGHTNESS = shift.KEY_BRIGHTNESS or 5 60 | 61 | shift.BRIGHTNESS_LEVELS = shift.BRIGHTNESS_LEVELS or { 70, 100, 40 } 62 | 63 | 64 | -- Internal flag for detecting SHIFT press-release sequences without any SHIFTed 65 | -- function. 66 | local shift_only = false 67 | local grabbed_key_count = 0 68 | 69 | -- Activates the first brightness level... 70 | shift.brightnesses = table.pack(table.unpack(shift.BRIGHTNESS_LEVELS)) 71 | mb.cycle_brightness(shift.brightnesses) 72 | 73 | -- Switches to the next SHIFT layer within the currently active keyboard layout. 74 | -- SHIFT layer(s) are wired up as a circular list of keymaps, linked using their 75 | -- "shift_to" elements. 76 | function shift.shift_secondary_keymap() 77 | local keymap = mb.current_keymap 78 | if keymap and keymap.shift_to then 79 | mb.activate_keymap(keymap.shift_to.name) 80 | end 81 | end 82 | 83 | -- Remember how many grabbed keys are pressed, so we won't ungrab later until 84 | -- all keys have been released. 85 | function shift.any_press(_) 86 | grabbed_key_count = grabbed_key_count + 1 87 | end 88 | 89 | -- Only ungrab after last key has been released 90 | function shift.any_release(_) 91 | if grabbed_key_count > 0 then 92 | grabbed_key_count = grabbed_key_count - 1 93 | if grabbed_key_count == 0 then 94 | -- Ungrabs after last key released. 95 | mb.ungrab() 96 | -- And switches between keymaps within the same set. 97 | if shift_only then 98 | shift.shift_secondary_keymap() 99 | end 100 | end 101 | end 102 | end 103 | 104 | -- SHIFT press: switches into grabbed SHIFT mode, activating the in-SHIFT keys 105 | -- for brightness change, keymap cycling, et cetera. 106 | function shift.shift(key) 107 | shift_only = true 108 | shift.any_press(key) 109 | mb.grab(shift.keymap_shifted.name) 110 | end 111 | 112 | -- Cycles to the next primary keyboard layout (keymap) 113 | function shift.cycle(_) 114 | shift_only = false 115 | mb.cycle_primary_keymaps() 116 | end 117 | 118 | -- Changes the Keybow LED brightness, by cycling through different brightness 119 | -- levels 120 | function shift.brightness(key) 121 | shift_only = false 122 | mb.cycle_brightness(shift.brightnesses) 123 | mb.led(key, shift.next_brightness_color()) 124 | end 125 | 126 | -- Returns the color for the BRIGHTNESS key LED: this is the next brightness 127 | -- level in the sequence of brightness levels as a color of gray/white. 128 | function shift.next_brightness_color() 129 | local br = shift.brightnesses[1] 130 | br = br > 1.0 and br / 100 or br 131 | return { r=br, g=br, b=br } 132 | end 133 | 134 | -- define and register our keymaps: the permanent SHIFT key-only keymap, as well 135 | -- as a temporary grabbing keymap while the SHIFT key is being pressed and held. 136 | shift.keymap = { 137 | name="shift", 138 | permanent=true, 139 | [shift.KEY_SHIFT] = {c={r=1, g=1, b=1}, press=shift.shift}, 140 | } 141 | shift.keymap_shifted = { 142 | name="shift-shifted", 143 | secondary=true, 144 | [-1] = {press=shift.any_press, release=shift.any_release}, 145 | [shift.KEY_SHIFT] = {c={r=1, g=1, b=1}}, 146 | 147 | [shift.KEY_LAYOUT] = {c={r=0, g=1, b=1}, press=shift.cycle, release=shift.release_other}, 148 | [shift.KEY_BRIGHTNESS] = {c=shift.next_brightness_color, press=shift.brightness, release=shift.release_other} 149 | } 150 | mb.register_keymap(shift.keymap) 151 | mb.register_keymap(shift.keymap_shifted) 152 | 153 | 154 | return shift -- module 155 | -------------------------------------------------------------------------------- /sdcard/layouts/vsc-golang.lua: -------------------------------------------------------------------------------- 1 | -- A Multibow keyboard layout for the VisualStudio Go extension 2 | -- (https://github.com/Microsoft/vscode-go). 3 | 4 | --[[ 5 | Copyright 2019 Harald Albrecht 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | ]]-- 25 | 26 | local vscgo = _G.vscgo or {} -- module 27 | 28 | local mb = require "snippets/multibow" 29 | 30 | --[[ 31 | The Keybow layout is as follows when in landscape orientation, with the USB 32 | cable going off "northwards": 33 | 34 | ┋┋ 35 | ┌╌╌╌╌┐ ╔════╗ ╔════╗ ╔════╗ 36 | ┊ 11 ┊ ║ 8 ║ ║ 5 ║ ║ 2 ║ 37 | └╌╌╌╌┘ ╚════╝ ╚════╝ ╚════╝ 38 | OUTPUT DEBUG CLOSE PANE 39 | ╔════╗ ╔════╗ ╔════╗ ╔════╗ 40 | ║ 10 ║ ║ 7 ║ ║ 4 ║ ║ 1 ║ 41 | ╚════╝ ╚════╝ ╚════╝ ╚════╝ 42 | ▶ ⏹STOP ↺RELOAD TSTPKG 43 | ╔════╗ ╔════╗ ╔════╗ ╔════╗ 44 | ║ 9 ║ ║ 6 ║ ║ 3 ║ ║ 0 ║ 45 | ╚════╝ ╚════╝ ╚════╝ ╚════╝ 46 | ▮▶ ⮧INTO ⭢STEP ⮥OUT 47 | 48 | ]]-- 49 | 50 | -- Default hardware key to function assignments, can be overriden by users 51 | vscgo.KEY_RUN = vscgo.KEY_RUN or 10 52 | vscgo.KEY_STOP = vscgo.KEY_STOP or 7 53 | vscgo.KEY_RELOAD = vscgo.KEY_RELOAD or 4 54 | vscgo.KEY_TESTPKG = vscgo.KEY_TESTPKG or 1 55 | vscgo.KEY_CONT = vscgo.KEY_CONT or 9 56 | vscgo.KEY_STEPINTO = vscgo.KEY_STEPINTO or 6 57 | vscgo.KEY_STEPOVER = vscgo.KEY_STEPOVER or 3 58 | vscgo.KEY_STEPOUT = vscgo.KEY_STEPOUT or 0 59 | 60 | vscgo.KEY_VIEWOUTPUT = vscgo.KEY_VIEWOUTPUT or 8 61 | vscgo.KEY_VIEWDEBUG = vscgo.KEY_VIEWDEBUG or 5 62 | vscgo.KEY_CLOSEPANEL = vscgo.KEY_CLOSEPANEL or 2 63 | 64 | vscgo.RED = { r=1, g=0, b=0 } 65 | vscgo.YELLOW = { r=1, g=0.7, b=0 } 66 | vscgo.GREEN = { r=0, g=1, b=0 } 67 | vscgo.GREENISH = { r=0.5, g=0.8, b=0.5} 68 | vscgo.BLUE = { r=0, g=0, b=1 } 69 | vscgo.BLUECYAN = { r=0, g=0.7, b=1 } 70 | vscgo.BLUEGRAY = { r=0.7, g=0.7, b=1 } 71 | vscgo.CYAN = { r=0, g=1, b=1 } 72 | 73 | vscgo.COLOR_RUN = vscgo.COLOR_RUN or vscgo.GREENISH 74 | vscgo.COLOR_STOP = vscgo.COLOR_STOP or vscgo.RED 75 | vscgo.COLOR_RELOAD = vscgo.COLOR_RELOAD or vscgo.YELLOW 76 | vscgo.COLOR_TESTPKG = vscgo.COLOR_TESTPKG or vscgo.CYAN 77 | vscgo.COLOR_CONT = vscgo.COLOR_CONT or vscgo.GREEN 78 | vscgo.COLOR_STEPINTO = vscgo.COLOR_STEPINTO or vscgo.BLUECYAN 79 | vscgo.COLOR_STEPOVER = vscgo.COLOR_STEPOVER or vscgo.BLUE 80 | vscgo.COLOR_STEPOUT = vscgo.COLOR_STEPOUT or vscgo.BLUEGRAY 81 | 82 | vscgo.COLOR_VIEWOUTPUT = vscgo.COLOR_VIEWOUTPUT or vscgo.GREENISH 83 | vscgo.COLOR_VIEWDEBUG = vscgo.COLOR_VIEWDEBUG or vscgo.GREEN 84 | vscgo.COLOR_CLOSEPANEL = vscgo.COLOR_CLOSEPANEL or vscgo.RED 85 | 86 | -- AND NOW FOR SOMETHING DIFFERENT: THE REAL MEAT -- 87 | 88 | function vscgo.command(cmd) 89 | mb.tap("P", keybow.LEFT_SHIFT, keybow.LEFT_CTRL) 90 | keybow.sleep(100) 91 | keybow.text(cmd) 92 | keybow.tap_enter() 93 | end 94 | 95 | function vscgo.go_test_package(_) 96 | vscgo.command("go test package") 97 | end 98 | 99 | -- luacheck: ignore 631 100 | vscgo.keymap = { 101 | name="vsc-golang-debug", 102 | [vscgo.KEY_RUN] = {c=vscgo.COLOR_RUN, press=function(_) mb.tap(keybow.F5, keybow.LEFT_CTRL) end}, 103 | [vscgo.KEY_STOP] = {c=vscgo.COLOR_STOP, press=function(_) mb.tap(keybow.F5, keybow.LEFT_SHIFT) end}, 104 | [vscgo.KEY_RELOAD] = {c=vscgo.COLOR_RELOAD, press=function(_) mb.tap(keybow.F5, keybow.LEFT_SHIFT, keybow.LEFT_CTRL) end}, 105 | [vscgo.KEY_TESTPKG] = {c=vscgo.COLOR_TESTPKG, press=vscgo.go_test_package}, 106 | 107 | [vscgo.KEY_CONT] = {c=vscgo.COLOR_CONT, press=function(_) mb.tap(keybow.F5) end}, 108 | [vscgo.KEY_STEPINTO] = {c=vscgo.COLOR_STEPINTO, press=function(_) mb.tap(keybow.F11) end}, 109 | [vscgo.KEY_STEPOVER] = {c=vscgo.COLOR_STEPOVER, press=function(_) mb.tap(keybow.F10) end}, 110 | [vscgo.KEY_STEPOUT] = {c=vscgo.COLOR_STEPOUT, press=function(_) mb.tap(keybow.F11, keybow.LEFT_SHIFT) end}, 111 | 112 | [vscgo.KEY_VIEWOUTPUT] = {c=vscgo.COLOR_VIEWOUTPUT, press=function(_) vscgo.command("view toggle output") end}, 113 | [vscgo.KEY_VIEWDEBUG] = {c=vscgo.COLOR_VIEWDEBUG, press=function(_) vscgo.command("view debug console") end}, 114 | [vscgo.KEY_CLOSEPANEL] = {c=vscgo.COLOR_CLOSEPANEL, press=function(_) vscgo.command("view close panel") end}, 115 | } 116 | 117 | mb.register_keymap(vscgo.keymap) 118 | 119 | return vscgo -- module 120 | -------------------------------------------------------------------------------- /sdcard/snippets/mb/keymaps.lua: -------------------------------------------------------------------------------- 1 | -- Multibow internal "module" implementing keymap-related management and 2 | -- handling. 3 | 4 | --[[ 5 | Copyright 2019 Harald Albrecht 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | ]]-- 25 | 26 | -- luacheck: globals mb 27 | 28 | -- Internal variables for housekeeping... 29 | 30 | -- The registered keymaps, indexed by their names (.name field). 31 | mb.keymaps = {} 32 | -- The ordered sequence of primary keymap, in the sequence they were 33 | -- registered. 34 | mb.primary_keymaps = {} 35 | -- The currently activated keymap. 36 | mb.current_keymap = nil 37 | -- A temporary keymap while grabbing. 38 | mb.grab_keymap = nil 39 | 40 | -- Registers a keymap (by name), so it can be easily activated later by its name. 41 | -- Multiple keymaps can be registered. Keymaps can be either "primary" by 42 | -- default, or permanent or secondary keymaps. 43 | -- 44 | -- A primary keymap is any keymap without either a "permanent" or "secondary" 45 | -- table element. Users can cycle through primary keymaps using the "shift" 46 | -- permanent keyboard layout. 47 | -- 48 | -- permanent keymaps (marked by table element "permanent=true") are always 49 | -- active, thus they don't need to be activated. 50 | -- 51 | -- Secondary keymaps (marked by table element "secondary=true") are intended 52 | -- as SHIFT/modifier layers. As such the get ignored by cycling, but instead 53 | -- need to be activated explicitly. The "shift" permanent keyboard layout 54 | -- automates this. 55 | -- 56 | -- If this is the first keymap getting registered, then it will also made 57 | -- activated. 58 | function mb.register_keymap(keymap) 59 | local name = keymap.name 60 | -- register 61 | mb.keymaps[name] = keymap 62 | -- ensure that first registered keymap also automatically gets activated 63 | -- (albeit the LEDs will only update later). Also maintain the (ordered) 64 | -- sequence of registered primary keymaps. 65 | if not (keymap.permanent or keymap.secondary) then 66 | mb.current_keymap = mb.current_keymap or keymap 67 | table.insert(mb.primary_keymaps, keymap) 68 | end 69 | end 70 | 71 | -- Returns the list of currently registered keymaps; this list is a table, 72 | -- with its registered keymaps at indices 1, 2, ... 73 | function mb.registered_keymaps() 74 | local keymaps = {} 75 | for _, keymap in pairs(mb.keymaps) do 76 | table.insert(keymaps, keymap) 77 | end 78 | return keymaps 79 | end 80 | 81 | -- Returns the list of currently registered *primary* keymaps, in the same order 82 | -- as they were registered. First primary is at index 1, second at 2, ... 83 | function mb.registered_primary_keymaps() 84 | return mb.primary_keymaps 85 | end 86 | 87 | -- Cycles through the available (primary) keymaps, ignoring secondary and 88 | -- permanent keymaps. This is convenient for assigning primary keymap switching 89 | -- using a key on the Keybow device itself. 90 | function mb.cycle_primary_keymaps() 91 | local km = mb.current_keymap 92 | if km == nil then return end 93 | -- If this is a secondary keymap, locate its corresponding primary keymap. 94 | if km.secondary then 95 | if not km.shift_to then 96 | -- No SHIFT's shift_to cyclic chain available, so rely on the naming 97 | -- schema instead and try to locate the primary keymap with the first 98 | -- match instead. This assumes that the name of the secondary keymaps 99 | -- have some suffix and thus are longer than the name of their 100 | -- corresponding primary keymap. 101 | for _, pkm in ipairs(mb.primary_keymaps) do 102 | if string.sub(km.name, 1, #pkm.name) == pkm.name then 103 | km = pkm 104 | break 105 | end 106 | end 107 | -- Checks if locating the primary keymap failed and then bails out 108 | -- immediately. 109 | if km.secondary then return end 110 | else 111 | -- Follows the cyclic chain of SHIFT's shift_to keymaps, until we get 112 | -- to the primary keymap in the cycle, or until we have completed one 113 | -- cycle. 114 | repeat 115 | km = km.shift_to 116 | if not km or km == mb.current_keymap then 117 | return 118 | end 119 | until not(km.secondary) 120 | end 121 | end 122 | -- Move on to the next primary keymap, rolling over at the end of our list. 123 | for idx, pkm in ipairs(mb.primary_keymaps) do 124 | if pkm == km then 125 | idx = idx + 1 126 | if idx > #mb.primary_keymaps then idx = 1 end 127 | mb.activate_keymap(mb.primary_keymaps[idx].name) 128 | end 129 | end 130 | end 131 | 132 | -- Activates a specific keymap by name. Please note that it isn't necessary 133 | -- to "activate" permanent keymaps at all (and thus this deed cannot be done). 134 | function mb.activate_keymap(name) 135 | name = type(name) == "table" and name.name or name 136 | local keymap = mb.keymaps[name] 137 | if keymap and not keymap.permanent then 138 | mb.current_keymap = keymap 139 | mb.activate_leds() 140 | end 141 | end 142 | 143 | -- Sets a "grabbing" keymap that takes (temporarily) grabs all keys. While a 144 | -- grab keymap is in place, key presses and releases will only be routed to 145 | -- the grab keymap, but never to the permanent keymaps, nor the previously 146 | -- "active" primary keymap. 147 | function mb.grab(name) 148 | name = type(name) == "table" and name.name or name 149 | mb.grab_keymap = mb.keymaps[name] 150 | mb.activate_leds() 151 | end 152 | 153 | -- Removes a "grabbing" keymap, thus reactivating the permanent keymaps, as 154 | -- well as the previously active primary keymap. 155 | function mb.ungrab() 156 | mb.grab_keymap = nil 157 | mb.activate_leds() 158 | end 159 | -------------------------------------------------------------------------------- /sdcard/snippets/mb/keys.lua: -------------------------------------------------------------------------------- 1 | -- Multibow internal "module" implementing convenience functions for sending 2 | -- key presses to the USB host to which the Keybow device is connected to. 3 | 4 | --[[ 5 | Copyright 2019 Harald Albrecht 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | ]]-- 25 | 26 | -- luacheck: globals mb 27 | 28 | -- Default delay between rapidly (repeated) key presses, can be overridden. 29 | mb.KEY_DELAY_MS = mb.KEY_DELAY_MS or 100 30 | 31 | -- Delay between key presses 32 | function mb.delay() 33 | keybow.sleep(mb.KEY_DELAY_MS) 34 | end 35 | 36 | -- Sends a single key tap to the USB host, optionally with modifier keys, such 37 | -- as SHIFT (keybow.LEFT_SHIFT), CTRL (keybow.LEFT_CTRL), et cetera. The "key" 38 | -- parameter can be a string or a Keybow key code, such as keybow.HOME, et 39 | -- cetera. 40 | function mb.tap(key, ...) 41 | mb.tap_times(key, 1, ...) 42 | end 43 | 44 | -- Taps the same key multiple times, optionally with modifier keys; however, 45 | -- for optimization, these modifiers are only pressed once before the tap 46 | -- sequence, and only released once after all taps. 47 | function mb.tap_times(key, times, ...) 48 | for modifier_argno = 1, select("#", ...) do 49 | local modifier = select(modifier_argno, ...) 50 | if modifier then keybow.set_modifier(modifier, keybow.KEY_DOWN) end 51 | end 52 | for _ = 1, times do 53 | keybow.tap_key(key) 54 | mb.delay() 55 | end 56 | for modifier_argno = 1, select("#", ...) do 57 | local modifier = select(modifier_argno, ...) 58 | if modifier then keybow.set_modifier(modifier, keybow.KEY_UP) end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /sdcard/snippets/mb/leds.lua: -------------------------------------------------------------------------------- 1 | -- Multibow internal "module" implementing Keybow LED-related functionality, 2 | -- such as brightness control and "lighting up" a (multibow) keymap. 3 | 4 | --[[ 5 | Copyright 2019 Harald Albrecht 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | ]]-- 25 | 26 | 27 | -- luacheck: globals mb 28 | 29 | mb.MIN_BRIGHTNESS = mb.MIN_BRIGHTNESS or 0.1 30 | 31 | -- Default LED brightness in the [0.1..1] range. 32 | mb.brightness = 1 33 | 34 | -- Sets the Keybow key LEDs maximum brightness, in the range [0.1..1] or 35 | -- [10..100] (percent). The minim brightness is clamped on purpose to avoid 36 | -- unlit LEDs. The lower clamp defaults to mb.MIN_BRIGHTNESS. 37 | function mb.set_brightness(brightness) 38 | brightness = brightness > 1.0 and brightness / 100 or brightness 39 | if brightness < mb.MIN_BRIGHTNESS then brightness = mb.MIN_BRIGHTNESS end 40 | if brightness > 1 then brightness = 1 end 41 | mb.brightness = brightness 42 | mb.activate_leds() 43 | end 44 | 45 | -- Cycles through a list of brightness values, always taking the first 46 | -- value and modifying the list by cycling it. Brightness values can be 47 | -- either [0..1] or [0..100] (that is, percent) 48 | function mb.cycle_brightness(brightnesses) 49 | local brightness = table.remove(brightnesses, 1) 50 | table.insert(brightnesses, brightness) 51 | mb.set_brightness(brightness) 52 | return brightnesses 53 | end 54 | 55 | -- Sets key LED to specific color, taking brightness into consideration. 56 | -- The color is a triple (table) with the elements r, g, and b. Each color 57 | -- component is in the range [0..1]. 58 | function mb.led(keyno, color) 59 | if color then 60 | local b = mb.brightness * 255 61 | keybow.set_pixel(keyno, color.r * b, color.g * b, color.b * b) 62 | else 63 | keybow.set_pixel(keyno, 0, 0, 0) 64 | end 65 | end 66 | 67 | -- Restores Keybow LEDs according to current keymap and the permanent keymaps. 68 | function mb.activate_leds() 69 | keybow.clear_lights() 70 | -- if a grab is in place then it takes absolute priority 71 | if mb.grab_keymap then 72 | mb.activate_keymap_leds(mb.grab_keymap) 73 | else 74 | -- first update LEDs for the current keymap... 75 | if mb.current_keymap ~= nil then 76 | mb.activate_keymap_leds(mb.current_keymap) 77 | end 78 | -- ...then update LEDs from permanent keymap(s), as this ensures that 79 | -- the permanent keymaps take precedence. 80 | for _, keymap in pairs(mb.keymaps) do 81 | if keymap.permanent then 82 | mb.activate_keymap_leds(keymap) 83 | end 84 | end 85 | end 86 | end 87 | 88 | -- Helper function that iterates over all keymap elements but skipping non-key 89 | -- bindings. 90 | function mb.activate_keymap_leds(keymap) 91 | for keyno, keydef in pairs(keymap) do 92 | -- Only iterates over keys, skipping any other keymap definitions. 93 | if type(keyno) == "number" and keydef.c then 94 | local color = type(keydef.c) == "function" and keydef.c() or keydef.c 95 | mb.led(keyno, color) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /sdcard/snippets/mb/morekeys.lua: -------------------------------------------------------------------------------- 1 | -- Multibow module providing additional USB HID keycode definitions to augment 2 | -- the existing keybow definitions. For more information about USB HID 3 | -- keyboard scan codes, for instance, see: 4 | -- https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2 5 | 6 | --[[ 7 | Copyright 2019 Harald Albrecht 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | ]]-- 27 | 28 | require("keybow") 29 | 30 | -- Tell luacheck that it is okay in this specific case to change the keybow 31 | -- global. 32 | 33 | -- luacheck: globals keybow 34 | 35 | keybow.SYSRQ = 0x46 36 | keybow.SCROLLLOCK = 0x47 37 | keybow.PAUSE = 0x48 38 | keybow.INSERT = 0x49 39 | keybow.DELETE = 0x4c 40 | keybow.HOME = 0x4a 41 | keybow.END = 0x4d 42 | keybow.PAGEUP = 0x4b 43 | keybow.PAGEOWN = 0x4e 44 | 45 | keybow.F13 = 0x68 46 | keybow.F14 = 0x69 47 | keybow.F15 = 0x6a 48 | keybow.F16 = 0x6b 49 | keybow.F17 = 0x6c 50 | keybow.F18 = 0x6d 51 | keybow.F19 = 0x6e 52 | keybow.F20 = 0x6f 53 | keybow.F21 = 0x70 54 | keybow.F22 = 0x71 55 | keybow.F23 = 0x72 56 | keybow.F24 = 0x73 57 | 58 | keybow.KPSLASH = 0x54 59 | keybow.KPASTERISK = 0x55 60 | keybow.KPMINUS = 0x56 61 | keybow.KPPLUS = 0x57 62 | keybow.KPENTER = 0x58 63 | keybow.KP1 = 0x59 64 | keybow.KP2 = 0x5a 65 | keybow.KP3 = 0x5b 66 | keybow.KP4 = 0x5c 67 | keybow.KP5 = 0x5d 68 | keybow.KP6 = 0x5e 69 | keybow.KP7 = 0x5f 70 | keybow.KP8 = 0x60 71 | keybow.KP9 = 0x61 72 | keybow.KP0 = 0x62 73 | keybow.KPDOT = 0x63 74 | keybow.KPEQUAL = 0x67 75 | 76 | keybow.COMPOSE = 0x65 77 | keybow.POWER = 0x66 78 | 79 | keybow.OPEN = 0x74 80 | keybow.HELP = 0x75 81 | keybow.PROPS = 0x76 82 | keybow.FRONT = 0x77 83 | keybow.STOP = 0x78 84 | keybow.AGAIN = 0x79 85 | keybow.UNDO = 0x7a 86 | keybow.CUT = 0x7b 87 | keybow.COPY = 0x7c 88 | keybow.PASTE = 0x7d 89 | keybow.FIND = 0x7e 90 | keybow.MUTE = 0x7f 91 | keybow.VOLUMEUP = 0x80 92 | keybow.VOLUMEDOWN = 0x81 93 | 94 | keybow.MEDIA_PLAYPAUSE = 0xe8 95 | keybow.MEDIA_STOPCD = 0xe9 96 | keybow.MEDIA_PREVIOUSSONG = 0xea 97 | keybow.MEDIA_NEXTSONG = 0xeb 98 | keybow.MEDIA_EJECTCD = 0xec 99 | keybow.MEDIA_VOLUMEUP = 0xed 100 | keybow.MEDIA_VOLUMEDOWN = 0xee 101 | keybow.MEDIA_MUTE = 0xef 102 | keybow.MEDIA_WWW = 0xf0 103 | keybow.MEDIA_BACK = 0xf1 104 | keybow.MEDIA_FORWARD = 0xf2 105 | keybow.MEDIA_STOP = 0xf3 106 | keybow.MEDIA_FIND = 0xf4 107 | keybow.MEDIA_SCROLLUP = 0xf5 108 | keybow.MEDIA_SCROLLDOWN = 0xf6 109 | keybow.MEDIA_EDIT = 0xf7 110 | keybow.MEDIA_SLEEP = 0xf8 111 | keybow.MEDIA_COFFEE = 0xf9 112 | keybow.MEDIA_REFRESH = 0xfa 113 | keybow.MEDIA_CALC = 0xfb 114 | 115 | 116 | return keybow -- module 117 | -------------------------------------------------------------------------------- /sdcard/snippets/mb/routehandlers.lua: -------------------------------------------------------------------------------- 1 | -- Multibow internal "module" implementing routing Keybow hardware key presses 2 | -- and releases from the Keybow Lua firmware to our keymaps with their own key 3 | -- handlers. 4 | 5 | --[[ 6 | Copyright 2019 Harald Albrecht 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ]] -- 26 | 27 | 28 | -- luacheck: globals mb 29 | 30 | -- This all-key, central key router forwards Keybow key events to their 31 | -- correct handlers, depending on which keyboard layout currently is active. 32 | -- 33 | -- A keymap grab will always only route keys to the "grabbing" keymap, and to 34 | -- no other keymap. Without a keymap grab in place, permanent keymaps will be 35 | -- searched first for a matching key definition, with the first hit to score. 36 | -- The current keymap will be considered only last, after all permanent 37 | -- keymaps have been exhausted. 38 | -- 39 | -- Additionally, "any" keyhandlers will be considered only for (1) the 40 | -- grabbing keymap (if a grab is active) and for (2) the current keymap. 41 | -- Permanent keymaps cannot have "any" key handlers (or rather, they will be 42 | -- ignored.) 43 | function mb.route(keyno, pressed) 44 | local keydef, any_kdeydef 45 | -- Checks for a keymap grab being enforced at this time; if the grabbing 46 | -- keymap does not define the key, then it's game over. Additionally, 47 | -- grabbing keymaps may define "any" handlers. 48 | if mb.grab_keymap then 49 | keydef = mb.grab_keymap[keyno] 50 | any_kdeydef = mb.grab_keymap[-1] 51 | else 52 | -- No grab in place, so continue checking for a matching key in the 53 | -- permanent keymaps first. Remember, there cannot be "any" handlers 54 | -- with permanent keymaps. 55 | for _, keymap in pairs(mb.keymaps) do 56 | if keymap.permanent then 57 | keydef = keymap[keyno] 58 | if keydef then 59 | break 60 | end 61 | end 62 | end 63 | -- If no permanent key matched then finally check for our key in the 64 | -- current keymap. Additionally, the current keymap is allowed to 65 | -- define an "any" handler. 66 | if not keydef and mb.current_keymap then 67 | keydef = mb.current_keymap[keyno] 68 | any_kdeydef = mb.current_keymap[-1] 69 | end 70 | end 71 | 72 | -- Bails out now if either a specific nor an "any" key definition to route 73 | -- to could be found. 74 | if not (keydef or any_kdeydef) then 75 | return 76 | end 77 | 78 | -- We found a route, so we need to call its associated handler and 79 | -- also handle the LED lightshow. 80 | if pressed then 81 | for led = 0, 11 do 82 | if led ~= keyno then 83 | mb.led(led, {r = 0, g = 0, b = 0}) 84 | end 85 | end 86 | 87 | if keydef and keydef.press then keydef.press(keyno) end 88 | if any_kdeydef and any_kdeydef.press then any_kdeydef.press(keyno) end 89 | else 90 | if keydef and keydef.release then keydef.release(keyno) end 91 | if any_kdeydef and any_kdeydef.release then any_kdeydef.release(keyno) end 92 | 93 | mb.activate_leds() 94 | end 95 | end 96 | 97 | -- Routes all keybow key handling through our central key router 98 | -- by creating the required set of global handler functions expected 99 | -- by the Keybow firmware. 100 | for keyno = 0, 11 do 101 | _G[string.format("handle_key_%02d", keyno)] = function(pressed) 102 | mb.route(keyno, pressed) 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /sdcard/snippets/multibow.lua: -------------------------------------------------------------------------------- 1 | -- "Multibow" is a Lua module for Pimoroni's Keybow firmware that offers and 2 | -- manages multiple keyboard layouts. 3 | 4 | --[[ 5 | Copyright 2019 Harald Albrecht 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | ]]-- 25 | 26 | -- luacheck: globals mb 27 | mb = mb or {} -- module 28 | 29 | require "keybow" 30 | 31 | -- Pulls in the individual modules that make up Multibow. 32 | mb.path = (...):match("^(.-)[^%/]+$") 33 | 34 | require(mb.path .. "mb/morekeys") 35 | require(mb.path .. "mb/keymaps") 36 | require(mb.path .. "mb/keys") 37 | require(mb.path .. "mb/routehandlers") 38 | require(mb.path .. "mb/leds") 39 | 40 | 41 | -- Disables the automatic Keybow lightshow and sets the key LED colors. This 42 | -- is a well-known (hook) function that gets called by the Keybow firmware 43 | -- after initialization immediately before waiting for key events. 44 | -- luacheck: globals setup 45 | function setup() 46 | -- Disables the automatic keybow lightshow and switches all key LEDs off 47 | -- because the LEDs might be in a random state after power on. 48 | keybow.auto_lights(false) 49 | keybow.clear_lights() 50 | mb.activate_leds() 51 | end 52 | 53 | 54 | return mb -- module 55 | -------------------------------------------------------------------------------- /setup-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Installs the required libraries on Ubuntu ~18.10 LTS for Lua testing. 3 | 4 | #sudo apt-get remove lua* 5 | sudo apt-get install --yes lua5.3 liblua5.3-dev 6 | sudo update-alternatives --install /usr/bin/lua lua /usr/bin/lua5.3 10 7 | sudo apt-get install --yes luarocks 8 | sudo luarocks install busted 9 | sudo luarocks install luasocket 10 | sudo luarocks install luacheck 11 | -------------------------------------------------------------------------------- /spec/hwkeys.lua: -------------------------------------------------------------------------------- 1 | -- Stimulates Keybow "hardware key" presses and releases at the Keybow handler 2 | -- level, thus involving everything starting with the Keybow "firmware" key 3 | -- Lua handlers handle_key_xx. 4 | 5 | --[[ 6 | Copyright 2019 Harald Albrecht 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ]]-- 26 | 27 | local hwk = {} -- module 28 | 29 | -- Convenience: returns the name of a Keybow key handler function for the 30 | -- given key number. 31 | function hwk.handler_name(keyno) 32 | return string.format("handle_key_%02d", keyno) 33 | end 34 | 35 | -- Convenience: call Keybow key handler for key presses and releases by key 36 | -- number instead of name. 37 | function hwk.press(keyno) 38 | _G[hwk.handler_name(keyno)](true) 39 | end 40 | 41 | function hwk.release(keyno) 42 | _G[hwk.handler_name(keyno)](false) 43 | end 44 | 45 | -- Convenience: taps a Keybow key, triggering the corresponding key handler 46 | -- twice. 47 | function hwk.tap(keyno) 48 | hwk.press(keyno) 49 | hwk.release(keyno) 50 | end 51 | 52 | return hwk -- module 53 | -------------------------------------------------------------------------------- /spec/hwkeys_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | -- luacheck: globals handle_key_00 handle_key_01 24 | 25 | local hwk = require("spec/hwkeys") 26 | 27 | describe("Keybow hardware key handler module", function() 28 | 29 | it("returns proper Keybow handler name", function() 30 | assert.equals(hwk.handler_name(0), "handle_key_00") 31 | end) 32 | 33 | inslit("calls correct Keybow key handler", function() 34 | stub(_G, "handle_key_00") -- note: no need to revert later due to insulation. 35 | stub(_G, "handle_key_01") 36 | 37 | hwk.press(0) 38 | assert.stub(handle_key_00).was.called(1) 39 | assert.stub(handle_key_00).was.called_with(true) 40 | assert.stub(handle_key_01).was_not.called() 41 | end) 42 | 43 | describe("wrapped Keybow key handler 00", function() 44 | 45 | local old 46 | local seq 47 | 48 | before_each(function() 49 | old = _G.handle_key_00 50 | seq = {} 51 | _G.handle_key_00 = function(pressed) 52 | table.insert(seq, pressed) 53 | end 54 | end) 55 | 56 | after_each(function() 57 | _G.handle_key_00 = old 58 | end) 59 | 60 | it("taps Keybow key handler with press and release", function() 61 | -- this also implicitly tests hwk.press() and hw.release() 62 | hwk.tap(0) 63 | assert.equals(2, #seq) 64 | assert.same({true, false}, seq) 65 | end) 66 | 67 | end) 68 | 69 | end) 70 | -------------------------------------------------------------------------------- /spec/integration_spec.lua: -------------------------------------------------------------------------------- 1 | -- Tests all available multibow layouts in a single setup, thus trying to 2 | -- mimic the final Keybow firmware installation as close as possible (for some 3 | -- suitable definition of "close"). 4 | 5 | --[[ 6 | Copyright 2019 Harald Albrecht 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ]]-- 26 | 27 | require("mocked-keybow") 28 | 29 | require "layouts/shift" 30 | require "layouts/kdenlive" 31 | local vscgolang = require "layouts/vsc-golang" 32 | require "layouts/empty" 33 | 34 | local mb = require("snippets/multibow") 35 | local hwk = require("spec/hwkeys") 36 | 37 | describe("final Multibow integration", function() 38 | 39 | _G.setup() 40 | 41 | it("integrates all keymaps", function() 42 | local kms = mb.registered_keymaps() 43 | -- shift: 2 registered keymaps 44 | -- vsc-golang: 1 reg keymap 45 | -- kdenlive: 2 reg keymaps 46 | -- empty: 1 reg keymap 47 | assert.is.equal(6, #kms) 48 | end) 49 | 50 | -- inslit() is an insulated(it()) ensuring that all stubs and spies get 51 | -- removed after this test in any case. 52 | inslit("starts gonelang debugging :)", function() 53 | -- Switches to the VSC Golang Multibow keymap, regardless of the 54 | -- order of keymap imports. 55 | mb.activate_keymap(vscgolang.keymap.name) 56 | assert.is.equal(vscgolang.keymap.name, mb.current_keymap.name) 57 | assert.is.equal(vscgolang.go_test_package, vscgolang.keymap[vscgolang.KEY_TESTPKG].press) 58 | 59 | -- Checks that a press of the "Continue Debugging" key does in fact 60 | -- trigger the corresponding keymap handler. 61 | local s = stub(vscgolang.keymap[vscgolang.KEY_CONT], "press") 62 | hwk.tap(vscgolang.KEY_STEPOVER) 63 | assert.stub(s).was_not.called() 64 | hwk.tap(vscgolang.KEY_CONT) 65 | assert.stub(s).was.called(1) 66 | end) 67 | 68 | end) 69 | -------------------------------------------------------------------------------- /spec/layouts/empty_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | 25 | describe("empty multibow keymap", function() 26 | 27 | local mb = require("snippets/multibow") 28 | 29 | insl(function() 30 | it("installs a single empty primary keymap", function() 31 | -- Sanity check that there are no registered keymaps yet. 32 | assert.is.equal(#mb.registered_keymaps(), 0) 33 | 34 | local empty = require("layouts/empty") 35 | assert.is_not_nil(empty) -- we're going over the top here... 36 | assert.is_not_nil(empty.keymap) -- ...even more so. 37 | 38 | -- empty must register exactly one keymap, and it must be 39 | -- a primary keymap, not permanent or secondary. 40 | local kms = mb.registered_keymaps() 41 | assert.is.equal(1, #kms) 42 | local keymap = kms[1] 43 | assert.is_falsy(keymap.permanent) 44 | assert.is_falsy(keymap.secondary) 45 | end) 46 | end) 47 | 48 | end) 49 | -------------------------------------------------------------------------------- /spec/layouts/kdenlive_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | local hwk = require("spec/hwkeys") 25 | 26 | 27 | describe("Kdenlive keymap", function() 28 | 29 | it("initializes", function() 30 | local mb = require("snippets/multibow") 31 | local k = require("layouts/kdenlive") 32 | assert.is.equal(k.keymap.name, mb.current_keymap.name) 33 | 34 | local kms = mb.registered_keymaps() 35 | assert.is.equal(2, #kms) 36 | end) 37 | 38 | describe("with setup", function() 39 | 40 | local mb, shift, k 41 | 42 | before_each(function() 43 | mb = require("snippets/multibow") 44 | shift = require("layouts/shift") 45 | k = require("layouts/kdenlive") 46 | _G.setup() 47 | end) 48 | 49 | inslit("colors its keys", function() 50 | for _, keymap in pairs(mb.registered_keymaps()) do 51 | if string.sub(keymap.name, 1, #"kdenlive") == "kdenlive" then 52 | for keyno = 0, 11 do 53 | local keydef = keymap[keyno] 54 | if keydef then 55 | assert.is_truthy(keydef.c) 56 | end 57 | end 58 | end 59 | end 60 | end) 61 | 62 | inslit("automatically un-shifts after key press", function() 63 | local some_key = shift.KEY_SHIFT ~= 0 and 0 or 1 64 | 65 | for round = 1, 2 do -- luacheck: ignore 213 66 | for round = 1, 2 do -- luacheck: ignore 213 423 67 | assert.equals(k.keymap.name, mb.current_keymap.name) 68 | hwk.tap(shift.KEY_SHIFT) 69 | assert.equals(k.keymap_shifted.name, mb.current_keymap.name) 70 | hwk.tap(some_key) 71 | assert.equals(k.keymap.name, mb.current_keymap.name) 72 | end 73 | for round = 1, 2 do -- luacheck: ignore 213 423 74 | hwk.tap(shift.KEY_SHIFT) 75 | assert.equals(k.keymap_shifted.name, mb.current_keymap.name) 76 | hwk.tap(shift.KEY_SHIFT) 77 | assert.equals(k.keymap.name, mb.current_keymap.name) 78 | end 79 | end 80 | end) 81 | 82 | inslit("taps unshifted", function() 83 | local s = spy.on(mb, "tap") 84 | local sm = spy.on(keybow, "set_modifier") 85 | 86 | hwk.tap(k.KEY_PROJECT_BEGIN) 87 | assert.spy(s).was.called(1) 88 | assert.spy(sm).was_not.called() 89 | 90 | s:clear() 91 | hwk.tap(shift.KEY_SHIFT) 92 | assert.equals(k.keymap_shifted.name, mb.current_keymap.name) 93 | hwk.tap(k.KEY_CLIP_BEGIN) 94 | assert.spy(s).was.called(1) 95 | assert.spy(sm).was.called() 96 | end) 97 | 98 | inslit("taps", function() 99 | local s = spy.on(mb, "tap") 100 | 101 | hwk.tap(k.KEY_PLAY_AROUND_MOUSE) 102 | assert.spy(s).was.called() 103 | 104 | hwk.tap(shift.KEY_SHIFT) 105 | hwk.tap(k.KEY_PLAY_AROUND_MOUSE) 106 | end) 107 | 108 | end) 109 | 110 | end) 111 | -------------------------------------------------------------------------------- /spec/layouts/keymap-template_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | 25 | describe("template multibow keymap", function() 26 | 27 | local mb = require("snippets/multibow") 28 | local hwk = require("spec/hwkeys") 29 | local kmt = require("layouts/keymap-template") 30 | 31 | inslit("installs a single primary keymap", function() 32 | assert.is_not_nil(kmt) -- we're going over the top here... 33 | assert.is_not_nil(kmt.keymap) -- ...even more so. 34 | 35 | -- empty must register exactly one keymap, and it must be 36 | -- a primary keymap, not permanent or secondary. 37 | local kms = mb.registered_keymaps() 38 | assert.is.equal(1, #kms) 39 | local keymap = kms[1] 40 | assert.is_falsy(keymap.permanent) 41 | assert.is_falsy(keymap.secondary) 42 | end) 43 | 44 | 45 | inslit("calls press and release handlers", function() 46 | local mp = spy.on(kmt.keymap[1], "press") 47 | local mr = spy.on(kmt.keymap[2], "release") 48 | 49 | hwk.tap(1) 50 | assert.spy(mp).was.called(1) 51 | hwk.tap(2) 52 | assert.spy(mr).was.called(1) 53 | end) 54 | 55 | end) 56 | -------------------------------------------------------------------------------- /spec/layouts/shift_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | require "snippets/multibow" 25 | local hwk = require("spec/hwkeys") 26 | local mb = require("snippets/multibow") 27 | 28 | describe("SHIFT multibow keymap", function() 29 | 30 | inslit("installs the SHIFT keymap", function() 31 | -- Sanity check that there are no registered keymaps yet. 32 | assert.is.equal(#mb.registered_keymaps(), 0) 33 | 34 | local shift = require("layouts/shift") 35 | assert.is_not_nil(shift) -- we're going slightly over the top here... 36 | assert.is_not_nil(shift.keymap) 37 | 38 | -- SHIFT must register exactly two keymaps, a primary and a secondary one. 39 | local kms = mb.registered_keymaps() 40 | assert.is.equal(#kms, 2) 41 | for _, keymap in ipairs(kms) do 42 | if keymap == "shift" then 43 | assert.is_falsy(keymap.permanent) 44 | assert.is_falsy(keymap.secondary) 45 | elseif keymap == "shift-shifted" then 46 | assert.is_falsy(keymap.permanent) 47 | assert.is_true(keymap.secondary) 48 | end 49 | end 50 | end) 51 | 52 | inslit("accepts changes form default", function() 53 | local override = 42 54 | 55 | _G.shift = { KEY_SHIFT=override } 56 | local shift = require("layouts/shift") 57 | assert.is.equal(shift.KEY_SHIFT, override) 58 | end) 59 | 60 | describe("SHIFTY", function() 61 | 62 | -- ensure to get a fresh SHIFT layout instance each time we run 63 | -- an isolated, erm, insulated test. 64 | local shift 65 | 66 | before_each(function() 67 | shift = require("layouts/shift") 68 | end) 69 | 70 | inslit("SHIFT grabs", function() 71 | spy.on(mb, "grab") 72 | spy.on(mb, "ungrab") 73 | 74 | -- route in the SHIFT permanent keymap 75 | hwk.press(shift.KEY_SHIFT) 76 | assert.spy(mb.grab).was.called(1) 77 | assert.spy(mb.grab).was.called_with(shift.keymap_shifted.name) 78 | 79 | -- route in the shifted(!) SHIFT keymap, so this checks 80 | -- that we ungrab correctly 81 | mb.grab:clear() 82 | hwk.release(shift.KEY_SHIFT) 83 | assert.spy(mb.grab).was_not.called() 84 | assert.spy(mb.ungrab).was.called(1) 85 | 86 | mb.grab:revert() 87 | mb.ungrab:revert() 88 | end) 89 | 90 | inslit("only lonely SHIFT triggers shift and no dangling grabs", function() 91 | stub(shift, "shift_secondary_keymap") 92 | 93 | -- test that lonely SHIFT triggers... 94 | hwk.tap(shift.KEY_SHIFT) 95 | assert.is_nil(mb.grab_keymap) 96 | assert.stub(shift.shift_secondary_keymap).was.called(1) 97 | 98 | -- but that SHIFT followed by another function doesn't shift. 99 | shift.shift_secondary_keymap:clear() 100 | for _, key in ipairs({ 101 | shift.KEY_LAYOUT, 102 | shift.KEY_BRIGHTNESS 103 | }) do 104 | hwk.press(shift.KEY_SHIFT) 105 | hwk.tap(key) 106 | hwk.release(shift.KEY_SHIFT) 107 | assert.is_nil(mb.grab_keymap) 108 | assert.stub(shift.shift_secondary_keymap).was_not.called() 109 | end 110 | end) 111 | 112 | inslit("lonly SHIFTs shift around", function() 113 | local keymap = { 114 | name="test" 115 | } 116 | local keymap_shifted = { 117 | name="test-shifted", 118 | [0]={press=function(_) end} 119 | } 120 | keymap.shift_to = keymap_shifted 121 | keymap_shifted.shift_to = keymap 122 | mb.register_keymap(keymap) 123 | mb.register_keymap(keymap_shifted) 124 | assert.is.equal(mb.current_keymap, keymap) 125 | 126 | spy.on(mb, "activate_keymap") 127 | local s = stub(keymap_shifted[0], "press") 128 | 129 | hwk.tap(shift.KEY_SHIFT) 130 | assert.spy(mb.activate_keymap).was.called_with(keymap_shifted.name) 131 | assert.is.equal(mb.current_keymap, keymap_shifted) 132 | 133 | hwk.tap(0) 134 | assert.stub(s).was.called(1) 135 | 136 | hwk.tap(shift.KEY_SHIFT) 137 | assert.is.equal(mb.current_keymap, keymap) 138 | 139 | s:clear() 140 | hwk.tap(0) 141 | assert.stub(s).was_not.called() 142 | 143 | mb.activate_keymap:revert() 144 | s:revert() 145 | end) 146 | 147 | insl(function() 148 | describe("while SHIFTed", function() 149 | 150 | before_each(function() 151 | hwk.press(shift.KEY_SHIFT) 152 | end) 153 | 154 | after_each(function() 155 | hwk.release(shift.KEY_SHIFT) 156 | end) 157 | 158 | it("cycles primary keymaps", function() 159 | stub(mb, "cycle_primary_keymaps") 160 | 161 | hwk.tap(shift.KEY_LAYOUT) 162 | assert.stub(mb.cycle_primary_keymaps).was.called(1) 163 | 164 | mb.cycle_primary_keymaps:revert() 165 | end) 166 | 167 | it("changes brightness", function() 168 | stub(mb, "set_brightness") 169 | 170 | hwk.tap(shift.KEY_BRIGHTNESS) 171 | hwk.tap(shift.KEY_BRIGHTNESS) 172 | assert.stub(mb.set_brightness).was.called(2) 173 | 174 | mb.set_brightness:revert() 175 | end) 176 | 177 | end) 178 | end) 179 | 180 | inslit("cycles brightness", function() 181 | local s = spy.on(mb, "led") 182 | 183 | local len = #shift.BRIGHTNESS_LEVELS 184 | for i = 1, len do 185 | assert.equals(mb.brightness * 100, shift.BRIGHTNESS_LEVELS[i]) 186 | -- enters SHIFT and check the brightness of brightness key... 187 | s:clear() 188 | hwk.press(shift.KEY_SHIFT) 189 | assert.spy(s).was.called_with( 190 | shift.KEY_BRIGHTNESS, 191 | shift.next_brightness_color()) 192 | -- cycles to next brightness 193 | hwk.tap(shift.KEY_BRIGHTNESS) 194 | hwk.release(shift.KEY_SHIFT) 195 | end 196 | assert.equals(mb.brightness * 100, shift.BRIGHTNESS_LEVELS[1]) 197 | end) 198 | 199 | end) 200 | 201 | end) 202 | -------------------------------------------------------------------------------- /spec/layouts/vsc-golang_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | require("spec/hwkeys") 25 | 26 | describe("VSC golang keymap", function() 27 | 28 | local mb, go 29 | 30 | it("initializes", function() 31 | mb = require("snippets/multibow") 32 | go = require("layouts/vsc-golang") 33 | assert.is.equal(go.keymap.name, mb.current_keymap.name) 34 | assert.is.equal(1, #mb.registered_keymaps()) 35 | end) 36 | 37 | it("colors its keys", function() 38 | for _, keymap in pairs({go.keymap}) do 39 | for keyno = 0, 11 do 40 | local keydef = keymap[keyno] 41 | if keydef then 42 | assert.is_truthy(keydef.c) 43 | end 44 | end 45 | end 46 | end) 47 | 48 | end) 49 | -------------------------------------------------------------------------------- /spec/mock/keybow.lua: -------------------------------------------------------------------------------- 1 | keybow = {} 2 | 3 | local KEYCODES = "abcdefghijklmnopqrstuvwxyz1234567890\n\a\b\t -=[]\\#;'`,./" 4 | local SHIFTED_KEYCODES = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()\a\a\a\a\a_+{}|~:\"~<>?" 5 | 6 | keybow.LEFT_CTRL = 0 7 | keybow.LEFT_SHIFT = 1 8 | keybow.LEFT_ALT = 2 9 | keybow.LEFT_META = 3 10 | 11 | keybow.RIGHT_CTRL = 4 12 | keybow.RIGHT_SHIFT = 5 13 | keybow.RIGHT_ALT = 6 14 | keybow.RIGHT_META = 7 15 | 16 | keybow.ENTER = 0x28 17 | keybow.ESC = 0x29 18 | keybow.BACKSPACE = 0x2a 19 | keybow.TAB = 0x2b 20 | keybow.SPACE = 0x2c 21 | keybow.CAPSLOCK = 0x39 22 | 23 | keybow.LEFT_ARROW = 0x50 24 | keybow.RIGHT_ARROW = 0x4f 25 | keybow.UP_ARROW = 0x52 26 | keybow.DOWN_ARROW = 0x51 27 | 28 | keybow.F1 = 0x3a 29 | keybow.F2 = 0x3b 30 | keybow.F3 = 0x3c 31 | keybow.F4 = 0x3d 32 | keybow.F5 = 0x3e 33 | keybow.F6 = 0x3f 34 | keybow.F7 = 0x40 35 | keybow.F8 = 0x41 36 | keybow.F9 = 0x42 37 | keybow.F10 = 0x43 38 | keybow.F11 = 0x44 39 | keybow.F12 = 0x45 40 | 41 | keybow.KEY_DOWN = true 42 | keybow.KEY_UP = false 43 | 44 | -- Functions exposed from C 45 | 46 | function keybow.set_modifier(key, state) 47 | keybow_set_modifier(key, state) 48 | end 49 | 50 | function keybow.sleep(time) 51 | keybow_sleep(time) 52 | end 53 | 54 | function keybow.usleep(time) 55 | keybow_usleep(time) 56 | end 57 | 58 | function keybow.text(text) 59 | for i = 1, #text do 60 | local c = text:sub(i, i) 61 | keybow.tap_key(c) 62 | end 63 | 64 | keybow.set_modifier(keybow.LEFT_SHIFT, false) 65 | end 66 | 67 | -- Lighting control 68 | 69 | function keybow.set_pixel(x, r, g, b) 70 | keybow_set_pixel(x, r, g, b) 71 | end 72 | 73 | function keybow.auto_lights(state) 74 | keybow_auto_lights(state) 75 | end 76 | 77 | function keybow.clear_lights() 78 | keybow_clear_lights() 79 | end 80 | 81 | function keybow.load_pattern(file) 82 | keybow_load_pattern(file) 83 | end 84 | 85 | -- Meta keys - ctrl, shift, alt and win/apple 86 | 87 | function keybow.tap_left_ctrl() 88 | keybow.set_modifier(keybow.LEFT_CTRL, keybow.KEY_DOWN) 89 | keybow.set_modifier(keybow.LEFT_CTRL, keybow.KEY_UP) 90 | end 91 | 92 | function keybow.tap_right_ctrl() 93 | keybow.set_modifier(keybow.RIGHT_CTRL, keybow.KEY_DOWN) 94 | keybow.set_modifier(keybow.RIGHT_CTRL, keybow.KEY_UP) 95 | end 96 | 97 | function keybow.tap_left_shift() 98 | keybow.set_modifier(keybow.LEFT_SHIFT, keybow.KEY_DOWN) 99 | keybow.set_modifier(keybow.LEFT_SHIFT, keybow.KEY_UP) 100 | end 101 | 102 | function keybow.tap_right_shift() 103 | keybow.set_modifier(keybow.RIGHT_SHIFT, keybow.KEY_DOWN) 104 | keybow.set_modifier(keybow.RIGHT_SHIFT, keybow.KEY_UP) 105 | end 106 | 107 | function keybow.tap_left_alt() 108 | keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_DOWN) 109 | keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_UP) 110 | end 111 | 112 | function keybow.tap_right_alt() 113 | keybow.set_modifier(keybow.RIGHT_ALT, keybow.KEY_DOWN) 114 | keybow.set_modifier(keybow.RIGHT_ALT, keybow.KEY_UP) 115 | end 116 | 117 | function keybow.tap_left_meta() 118 | keybow.set_modifier(keybow.LEFT_META, keybow.KEY_DOWN) 119 | keybow.set_modifier(keybow.LEFT_META, keybow.KEY_UP) 120 | end 121 | 122 | function keybow.tap_right_meta() 123 | keybow.set_modifier(keybow.RIGHT_META, keybow.KEY_DOWN) 124 | keybow.set_modifier(keybow.RIGHT_META, keybow.KEY_UP) 125 | end 126 | 127 | -- Function keys 128 | 129 | function keybow.tap_function_key(index) 130 | index = 57 + index -- Offset to 0x39 (F1 is 0x3a) 131 | keybow.set_key(index, true) 132 | keybow.set_key(index, false) 133 | end 134 | 135 | function keybow.ascii_to_shift(key) 136 | if not (type(key) == "string") then 137 | return false 138 | end 139 | 140 | return SHIFTED_KEYCODES.find(key) ~= nil 141 | end 142 | 143 | function keybow.ascii_to_hid(key) 144 | if not (type(key) == "string") then 145 | return key 146 | end 147 | 148 | key = key:lower() 149 | 150 | code = KEYCODES:find(key) 151 | 152 | if code == nil then return nil end 153 | 154 | return code + 3 155 | end 156 | 157 | function keybow.set_key(key, pressed) 158 | if type(key) == "string" then 159 | local hid_code = nil 160 | local shifted = SHIFTED_KEYCODES:find(key, 1, true) ~= nil 161 | 162 | if shifted then 163 | hid_code = SHIFTED_KEYCODES:find(key, 1, true) 164 | else 165 | hid_code = KEYCODES:find(key, 1, true) 166 | end 167 | 168 | if not (hid_code == nil) then 169 | hid_code = hid_code + 3 170 | if shifted then keybow.set_modifier(keybow.LEFT_SHIFT, pressed) end 171 | keybow_set_key(hid_code, pressed) 172 | end 173 | 174 | else -- already a key code 175 | keybow_set_key(key, pressed) 176 | end 177 | end 178 | 179 | function keybow.tap_enter() 180 | keybow.set_key(keybow.ENTER, true) 181 | keybow.set_key(keybow.ENTER, false) 182 | end 183 | 184 | function keybow.tap_space() 185 | keybow.set_key(keybow.SPACE, true) 186 | keybow.set_key(keybow.SPACE, false) 187 | end 188 | 189 | function keybow.tap_shift() 190 | keybow.set_key(keybow.LEFT_SHIFT, true) 191 | keybow.set_key(keybow.LEFT_SHIFT, false) 192 | end 193 | 194 | function keybow.tap_tab() 195 | keybow.set_key(keybow.TAB, true) 196 | keybow.set_key(keybow.TAB, false) 197 | end 198 | 199 | function keybow.tap_key(key) 200 | keybow.set_key(key, true) 201 | keybow.set_key(key, false) 202 | end 203 | 204 | function keybow.press_key(key) 205 | keybow.set_key(key, true) 206 | end 207 | 208 | function keybow.release_key(key) 209 | keybow.set_key(key, false) 210 | end 211 | -------------------------------------------------------------------------------- /spec/mock/mocked-keybow.lua: -------------------------------------------------------------------------------- 1 | -- Mocks some parts of the Keybow Lua module during unit tests, so we can run 2 | -- the tests outside the Keybow firmware on a standard (full-blown) Lua host 3 | -- system. 4 | 5 | --[[ 6 | Copyright 2019 Harald Albrecht 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ]]-- 26 | 27 | local busted=require("busted") 28 | local sock=require("socket") 29 | 30 | require "keybow" 31 | -- luacheck: globals keybow.no_delay 32 | keybow.no_delay = keybow.no_delay or true 33 | 34 | busted.stub(keybow, "auto_lights") 35 | busted.stub(keybow, "clear_lights") 36 | busted.stub(keybow, "load_pattern") 37 | busted.stub(keybow, "set_pixel") 38 | busted.stub(keybow, "set_key") 39 | busted.stub(keybow, "set_modifier") 40 | busted.stub(keybow, "tap_key") 41 | 42 | -- luacheck: globals keybow.sleep 43 | function keybow.sleep(ms) 44 | if not keybow.no_delay then 45 | sock.sleep(ms / 1000) 46 | end 47 | end 48 | 49 | -- luacheck: globals keybow.usleep 50 | function keybow.usleep(us) 51 | keybow.sleep(us / 1000) 52 | end 53 | 54 | return keybow -- adhere to Lua's (new) module rules 55 | -------------------------------------------------------------------------------- /spec/mocked-keybow_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | -- luacheck: globals keybow.no_delay 24 | require("mocked-keybow") 25 | 26 | describe("Mocked Keybow API", function() 27 | 28 | local sock=require("socket") 29 | 30 | local sleep = function(time, factor, sf, on) 31 | local old = keybow.no_delay 32 | keybow.no_delay = not on 33 | local start = sock.gettime() 34 | sf(time) 35 | local delay = (sock.gettime() - start) * factor 36 | keybow.no_delay = old 37 | return delay 38 | end 39 | 40 | it("delays ms or not", function() 41 | assert.is_true(sleep(10, 1000, keybow.sleep, true) >= 10) 42 | assert.is_true(sleep(10, 1000, keybow.sleep, false) < 10) 43 | end) 44 | 45 | it("delays us or not", function() 46 | assert.is_true(sleep(10, 1000*1000, keybow.usleep, true) >= 10) 47 | assert.is_true(sleep(10, 1000*1000, keybow.usleep, false) < 10) 48 | end) 49 | 50 | end) 51 | -------------------------------------------------------------------------------- /spec/snippets/keys_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | local mb = require("snippets/multibow") 25 | 26 | describe("multibow keys", function() 27 | 28 | local tap = spy.on(keybow, "tap_key") 29 | local mod = spy.on(keybow, "set_modifier") 30 | 31 | before_each(function() 32 | tap:clear() 33 | mod:clear() 34 | end) 35 | 36 | it("taps a plain honest key", function() 37 | mb.tap("x") 38 | assert.spy(tap).was.called(1) 39 | assert.spy(tap).was.called_with("x") 40 | assert.spy(mod).was_not.called() 41 | end) 42 | 43 | it("taps a plain honest key", function() 44 | mb.tap("x", keybow.LEFT_CTRL, keybow.LEFT_SHIFT) 45 | assert.spy(tap).was.called(1) 46 | assert.spy(mod).was.called(4) 47 | for _, ud in pairs({keybow.KEY_DOWN, keybow.KEY_UP}) do 48 | assert.spy(mod).was.called_with(keybow.LEFT_CTRL, ud) 49 | assert.spy(mod).was.called_with(keybow.LEFT_SHIFT, ud) 50 | end 51 | end) 52 | 53 | it("taps the same key repeatedly", function() 54 | mb.tap_times("x", 3) 55 | assert.spy(tap).was.called(3) 56 | assert.spy(tap).was.called_with("x") 57 | end) 58 | 59 | it("taps the same key repeatedly with modifiers", function() 60 | mb.tap_times("x", 3, keybow.LEFT_CTRL) 61 | assert.spy(tap).was.called(3) 62 | assert.spy(tap).was.called_with("x") 63 | assert.spy(mod).was.called(2) 64 | for _, ud in pairs({keybow.KEY_DOWN, keybow.KEY_UP}) do 65 | assert.spy(mod).was.called_with(keybow.LEFT_CTRL, ud) 66 | end 67 | end) 68 | 69 | end) 70 | -------------------------------------------------------------------------------- /spec/snippets/leds_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | local mb = require("snippets/multibow") 25 | 26 | describe("multibow LEDs", function() 27 | 28 | it("controls brightness", function() 29 | mb.set_brightness(0.5) 30 | assert.equals(0.5, mb.brightness) 31 | 32 | mb.set_brightness(1.0) 33 | assert.equals(1.0, mb.brightness) 34 | 35 | mb.set_brightness(0) 36 | assert.equals(mb.MIN_BRIGHTNESS, mb.brightness) 37 | 38 | mb.set_brightness(20) 39 | assert.equals(0.2, mb.brightness) 40 | end) 41 | 42 | it("cycles brightness", function() 43 | local f = function(b, scale) 44 | local copy = table.pack(table.unpack(b)) 45 | local len = #b 46 | for i = 1, len do 47 | mb.cycle_brightness(copy) 48 | assert.equals(b[i], mb.brightness * scale) 49 | end 50 | end 51 | 52 | f({ 0.7, 1.0, 0.4 }, 1) 53 | f({ 70, 100, 40 }, 100) 54 | end) 55 | 56 | inslit("accepts LED color functions in keymaps", function() 57 | local s = spy.on(mb, "led") 58 | local km = { 59 | name="test", 60 | [0]={c={r=0, g=1, b=0}}, 61 | [1]={c=function() return {r=1, g=1, b=1} end} 62 | } 63 | 64 | mb.activate_keymap_leds(km) 65 | assert.spy(s).was.called(2) 66 | assert.spy(s).was.called_with(0, {r=0, g=1, b=0}) 67 | assert.spy(s).was.called_with(1, {r=1, g=1, b=1}) 68 | end) 69 | 70 | end) 71 | -------------------------------------------------------------------------------- /spec/snippets/multibow_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | 25 | describe("multibow", function() 26 | 27 | -- ensure to get a fresh multibow module instance each time we run 28 | -- an isolated, nope, insulated test... 29 | local mb 30 | 31 | before_each(function() 32 | require("keybow") 33 | mb = require("snippets/multibow") 34 | end) 35 | 36 | inslit("adds permanent keyboard layout, but doesn't activate it", function() 37 | local permkm = { 38 | name="permanent", 39 | permanent=true 40 | } 41 | assert.is_nil(mb.keymaps["permanent"]) 42 | mb.register_keymap(permkm) 43 | assert.is.equal(mb.keymaps["permanent"], permkm) 44 | assert.is_nil(mb.current_keymap) 45 | end) 46 | 47 | inslit("checks multibow module is fresh again", function() 48 | assert.is_nil(mb.keymaps["permanent"]) 49 | end) 50 | 51 | inslit("adds permanent, then two primary layouts, activates only first primary layout", function() 52 | local permkm = { 53 | name="permanent", 54 | permanent=true 55 | } 56 | mb.register_keymap(permkm) 57 | local prim1km = { name="bavaria-one" } 58 | local prim2km = { name="primary-two" } 59 | mb.register_keymap(prim1km) 60 | mb.register_keymap(prim2km) 61 | assert.is.equal(prim1km, mb.current_keymap) 62 | end) 63 | 64 | inslit("sequence of primary keymaps is in registration order", function() 65 | local prim1km = { name="last" } 66 | local prim2km = { name="first" } 67 | mb.register_keymap(prim1km) 68 | mb.register_keymap(prim2km) 69 | assert.is.same(mb.registered_primary_keymaps(), {prim1km, prim2km}) 70 | end) 71 | 72 | inslit("adds secondary, then primary layout, activates only primary layout", function() 73 | local primkm = { name="berlin" } 74 | local seckm = { name="munich", secondary=true } 75 | mb.register_keymap(seckm) 76 | mb.register_keymap(primkm) 77 | assert.is.equal(primkm, mb.current_keymap) 78 | end) 79 | 80 | inslit("cycles primary keymaps based on primary-secondary names substring match", function() 81 | -- on purpose, the names of the primary keymaps are in reverse lexical order, 82 | -- to make sure that cycling follows the registration order, but not the 83 | -- name order. 84 | local prim1km = { name= "last" } 85 | local sec1km = { name="last-shift", secondary=true } 86 | local sec2km = { name="xlast-shift", secondary=true} 87 | local prim2km = { name= "first" } 88 | mb.register_keymap(prim1km) 89 | mb.register_keymap(prim2km) 90 | mb.register_keymap(sec1km) 91 | mb.register_keymap(sec2km) 92 | assert.is.equal(4, #mb.registered_keymaps()) 93 | assert.is.same(mb.registered_primary_keymaps(), {prim1km, prim2km}) 94 | 95 | -- cycles from secondary to next primary 96 | mb.activate_keymap(sec1km.name) 97 | mb.cycle_primary_keymaps() 98 | assert.is.equal(prim2km.name, mb.current_keymap.name) 99 | -- cycles from last primary to first primary 100 | mb.cycle_primary_keymaps() 101 | assert.is.equal(prim1km.name, mb.current_keymap.name) 102 | -- cannot cycle from misnamed secondary without shift_to 103 | mb.activate_keymap(sec2km.name) 104 | mb.cycle_primary_keymaps() 105 | assert.is.equal(sec2km.name, mb.current_keymap.name) 106 | end) 107 | 108 | inslit("cycles primary keymaps based on shift_to", function() 109 | -- on purpose, the names of the primary keymaps are in reverse lexical order, 110 | -- to make sure that cycling follows the registration order, but not the 111 | -- name order. 112 | local prim1km = { name= "last", shift_to=nil } 113 | local sec1km = { name="last-shift", secondary=true, shift_to=nil } 114 | prim1km.shift_to = sec1km 115 | local prim2km = { name= "first" } 116 | mb.register_keymap(prim1km) 117 | mb.register_keymap(prim2km) 118 | mb.register_keymap(sec1km) 119 | assert.is.same(mb.registered_primary_keymaps(), {prim1km, prim2km}) 120 | 121 | -- cycles from secondary to next primary 122 | mb.activate_keymap(sec1km.name) 123 | mb.cycle_primary_keymaps() 124 | assert.is.equal(prim2km.name, mb.current_keymap.name) 125 | -- cycles from last primary to first primary 126 | mb.cycle_primary_keymaps() 127 | assert.is.equal(prim1km.name, mb.current_keymap.name) 128 | end) 129 | 130 | inslit("sets up multibow, activates lights", function() 131 | local s = spy.on(_G, "setup") 132 | local al = spy.on(mb, "activate_leds") 133 | 134 | _G.setup() 135 | assert.spy(s).was.called(1) 136 | assert.spy(al).was.called(1) 137 | 138 | s:revert() 139 | al:revert() 140 | end) 141 | 142 | inslit("has more keys", function() 143 | assert.is_not_nil(keybow.F13) 144 | assert.is.equal(0x68, keybow.F13) 145 | end) 146 | 147 | end) 148 | -------------------------------------------------------------------------------- /spec/snippets/routehandlers_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2019 Harald Albrecht 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ]]-- 22 | 23 | require "mocked-keybow" 24 | local hwk = require("spec/hwkeys") 25 | 26 | describe("Multibow route handlers", function() 27 | 28 | -- ensure to get a fresh multibow module instance each time we run 29 | -- an isolated test... 30 | local mb 31 | 32 | local spies = mock({ 33 | prim_key_press=function(_) end, 34 | prim_key_release=function(_) end, 35 | prim_otherkey_press=function(_) end, 36 | prim_otherkey_release=function(_) end, 37 | 38 | sec_key_press=function(_) end, 39 | sec_key_release=function(_) end, 40 | 41 | perm_key_press=function(_) end, 42 | perm_key_release=function(_) end, 43 | 44 | grab_key_press=function(_) end, 45 | grab_key_release=function(_) end, 46 | }) 47 | 48 | local primary_keymap = { 49 | name="test", 50 | [0]={press=spies.prim_key_press, release=spies.prim_key_release}, 51 | [1]={press=spies.prim_otherkey_press, release=spies.prim_otherkey_release}, 52 | } 53 | local secondary_keymap = { 54 | name="test-secondary", 55 | secondary=true, 56 | [0]={press=spies.sec_key_press, release=spies.sec_key_release}, 57 | } 58 | local permanent_keymap = { 59 | name="permanent", 60 | permanent=true, 61 | [0]={press=spies.perm_key_press, release=spies.perm_key_release} 62 | } 63 | local grab_keymap = { 64 | name="grab", 65 | [-1]={press=spies.grab_key_press, release=spies.grab_key_release}, 66 | [0]={press=spies.grab_key_press, release=spies.grab_key_release} 67 | } 68 | 69 | before_each(function() 70 | mb = require("snippets/multibow") 71 | -- make sure to clear our spies 72 | for _, schlapphut in pairs(spies) do 73 | schlapphut:clear() 74 | end 75 | end) 76 | 77 | insl(function() 78 | 79 | it("defines all Keybow key handlers for routing", function() 80 | for keyno = 0, 11 do 81 | assert.is_function(_G[string.format("handle_key_%02d", keyno)]) 82 | end 83 | end) 84 | 85 | it("calls routing handler with correct key number", function() 86 | stub(mb, "route") 87 | for keyno = 0, 11 do 88 | mb.route:clear() 89 | hwk.press(keyno) 90 | assert.stub(mb.route).was.called(1) 91 | assert.stub(mb.route).was.called_with(keyno, true) 92 | mb.route:clear() 93 | hwk.release(keyno) 94 | assert.stub(mb.route).was.called(1) 95 | assert.stub(mb.route).was.called_with(keyno, false) 96 | end 97 | mb.route:revert() 98 | end) 99 | 100 | end) 101 | 102 | insl(function() 103 | 104 | it("doesn't fail for unroutable keys", function() 105 | hwk.press(0) 106 | hwk.release(0) 107 | end) 108 | 109 | -- start with secondary keymap only :) 110 | it("doesn't route a key press to a registered secondary keymap without activation", function() 111 | assert.is_not_truthy(secondary_keymap.permanent) 112 | assert.is_truthy(secondary_keymap.secondary) 113 | mb.register_keymap(secondary_keymap) 114 | 115 | assert.spy(spies.sec_key_press).was_not.called() -- just a safety guard 116 | hwk.press(0) 117 | assert.spy(spies.sec_key_press).was_not.called() 118 | end) 119 | 120 | it("routes key press to activated secondary keymap", function() 121 | mb.activate_keymap(secondary_keymap.name) 122 | 123 | hwk.press(0) 124 | assert.spy(spies.sec_key_press).was.called(1) 125 | assert.spy(spies.sec_key_press).was.called_with(0) 126 | end) 127 | 128 | -- throw in a primary keymap 129 | it("routes key press to primary keymap, but not to secondary", function() 130 | assert.is_not_truthy(primary_keymap.permanent or primary_keymap.secondary) 131 | mb.register_keymap(primary_keymap) 132 | mb.activate_keymap(primary_keymap.name) 133 | 134 | assert.spy(spies.prim_key_press).was_not.called() -- just a safety guard 135 | hwk.press(0) 136 | assert.spy(spies.prim_key_press).was.called(1) 137 | assert.spy(spies.prim_key_press).was.called_with(0) 138 | assert.spy(spies.prim_key_release).was_not.called() 139 | end) 140 | 141 | it("routes key release to primary keymap", function() 142 | assert.spy(spies.prim_key_press).was_not.called() -- just a safety guard 143 | hwk.release(0) 144 | assert.spy(spies.prim_key_press).was_not.called() 145 | assert.spy(spies.prim_key_release).was.called(1) 146 | assert.spy(spies.prim_key_release).was.called_with(0) 147 | end) 148 | 149 | -- adds a permanent keymap on top 150 | it("routes with priority key press/release to permanent key", function() 151 | assert.is_truthy(permanent_keymap.permanent) 152 | assert.is_not_truthy(permanent_keymap.secondary) 153 | mb.register_keymap(permanent_keymap) 154 | 155 | hwk.press(0) 156 | assert.spy(spies.perm_key_press).was.called(1) 157 | assert.spy(spies.perm_key_press).was.called_with(0) 158 | assert.spy(spies.perm_key_release).was_not.called() 159 | 160 | assert.spy(spies.prim_key_press).was_not_called() 161 | end) 162 | 163 | it("correctly routes key press to un-overlaid primary key 2", function() 164 | assert.spy(spies.prim_otherkey_press).was_not_called() 165 | hwk.press(1) 166 | assert.spy(spies.prim_otherkey_press).was.called(1) 167 | assert.spy(spies.prim_otherkey_press).was.called_with(1) 168 | end) 169 | 170 | -- and finally adds a grab keymap, what a mess! 171 | it("routes to grab keymap", function() 172 | mb.register_keymap(grab_keymap) 173 | mb.grab(grab_keymap.name) 174 | 175 | -- grab routes to grab handler *AND* grab any handler 176 | assert.spy(spies.grab_key_press).was.called(0) 177 | hwk.press(0) 178 | -- remember: this grab has an "any" handler 179 | assert.spy(spies.grab_key_press).was.called(2) 180 | assert.spy(spies.grab_key_press).was.called_with(0) 181 | 182 | spies.grab_key_press:clear() 183 | hwk.press(1) 184 | assert.spy(spies.grab_key_press).was.called(1) 185 | assert.spy(spies.grab_key_press).was.called_with(1) 186 | 187 | spies.grab_key_press:clear() 188 | mb.ungrab() 189 | hwk.press(0) 190 | assert.spy(spies.grab_key_press).was.called(0) 191 | assert.spy(spies.perm_key_press).was.called(1) 192 | assert.spy(spies.prim_key_press).was.called(0) 193 | end) 194 | 195 | end) 196 | 197 | end) 198 | --------------------------------------------------------------------------------