That sounds very promising! Thanks for the feedback.
Been using this setup and haven’t run into any breaking issues so far. Things are working smoothly and as expected without disruption, except:
When I make other changes to the UHK within Agent and save to keyboard, I often get a variable error, like this
Error at hrm-timer 1/19/14: Variable not found: hrm_active
> 19 | if ($hrm_active == 0) {
Error at hrm-timer 1/22/10: Encountered unmatched brace
> 22 | }
Error at hrm-auto 1/3/8: Too many variables. Can't allocate more than 536935944 variables:
> 3 | setVar hrm_tick 0
> | ^
Error at hrm-auto 1/4/8: Too many variables. Can't allocate more than 536935944 variables:
> 4 | setVar hrm_tick_delay 10
> | ^
Error at hrm-auto 1/5/8: Too many variables. Can't allocate more than 536935944 variables:
> 5 | setVar hrm_tick_active 30
> | ^
Error at hrm-auto 1/6/8: Too many variables. Can't allocate more than 536935944 variables:
> 6 | setVar hrm_space 0
> | ^
Error at hrm-auto 1/7/8: Too many variables. Can't allocate more than 536935944 variables:
> 7 | setVar hrm_reduced_safety 80
> | ^
Error at hrm-auto 1/8/8: Too many variables. Can't allocate more than 536935944 variables:
> 8 | setVar safetyMargin $secondaryRole.advanced.safetyMargin
> | ^
Error at hrm-timer 1/2/8: Too many variables. Can't allocate more than 536935944 variables:
> 2 | setVar hrm_active_state $hrm_active
> | ^
Error at hrm-timer 1/5/12: Too many variables. Can't allocate more than 536935944 variables:
> 5 | setVar hrm_tick ($hrm_tick + 1)
> | ^
Err
Simply reconnecting the device resolves this, or sometimes making a small change, then reverting to trigger the ‘Save to keyboard’ button (such as moving a key).
Is this bug fixable? I noticed that you asked about a way to dismiss these errors in another thread. Looking at your code, these seem to be false errors, since they go away after a reset.
I tried running the hrm-auto
, hrm-on
, hrm-off
, hrm-timer
, switching keymaps back and forth, running the clearStatus
and resetConfiguration
commands, but none of these consistently resolve the error. The only solution that works regularly is reconnecting, and that is inconvenient.
The resetConfiguration
command seems to have been deprecated, although it listed in the Reference Manual, but there is no documentation regarding that command. It also throws an error in Agent when trying to use it.
Would declaring these variables in the $onInit
script work?
That’s a side effect of a firmware issue. It’s not stopping macros when the config gets reloaded, which then seems to lead to some memory corruption.
The only workaround that I found is to stop all running macros before saving the new configuration, i.e. run hrm-off and only after H--
(and -T-
) has been displayed, then save the new config.
Once the memory corruption has occurred, you have to power-cycle the keyboard.
Have you already seen this new approach to HRM?
Interesting, thanks for the link. When I read its claims:
The main idea of this library is to interpret pauses between key releases (not presses, as in every other approach).
I immediately thought: isn’t that what the UHK firmware already does with Advanced strategy “Trigger by release”? I think the net result is quite similar to what I built (or could build) with my macros.
@maexxx Not sure what I changed in my initial settings, but now I am getting this weird issue where the secondary modifiers randomly get disabled. Like, I’ll be using the keyboard and then suddenly, holding down a home row modifier key inputs repeating characters of the alpha key pressed. Powercycling does resolve the issue. How can I debug this issue?
@nacho Did you somehow switch it to H--
(off)? Did you switch the keymap back to your normal recurva keymap and the overlay layer from HRM mod is now gone?
What happens when you run hrm-off, then hrm-on? Do the hrms work after that (without auto logic)? What happens if you then run hrm-auto? What’s shown on the Led display?
No, the LED display still shows as H++
(or in my case +++
)
No, I never switched any keymaps.
This does resolve the issue.
Yes, as of now this seems to be the optimal solution when the HRM keys stop working.
Now it shows +++
I suspect that the hrm-overlay somehow disappeared and your recurva keymap (without the HRM keys) has been reinstated. I cannot tell why that happened.
hrm-on and hrm-auto both install the overlay with the overlayLayer base HRM mod
statement.
@maexxx could it hav something to do with too many macros? Or using too many at a time? Or within a short period? I think I notice it getting disabled either after returning to my desk or if I’ve been using a lot of macros/shortcuts, but I can’t be sure
@nacho I don’t have an answer to those questions, sorry.
I did not look into the details, as for myself I decided that in my setup it is not worth to have any potential trouble with HRM and because in my layout bottom-row mods work so well without any problems. My curiosity was not big enough to invest time to look into something which does not affect me. So not sure if that is similar.
I figured it out how to replicate my issue.
I had set the backspace button on my HRM keymap to switch keymaps back the Recurva. So, any time I would press this button on the Recurva layout, it would mess up the setup and I would need to reset the macros. I had done this during the period when I was debugging the initial setup and never changed it back.
That makes a lot of sense. When you switch back to the keymap, the overlay with the HRM macros is lost. Then of course the HRMs won’t work anymore.
Glad you could sort it out!
I’m still running into this issue fairly often, and notice I need to change up my typing habits to type with less mistakes. I’ve messed around with the hrm_tick_active
, hrm_tick_delay
, secondaryRole.advanced.timeout
, secondaryRole.advanced.safetyMargin
, and hrm_reduced_safety
, but can’t seem to find the optimal configuration. Honestly, it’s just so many variables that I can’t really figure out what’s doing what, that I just mess with one or two at a time and try experimenting with them to see if I magically figure it out.
That, and setting the values in $onInit
not matching the UI of Typing Behavior screen makes it difficult to figure out which setting is currently being used. On top of that, the Save to keyboard
button does not toggle if the first change I make is any slider setting on the Typing Behavior screen. I need to make some arbitrary change elsewhere to toggle the Save option, then adjust whatever setting, then I can save, but even after all that I don’t notice much of a difference. Any tips?
Settings in $onInit
override whatever you set in the sliders, just so you know.
I was browsing r/ErgoMechKeyboards and came across this post:
https://www.reddit.com/r/ErgoMechKeyboards/comments/1fxw6kl/homerow_mods_but_qwerty_row/
In the post, a user links a repo titled “Timeless homerow mods” here
The concept seems great, essentially enabling HMR with a timer-less setup to resolve the typing speed variation issues.
@maexxx What do you think of this? I’m not familiar with ZMK, and just glancing at the code looks like it still uses a timer… Is there any way to bring a “timer-less” config to UHK?
I use this approach in my ZMK keyboard. Essentially it’s a combination of "balanced" flavor of Hold-Tap
( PERMISSIVE_HOLD
in QMK, and secondaryRole.advanced.triggerByRelease
in UHK), require-prior-idle-ms
(Achordion mod
in QMK, missing in UHK), and hold-trigger-key-positions
+hold-trigger-on-release
(Achordion mod
in QMK, missing in UHK). Of course it uses timers internally, but you get a feeling as if you type naturally. That is, you never wait for time triggers, and can reach 100+WPM speed with mixed register.
Actually, despite UHK misses some of features that constitute this “timeless” HRM feature, it has one feature both QMK and ZMK miss. secondaryRole.advanced.safetyMargin
. This parameter is very useful in cases when you’re used to release Mod and Modified symbol simultaneously. It kind of makes a Mod a bit sticky. In QMK and ZMK, I had to relearn to slightly lag behind with releasing Mod. But, with a lot of practice, in sticks in motor memory anyway.
It’s more complicated than that.
<tl;dr>
Yes, my macros use a timer, but for a completely different purpose than the version of “timeless” that this original quoted ZMK post refers to.
</tl;dr>
Let me walk you through my experience with the levels of homerow mods that I’ve learned about. This pretty much summarises my understanding of homerow mods, and why I’ve built the macros the way I did.
Homerow Mods - the good, the bad, and the ugly
Level 1 - the initial “homerow mods”
All based on timing. Hold a homerow key for more than x ms and it will activate a modifier. Tap it briefly, and you will get the primary letter. To use this, you will have to slow down your typing each time you want a SHIFTed character.
You could implement this simple logic in UHK macros. Basically, this example macro requires you to hold down the key for more than 300ms to get modifier mod:
delayUntilReleaseMax 300
ifPlaytime 299 final holdKey mod
tapKey a
(haven’t tested this macro)
Level 2 - “timeless” homerow mods
To get rid of the hold-down interval to activate the modifier, you have to look at the subsequent keypress and the sequence of press and release events. This is independent of timing, thus “timeless”.
(For these examples, I am assuming two keys a
and b
, with a
sending scancode a and b
sending scancode b, and a
also used for the secondary modifier mod)
- press
a
- releasea
- pressb
- releaseb
=> types a followed by b - press
a
- pressb
- releasea
- releaseb
=> types a followed by b - press
a
- pressb
- releaseb
- releasea
=> types mod-b
As you can see, the decision between primary a and secondary mod is based on the order of releases (once both keys have been pressed down). If a
is released first, you get primary a; if b
is released first, you get secondary mod from the a
key. (You also get b from the b
key in both cases.)
No timers involved. This is what I believe the “Timeless homerow mods” article is talking about.
This is pretty much what the UHK can do. It actually offers a few variants. These differ in when the decision is made and when scancodes are output, and in the case of advancedStrategy
it also adds more parameters to choose timing offsets.
Level 2a - UHK secondary mods - simpleStrategy
The simpleStrategy will hold back the first press of a
to see whether another key will be typed at the same time. As soon as the next input event happens, it will then activate either the primary or the secondary action of a
.
The difference to the general description of “Level 2” is that the order of press events matters, not the order of release events. There are no timers involved. You can hold down a
as long as you like, and it will not activate anything. This also means you cannot use this for anything like shift-click; mod just won’t activate if you only hold down on a
.
Let’s break down that decision table in more detail:
-
press
a
- releasea
- pressb
- releaseb
=> types a followed by binput event output event press a
(no action) release a
press a, release a press b
press b release b
release b -
press
a
- pressb
- releasea
- releaseb
=> types mod-binput event output event press a
(no action) press b
press mod, press b release a
release mod release b
release b -
press
a
- pressb
- releaseb
- releasea
=> types mod-binput event output event press a
(no action) press b
press mod, press b release b
release b release a
release mod
This works, but only if you are very precise with your typing. To get primary keys, you cannot overlap the press events. As soon as a
and b
are down at the same time, you will get mod-b. This is a problem if you are rolling keys. When typing fast, often the next key is already going down before the previous key(s) are fully released. That is why this simpleStrategy
does not work well for homerow-mods.
Level 2b - UHK secondary mods - advancedStrategy
The UHK advancedStrategy
offers so much more options to influence the primary/secondary selection.
- There’s a timeout. Holding down the key for more than
secondaryRole.advanced.timeout
activates eitherprimary
orsecondary
(secondaryRole.advanced.timeoutAction
). - Choice whether to look at the order of press or release events. (
secondaryRole.advanced.triggerByPress
,secondaryRole.advanced.triggerByRelease
) - Choice whether to trigger secondary on mouse events (
secondaryRole.advanced.triggerByMouse
) - Double-tap (tap-then-hold) to keep the primary pressed (for autorepeat) (
secondaryRole.advanced.doubletapToPrimary
,secondaryRole.advanced.doubletapTime
)
This can now be used to implement the release order logic described for level 2.
Let’s configure:
set secondaryRole.defaultStrategy advanced
set secondaryRole.advanced.timeout 250
set secondaryRole.advanced.timeoutAction secondary
set secondaryRole.advanced.triggerByRelease 1
Because of the timeout, you can now mod-click if you hold down for more than 250ms.
Also, the release order is now important and you can have a bit of overlap between keys (rolling). As long as you release a
before b
, you will still get a and b:
-
press
a
- releasea
- pressb
- releaseb
=> types a followed by binput event output event press a
(no action) release a
press a, release a press b
press b release b
release b -
press
a
- pressb
- releasea
- releaseb
=> types a followed by binput event output event press a
(no action) press b
(no action) release a
press a; press b; release a release b
release b -
press
a
- pressb
- releaseb
- releasea
=> types mod-binput event output event press a
(no action) press b
(no action) release b
press mod, press b; release b release a
release mod
That’s now better. You can start rolling keys, and you can try using this for homerow-mods. But it’s far from perfect.
Sometimes we don’t release a
early enough (happens to me especially when I am tired and get sloppy with my typing), and that means mod will get activated sporadically. We don’t want that during fast typing as it will mess up the text (for example, spurious “Hift” instead of “shift” because s
was mistakenly triggering the shift mod). What’s worse, it may also trigger Control or Alt modifiers, causing havoc as control sequences and Alt-menu shortcuts get activated unintentionally.
There is a bit of adjustment possible with secondaryRole.advanced.safetyMargin
, which allows you to skew the detection a bit towards either overlap or non-overlap (secondary or primary). You can move it towards safety (i.e. lean towards detecting primary) but that then means you need to type slower again if you want a secondary. So you can tweak, but there are downsides again, and hey, you are no longer “timeless”.
Level 3 - cross-hand detection
We usually roll fast with fingers of the same hand. But we use modifiers with alternating hands. We can use that knowledge. Let’s only activate the secondary function if the modifier is combined with keys from the other hand.
My scripts do that with the ifShortcut
command. If they detect that you are typing a homerow-mod quickly followed by another key of the same hand, they forgo the secondary check and immediately activate the primary function of the homerow key. This would be bound to homerow mods on the right side:
ifShortcut timeoutIn 100 orGate noConsume y u i o p h j k l semicolonAndColon n m goTo primary
If, within 100ms, the press of the homerow-mod key is followed by any (orGate
) of the listed keys (all keys from the right side), then immediately resolve as a primary. noConsume
keeps that next keypress still in the event queue so it will still be processed next.
Yes, there’s a 100ms timer, but it doesn’t influence your typing speed. If you are typing fast, the next key will arrive well within those 100ms and the decision if you typed the shortcut or not can be done immediately on that next keystroke. So I would still consider this “timeless”, i.e. you don’t have to slow down your typing to hit certain timers.
Oh, and btw, this still allows you to type a modifier with a key on the same hand. You just have to hold down the modifier long enough for the timeout to occur. Once 100ms have passed, the code continues to the normal secondary detection logic. So if you must use the same hand, just go slow and hold the mod a bit before typing the next key.
This solves a lot of the inadvertent spurious mod-activations on fast finger rolls.
Level 4 - typing streaks detection
So now we get to the hardcore. If I am in the middle of a fast typing streak, I absolutely want to avoid accidential Control, Alt and Gui (Win) modifiers. We could discuss Shift, but for now, let’s treat all mods the same. When I am typing fast, let’s turn them off.
This is the last part of the puzzle, and that’s why I had to create the timer macro. Because at the moment, the UHK firmware doesn’t have a notion of “fast typing streak”, or even “time since the previous keystroke”. Each macro gets to know how long this current macro has been running, but it cannot find out how much time has passed from the previous keystroke to the current one. So the macro on it’s own doesn’t know if it is being called during fast typing.
So I created the timer background macro to basically just count time. To increment a counter. And in each hrm-key macro, I reset the counter to 0:
setVar hrm_tick 0
With this, I can disable the homerow-mods when the counter is low (meaning not a lot of time has passed since the previous keystroke; the user must be typing fast), and re-enable homerow-mods when the counter is higher (meaning the user paused for a moment). And that’s the job of the timer macro: to turn homerow-mods on or off depending on typing speed. It modifies hrm_active
to tell the other macros whether they should check for mods or not.
And thus the second part of the homerow-mod macros. Just after the same-hand check, I check for secondary – but only if hrm are supposed to be active:
if ($hrm_active > 0) ifSecondary final holdKey rightGui
And now, finally, we have timers again. Timers that determine how long hrm should be off once we are in a fast typing streak. What’s the time interval between “fast” keystrokes?
But these timers are VERY different from the timer that was meant in the initial “timeless homerow mods” post.
Conclusion
We are beyond “timeless” homerow-mods with the UHK. We were actually never at Level 1, where timeouts were needed to trigger mods at all. We always had Level 2 with some variations and configurability.
I now propose to extend the UHK firmware to add “same-hand secondary avoidance” (with a timeout, so you can slow down to deliberately use a mod with the same hand), and “fast typing streak secondary block-out” (either in the firmware itself, or at least providing macros a “time-since-previous-keypress” variable).
Until those features are implemented in the firmware, use my hrm macros. I truly believe they improve on the regular advancedStrategy
secondary detection of the UHK. Unfortunately, they are a bit awkward to set up
What does hold-trigger-key-positions
do?
(I’m not very knowledgeable on QMK.)