Attempt at dual-role modifier keys

Post your working scripts, libraries and tools for AHK v1.1 and older
42td
Posts: 4
Joined: 20 Oct 2018, 11:15

Attempt at dual-role modifier keys

21 Oct 2018, 11:32

I was asked at geekhack to share my attempts at dual-role modifier keys, so here it is.

The automated test suggests otherwise, but in practice it doesn't work reliably: Often, modifiers or layers get stuck, i.e. there seem to be key up events missing. I have currently no plans to continue work on this script, but maybe someone wants to take over or use parts of it. (I moved on to another layout with dedicated modifier keys only.)
The documentation parts (meant to display the currently active layer in a GUI) used to work in an older version, but have not been adapted to a major refactoring, so they are disabled.

The concrete mapping is meant for a keyboard rotated 180 degrees (so that the function keys become the thumb keys), but could be easily adapted to other needs.

I didn't copy the MIT license into all of the files / code sections, but it should apply to all of them.

AFAIK according to German laws one cannot fully disclaim liability, so I am sharing it in a deactivated form. If you want to use this at your own risk, you need to figure out how to enable the send part. Sorry for the inconvenience (msgboxes)!

File multilayer_kb_layout_turned.ahk (the file to customize & start; others are includes)

Code: Select all

; MIT License
; 
; Copyright (c) 2018 42td
; 
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
; 
; The above copyright notice and this permission notice shall be included in all
; copies or substantial portions of the Software.
; 
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
; SOFTWARE.

; Keyboard layout for a keyboard turned 180 degrees (so that the F keys become
; the thumb keys).

EnvGet, SystemRoot, SystemRoot
Menu Tray, Icon, %SystemRoot%\system32\shell32.dll, 45 ;174
FileEncoding, UTF-8-RAW

try {
#Include %A_ScriptDir%\util_dangerous.ahk
#Include %A_ScriptDir%\multilayer_kb_layout_util.ahk

defMod("L7")
defMod("LN")
defMod("L²")
defMod("LF")
defMod("L(")
defMod("L~")
defMod("L↤")
defMod("Lö")
defMod("L±")
defMod("L⊞")
defMod("Lα")
defMod("L∅")
defMod("L«")
defMod("L⇒")
defMod("L→")
defMod("L✓")
defMod("L┘")
defMod("L╝")
defMod("s")	; shift-like layer
defMod("Rp")	; repeat
defMod("Mc")	; macro
defMod("S",	"{Shift}"  )
defMod("C",	"{Control}")
defMod("A",	"{Alt}"    )
defMod("G",	"{AltGr}"  )
defMod("W",	"{LWin}"   )

defModFallback("s", "S")

defAlias("---"   , NOP          )
defAlias("Ret"   , "Return"     )
defAlias("Sp"    , "S:{Space}"  )
defAlias("SPC*"  , "u+00a0"     )
defAlias("FF"    , "u+240c"     )
defAlias("FS"    , "u+241c"     )
defAlias("GS"    , "u+241d"     )
defAlias("RS"    , "u+241e"     )
defAlias("US"    , "u+241f"     )
defAlias("Menu"  , "AppsKey"    )
defAlias("Men"   , "AppsKey"    )
defAlias("NPIns" , "NumpadIns"  )
defAlias("NPDel" , "NumpadDel"  )
defAlias("NPEnd" , "NumpadEnd"  )
defAlias("NPDn"  , "NumpadDown" )
defAlias("NPPDn" , "NumpadPgDn" )
defAlias("NPLe"  , "NumpadLeft" )
defAlias("NPCl"  , "NumpadClear")
defAlias("NPRi"  , "NumpadRight")
defAlias("NPHo"  , "NumpadHome" )
defAlias("NPUp"  , "NumpadUp"   )
defAlias("NPPUp" , "NumpadPgUp" )
defAlias("NP+"   , "NumpadAdd"  )
defAlias("NP-"   , "NumpadSub"  )
defAlias("NP*"   , "NumpadMult" )
defAlias("NP/"   , "NumpadDiv"  )
defAlias("NPEnt" , "NumpadEnter")
defAlias("(́́´)"   , "u+0301"     ) ; dead acute
defAlias("(́́`)"   , "u+0300"     ) ; dead grave
defAlias("(́́^)"   , "u+0302"     ) ; dead circumflex
defAlias("(~)"   , "u+0303"     ) ; dead tilde
defAlias("(́°)"   , "u+030a"     ) ; dead circle
defAlias("rp!"   , "F::repeat"  )

defPrefixAlias("rp", "F::enterRepeatCount")
defPrefixAlias("mr", "F::mr")
defPrefixAlias("mp", "F::mp")

mr(macroId) {
	macroRecord(macroId)
}
mp(macroId, n = 1) {
	macroPlay(macroId, n)
}

; === documentation support ===
; docTemplate should contain placeholders "§F10§", "§u§", etc
; corresponding to the used hwKeys. Because those variable length strings would
; make the character art unreadable, we initially write them as "[1]", "[2]",
; "[3]", "[T]", and replace those with "§{}§" using strReplace, and then the
; "{}" with "NumpadAdd" etc using format.
; TODO update docTemplate
global docTemplate
docTemplate =
( %
┌───┬───┬───────┐┌───┬───┬───┐┌────┬────┬────┬────┬────────────────────────┬────┬────┬────┐
│ ✗ │ ? │Men/WIN││ ← │ ↑ │ → ││ A← │ A→ │    │    │                        │    │    │    │
│ L ├───┼───┬───┤└───┼───┼───┘├────┴────┴┬───┼───┬┴──┬───┬───┬───┬───┬───┬─┴─┬──┴┬───┼────┤
│ 0 │[3]│[3]│[3]│    │ ↓ │    │     ⇑    │   │   │   │   │   │   │   │   │   │   │   │    │
├───┼───┼───┼───┤    └───┘    ├────┬───┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────┤
│[2]│[2]│[2]│[2]│             │    │ ♫+│ ◂◂│ ▸▸│   │   │[3]│[3]│[3]│   │   │   │   │      │
│   ├───┼───┼───┤┌───┬───┬───┐│ ↵  └┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬─────┤
│   │[1]│[1]│[1]││ P↑│Hom│Del││     │ ♫-│ ■ │ ▶ │   │   │[2]│[2]│[2]│[2]│   │   │   │     │
├───┼───┼───┼───┤├───┼───┼───┤├─────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬───┤
│E/C│ ⇥ │Spc│ ↵ ││ P↓│End│Ins││   ⇐   │ ♫✗│   │   │   │   │[1]│[1]│[1]│ ↵ │   │   │   │   │
└───┴───┴───┴───┘└───┴───┴───┘└───────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
                 ┌───┬───┬───┐┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐   ┌───┐
                 │[T]│[T]│[T]││CTR│ALT│SHI│WIN│ │[T]│[T]│[T]│[T]│ │CTR│ALT│SHI│WIN│   │Esc│
                 └───┴───┴───┘└───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘   └───┘
)
;docTemplate := format(strReplace(docTemplate, "[3]", "§{}§"), hwKeys3*)
;docTemplate := format(strReplace(docTemplate, "[2]", "§{}§"), hwKeys2*)
;docTemplate := format(strReplace(docTemplate, "[1]", "§{}§"), hwKeys1*)
;docTemplate := format(strReplace(docTemplate, "[T]", "§{}§"), hwKeysT*)

; === which hardware keys to use ===
; These are used in defineLayer: hwKeys1: top row, ..., hwKeys4: bottom row; hwKeysT: thumb row
global hwKeys := unpretty("│", " │─┌┬┐└┴┘├┼┤"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│       RShift        │ sc035 │ sc034 │ sc033 │   m   │   n   │   b   │   v   │   c   │   x   │   y   │ sc056 │  LShift │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │ sc02b │ sc028 │ sc027 │   l   │   k   │   j   │   h   │   g   │   f   │   d   │   s   │   a   │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   Enter   │ sc01b │ sc01a │   p   │   o   │   i   │   u   │   z   │   t   │   r   │   e   │   w   │   q   │    Tab    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   Backspace   │ sc00d │ sc00c │   0   │   9   │   8   │   7   │   6   │   5   │   4   │   3   │   2   │   1   │ sc029 │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  F8   │  F7   │  F6   │  F5   │   │  F4   │  F3   │  F2   │  F1   │       │       │")
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

; === assign keys to send ===
defKeys("", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "a", "main"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│     [Mc]Esc         │  F1   │  F2   │  F3   │  F4   │  F5   │  F6   │  F7   │  F8   │  F9   │  F10  │  F11  │   F12   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │ [LN]""│ [L╝]q │ [L┘]w │ [L→]e │ [L⇒]r │[L✓]t  │   z   │   u   │   i   │   o   │   p   │   ß   │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│    Tab    │ Space │ [L↤]a │ [Lö]s │ [L~]d │ [L(]f │ [L«]g │   h   │   j   │   k   │   l   │   x   │   y   │   Enter   │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│    [L⊞]Menu   │ [Lö]- │ [L±]+ │ [Lα]/ │ [L7]c │ [L²]v │ [L∅]b │   n   │   m   │   ,   │   .   │   |   │   ?   │   ^   │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  [A]  │  [C]  │  [s]  │  [s]  │   │  [C]  │  [A]  │  [W]  │ Menu  │       │       │"))
; └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

defKeys("s", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "A", "main caps"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│        +Esc         │  +F1  │  +F2  │  +F3  │  +F4  │  +F5  │  +F6  │  +F7  │  +F8  │  +F9  │ +F10  │ +F11  │  +F12   │"
; ├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤
,"│         │   '   │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ẞ   │             │"
; │         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤
,"│    +Tab   │+Space │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  +Enter   │"
; ├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤
,"│    Alt        │   _   │   *   │   \   │   &   │  ---  │  ---  │  ---  │  ---  │  `;   │   :   │  ---  │   !   │   ~   │"
; └───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │+Space │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
; └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

defKeys("L(", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "[", "brackets, …"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   ‑   │  gSP  │   ‒   │   –   │  ---  │  ---  │   ---   │"
; ├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ␣   │   {   │   }   │   %   │  ⸺  │  ---  │             │"
; │         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   -   │   (   │   )   │   _   │   =   │  ---  │    ---    │"
; ├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   &   │   [   │   ]   │   *   │   @   │  ---  │  ---  │"
; └───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
; └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

defKeys("L~", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "~", "shell, …"
,"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   …   │   ‥   │   ·   │   ´   │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ~   │   #   │   $   │   ^   │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   <   │   >   │   ""  │   '   │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   °   │   |   │   /   │   \   │  ``   │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

defKeys("L↤", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "↤", "movement"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │ +Ins  │ ^Left │  Up   │^Right │  BS   │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  Ins  │ Left  │ Down  │ Right │ PgUp  │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │ Home  │  Del  │  End  │ PgDn  │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

defKeys("LF", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "F", "function keys"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  F10  │  F11  │  F12  │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  F7   │  F8   │  F9   │  ---  │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  F4   │  F5   │  F6   │  ---  │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  F1   │  F2   │  F3   │  ---  │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

defKeys("L«", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "«", "quotes, question marks, vbars"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   ⁉   │   ⁈   │   ¿   │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   “   │   „   │   ”   │  ---  │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   «   │   »   │   ‖   │  ---  │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ≪   │   ≫  │   ¦   │  ---  │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

defKeys("Lö", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "ö", "umlauts, dead accents"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   Ä   │   Ö   │   Ü   │   ẞ   │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ä   │   ö   │   ü   │   ß   │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  (́°)  │  (́́´)  │  (́́`)  │  (́́^)  │  (~)  │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L±", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "±", "math"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │    ⁄   │   ∕   │   ∑   │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ≟   │   ¼   │   ½   │   ¾   │   ≉   │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ≘   │   ≤   │   ≥   │   ≠   │   ≈   │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ÷   │   ×   │   ✕   │   ⋆   │   ±   │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("Lα", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "α", "greek, currency, misc"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   ©   │   ®   │   ™   │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   §   │   ¶   │   π   │   µ   │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   α   │   β   │   γ   │   δ   │   ε   │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   €   │   ¢   │   £   │   ¤   │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L7", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "7", "numbers"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   +   │   -   │   *   │   /   │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │ L:000 │   7   │   8   │   9   │ L:000 │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   0   │   4   │   5   │   6   │   .   │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   0   │   1   │   2   │   3   │   ,   │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("LN", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "N", "num pad"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  NP+  │  NP-  │  NP*  │  NP/  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  NPHo │  NPUp │ NPPUp │ NPIns │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  NPLe │  NPCl │  NPRi │ NPDel │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │ NPEnd │  NPDn │ NPPDn │ NPEnt │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L²", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "²", "superscript numbers"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   ⁺   │   ⁻   │   ⁽   │   ⁾   │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │ L:⁰⁰⁰ │   ⁷   │   ⁸   │   ⁹   │   ⁿ   │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ⁰   │   ⁴   │   ⁵   │   ⁶   │  ---  │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ⁰   │   ¹   │   ²   │   ³   │  ---  │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L∅", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "∅", "set math, logic"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   ∀   │   ∃   │   ∄   │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ⊆   │   ⊇   │   ∩   │   ∪   │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ∅   │   ⊂   │   ⊃   │   ∈   │   ∉   │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ⦰   │   ∖   │   ¬   │   ∧   │   ∨   │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L⊞", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "⊞7", "Windows+Numbers"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  #7   │  #8   │  #9   │  ---  │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  #0   │  #4   │  #5   │  #6   │  ---  │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  #0   │  #1   │  #2   │  #3   │  ---  │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L┘", hwKeys, unpretty("║", " ║═╔╦╗╚╩╝╠╬╣"	; "┘", "box, single line"
;"╔═════════════════════╦═══════╦═══════╦═══════╦═══════╦═══════╦═══════╦═══════╦═══════╦═══════╦═══════╦═══════╦═════════╗"
,"║         ---         ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║   ---   ║"
;"╠═════════╦═══════╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═════════╣"
,"║         ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║   ┌   ║   ┬   ║   ┐   ║  ---  ║  ---  ║             ║"
;"║         ╚═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═════╩═╦═══════════╣"
,"║   ---     ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║   ├   ║   ┼   ║   ┤   ║   ─   ║  ---  ║    ---    ║"
;"╠═══════════╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══╩═══╦═══════╣"
,"║   ---         ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║  ---  ║   └   ║   ┴   ║   ┘   ║   │   ║  ---  ║  ---  ║"
;"╚═══════════════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════╝"
;"╔═══════╦═══════╦═══════╦═══════╗   ╔═══════╦═══════╦═══════╦═══════╗   ╔═══════╦═══════╦═══════╦═══════╗       ╔═══════╗"
,"║       ║       ║       ║       ║   ║  ---  ║  ---  ║  ---  ║  ---  ║   ║  ---  ║  ---  ║  ---  ║  ---  ║       ║       ║"))
;"╚═══════╩═══════╩═══════╩═══════╝   ╚═══════╩═══════╩═══════╩═══════╝   ╚═══════╩═══════╩═══════╩═══════╝       ╚═══════╝"

defKeys("L╝", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "╝", "box, double line"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ╔   │   ╦   │   ╗   │  ---  │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ╠   │   ╬   │   ╣   │   ═   │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ╚   │   ╩   │   ╝   │   ║   │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L→", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "→", "arrows"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   ☩   │   ↕   │   ✛   │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ↖   │   ↑   │   ↗   │  ---  │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ←   │   ↓   │   →   │  ---  │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ↙   │   ↔   │   ↘   │  ---  │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L⇒", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "⇒", "double arrows"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ⇕   │   FF  │   US  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ⇖   │   ⇑   │   ⇗   │   RS  │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ⇐   │   ⇓   │   ⇒   │   GS  │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ⇙   │   ⇔   │   ⇘   │   FS  │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("L✓", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "✓", "check symbols, cards, …"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │   ⌂   │   ★   │   ✻   │   ♬   │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ☑   │   ☒   │   ☐   │   ♫   │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   •   │   ✓   │   ✗   │   ‣   │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ♣   │   ♠   │   ♥   │   ♦   │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘"

defKeys("Rp", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "r…", "repeat"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  rp:7 │  rp:8 │  rp:9 │  ---  │  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  rp:0 │  rp:4 │  rp:5 │  rp:6 │  rp!  │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  rp:0 │  rp:1 │  rp:2 │  rp:3 │  rp:- │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

defKeys("Mc", hwKeys, unpretty("│", " │─┌┬┐└┴┘├┼┤"	; "M…", "macros"
;"┌─────────────────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┐"
,"│         ---         │  ---  │  ---  │  ---  │  ---  │mp:0:50│mp:1:50│mp:2:50│mp:3:50│mp:4:50│  ---  │  ---  │   ---   │"
;"├─────────┬───────┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴─────────┤"
,"│         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │mp:0:10│mp:1:10│mp:2:10│mp:3:10│mp:4:10│  ---  │             │"
;"│         └─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬─────┴─┬───────────┤"
,"│   ---     │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  mp:0 │  mp:1 │  mp:2 │  mp:3 │  mp:4 │  ---  │    ---    │"
;"├───────────┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───┴───┬───────┤"
,"│   ---         │  ---  │  ---  │  ---  │  ---  │  ---  │  ---  │  mr:0 │  mr:1 │  mr:2 │  mr:3 │  mr:4 │  ---  │  ---  │"
;"└───────────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘"
;"┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐   ┌───────┬───────┬───────┬───────┐       ┌───────┐"
,"│       │       │       │       │   │  ---  │  ---  │  ---  │  ---  │   │  ---  │  ---  │  ---  │  ---  │       │       │"))
;"└───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘   └───────┴───────┴───────┴───────┘       └───────┘

; === advanced hotkeys ===
defKey("", "RAlt", "[Rp]")
defKey("", "NumpadSub",  "[s]Enter")
defKey("", "NumpadMult", "[C]Home")
defKey("", "NumpadDiv",  "[A]PgDn")
defKey("", "NumLock",    "[W]End")
defkey("s", "NumpadDel", "Volume_Mute")

} catch e {
	msgbox, %e%
	exitapp
}

; === static hotkeys ===
sc039:: resetState()  ; space, avoiding conflict with F5 -> Space mapping
RWin::  toggleHelp(docTemplate, "§", 3)

*RShift:: Esc
*sc035::  F1
*sc034::  F2
*sc033::  F3
*m:: 	  F4
*n:: 	  F5
*b:: 	  F6
*v:: 	  F7
*c:: 	  F8
*x:: 	  F9
*y:: 	  F10
*sc056::  F11
*LShift:: F12

; *Enter:: Tab
; *Tab:: Enter

*F12:: Del
*F11:: Ins
*F10:: LShift
*F9::  LWin
;*F8::  RAlt
;*F7::  LAlt
;*F6::  LControl
;*F5::  Space
;*F4::  RControl
;*F3::  LAlt
;*F2::  RAlt
;*F1::  RWin

Right:: Pause
Down::  ScrollLock
Left::  PrintScreen
;Up::    

Pause::       Send #1
ScrollLock::  Send #2
PrintScreen:: Send #3
PgUp::        Send #4
Home::        Send #5
Ins::         Send #6
PgDn::        Send #7
End::         Send #8
Del::         Send #9

 NumpadEnter::  Media_Play_Pause
+NumpadEnter::  Media_Stop
 NumpadDel::    Volume_Down
 NumpadIns::    Volume_Up
*NumpadPgDn::   Media_Prev
*NumpadDown::   Up
*NumpadEnd::    Media_Next
*NumpadAdd::    Tab
*NumpadRight::  Left
*NumpadClear::  Down
*NumpadLeft::   Right
 NumpadPgUp::   !Left
 NumpadUp::     PgUp
*NumpadHome::   !Right
;*NumpadSub::    Enter
;*NumpadMult::   Home
;*NumpadDiv::    PgDn
;*NumLock::      End

Pause & ScrollLock::
	Suspend
	resetState()
	return
Pause & PrintScreen:: ExitApp
File multilayer_kb_layout_util.ahk (the interesting (?) part)

Code: Select all

; MIT License
; 
; Copyright (c) 2018 42td
; 
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
; 
; The above copyright notice and this permission notice shall be included in all
; copies or substantial portions of the Software.
; 
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
; SOFTWARE.

; Utilities for multi-layer keyboard layouts.
; Wording:
; - Hardware key: ("Hardware" used loosely) "The left side of the hotkey assignment", like a scancode, or character
;     referring to the OS keyboard layout.
; - Key action: What this script should send (key, key combination, and/or modifier).
;   Types of key actions:
;   • modifier (MK): one of SMK or LMK
;   • - subtype standard modifier (SMK): a modifier which is also exists on default keyboards (Shift, Alt, …)
;   • - subtype layer modifier (LMK): a modifier used internally in layer selection (but never sent to the outside)
;   • tap key (TK): a key which immediately performs some action, like entering a character or control (enter, arrows, …)
;     - subtype RTK: to be sent literally (using sendRaw command)
;     - subtype CTK: opposite of raw ("cooked"), i.e. to be sent using send command
;   • dual-role modifier / tap key (MTK): A key which acts as modifier when combined with another, and as TK when tapped.
;     - subtype SMTK: MTK with a standard modifier
;     - subtype LMTK: MTK with a layer modifier
; Features:
; - Sticky modifier: If a key is assigned to a modifier exclusively, it can also be tapped on its own to modify only
;   the next tap key.

#SingleInstance force
#Persistent
#NoEnv
SetBatchLines -1
ListLines Off
#Warn, , MsgBox
Process, Priority, , High
SendMode Input
#Include %A_ScriptDir%\util.ahk

; ==================== constants ====================
global RTK  := "RTK"
global CTK  := "CTK"
global SMK  := "SMK"
global LMK  := "LMK"
global SMTK := "SMTK"
global LMTK := "LMTK"

global NOP := "S:"
global MOD_SEP := "|"

; combinations of modStatesLock and modStatesSticky, see functions get1ModState and set1ModState
global MS_OFF    := 0	; inactive
global MS_LOCK   := 1	; locked (active until disabled)
global MS_STICKY := 3	; sticky (active once)

log("==================== " . A_ScriptName . " ====================")

; ==================== config variables ====================
global keyMap := {}	; map [layer, hardware key] -> KeyAction or Func object
global nextModBit := 1
global modMap := ComObjCreate("Scripting.Dictionary")	; map modifier name -> bit, e.g. "SH" -> 8; Dictionary because associative arrays are case insensitive
; map: standard modifier bit -> send string, e.g. 8 -> "{Shift}"
global standardModifierStrings := {}
; standard modifier bitset, e.g. modMap["SH"] | modMap["ALT"] | modMap["WIN"]
global standardModifiers := 0
global aliases := {}
global prefixAliases := {}
global modFallbacks := []

; ==================== state variables ====================
global physModStates    := 0 ; physical modifier states bit set (0 = up, 1 = down)
; TODO remove physModStatesTap?; are event.hwKey == prevKeyDown checks enough to distinguish tap and hold?
global physModStatesTap := 0 ; bit set: mods for which another key has been tapped while mod was down
global modStatesLock    := 0 ; resulting logical modifier states bit set: locked
global modStatesSticky  := 0 ; resulting logical modifier states bit set: sticky (only active once)
global pendingEvents := []
global standardModsSentDown := {}	; standard modifiers strings sent down but not up (in getModState().standard format) -> mod bitset
global activeModFallbacks := {}
global prevKeyDown := ""	; for detecting missing key up events when holding a key until it auto-repeats
global prevKey := ""
global hwKeyToDownEvent := Array()	; map hwKey -> KeyEvent at the time when hwKey was pressed down
global repeatCount := 0
global showHelp := 0
global uu := 0	; unused (more precisely unread; e.g. for `for key, value ..' loops where only one of key and value is used)

; ==================== defining key maps ====================

class KeyAction {
	; member variables:
	; - description: the representation as this was parsed from, e.g. "[M1]^Left"
	; - typ: one of RTK, CTK, SMK, LMK, SMTK, LMTK
	; - mods: (standard and layer) modifier bitset (i.e. how this action modifies other key events)
	; - tap: for MTKs, the TK as another KeyAction
	; - tk: the value to send[Raw], e.g. "F1"
	; - sendMods: modifier bitset to send in addition to tk; not used in config, only in event handling:
	;     when a fallback to standard modifiers happens, the configured action is replaced (temporarily
	;     in return values, not in the config) with an action with sendMods set accordingly.

	; description: see function defKey
	__New(description) {
		local match, standard
		log("KeyAction ctor", "new KeyAction(""" . description . """)")	;?
		this.description := description
		this.mods := 0
		this.sendMods := 0
		if (RegexMatch(description, "O)\[(.+)\](.*)", match)) {	; [MK]TK
			local modName := match.Value(1)
			this.tap := match.Value(2)
			log("KeyAction ctor", "modifier: " . modName . ", else " . this.tap)	;?
			if (this.tap != "") {
				this.tap := new KeyAction(resolveAlias(this.tap))
			}
			this.mods := modNamesToBitset(modName)
			standard := standardModifiers & this.mods != 0
			this.typ := this.tap == "" ? (standard ? SMK : LMK) : (standard ? SMTK : LMTK)
			; check: mod must be an int
			if (this.mods == "" || this.mods == 0 || this.mods != floor(this.mods)) {
				; workaround because throw in __New seems to be ignored
				this.error := "undefined or invalid modifier " . mod . " == " . this.mods . " (must be an int != 0, usually a power of 2)"
				log("KeyAction ctor", "error: " . this.error)	;?
			}
		} else {
			description := resolveAlias(description)
			if (instr(description, "L:", false) == 1) {
				this.typ := RTK
				this.tk := substr(description, 3)
			} else if (instr(description, "S:", false) == 1) {
				this.typ := CTK
				this.tk := substr(description, 3)
			} else if (strLen(description) == 1) {
				this.typ := RTK
				this.tk := description
			} else {
				; add braces, but not around modifier prefixes ^ etc
				RegexMatch(description, "O)([+^!#]*)(.+)", match)
				this.typ := CTK
				this.tk := match.Value(1) . "{" . match.Value(2) . "}"
			}
		}

		log("KeyAction ctor", "new KeyAction(" . description . ") == " . this.toString())	;?
	}

	toString() {
		return "KeyAction("
			. (this.error != "" ? ("error: """ . this.error . """, ") : "")
			. "typ: " . this.typ
			. (this.tk != "" ? (", tk: " . this.tk) : "")
			. (this.mods != 0 ? (", mods: " . this.mods) : "")
			. (this.tap != "" ? (", tap: " . this.tap.toString()) : "")
			. (this.sendMods != 0 ? (", sendMods: " + this.sendMods) : "")
			. ")"
	}

	isEmpty() {
		return this.tk == "" && this.mods == 0
	}

	isTk() {
		return this.typ == RTK || this.typ == CTK
	}

	isMk() {
		return this.typ == LMK || this.typ == SMK
	}

	isMtk() {
		return this.typ == LMTK || this.typ == SMTK
	}

	sendTk(down, modState) {
		local k, v
		log(A_ThisFunc, "sendTk(" . upDownStr(down) . ", " . modState.toString() . ") on " . this.toString())	;?
		if (this.sendMods != 0) {
			modState := deepClone(modState)
			modState.standard := standardModsMap(this.sendMods)
			log(A_ThisFunc, "override modState: " . modState.toString())	;?
		}
		if (this.tap != "") {
			this.tap.sendTk(down, modState)
		} else if (this.tk != "") {
			sendKeyWithState(this.tk, this.typ == RTK, down, modState)
		}
	}
}

resolveAlias(k) {
	local result := aliases[k]
	if (result == "") {
		result := resolvePrefixAlias(k)
	}
	log(A_ThisFunc, A_ThisFunc . "(" . k . ") == " . result)	;?
	return result
}

resolvePrefixAlias(k) {
	local p, a
	for p, a in prefixAliases {
		if (startsWith(k, p)) {
			return a
		}
	}
	return k
}

; E.g. defMod("M1") for a layer modifier
; e.g. defMod("SH", "{Shift}") for a standard modifier
defMod(modName, standardModSendStr = "") {
	if (inStr(modName, MOD_SEP)) {
		throw "modifier name " . modName . " contains " . MOD_SEP
	}
	modMap.item(modName) := nextModBit
	log(A_ThisFunc, intToBinaryString(nextModBit) . " (" . nextModBit . ")`t-> " . modName . "`t(" . standardModSendStr . ")")	;?
	if (standardModSendStr != "") {
		standardModifierStrings[nextModBit] := standardModSendStr
		standardModifiers |= nextModBit
	}
	nextModBit := nextModBit << 1
}

modNamesToBitset(modNames) {
	local i, modName, bit, bits := 0
	for l, modName in strSplit(modNames, MOD_SEP) {
		bit := modMap.item(modName)
		if (bit == "") {
			throw "undefined modifier " . modName
		}
		bits |= bit
	}
	log(A_ThisFunc, modNames . " -> " . bits)	;?
	return bits
}

; Parameters:
; - modNames: MOD_SEP-separated list of modifier names
; - hwKey: the key to map, as understood by the hotkey function
; - keyActionDesc: Supported formats parsed by KeyAction constructor:
;   * "[MMM]": modifier MMM
;   * "L:XXX": send literal hotstring XXX
;   * "L:": send nothing, but "flush" standard modifiers (e.g. to send pure "{Alt}" to select a menu)
;   * "S:XXX": send symbolic XXX (e.g. "S:{Left}")
;   * "X" (single character): shorthand for "L:X"
;   * "XXX" (two or more characters): shorthand for "S:{XXX}"
;   * "[MMM]XXX": modifier MMM when combined; XXX (one of the above) when tapped separately
;   plus (handled here):
;   * "F:XYYY": call function; X is a separator char; YYY is split at X to produce a list of function name and optional arguments
defKey(modNames, hwKey, keyActionDesc) {
	local a, mods
	log(A_ThisFunc, modNames . ", " . hwKey . "`t-> " . keyActionDesc)	;?
	if (instr(keyActionDesc, "F:", false) == 1) {
		a := parseFuncCall(substr(keyActionDesc, 3))
	} else {
		a := new KeyAction(keyActionDesc == NOP ? "" : keyActionDesc)
		if (a.error != "") {
			throw a.error
		}
	}
	mods := modNamesToBitset(modNames)
	keyMap[mods, hwKey] := a
	hotkey_("*" . hwKey, "onKey", new KeyEvent(hwKey, 1), new KeyEvent(hwKey, 0))
}

parseFuncCall(desc) {
	local sep, funcName, args, func
	if (desc.length() < 2) {
		throw "function call description must contain separator and function name at least"
	}
	sep := substr(desc, 1, 1)
	desc := substr(desc, 2)
	args := strSplit(desc, sep)
	funcName := args.removeAt(1)
	return Func(funcName).Bind(args*)
}

; TODO add params shortName, longName; display them in help
defKeys(layer, hwKeys, keyActions) {
	local keyActionsOrig := keyActions, hwKey, fn, i
	log(A_ThisFunc, "hwKeys = " . arrayToString(hwKeys))	;?
	if (!keyActions.maxIndex()) {	; not an array => split string into array
		keyActions := regexReplace(keyActions, "^\s+", "")
		keyActions := regexReplace(keyActions, "\s+$", "")
		keyActions := regexReplace(keyActions, "\s+", " ")
		keyActions := strSplit(keyActions, " ")
		log(A_ThisFunc, "keyActions = " . keyActionsOrig . " -> " . arrayToString(keyActions))	;?
	}
	if (keyActions.length() != hwKeys.length()) {
		throw "count mismatch (" . hwKeys.length() . " -> " . keyActions.length()
			. ") in defineKeys(" . layer . ", .., " . arrayToString(keyActionsOrig) . ")"
	}
	for i, hwKey in hwKeys {
		defKey(layer, hwKey, keyActions[i])
	}
}

defAlias(alias, value) {
	aliases[alias] := value
}

defPrefixAlias(alias, value) {
	prefixAliases[alias] := value
}

; Returns: KeyAction for current layer (or best matching fall back layer)
getKeyAction(layer, hwKey) {
	local result
	log(A_ThisFunc, A_ThisFunc . "(" . layer . ", " . hwKey . ")")	;?
	result := getKeyActionImpl(layer, hwKey)
	log(A_ThisFunc, A_ThisFunc . "(" . layer . ", " . hwKey . ") == " . result.toString())	;?
	return result
}
getKeyActionImpl(layer, hwKey) {
	local i, fallBackPair, fallBackFrom, fallBackTo, result, result2, sendMods
	local k := keyMap[layer, hwKey]
	log(A_ThisFunc, "keyMap[" . layer . ", " . hwKey . "] == " . k.toString())	;?

	; fall back to e.g. corresponding non-shifted layer
	if (k == "" || k.isEmpty()) {
		for uu, fallBackPair in modFallbacks {
			fallBackFrom := fallBackPair[1]
			fallBackTo   := fallBackPair[2]
			if (allBitsSet(layer, fallBackFrom)) {
				layer := unsetBits(layer, fallBackFrom)
				layer := setBits(layer, fallBackTo & ~standardModifiers)
				log(A_ThisFunc, "fall back " . fallBackFrom . " -> " . fallBackTo . " => layer " . layer)	;?
				activeModFallbacks[fallBackFrom] := fallBackTo
				result := getKeyAction(layer, hwKey)
				sendMods := fallBackTo & standardModifiers
				if (sendMods != 0) {
					result2 := deepClone(result)
					result2.sendMods := sendMods
					log(A_ThisFunc, "after deepClone & setting sendMods: " . result2.toString())	;?
					log(A_ThisFunc, "original:                           " . result.toString())	;?
					result := result2
				}
				return result 
			}
		}
	}

	; fall back to layer 0
	if ((k == "" || k.isEmpty()) && layer != 0) {
		log(A_ThisFunc, "fall back to layer 0")	;?
		return getKeyAction(0, hwKey)
	}

	return k
}

defModFallback(fromMod, toMod) {
	fromMod := modNamesToBitset(fromMod)
	toMod   := modNamesToBitset(toMod)
	log(A_ThisFunc, fromMod . "`t-> " . toMod)	;?
	modFallbacks.push(Array(fromMod, toMod))
}

; ==================== handle state ====================

logState(s = "") {
	log("(" . s . ")", "physModStates    = " . intToBinaryString(physModStates))	;?
	log("(" . s . ")", "physModStatesTap = " . intToBinaryString(physModStatesTap))	;?
	log("(" . s . ")", "modStatesLock    = " . intToBinaryString(modStatesLock))	;?
	log("(" . s . ")", "modStatesSticky  = " . intToBinaryString(modStatesSticky))	;?
;	log("(" . s . ")", "pendingEvents = " . arrayToString(pendingEvents, ", ", Func("PendingKeyEvent.toString")))	;?
	log("(" . s . ")", "prevKey = " . prevKey . ",`tprevKeyDown = " . prevKeyDown)	;?
	log("(" . s . ")", "standardModsSentDown = " . mapToString(standardModsSentDown) . "`tactiveModFallbacks = " . mapToString(activeModFallbacks))	;?
}

resetState() {
	physModStates    := 0
	physModStatesTap := 0
	modStatesLock    := 0
	modStatesSticky  := 0
	pendingEvents := []
	standardModsSentDown := []
	activeModFallbacks := {}
	prevKey := ""
	prevKeyDown := ""
	hwKeyToDownEvent := Array()
	showHelp := 0
	repeatCount := 0
	uu := 0
	logState("after resetState")
}

; Converts a given modifiers bitset to a map of only the standard mods in it.
; Returns: map (string corresponding to standard modifier) -> (mod bitset),
; e.g. { "{Alt}" -> 8, "{Control}" -> 16 };
; or "" if empty (because that is easier to check)
standardModsMap(mods, respectActiveFallbacks = 0) {
	local bit, str, result := {}, fbBits, found := 0
	for bit, str in standardModifierStrings {
		if (respectActiveFallbacks) {
			fbBits := getKeys(activeModFallbacks, bit)
			if (fbBits != "") {
				log(A_ThisFunc, "re fall back: " . bit . " -> " . fbBits[1])	;?
				bit := fbBits[1]
				activeModFallbacks.remove(bit)
			}
		}
		if (mods & bit != 0) {
			result[str] := bit
			found := 1
		}
	}
	result := found ? result : ""
	log(A_ThisFunc, "standardModsMap(" . mods . ", " . respectActiveFallbacks . ") == " . mapToString(result))	;?
	return result
}

; - layer: current (layer-relevant) modifier state bitset, e.g. (M2 | M6)
; - standard: standardModsMap(..) result
class ModState {
	toString() {
		return "ModState(layer: " . this.layer . ", standard: " . arrayToString(this.standard) . ")"
	}
}

getModState() {
	local bit, str, result := new ModState()
	; physical state toggles (i.e. holding mod key when that mod is already locked should unlock temporarily)
	; => mod is active if it is set in modStates xor physModStates
	local resultModStates := (modStatesLock | modStatesSticky) ^ physModStates
	log(A_ThisFunc, "resultModStates == " . intToBinaryString(resultModStates))	;?
	result.layer := resultModStates & ~standardModifiers
	result.standard := standardModsMap(resultModStates)
	log(A_ThisFunc, A_ThisFunc . "() == " . result.toString())	;?
	return result
}

; returns the combined (modStatesLock and modStatesSticky) mod state for a given
; single mod bit, i.e. one of MS_OFF, MS_STICKY, MS_LOCK.
get1ModState(mod) {
	local result := (modStatesLock & mod ? 1 : 0) | (modStatesSticky & mod ? 2 : 0)
	log(A_ThisFunc, A_ThisFunc . "(" . mod . ") == " . result)	;?
	return result
}

set1ModState(mod, ms) {
	log(A_ThisFunc, A_ThisFunc . "(" . mod . ", " . ms . ")   " . (ms & 1) . " /   " . ((ms >> 1) & 1))	;?
	modStatesLock   := setBit(modStatesLock,   mod, ms & 1)
	modStatesSticky := setBit(modStatesSticky, mod, (ms >> 1) & 1)
}

; ==================== handle key events ====================

class KeyEvent {
	__New(hwKey, down) {
		this.hwKey := hwKey
		this.down := down
		this.action := ""
		this.modState := ""
	}

	toString() {
		return "KeyEvent(" . this.hwKey
			. ", " . upDownStr(this.down)
			. (this.action == "" ? "" : (", action: " . this.action.toString()))
			. (this.modState == "" ? "" : (", " . this.modState.toString()))
			. ")"
	}

	isDown() {
		return this.down
	}
	isUp() {
		return !this.down
	}

	setModState(modState) {
		this.modState := modState
		this.action := getKeyAction(modState.layer, this.hwKey)
	}
}

upDownStr(down) {
	return down ? "down" : "up"
}

onKey(event, logAsHeader = 1) {
	log(A_ThisFunc, (logAsHeader ? "========== " : "") . "onKey(" . event.toString() . ")" . (logAsHeader ? " ==========" : ""))	;?

	; clone; otherwise the event bound to the hotkey function will be the modified (with .action & .sendMods) one
	event := deepClone(event)

	onKeyImpl(event)
	logState("after onKey")
}
onKeyImpl(event) {
	local fakeEvent, pendingEvent
	if (event.isDown()) {
		; hack against missing up events (when a held key auto-repeats)
		if (event.hwKey == prevKeyDown) {
			log(A_ThisFunc, "calling additional onKey(KeyEvent(" . event.hwKey . ", up), ...) for repeating key")	;?
			fakeEvent := new KeyEvent(event.hwKey, 0)
			fakeEvent.modState := event.modState
			fakeEvent.action := event.action
			onKey(fakeEvent, 0)
		}

		prevKeyDown := event.hwKey
		if (event.modState == "") {
			event.setModState(getModState())
		}
        hwKeyToDownEvent[event.hwKey] := event
	} else {
		prevKeyDown := ""
		if (handleRollover(event)) {
			return
		}
        event := new KeyEvent(event.hwKey, 0)	; on up event, use layer as it was on corresponding down
		event.modState := hwKeyToDownEvent[event.hwKey].modState
		event.action   := hwKeyToDownEvent[event.hwKey].action
        log(A_ThisFunc, "turned restored down event into up: " . event.toString())	;?
        hwKeyToDownEvent[event.hwKey] := ""
	}

	if (event.action.base == KeyAction) {
		; already sendable on down:
		; - (pure) mod
		; - TK, but only if no MTK is down; that distinction is for proper roll-over detection:
		;   [MTK↓, TK↓, MTK↑, TK↑] should be treated as [MTK, TK], not MTK & TK;
		;   i.e. choose TK role of MTK
		local immediate := event.action.isMk() || (event.action.isTk() && !isPendingMtk())
		log(A_ThisFunc, "immediate == " . immediate)	;?
		if (immediate) {
			handlePendingKeys(event)
			handleKey(event, 0)	; in the current event, treat MTK as TK
		} else {
			if (event.isDown()) {
				log(A_ThisFunc, "save pending: " . event.toString())	;?
				pendingEvents.push(event)
			} else {
				handlePendingKeys(event)
				handleKey(event, 0)	; in the current event, treat MTK as TK
			}
		}
	} else if (event.action != "") {
		log(A_ThisFunc, "key action is a function: " . event.action)	;?
		event.action.call()
	}

	prevKey := event.hwKey
}

handleRollover(event) {
	local lastPendingDownEvent := lastPendingDownEvent(), pendingEvent
	if (lastPendingDownEvent != "" && lastPendingDownEvent.hwKey != event.hwKey) {
		pendingEvent := pendingEvents.pop()
		log(A_ThisFunc, "rollover detected, handling " . event.toString() . " before " . pendingEvent.toString())	;?
		prevKey := pendingEvents[pendingEvents.maxIndex()].hwKey
		onKeyImpl(event)
		onKeyImpl(pendingEvent)
		return 1
	}
}

handlePendingKeys(e) {
	; treat each MTK as MK;
	; exception: if the last one is the down event corresponding to the current up event, prefer TK
	local i, pendingEvent, tap
	log(A_ThisFunc, A_ThisFunc . "(" . e.toString() . ", " . e.action.toString() . ")")	;?

	for i, pendingEvent in pendingEvents {
		log(A_ThisFunc, "pending [" . i . "] " . pendingEvent.toString())	;?
		; detect exception described above
		tap := 0
		if (i == pendingEvents.MaxIndex()) {
			tap := pendingEvent.hwKey == e.hwKey
			log(A_ThisFunc, "last pending event " . pendingEvent.hwKey . "`t=> tap == " . tap)	;?
		}

		; mod state can have changed due to previous pendingEvent
		if (i != pendingEvent.MinIndex()) {
			pendingEvent.setModState(getModState())
		}

		handleKey(pendingEvent, !tap)
	}

	; mod state can have changed due to pendingEvents
	if (pendingEvents.length() > 0) {
		e.setModState(getModState())
	}

	log(A_ThisFunc, "clear pending")	;?
	pendingEvents := []
}

handleKey(e, preferMod) {
	local standardMods
	log(A_ThisFunc, "-- { " . A_ThisFunc . "(" . e.toString() . ", " . preferMod . ")")	;?

	if (e.isDown()) {
		; all mods currently down are now "down-with-tap"
		physModStatesTap |= physModStates
		logState("down->downTap")
	}

	if (e.action.isMk()
			|| ((preferMod || prevKey != e.hwKey)  ; preferMod || key is down in combination with another
					&& e.action.isMtk())) {
		; MTK as MK or pure MK
		log(A_ThisFunc, "MK or MTK as MK")	;?
		if (e.isDown()) {
			modifierDown(e.action.mods)
		} else {
			modifierUp(e.action)
		}

		standardMods := standardModsMap(e.action.mods, 1)
		if (standardMods != "") {	; standard mod
			sendStandardMods(standardMods, e.down)
		}
	} else {	; (MTK as or pure) TK
		e.action.sendTk(e.isDown(), e.modState)

		if (e.isUp()) {
			; sticky mods have been consumed
			releaseStickyMods()
		}
	}
	log(A_ThisFunc, "} " . A_ThisFunc . "(" . e.toString() . ", " . preferMod . ")")	;?
}

lastPendingDownEvent() {
	local pe, lastDown := ""
	for uu, pe in pendingEvents {
		if (pe.isDown()) {
			lastDown := pe
		}
	}
;	log(A_ThisFunc, A_ThisFunc . "() == " . lastDown.toString())	;?
	return lastDown
}

; whether pendingEvents contains an MTK down
isPendingMtk() {
	local pe
	for uu, pe in pendingEvents {
		log(A_ThisFunc, "check pending: " . pe.toString())	;?
		if (pe.isDown() && pe.action.isMtk()) {
			log(A_ThisFunc, "pending MTK found: " . pe.toString())	;?
			return 1
		}
	}
	log(A_ThisFunc, "no pending MTK")	;?
	return 0
}

; physical modifier state update, not only when current event is a modifier
physModifierUpOrDown(mods, down) {
	log(A_ThisFunc, A_ThisFunc . "(" . mods . ", " . upDownStr(down) . ")")	;?
	if (mods != "") {
		if (down) {	; mod down
			physModStates |= mods
		} else {	; mod up
			physModStates    := unsetBits(physModStates,    mods)
			physModStatesTap := unsetBits(physModStatesTap, mods)
		}
	}
	logState("after physModUpOr..")
}

modifierDown(mods) {
 	log(A_ThisFunc, "modifierDown(" . mods . ")")	;?
	physModifierUpOrDown(mods, 1)
}

modifierUp(keyAction) {
	local prev, mods, mod, ms
	log(A_ThisFunc, "modifierUp(" . keyAction.toString() . ") per hotkey " . A_ThisHotkey)	;?
	mods := splitBits(keyAction.mods)
	for uu, mod in mods {
		ms := get1ModState(mod)
		if (anyBitSet(physModStatesTap, mod)) {
			; i.e. modifier has been held down during TK press;
			; modifier has already been "consumed" in TK's hotkey => unset mod
			log(A_ThisFunc, "modifier was pressed in combination")	;?
			if (ms == MS_STICKY) {
				set1ModState(mod, MS_OFF)
			}
		} else {	; modifier tapped
			if (ms == MS_OFF) {
				log(A_ThisFunc, "tapped 1st time")	;?
				set1ModState(mod, MS_STICKY)
			} else if (ms == MS_STICKY) {
				log(A_ThisFunc, "tapped repeatedly")	;?
				; same modifier twice => lock all current modifiers;
				; i.e. replace all MS_STICKY with MS_LOCK
				modStatesLock |= modStatesSticky
				modStatesSticky := 0
			} else {
				log(A_ThisFunc, "tapped to unlock")	;?
				modStatesLock   := unsetBits(modStatesLock,   mod)
				modStatesSticky := unsetBits(modStatesSticky, mod)
			}
		}
	
		physModifierUpOrDown(mod, 0)
	}
	logState("after modifierUp")
}

; send[Raw] given key plus currently active standard mods
sendKeyWithState(keysStr, raw, down, modState) {
	log(A_ThisFunc, A_ThisFunc . "(" . keysStr . ", " . raw . ", " . upDownStr(down) . ", " . modState . ")")	;?
	sendStandardMods(modState.standard, 1)
	doSendKey(keysStr, raw, down)
}

sendStandardMods(standardModsMap, down) {
	local standardModStr, mod, mod2, newStandardModsMap := standardModsMap.clone(), isDown, upDownStr := upDownStr(down)
	log(A_ThisFunc, A_ThisFunc . "(" . mapToString(standardModsMap) . ", " . upDownStr . ")")	;?
	for standardModStr, mod in standardModsSentDown {
		if (standardModsMap[standardModStr] != "") {	; both in current standardModsSentDown & in new given standardModsMap
			mod2 := activeModFallbacks[mod]
			if (mod2 != "") {
				activeModFallbacks.delete(mod)
				log(A_ThisFunc, "revert fall back " . mod . " -> " . mod2)	;?
				mod := mod2
			}
			newStandardModsMap.remove(standardModStr)
			if (!down) {
				if (allBitsSet(modStatesSticky, mod)) {
					log(A_ThisFunc, "stays down (sticky): " . standardModStr)	;?
				} else if (!allBitsSet(modStatesLock, mod)) {
					log(A_ThisFunc, "is down, up wanted: " . standardModStr)	;?
					doSendKey(standardModStr, 0, 0)
					standardModsSentDown.remove(standardModStr)
				}
			}
		}
	}
	for standardModStr, mod in newStandardModsMap {
		log(A_ThisFunc, "newly " . upDownStr . ": " . standardModStr)	;?
		doSendKey(standardModStr, 0, down)
		if (down) {
			standardModsSentDown[standardModStr] := mod
		} else {
			standardModsSentDown.remove(standardModStr)
		}
	}
}

releaseStickyMods() {
	local modStr, mod, e
	log(A_ThisFunc, A_ThisFunc . "()")	;?
	modStatesLock := unsetBits(modStatesLock, modStatesSticky)
	for modStr, mod in standardModsSentDown {
		if (allBitsSet(modStatesSticky, mod)) {
			log(A_ThisFunc, "release sticky " . modStr)	;?
			doSendKey(modStr, 0, 0)
			standardModsSentDown.remove(modStr)
		}
	}
	modStatesSticky := 0

	for uu, e in hwKeyToDownEvent {
		; On the upcoming up event, we must not restore sticky-and-already-consumed modifiers.
		; But do not call setModState which also sets the action. (That breaks sticky LMK somehow.)
		e.modState := getModState()
	}
}

doSendKey(keysStr, raw, down) {
	if (keysStr == "") {
		return
	}
	if (raw) {
		if (!down) {
			log(A_ThisFunc, "skipping sendRaw(" . keysStr . "), because raw does not support ""up""")	;?
			return
		}
	} else {
		keysStr := strReplace(keysStr, "}", " " . upDownStr(down) . "}")
	}
	log(A_ThisFunc, ">>> send" . (raw ? "Raw" : "") . "(" . keysStr . ")")	;?
	if (raw) {
		sendRaw_(keysStr)
	} else {
		send_(keysStr)
	}
}

; ==================== repeat last sent key ====================

enterRepeatCount(digit) {
	log(A_ThisFunc, A_ThisFunc . "(" . digit . ")")	;?
	if (digit == "-") {
		repeatCount := 0
	} else {
		repeatCount := 10 * repeatCount + digit
	}
}

repeat() {
	log(A_ThisFunc, A_ThisFunc . "()")	;?
	; TODO
}

; ==================== macros ====================

macroRecord(macroId) {
	log(A_ThisFunc, A_ThisFunc . "(" . macroId . ")")	;?
	; TODO
}

macroPlay(macroId, n = 1) {
	log(A_ThisFunc, A_ThisFunc . "(" . macroId . ", " . n . ")")	;?
	; TODO
}

; ==================== Help GUI ====================

toggleHelp(docTemplate, placeHolderChar, replaceWidth) {
	showHelp := 1 - showHelp
	if (showHelp) {
		showHelp(docTemplate, placeHolderChar, replaceWidth)
	} else {
		Gui Destroy
	}
}

showHelp(docTemplate, placeHolderChar, replaceWidth) {
	; draw (with empty placeholder replacements) to determine window size
	local wHandle := initGui()
	guiAddHelpText(docTemplate, placeHolderChar, replaceWidth, 1)

	local dx, dy, dw, dh, ax, ay, aw, ah, x, y, w, h, opac := 255
	WinGetPos, ax, ay, aw, ah, A
	log(A_ThisFunc, "active window: " . ax . ", " . ay . ", " . aw . "x" . ah)	;?
	WinGetPos, dx, dy, dw, dh, ahk_class Progman
	log(A_ThisFunc, "desktop: " . dx . ", " . dy . ", " . dw . "x" . dh)	;?
	gui Show, NA x %dw%
	WinGetPos, , , w, h, ahk_id %wHandle%
	log(A_ThisFunc, "GUI size: " . w . "x" . h)	;?
	gui Destroy
	
	; determine window position
	if (h < ay + 5) {	; GUI above active window
		x := ax
		y := ay - h
	} else if (h < dh - ay - ah) {	; GUI below active window
		x := ax
		y := ay + ah
	} else if (w < dw - ax - aw) {	; GUI right of active window
		x := ax + aw
		y := ay
	} else if (w < ax) {	; GUI left of active window
		x := ax - w
		y := ay
	} else {	; upper right desktop corner
		x := dx + dw - w
		y := dy
		opac := 170
	}
	log(A_ThisFunc, "x: " . x . ", y: " . y)	;?
	
	wHandle := initGui()
	guiAddHelpText(docTemplate, placeHolderChar, replaceWidth, 0)
	gui Show, NA x %x% y %y%
	WinSet Transparent, %opac%, ahk_id %wHandle%
}

initGui() {
	local wHandle
	gui +HwndwHandle +AlwaysOnTop -Caption -Border +Disabled +LastFound -Resize +ToolWindow
	gui color, Black
	WinSet TransColor, Black
	WinSet Transparent, 255
	gui Font, , Lucida Console
	gui Font, , Consolas
	gui Font, , DejaVu Sans Mono
	return wHandle
}

; TODO
guiAddHelpText(docTemplate, placeHolderChar, replaceWidth, dummy = "") {
; 	local i := 1, iPhStart, iPhEnd, keyActionAndMods, typeAndKey := Array("L", "*")
; 	local defaultColor := guiText("", "")[1]
; 	gui Add, Text, X4, `n
; 	; TODO print layer's longName
; 	while (i <= strLen(docTemplate)) {
; 		iPhStart := inStr(docTemplate, placeHolderChar, true, i)
; 		if (!iPhStart) {
; 			break
; 		}
; 		iPhEnd := inStr(docTemplate, placeHolderChar, true, iPhStart + 2)
; 		if (!iPhEnd) {
; 			break
; 		}
; 		guiAddText(defaultColor, subStr(docTemplate, i, iPhStart - i))
; 		if (!dummy) {
; 			local hwKey := subStr(docTemplate, iPhStart + 1, iPhEnd - iPhStart - 1)
; 			keyActionAndMods := getKeyAction(hwKey)
; 			typeAndKey := parseKeyAction(keyActionAndMods*)
; 		}
; 		local colorAndText := guiText(typeAndKey[1], typeAndKey[2])
; 		guiAddText(colorAndText[1], colorAndText[2], replaceWidth)
; 		i := iPhEnd + 1
; 	}
; 	guiAddText(defaultColor, subStr(docTemplate, i))
}

guiAddText(color, text, width = -1) {
	local pos := "X+0"
	log(A_ThisFunc, "guiAddText(" . color . ", """ . text . """, " . width . ")")	;?
	if (width >= 0) {
		text := substr(format("{1:-" . width . "}", text), 1, width)
	}
	gui Font, Q5 c%color% s10
	; write each line separately, so the last y position (for continuing a
	; line) is that of the current line
	Loop, Parse, text, `n
	{
		gui Add, Text, 0x80 %pos%, %A_LoopField%
		pos := "X4 Y+0"
	}
}

guiText(type, key) {
	log(A_ThisFunc, "guiText(""" . type . """, """ . key . """)")	;?
	if (type == LMK) {
		return Array("FF0000", key)
	} else if (type == SMK) {
		return Array("FF9000", key)
	} else if (type == CTK) {
		key := regexReplace(key, "^\{([^}]+)\}$", "$1")
		return Array("FFFF00", key)
	} else {
		return Array("FBFBFB", key)
	}
}
File util.ahk

Code: Select all

log(arg0, arg1 = "") {
	local e, arg0nl
	if (neq(arg1, "")) {
		log(format("{:-22.22s}:{}", arg0, arg1))
	} else {
		try {
			arg0nl := arg0 . "`n"
			FileAppend, %arg0nl%, **
		} catch e {
			OutputDebug %arg0%
		}
	}
}

; compares values for strict (string) equality
; see https://autohotkey.com/board/topic/28987-compare-two-variables-as-strings/
eq(val1, vals*) {
	local i, val2
	for i, val2 in vals {
		if (("" . val1) != ("" . val2)) {
			return 0
		}
	}
	return 1
}
neq(val1, vals*) {
	return !eq(val1, vals*)
}

winActivateOrStart(winTitle, cmd = "") {
	if (! WinExist(winTitle) && neq(cmd, "")) {
		Run %cmd%
		Sleep 2000
		WinWait %winTitle%
	} else {
		;cycle through matching windows, starting with most recently activated
		IfWinActive %winTitle%
;			WinActivateBottom %winTitle%
			Send ^{sc02B}	; hotkey defined in iswitchw-plus.ahk
		else
			WinActivate %winTitle%
	}
}

condHotKey(toggleKey, onKey, offKey) {
	if GetKeyState(toggleKey, "T") {
		SendPlay %onKey%
	} else {
		SendPlay %offKey%
	}
	return
}

setBit(bitset, bit, value) {
	return value ? setBits(bitset, bit) : unsetBits(bitset, bit)
}

setBits(bitset, bits) {
	return bits == "" ? bitset : (bitset | bits)
}

unsetBits(bitset, bits) {
	return bits == "" ? bitset : (bitset & ~bits)
}

anyBitSet(bitset, bits) {
	return bitset & bits != 0
}

allBitsSet(bitset, bits) {
	local r := bitset | bits == bitset
	log(A_ThisFunc, A_ThisFunc . "(" . bitset . ", " . bits . ") == " . r)
	return r
}

allBitsUnset(bitset, bits) {
	local r := bitset & ~bits == bitset
	log(A_ThisFunc, A_ThisFunc . "(" . bitset . ", " . bits . ") == " . r)
	return r
}

; Given an int bitset, returns an array of single-bit ints so that they'd
; equal that bitset when joined with bitwise or.
; Order: from least to most significant bit.
splitBits(bits) {
; 	local results := Array(), bit := 1, bits2 := bits
; 	while (bits2 != 0) {
; 		log("bit == " . bit . ", ~bit == " . (~bit) . ",`tbits2 == " . bits2)
; 		if (bits2 & bit != 0) {
; 			results.push(bit)
; 			log("bits2: " . bits2 . "`t-> " . (bits2 & ~bit))
; 			bits2 := bits2 & ~bit
; 		}
; 		bit := bit << 1
; 	}
	local results := Array(), bits2 := bits, bit := 1
	while (bits2 != 0) {
		if (mod(bits2, 2) != 0) {
			results.push(bit)
		}
		bits2 := bits2 // 2
		bit := bit << 1
	}
; 	local toString := Func("intToBinaryString64")
;	log(A_ThisFunc, toString.call(bits) . "`t-> " . arrayToString(results, " | ", toString, 0))
	log(A_ThisFunc, bits . "`t-> " . arrayToString(results, " | ", "", 0))
	return results
}

intToBinaryString64(n, group = 8) {
	return intToBinaryString(n, 64, group)
}

intToBinaryString(n, pad = 32, group = 4) {
	local s := "", len := 0
 	if (n < 0) {
 		throw "intToBinaryString(" . n . "): negative numbers not supported"
 	}
	while (n != 0 || len < pad) {
		if (len > 0 && mod(len, group) == 0) {
			s := " " . s
		}
		s := (n & 1) . s
		len++
		n >>= 1
	}
	s := eq(s, "") ? "0" : s
	return format("{:-" . pad . "}", s)
}

mapToString(map, ignoreValue = "") {
	local k, v, s
	s := ""
	for k, v in map {
		if (neq(v, ignoreValue)) {
			s .= eq(s, "") ? "" : ", "
			s .= k . ":" . v
		}
	}
	return s
}

getOrDefault(map, key, defaultValue) {
	local value := map[key]
	return neq(value, "") ? value : defaultValue
}

; Returns the keys mapped to the given value, or "" if empty (because that is easier to check)
getKeys(map, value) {
	local k, v, keys := [], found := 0
	for k, v in map {
		if (eq(v, value)) {
			found := 1
			keys.push(k)
		}
	}
	return found ? keys : ""
}

arrayIndexOf(array, element) {
	local i, e
	for i, e in array {
		if (eq(e, element)) {
			return i
		}
	}
	return ""
}

arrayToString(array, separator = ", ", elemToStringFunc = "", withIndex = 1) {
	local i, e, s
	s := ""
	for i, e in array {
		s .= eq(s, "") ? "" : separator
		if (withIndex) {
			s .= "[" . i . "] "
		}
		s .= (elemToStringFunc == "" ? e : elemToStringFunc.call(e))
	}
	return s
}

arraysEqual(array1, array2) {
	local i, elem
	if (array1.minIndex() != array2.minIndex()
			|| array1.maxIndex() != array2.maxIndex()) {
		return 0
	}
	for i, elem in array1 {
		if (neq(elem, array2[i])) {
			return 0
		}
	}
	return 1
}

join(array, separator = ", ", elemToStringFunc = "") {
	return arrayToString(array, separator, elemToStringFunc, 0)
}

startsWith(str, prefix) {
	return prefix.length() <= str.length() && eq(substr(str, 1, prefix.length()), prefix)
}

pushAll(targetArray, newElementsArray) {
	local i, e,
	for i, e in newElementsArray {
		targetArray.push(e)
	}
}

replaceValues(byref map, oldValue, newValue) {
	local k, v
	for k, v in map {
		if (eq(v, oldValue)) {
			map[k] := newValue
		}
	}
}

; Extracts substrings from a given string, discarding some characters
; (e.g. to extract keys from a pretty-printed keyboard layout).
; This function undoes the pretty-printing by
; 1. discarding all characters in ignoredChars;
; 2. splitting each param at separators (a string or array of strings, see standard function strSplit);
; 3. removing empty split results;
; 4. then joining the split results into one array.
; Example: unpretty("|", " _"
;   , "|_q_|_w_|_e_|_r_|"
;   , "|   |F10| d |")
; == ["q", "w", "e", "r", "F10", "d"]
unpretty(separators, ignoredChars, decoratedKeys*) {
	local results := []
	local decoratedKey, part
	for uu, decoratedKey in decoratedKeys {
		local parts := strSplit(decoratedKey, separators, ignoredChars)
		for uu, part in parts {
			if (neq(part, "")) {
				results.push(part)
			}
		}
	}
	log(A_ThisFunc, "unpretty(..) == " . arrayToString(results))	;?
	return results
}

deepClone(o) {
	local k, v, c := o.Clone()
	for k, v in o {
		c[k] := IsObject(v) ? deepClone(v) : v
	}
	return c
}

sleep(t) {
	sleep, %t%
}

; Checks physical key states & sends corresponding mapped keys.
; E.g. sendIf("!", "F1", "/", "F2", "\") sends
; - "/" if F1 is down
; - "\" if F2 is down
; - "!" else
; This is usefull to have F1 and F2 act as modifiers without making them
; prefix keys, so that they can be otherwise remapped.
sendIf(defaultKey, cases*) {
	local cond, sendAction
	if (mod(cases.length(), 2) != 0) {
		throw "even number of cases expected (alternating if-down / thenSend pairs)"
	}
	while (cases.length() > 0) {
		cond := cases.remove(1)
		sendAction := cases.remove(1)
		if (getKeyState(cond, "P")) {
			send %sendAction%
			return
		}
	}
	send %defaultKey%
}
File util_dangerous.ahk

Code: Select all

send_(k) {
	msgbox this might be a good place to send %k%
}

sendRaw_(k) {
	msgbox this might be a good place to sendRaw %k%
}

hotkey_(key, funcName, downArg, upArg) {
	local func := Func(funcName).Bind(downArg)
	hotkey, %key%, % func
	func := Func(funcName).Bind(upArg)
	hotkey, %key% Up, % func
}
File multilayer_kb_layout_test.ahk (automated test)

Code: Select all

try {
FileEncoding, UTF-8-RAW
#Include %A_ScriptDir%\multilayer_kb_layout_util.ahk
#Include %A_ScriptDir%\test_util.ahk

defMod("S", "{Shift}")
defMod("C", "{Control}")
defMod("A", "{Alt}")
defMod("G", "{AltGr}")
defMod("W", "{LWin}")
defMod("s")	; shift-like layer
defMod("1")
defMod("2")
defModFallback("s", "S")

defAlias("---"   , NOP        )
defAlias("Ret"   , "Return"   )
defAlias("Sp"    , "S:{Space}")
defAlias("SPC*"  , "u+00a0"   )
defAlias("Menu"  , "AppsKey"  )
defAlias("Men"   , "AppsKey"  )

global testLog := ""
global testResults := Array()

s(k) {	; simulate send (down if first character is an opening bracket, up if closing)
	local matcher
	if (regexMatch(k, "O)^[(<\[{](.+)", matcher)) {
		return new KeyEvent(matcher.value(1), 1)
	} else if (regexMatch(k, "O)^[)>\]}](.+)", matcher)) {
		return new KeyEvent(matcher.value(1), 0)
	} else {
		throw "illegal key event " . k
	}
}
xd(k) {	; expect down
	return new Expectation(k . "∇")
}
xu(k) {	; expect up
	return new Expectation(k . "∆")
}
xr(k) {	; expect raw
	return new Expectation(k . "°")
}

prettyPrint(k, down) {
	k := strReplace(k, " up}", "}∆")
	k := strReplace(k, " down}", "}∇")
	return k
}

send_(k) {
	k := strReplace(k, " up}", "}∆")
	k := strReplace(k, " down}", "}∇")
	testLog.push("‒▶" . k)
}
sendRaw_(k) {
	testLog.push("‒▶" . k . "°")
}

hotkey_(key, funcName, downArg, upArg) {
	log(A_ThisFunc, key . "`t-> " . funcName)
}

test(name, steps*) {
	local i, step, expectedTestLog := Array(), s, passed

	if (testDisabled(name)) {
		return
	}

	log("`n`n******************** " . name . " ********************")
	testLog := Array()
	for i, step in steps {
		if (step.base == KeyEvent) {
			s :=  step.hwKey . (step.down ? "∇" : "∆")
			expectedTestLog.push(s)
			testLog.push(s)
			onKey(step)
		} else if (step.base == Expectation) {
			expectedTestLog.push(step.toString())
		} else {
			throw "illegal argument " . step
		}
	}

	local expectedTestLogStr := arrayToString(expectedTestLog, " | ", "", 0)
	local testLogStr := arrayToString(testLog, " | ", "", 0)
	log("`t=== " . name . " results ===")
	passed := checkEquals(expectedTestLogStr, testLogStr)
		& checkInitialState()	; no short-circuit
	testResults.push({ "name" : name, "errorMsg" : (passed ? "" : testLogStr) })
	resetState()
}

checkInitialState() {
	; check all, so no short-circuit &&
	return checkEquals(0, physModStates, "physModStates")
		& checkEquals(0, physModStatesTap, "physModStatesTap")
		& checkEquals(0, modStatesLock, "modStatesLock")
		& checkEquals(0, modStatesSticky, "modStatesSticky")
		& checkEquals("", arrayToString(pendingEvents), "pendingEvents")
		& checkEquals("", arrayToString(standardModsSentDown), "standardModsSentDown")
}

;                                         |SMK____|LMK____|SMTK___|LMTK___|SMTK___|TK______|TK/LMTK|TK/LMTK|LMTK   |
global hwKeys :=       unpretty("|", " ", "F1     |F2     |F3     |F4     |F5     |a       |b      |c      |d      ")
defKeys("",    hwKeys, unpretty("|", " ", "[S]    |[1]    |[A]BS  |[2]Tab |[C]Ret |k       |[s]l   |[s]m   |[s]x   "))
defKeys("s",   hwKeys, unpretty("|", " ", "---    |---    |---    |---    |---    |---     |---    |---    |---    "))
defKeys("1",   hwKeys, unpretty("|", " ", "---    |---    |---    |---    |---    |F10     |---    |---    |[2]p   "))
defKeys("2",   hwKeys, unpretty("|", " ", "---    |---    |[A]PgUp|---    |---    |Left    |!Left  |[A]Del |---    "))
defKeys("1|2", hwKeys, unpretty("|", " ", "---    |---    |---    |---    |---    |Right   |q      |---    |---    "))

test(                                   "tap TK"
, s("[a"), xr("k")
, s("]a")	)

test(                                   "SMK & TK"
, s("[F1"), xd("{Shift}")
, s("[a" ), xr("k")
, s("]a" )
, s("]F1"), xu("{Shift}")	)

test(                                   "LMK & TK"
, s("[F2")
, s("[a" ), xd("{F10}")
, s("]a" ), xu("{F10}")
, s("]F2")	)

test(                                   "SMTK & TK"
, s("[F3")
, s("[a" )
, s("]a" ), xd("{Alt}"), xr("k")
, s("]F3"), xu("{Alt}")	)

test(                                   "LMTK & TK"
, s("[F4")
, s("[a" )
, s("]a" ), xd("{Left}"), xu("{Left}")
, s("]F4")	)

test(                                   "SMTK & SMTK"
, s("[F3")
, s("[F5")
, s("]F5"), xd("{Alt}"), xd("{Return}"), xu("{Return}")
, s("]F3"), xu("{Alt}")	)

test(                                   "LMTK & LMTK => LMK & TK"
, s("[F4")
, s("[c" )
, s("]c" ), xd("{Del}"), xu("{Del}")
, s("]F4")	)

test(                                   "SMTK & LMTK"
, s("[F3")
, s("[F4")
, s("]F4"), xd("{Alt}"), xd("{Tab}"), xu("{Tab}")
, s("]F3"), xu("{Alt}")	)


test(                                   "LMTK & SMTK"
, s("[F4")
, s("[F3")
, s("]F3"), xd("{PgUp}"), xu("{PgUp}")
, s("]F4")	)

test(                                   "LMTK1 & LMTK2 (same mod) => LMK1 & TK2; + fallback LS -> SH"
, s("[b" )
, s("[c" )
, s("]c" ), xd("{Shift}"), xr("m")
, s("]b" ), xu("{Shift}")
, s("[c" )
, s("]c" ), xr("m")	)

test(                                   "LMTK2 & LMTK1 (same mod) => LMK2 & TK1; + fallback LS -> SH"
, s("[c" )
, s("[b" )
, s("]b" ), xd("{Shift}"), xr("l")
, s("]c" ), xu("{Shift}")
, s("[b" )
, s("]b" ), xr("l")	)

test(                                   "tap SMK => sticky"
, s("[F1"), xd("{Shift}")
, s("]F1")
, s("[a" ), xr("k")
, s("]a" ), xu("{Shift}")
, s("[a" ), xr("k")
, s("]a" )	)

test(                                   "tap LMK => sticky"
, s("[F2")
, s("]F2")
, s("[a" ), xd("{F10}")
, s("]a" ), xu("{F10}")
, s("[a" ), xr("k")
, s("]a" )	)

test(                                   "SMTK & SMK & TK"
, s("[F3")
, s("[F1"), xd("{Alt}"), xd("{Shift}")
, s("[a" ), xr("k")
, s("]a" )
, s("]F1"), xu("{Shift}")
, s("]F3"), xu("{Alt}")	)

test(                                   "SMK & SMTK & TK"
, s("[F1"), xd("{Shift}")
, s("[F3")
, s("[a" )
, s("]a" ), xd("{Alt}"), xr("k")
, s("]F3"), xu("{Alt}")
, s("]F1"), xu("{Shift}")	)

test(                                   "SMTK & LMTK & TK"
, s("[F3")
, s("[F4")
, s("[a" )
, s("]a" ), xd("{Alt}"), xd("{Left}"), xu("{Left}")
, s("]F4")
, s("]F3"), xu("{Alt}")	)

test(                                   "SMTK & SMK & TK"
, s("[F3")
, s("[F1"), xd("{Alt}"), xd("{Shift}")
, s("[a" ), xr("k")
, s("]a" )
, s("]F1"), xu("{Shift}")
, s("]F3"), xu("{Alt}")	)

test(                                   "SMTK & SMTK & LMTK"
, s("[F5")
, s("[F3")
, s("[b" )
, s("]b" ), xd("{Control}"), xd("{Alt}"), xr("l")
, s("]b" )
, s("]F3"), xu("{Alt}")
, s("]F5"), xu("{Control}")	)

test(                                   "SMTK & LMK & TK"
, s("[F3")
, s("[F2"), xd("{Alt}")
, s("[a" ), xd("{F10}")
, s("]a" ), xu("{F10}")
, s("]F2")
, s("]F3"), xu("{Alt}")	)

test(                                   "LMTK as TK due to rollover"
, s("[b" )
, s("(c" )
, s("]b" ), xr("l")
, s(")c" ), xr("m")	)

test(                                   "tap SMK1 (=> sticky); LMTK2 & TK3 => SMK1 & LMK2 & TK3"
;                                        plus, TK3 is LMTK in layer 0
, s("[F1"), xd("{Shift}")
, s("]F1")
, s("[F4")
, s("[b" )
, s("]b" ), xd("!{Left}"), xu("!{Left}"), xu("{Shift}")
, s("]F4")	)

test(                                   "tap SMK1 (=> sticky); LMTK2 & LMTK3 => SMK1 & LMK2 & TK3"
, s("[F1"), xd("{Shift}")
, s("]F1")
, s("[F4")
, s("[c" )
, s("]c" ), xd("{Del}"), xu("{Del}"), xu("{Shift}")
, s("]F4")	)

test(                                   "tap SMK1 (=> sticky); SMTK2 & LMTK3 & LMTK4; TK5 => SMK1 & SMK2 & LMK3 & TK4; TK5"
, s("[F1"), xd("{Shift}")    ; SMK1 Shift
, s("]F1")
, s("[F5")	; SMTK2 Control
, s("[F4")	; LMTK3 M2
, s("[b" )	; LMTK4 !Left
, s("]b" )
	, xd("{Control}")  ; SMK2
	, xd("!{Left}")    ; LMTK4 in layer M2
	, xu("!{Left}")
	, xu("{Shift}")
, s("]F4")
, s("]F5")
	, xu("{Control}")
, s("[a" )
	, xr("k")
, s("]a" )	)

test(                                   "SMK & 2*TK"
, s("[F1"), xd("{Shift}")
, s("[a" ), xr("k")
, s("]a" )
, s("[a" ), xr("k")
, s("]a" )
, s("]F1"), xu("{Shift}")	)

test(                                   "SMTK & 2*SMTK"
, s("[F3")
, s("[F5")
, s("]F5"), xd("{Alt}"), xd("{Return}"), xu("{Return}")
, s("[F5")
, s("]F5"), xd("{Return}"), xu("{Return}")
, s("]F3"), xu("{Alt}")	)

test(                                   "SMTK & 3*SMTK"
, s("[F3")
, s("[F5")
, s("]F5"), xd("{Alt}"), xd("{Return}"), xu("{Return}")
, s("[F5")
, s("]F5"), xd("{Return}"), xu("{Return}")
, s("[F5")
, s("]F5"), xd("{Return}"), xu("{Return}")
, s("]F3"), xu("{Alt}")	)

test(                                   "2 * (LMTK1 & LMTK2); fallback"
, s("[b" )
, s("(c" )
, s(")c" ), xd("{Shift}"), xr("m")
, s("]b" ), xu("{Shift}")
, s("[b" )
, s("[c" )
, s("]c" ), xd("{Shift}"), xr("m")
, s("]b" ), xu("{Shift}")
, s("[c" )
, s("]c" ), xr("m") )

test(                                   "LMTK1 & 3*LMTK2; ...; fallback"
, s("[b" )
, s("[c" )
, s("]c" ), xd("{Shift}"), xr("m")
, s("[c" )
, s("]c" ), xr("m")
, s("[c" )
, s("]c" ), xr("m")
, s("]b" ), xu("{Shift}")
, s("[c" )
, s("]c" ), xr("m")
, s("[c" )
, s("]c" ), xr("m")
, s("[b" )
, s("[c" )
, s("]c" ), xd("{Shift}"), xr("m")
, s("[c" )
, s("]c" ), xr("m")
, s("[c" )
, s("]c" ), xr("m")
, s("]b" ), xu("{Shift}")
, s("[c" )
, s("]c" ), xr("m") )

test(                                   "SMK lock; TK; SMTK & TK"
, s("[F1"), xd("{Shift}")	; tap 1
, s("]F1")
, s("[F1")	; tap 2 => lock
, s("]F1")
, s("[a" ), xr("k")
, s("]a" )
, s("[a" ), xr("k")
, s("]a" )
, s("[F3")
, s("[a" )
, s("]a" ), xd("{Alt}"), xr("k")
, s("]F3"), xu("{Alt}")
, s("[F1")	; unlock
, s("]F1"), xu("{Shift}")
, s("[a" ), xr("k")
, s("]a" ) )

test(                                   "LMK lock; TK; LMTK & TK"
, s("[F2")	; tap 1
, s("]F2")
, s("[F2")	; tap 2 => lock
, s("]F2")
, s("[a" ), xd("{F10}")
, s("]a" ), xu("{F10}")
, s("[a" ), xd("{F10}")
, s("]a" ), xu("{F10}")
, s("[F4")
, s("[a" )
, s("]a" ), xd("{Right}"), xu("{Right}")
, s("]F4")
, s("[F2")	; unlock
, s("]F2")
, s("[a" ), xr("k")
, s("]a" ) )

test(                                   "LMTK1 & LMTK2 & TK; LMTK2 is TK in layer 0; LMTK1 makes it a LMTK"
, s("[F2")
, s("[d" )
, s("[b" )
, s("]b" ), xr("q")
, s("]F2")
, s("]d" ) )

test(                                   "SMTK1 & LMTK2 fallback & LMTK3"
, s("[F3")
, s("(c" )
, s("{F4")
, s("}F4"), xd("{Alt}"), xd("{Shift}"), xd("{Tab}"), xu("{Tab}")
, s(")c" ), xu("{Shift}")
, s("]F3"), xu("{Alt}")	)

test(                                   "hold one SMTK all the time; another temporarily"
;                                       real-life example: Alt+(Shift+)Tab in both directions
, s("[F3")
, s("{F4")
, s("}F4"), xd("{Alt}"), xd("{Tab}"), xu("{Tab}")
, s("(c" )
, s("{F4")
, s("}F4"), xd("{Shift}"), xd("{Tab}"), xu("{Tab}")
, s("{F4")
, s("}F4"), xd("{Tab}"), xu("{Tab}")
, s(")c" ), xu("{Shift}")
, s("{F4")
, s("}F4"), xd("{Tab}"), xu("{Tab}")
, s("]F3"), xu("{Alt}")	)

test(                                   "LMTK & LMTK fallback & LMTK"
;                                       real-life example: movement layer & dual-role shift & .. -> Shift & Left
, s("[F4")
, s("(d" )
, s("{a" )
, s("}a" ), xd("{Shift}"), xd("{Left}"), xu("{Left}")
, s(")d" ), xu("{Shift}")
, s("]F4")	)

; ======================================================================
testReport()
} catch e {
	log("Exception: " . e)
	exitapp 1
}
File test_util.ahk (utilities for the automated test)

Code: Select all

class Expectation {
	__New(k) {
		this.k := k
	}
	toString() {
		return "‒▶" . this.k
	}
}

checkEquals(expected, actual, msg = "") {
	if (expected != actual) {
		msg .= msg == "" ? "" : ":`t"
		log("failed: " . msg . "expected: " . expected)
		log("        " . msg . "actual:   " . actual)
		return 0
	} else {
		log("passed: " . (msg != "" ? msg : (" (" . actual . ")")))
		return 1
	}
}

testReport() {
	local failureCount := 0, i, result
	log("")
	for i, result in testResults {
		local passed := result.errorMsg == ""
		log(format("{:-4.4} {:-70.70} {}", passed ? "ok" : "failed", result.name, result.errorMsg))
		if (!passed) {
			failureCount++
		}
	}
	log("`n" . (testResults.length() - failureCount) . " passed, " . failureCount . " failed tests")
	checkAllArgsRecognized()
	exitapp (failureCount == 0 ? 0 : 1)
}

testDisabled(testName) {
	; run only tests given as command-line args, but all tests with no args
	return A_Args.length() > 0 && arrayIndexOf(A_Args, testName) == ""
}

checkAllArgsRecognized() {
	local i, arg, j, testResult, ok
	for i, arg in A_Args {
		ok := 0
		for j, testResult in testResults {
			if (testResult.name == arg) {
				ok := 1
				break
			}
		}
		if (!ok) {
			log("unknown test: " . arg)
			exitapp 4
		}
	}
}
42td
Posts: 4
Joined: 20 Oct 2018, 11:15

Re: Attempt at dual-role modifier keys

22 Oct 2018, 00:06

I have seen it, but only after I had already put a lot of work into my script. I continued mine, because it needs no timing to tell modifier and tap usage apart, so i hoped it would feel better no matter how fast you type.
guest3456
Posts: 3463
Joined: 09 Oct 2013, 10:31

Re: Attempt at dual-role modifier keys

22 Oct 2018, 12:10

yeah, i'm using an AHK script to emulate StickyShift, but it has some slight bugs that pop up from time to time.

if i was in real need for these functions, i'd just by a mechanical keyboard that can be programmed with the QMK firmware


Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 93 guests