In the variable "keys" we store the key names to be recognized. Only lower case letters can be listed. A parse loop generates hotkeys for each key in "keys", and its shifted (capital) version, and also hotkeys activated at the release of these keys. The "down" hotkeys record the key name in a variable named after the key (like a = A) and also the current time in "KeyTick", as the tick count since the PC was switched on. The "up" hotkeys clear the corresponding variables. This way modified keys (Ctrl-, Alt-...) are not affected.
The values of these variables are used in the timer subroutine "Action", activated in every 15 ms.
- After a key is pressed, the subroutine waits a little (30 ms), to allow further keystrokes to be registered as key combinations.
- When the key combination pressed does not change for 30 ms, the substitute is sent. These are selected in a long else-if sequence. If a key is pressed alone, we send it unchanged, so the normal keyboard behavior is not altered.
- After a substitute is sent, there is a blackout interval (360 ms) for the same key combination, to prevent too fast auto-repeating.
- After an already auto-repeated key the substitute is sent repeatedly in shorter period (50 ms).
This is the basic function, the keys and the substitutes for key combinations need to be defined according to your needs. A little more code is needed for non-letter keys, like ";". Because of the physical design of keyboards, some key combinations are not recognized, or the keys have to be pressed in a certain order to be seen as key combinations, so some experimenting is necessary.
StringCaseSense On Process Priority,,High #MaxHotkeysPerInterval 999 #UseHook SetKeyDelay -1 SentTick = 0 keys = asdfghjkl Loop Parse, keys { AllKeys = %AllKeys%`%%A_LoopField%`% HotKey %A_LoopField%, KeyDown HotKey +%A_LoopField%, CKeyDown HotKey %A_LoopField% up, KeyUp HotKey +%A_LoopField% up,CKeyUp } SetTimer Action, 15 Return KeyDown: KeyTick = %A_TickCount% %A_ThisHotKey% = %A_ThisHotKey% Return CKeyDown: KeyTick = %A_TickCount% StringRight key, A_ThisHotKey, 1 StringUpper key, Key %key% = %key% Return KeyUp: StringLeft key, A_ThisHotKey, 1 %key% = Return CKeyUp: StringMid key, A_ThisHotKey, 2, 1 %key% = Return Action: Transform keys, Deref, %AllKeys% If (A_TickCount - KeyTick < 30 ; wait for 2nd key or A_TickCount - SentTick < 360 and key1 <> keys ; first auto-repeat or A_TickCount - SentTick < 50 and key1 == keys) ; auto-repeat Return key1 = %key0% key0 = %keys% SentTick = %A_TickCount% If StrLen(keys) = 1 Send %keys% Else IfEqual keys,as, Send z Else IfEqual keys,AS, Send As I have said Else IfEqual keys,sd, Send x Else IfEqual keys,df, Send c Else IfEqual keys,sdf,Send self defense Else SetEnv SentTick, 0 ; when no key sent ReturnBelow is a more complicated version of the chording keyboard script, which can handle numbers and special characters, like `, %, ;. To avoid naming problems, each key has an associated variable, x
, where stands for the decimal ANSI code of the key. We have to extend the upper case conversion to handle special shifted characters, which are now taken from a character list from the same position as the un-shifted variant is in its own list. With two constantsLow = ``1234567890-=qwertyuiop[]\asdfghjkl`;'zxcvbnm,./
Shft= ~!@#$`%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?
a single instruction does the conversion of key "k" to its shifted variant, put in "U".StringMid U,Shft,InStr(Low,k,1),1
To improve responsiveness the hotkey subroutines below do not return, and wait for the timer to activate Action, but directly jump to Action. This way practically no keys are lost, even if they were pressed briefly. (Very rarely they still can be lost, if the system was busy and could not trigger the corresponding hotkey.)
Pressing more keys never happens in exactly the same time, so we have to leave a small time-window, where different keystrokes are considered simultaneous. If two keys are pressed quickly one after the other, the script cannot tell if they were meant as a key combination or two separate keystrokes. To minimize this problem, if there is no substitution defined for a key combination, the last key is sent unchanged, assuming the preceding key was hit accidentally. In addition, if one key is not yet released, while the next is stricken, we see an unwanted key combination for a short time. This event is caught, and the same action is triggered as when the first key was released before the next press.
Design goals:
- Key combinations send replacement strings (or trigger other actions, like launch programs)
- Keys have to be pressed together, within a short tolerance, in any order
- Auto repeat should function normally even with substitutions (needed for special characters)
- No other timing requirements (e.g. hold down or wait between keys for activation)
- Modified (Ctrl, Alt, Win) keys should not be disturbed
- As short script as possible
Rules:
- Any key combination physically distinguished is registered, a change is time stamped
- No replacement is sent for T0 time (20..40ms)
- A single key released within T0 (short press), still triggers a replacement (at key up)
- A key combination sends its replacement after T0 time
- After T1 (150..400ms) a second replacement is sent
- After further T2 time periods (40..100ms) further replacements are periodically sent, until one of the keys of the combination is released
- If there is no replacement defined, the last pressed key is sent (accidental key press)
- At slow releasing keys of combinations (decreasing key-combination lengths) no action is taken
- Event sequences key1-key2-key1up-key2up in rapid successions are treated as key1-key1up, key2-key2up
Limitations:
- If there are larger time differences between key presses, they might not be recognized as combinations, but as separate keystrokes (that is, the script cannot read you mind).
- Characters are recognized and sent by the interpreted script, not by low-level drivers, therefore, more system resources are used and the reaction time is dependent on other activities in the system.
- In rare occasions, at very fast typing, key-up events could be registered earlier than the key-down. If this happens, a key may remain registered as pressed down and repeat indefinitely. Pressing the key again stops the rapid fire.
Self-healing:
To prevent these repeat cycles we check each key in a lower priority loop if it is physically pressed down. If not, we clear the record.
ToDo:
- Replacement rules are read from an ini file
- On-line additions, changing of replacement rules, save/load rules
- Arbitrary actions triggered by key combinations
Other approaches:
There are other approaches to the problem, too. For example, key combinations could be activated with one of their keys pressed down for a certain period of time, followed by the other keys of the combination, or certain other patterns in key timings. In practice, these turned out to be unusable without long training, because normal typing has varying speed and key press times: some letters we type fast after each other, others have a short lag, enough to activate unwanted replacements. Furthermore, for auto-repeated keys we keep some keys down for a while, which have to be distinguished from the replacement triggers. This makes the activation timing sensitive, and our typing full of surprises. After experimenting with different schemes, the method described here proved to be the least obtrusive.
#MaxThreadsPerHotkey 10
#MaxThreadsBuffer ON
#MaxHotkeysPerInterval 999
#UseHook
StringCaseSense On
Process Priority,,Realtime
SetKeyDelay -1
BlockInput Send
Low = ``1234567890-=qwertyuiop[]\asdfghjkl`;'zxcvbnm,./
Shft= ~!@#$`%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?
KeySet = ``1234567890-=qwertyuiop[]\asdfghjkl`;'zxcvbnm,./
Loop Parse, KeySet
{
AllKeys := AllKeys "%x" Asc(A_LoopField) "%"
HotKey %A_LoopField%, KeyDown, B
HotKey %A_LoopField% up, KeyUp, B
HotKey +%A_LoopField%, CKeyDown, B
HotKey +%A_LoopField% up, KeyUp, B
}
SentTick = 0
SetTimer Action, 10 ; handle key repeat
RI = 0
Loop ; self healing missed key releases
{ ; to prevent infinite repeat
Sleep 10
RI := Mod(RI, StrLen(KeySet)) + 1
StringMid r, KeySet, RI, 1
If GetKeyState(r,"P")
Continue
r := "x" Asc(r)
%r% =
}
Return
KeyDown: ; register keys pressed
key := "x" Asc(A_ThisHotKey)
%key% = %A_ThisHotKey%
GoTo Tick
CKeyDown: ; register shifted keys pressed
StringRight k, A_ThisHotKey, 1 ; remove "+"
StringMid U,Shft,InStr(Low,k,1),1 ; convert k to Shift-%k%
key := "x" Asc(k)
%key% = %U%
GoTo Tick
KeyUp: ; register key release
StringReplace k, A_ThisHotKey, + ; remove "+"
key := "x" Asc(k)
%key% =
Tick: ; register time of key event
KeyTick = %A_TickCount%
Action:
Transform keys, Deref, %AllKeys%
IfNotEqual keys, %key0%, { ; KEYS CHANGED keys <> key0
If (keyn = 0 and len0 = 1 and len1 = 0 and keys = "")
GoSub SENDX ; short single key press
Else If (keyn = 1 and len0 = 1 and len1 = 2 and keys = "" and SentKeys <> key0 and StrLen(SentKeys) = 1)
GoSub SENDX ; overlapping keys
len1:= StrLen(key0)
len0:= StrLen(keys)
key0 = %keys% ; previous key combination
keyn = 0 ; the number of repetitions
}
Else { ; NO KEY CHANGE keys == key0
if (keys = ""
or A_TickCount - KeyTick < 40 and keyn = 0
or A_TickCount - SentTick < 360 and keyn = 1
or A_TickCount - SentTick < 60 and keyn > 1)
Return
keyn++
GoTo SEND
}
Return
SEND:
IfLess len0,%len1%, Return ; no send at releasing keys
SENDX:
SentTick = %A_TickCount% ; remember time of send
SentKeys = %key0%
If StrLen(key0) = 1
Send {%key0%} ; single keys unchanged
Else IfEqual key0,as, Send z ; examples ... edit here
Else IfEqual key0,AS, Send As I have said
Else IfEqual key0,sd, Send x
Else IfEqual key0,df, Send c
Else IfEqual key0,sdf,Send self defense
Else IfEqual key0,wo, Send without
Else IfEqual key0,[], Send in the box
Else IfEqual key0,tc, Send [TC]
Else {
L := %key%
SendRaw %L% ; send last pressed key
;SentTick = 0xFFFFFFFF ; uncomment for no auto repeat undefined combos
}
Return
Edit 2005.12.18: more complex variant added.
Edit 2005.12.24: New version with practically no skip, and with self-healing