HRMs are hard to get right. I tried to use them a couple of times, but every time, I ended up stopping again because it was simply too hard to avoid triggering something accidentally. I have ideas of how to fix all of that, and so I will throw my two cents into the Scrooge McDuck money pit sized pile of pennies that is the opinions on HRMs.
The UHK has the most important resolution rule: Trigger by release. This one solves most of the issues. But it just isn’t enough! To get to a stage where accidental mod triggers are truly rare, while intentional triggers are still easy and effortless, something else is needed: Only trigger from activation from keys from the other half of the keyboard! This one feature would elevate HRM usage on the UHK to a whole new level!
So if the Ultimate Hacking Keyboard crew could just implement that as a configuration option, that would be awesome!
Thank you for coming to my Ted Talk.
Now, I speak with such confidence because I have tried cross-half activated Alt Role Keys, and I have seen the Light! And now you can too! Below is a Smart Macro which implements this exact functionality!
It has the following features:
- Every feature that the current UHK resolution strategy has, simple and advanced, including tie-in so the values are pulled from the UI configuration. The only exception is separately configurable double-tap timeout. Double-tap for these keys uses the general time-out.
- Optional (default) same-half lock-out, meaning a key release (or presses for simple resolution) from the same keyboard half as the held key will cause the held key to trigger primary rather than secondary. Once a key has resolved to secondary, through time-out or key release from other half, it stays secondary until release.
- Optional resolution override for each half individually to trigger keys directly as primary or secondary. Great for gaming. An additional macro is provided for cycling the override.
- Works perfectly fine with run-time macros as far as I can tell.
- Could be easily modified to allow specific actions for each of the event types: Primary (normal press), secondary (triggered by other key release), timeout (instead of primary/secondary), and double-tap.
- Works well across layers, just set up the right logic for it, as per my mod layer example
It has the following limitations:
- Requires the blocking scheduler, but it seems like that is the default one by now, if not the only one available, so really a complete non-issue.
- Has a running macro for every active key, so limited by the limited active macro slots. Never had this be an issue in practice.
- Takes up one of the very valuable variable slots. Only one, though.
- It’s quite large and, due to a necessity for tight control of macro and queue execution, not refactorable since macro calls yield and macro scopes are in short supply. This also means that it probably won’t work nicely with any background macro services which might be running.
- Not the easiest to set up, especially with keys whose secondary roles are to hold layers, or which are to not have board-half lockout
- All keys and actions for thumb modules are considered left half of the keyboard. This essentially only means something for keys on the mouse modules.
- A lot of the statements are long enough that editing them directly in Agent will freeze Agent, so development in another editor is more or less mandatory.
- My work-around for Issue #1331 is not completely air-tight and does very rarely cause false triggers on queue-released presses which then time out. Once that bug is fixed, this should stop happening without any harmful effects from my work-around.
To use the system, modify the macro AltRoleKey
. At the relevant places, put in the actions desired for each key you plan to bind. These are the places where it says primary resolution
and secondary resolution
. Primary resolution is straightforward, just press the keys you want for each key, or possibly more. Secondary resolution is generally equally straightforward unless you toggle mod layers. If so, you have to toggle back to the main layer near the end of the macro because we don’t have a “pressLayer” sort of function. For all keys presses, always use pressKey, NOT holdKey, as the macro takes very tight control of the flow of queue unrolls and the resolution happens under an active postponeKeys. The same goes for layers, don’t holdLayer, toggle it and release explicitly. For layers with multiple simultaneus activation possibilities, remember to check for simultaneus activation before toggling back to main layer, as per example in the macro (Key 28 and 94)
Of course, you might want alt role keys which are not on the home row, and for which secondary resolution for keys on the same half is wanted. Thumb keys are an example. These can be configured at the comment about “Board-half agnostic keys”, keys 28 and 94 in the example. Such keys will also ignore the override configurations and will always work as alt-role keys. This could be split such that ignoring override and ignoring board-half is configured separately, but I have not yet had a need to.
Once all of the desired key actions are defined, just bind the macro to all of the keys, setVar hrmState 0
in $onInit
, and type away.
To toggle the override for a keyboard half, just bind the macro AltRoleToggleOverride
to a key on one or both keyboard halves. Pressing the key will then cycle the resolution strategy for the half on which the key is placed through Auto, Primary and Secondary,
I think I have ironed out all of the kinks in the macro by now. Issue #1331 was a particularly fun one. I sometimes see missing cross-half activations, but it’s rare enough to not be a big issue, and much less harmful than unintended secondary activations anyway. I think it’s down to execution time of the loop.
The macro is developed on, and works well on, the UHK60 v1 with firmware 15.1.0. It should work on all UHKs with recent/latest firmware, but I can’t promise that.
I am open to questions and possibly even feature requests. I could still squeeze 7 more bits into the state variable I think, so still lots of room to play.
Linked is also my full keyboard config for if someone wants to just play with the feel of the HRMs without doing any work on setting it up first. The layout can be viewed visually at https://yuzukeycaps.com/c/c13727e1-6834-45a1-8a33-b608ad442318. The base key colors mean nothing right now, but the symbol colors are white for base layer, yellow for mod layer, and orange for secondary role. Mod is accessed as secondary on the thumb keys. It’s presumes US International keymap on the computer.
By the way, in light of the limited working memory, some bitwise operations for the macro language would be awesome, such as bitwise operators and shifts. Just an idle request.
And now for the actual macro: Feast your eyes on AltRoleKey
// Memory layout - 31 bits, so much room for activities!
// From LSB Multiplier Capacity Use
// 0-3 0 16 Iterator and resolution
// 4-11 16 256 Time shift and queued tap work-around
// 12 4096 2 Consecutive press indicator to filter false positive doublepresses
// 13 8192 2 Side-aware run
// 14-21 16384 256 Last key, for consecutive press indicator
// 22-23 4194304 4 Right half override
// 24-25 16777216 4 Left half override
// 26-30 67108864 32 It is the waste of space, the waste of space!
// 31 2147483648 2 No-man's land. The int is signed, and mod and division don't work as bit shift tools with negatives, so this bit is off-limits!
postponeKeys {
// initiate working memory. We store if it's a consecutive same key, and current keyId for next key to do the same test, plus indicator that it's a side-specific key
// we also put in half of the work-around for https://github.com/UltimateHackingKeyboard/firmware/issues/1331 here, the other half is at the end of the macro, read the explanation there.
setVar hrmState ($hrmState / 4194304 * 4194304 + ($thisKeyId == $hrmState / 16384 % 256) * 4096 + $thisKeyId * 16384 + ($thisKeyId == $hrmState % 4096 / 16) + 8192)
// Remove the indicator that the key should trigger primary for same-half releases for a set of specific keys. Thumb keys and such
if ($thisKeyId == 94 || $thisKeyId == 28) setVar hrmState ($hrmState - 8192)
if($hrmState % 16384 >= 8192 && $hrmState / 4194304 / (($thisKeyId >= 64) * 3 + 1) % 4) { // this half has override resolution, set it directly
setVar hrmState ($hrmState + $hrmState / 4194304 / (($thisKeyId >= 64) * 3 + 1) % 4) // note that this fature only applies to side-aware keys
}
while ($hrmState % 16 == 0) {
ifPlaytime $secondaryRole.advanced.timeout setVar hrmState ($hrmState + 3 - $secondaryRole.advanced.timeoutAction) // we are at timeout, resolve what the user wants
else if($secondaryRole.advanced.doubletapToPrimary) ifDoubletap if ($hrmState % 8192 >= 4096) setVar hrmState ($hrmState + 1) // doubletap to primaryNote that double tap triggers from the macro id alone, regardless of which key ran it, so we have to verify that last macro run was the same key
if ($hrmState % 16 == 0) ifPending 1 { // other keys have been pressed.
if (!$secondaryRole.advanced.triggerByRelease) { // direct resolution as per user request.
if ($hrmState % 16384 >= 8192 && ($thisKeyId >= 64) == ($queuedKeyId.0 >= 64)) setVar hrmState ($hrmState + 1) // we are side-aware and the key is from this side, resolve primary
else setVar hrmState ($hrmState + 2) // otherwise, resolve secondary
}
else if ($hrmState % 4096 == 0 || $secondaryRole.advanced.safetyMargin < 0) while (1) { // if we are not already timing out, or are timing out on primary
ifPendingKeyReleased ($hrmState % 16) { // A pending key has been released, we can resolve
if ($hrmState % 16384 >= 8192 && ($thisKeyId >= 64) == ($queuedKeyId.($hrmState % 16) >= 64)) setVar hrmState ($hrmState / 16 * 16 + 1) // released key is from same side of the keyboard as this key, and we are side aware, so we trigger primary
else { // if time-shifting towards primary role, we don't resolve right now, but set the time-out after which we will resolve, if we haven't already
if ($secondaryRole.advanced.triggerByRelease && $secondaryRole.advanced.safetyMargin > 0 && $hrmState % 4096 / 16 == 0) setVar hrmState ($hrmState / 16 * 16 + $currentTime % 256 * 16)
else if (!$secondaryRole.advanced.triggerByRelease || $secondaryRole.advanced.safetyMargin <= 0) setVar hrmState ($hrmState / 16 * 16 + 2) // otherwise, we resolve now
}
break
}
else ifPending ($hrmState % 16 + 2) setVar hrmState ($hrmState + 1) // check next key
else {
setVar hrmState ($hrmState / 16 * 16) // no more keys to check, clear working memory
break
}
}
if (1) noOp // https://github.com/UltimateHackingKeyboard/firmware/issues/1330
}
if ($hrmState % 16 == 0) ifReleased {
// if time-shifting towards secondary role, we don't resolve right now, but start the time-out to resolve if we haven't already
if ($secondaryRole.advanced.triggerByRelease && $secondaryRole.advanced.safetyMargin < 0 && $hrmState % 4096 == 0) setVar hrmState ($hrmState + $currentTime % 256 * 16)
else if (!$secondaryRole.advanced.triggerByRelease || $secondaryRole.advanced.safetyMargin >= 0) setVar hrmState ($hrmState + 1) // not time-shifting, resolve now
}
// check if we are time-shifting and have timed out, only if we have no other resolution
if ($hrmState % 16 == 0 && $secondaryRole.advanced.triggerByRelease && $hrmState % 4096 != 0 && ($currentTime - $hrmState % 4096 / 16) % 256 > $secondaryRole.advanced.safetyMargin && ($currentTime - $hrmState % 4096 / 16) % 256 > -$secondaryRole.advanced.safetyMargin) {
// time shift has timed out, so we resolve according to which action was shifted
if ($secondaryRole.advanced.safetyMargin < 0) {
setVar hrmState ($hrmState + 1)
}
else setVar hrmState ($hrmState + 2)
}
}
if (1) noOp // https://github.com/UltimateHackingKeyboard/firmware/issues/1330
// act on resolution. We start the resolution under the postponeKeys because otherwise mods won't reliably apply to the first key pressed
if ($hrmState % 16 == 1) {
// layer agnostic key primaries
if ($thisKeyId == 86) pressKey dotAndGreaterThanSign
else if ($thisKeyId == 94) {
ifCapsLockOn tapKey capsLock
ifLayer mod ifNotKeyActive 28 toggleLayer base
pressKey enter
}
else if ($thisKeyId == 28) {
ifCapsLockOn final tapKey capsLock
ifLayer mod ifNotKeyActive 94 final toggleLayer base
else pressKey escape
}
else ifLayer base { // base layer key primaries
if ($thisKeyId == 78) pressKey a
else if ($thisKeyId == 18) pressKey n
else if ($thisKeyId == 17) pressKey o
else if ($thisKeyId == 79) pressKey t
else if ($thisKeyId == 80) pressKey e
else if ($thisKeyId == 81) pressKey r
else if ($thisKeyId == 16) pressKey i
else if ($thisKeyId == 19) pressKey s
else if ($thisKeyId == 24) pressKey w
}
else ifLayer mod { // mod layer key primaries
if ($thisKeyId == 78) pressKey slashAndQuestionMark
else if ($thisKeyId == 79) pressKey openingBracketAndOpeningBrace
else if ($thisKeyId == 80) pressKey closingBracketAndClosingBrace
else if ($thisKeyId == 81) pressKey minusAndUnderscore
else if ($thisKeyId == 16) pressKey downArrow
else if ($thisKeyId == 17) pressKey upArrow
else if ($thisKeyId == 18) pressKey leftArrow
else if ($thisKeyId == 19) pressKey rightArrow
else if ($thisKeyId == 24) pressKey home
}
}
else { // secondary resolution
// general template for modifier key action - the same mod can be on different keys
if ($thisKeyId == 86 || $thisKeyId == 24) pressKey rightAlt
if ($thisKeyId == 79 || $thisKeyId == 18) pressKey leftAlt
if ($thisKeyId == 78) pressKey leftGui
if ($thisKeyId == 80) pressKey leftControl
if ($thisKeyId == 81) pressKey leftShift
if ($thisKeyId == 16) pressKey rightShift
if ($thisKeyId == 17) pressKey rightControl
if ($thisKeyId == 19) pressKey rightGui
if ($thisKeyId == 94 || $thisKeyId == 28) {
toggleLayer mod // toggle layer, remember to release it, see bottom of macro
}
}
}
if ($hrmState % 16 == 1) {
// The other half of the work-around for https://github.com/UltimateHackingKeyboard/firmware/issues/1331
// We know that ifReleased won't work for any key which was released while it was being postponed,
// so if the next key on the queue is released, note that for the next macro run and it will know it's been released that way
ifPendingKeyReleased 0 setVar hrmState ($hrmState / 16384 * 16384 + $queuedKeyId.0 * 16)
// beyond this point, consider the working memory lost to the next key.
delayUntilRelease
}
else {
// The other half of the work-around for https://github.com/UltimateHackingKeyboard/firmware/issues/1331
// We know that ifReleased won't work for any key which was released while it was being postponed,
// so if the next key on the queue is released, note that for the next macro run and it will know it's been released that way
ifPendingKeyReleased 0 setVar hrmState ($hrmState / 16384 * 16384 + $queuedKeyId.0 * 16)
// beyond this point, consider the working memory lost to the next key.
delayUntilRelease
while (1) { // ensure queue unroll before releasing
delayUntil max($keystrokeDelay, 4)
ifNotPending 1 break
}
if (1) noOp // // swallow the bug from https://github.com/UltimateHackingKeyboard/firmware/issues/1330
// layer toggles require explicit reset since we don't have a "pressLayer" style function
if ($thisKeyId == 94) ifNotKeyActive 28 toggleLayer base
if ($thisKeyId == 28) ifNotKeyActive 94 toggleLayer base
}
And the macro to toggle the resolution override, AltRoleToggleOverride
:
setVar hrmState ($hrmState + 4194304 * (($thisKeyId >= 64) * 3 + 1))
if ($hrmState / 4194304 / (($thisKeyId >= 64) * 3 + 1) % 4 == 3) setVar hrmState ($hrmState - 4194304 * 3 * (($thisKeyId >= 64) * 3 + 1))
goTo ($currentAddress + 1 + $hrmState / 4194304 / (($thisKeyId >= 64) * 3 + 1) % 4)
final setLedTxt 1000 "AUT"
final setLedTxt 1000 "PRI"
final setLedTxt 1000 "SEC"
And finally a link to download my current keyboard config: Proton Drive