kareltucek / firmware

This is fork of UHK's firmware featuring extended macro engine.
Other
82 stars 9 forks source link

Add missing commands for keyboard press/hold/release and possibly mouse too #11

Closed kareltucek closed 5 years ago

kareltucek commented 5 years ago
orthoceros commented 5 years ago

I wonder if this idea would help me realize what is sometimes called 'rocker gesture' in the mouse button context? I.e. assign actions to leftHold->rightClick or rightHold->leftClick. On the keyboard, the analogue would be, for example: d:=if(isPressed(f)) send(Ctrl+C); else send(d); f:=if(isPressed(d)) send(Ctrl+V); else send(f); I.e. a kind conditional secondaries via macro, without the need for a full layer. Like with secondary detection, an additional option to the isPressed function should enable checking for the key release event order before deciding on the intended if branch. (For robustness, so that one can still write fast any word containing df or fd key sequences, as long as the first key is released before the second...) Would this kind of two-finger gestures be possible?

kareltucek commented 5 years ago

I wonder if this idea would help me realize what is sometimes called 'rocker gesture' in the mouse button context?

Yes, it would, although I do not feel very strongly about that way of implementing the thing - implementing it (I mean writing the actual macro) would be a bit more complicated than you think, because you would have to add additional no-op delays in order to allow activation of the second key before checking for the conditions.

Actually, the resolveSecondary command is all you need to make the rocker gestures work. You can afford to allocate one layer to the rocker gestures because the firmware allows you to refer to layers of different keymaps. All you need is to use the holdKeymapLayer KEYMAPID LAYERID command instead of simple holdLayer. I.e., the number of layers that may be used within one mapping is not limited with this firmware.

orthoceros commented 5 years ago

Thanks for your quick reply! I copied the QUERTY keymap in agent today and named it MAK. Then I wrote the following makros:

A few remarks and test results:

Suggested solution:

kareltucek commented 5 years ago
  • I had to reduce the resolveSecondary delay from suggested 350ms to 200ms to get into a more comfortable zone
  • However, the downside of a shorter delay is that if I tap just f (or just d) a bit to long now, nothing happens at all (missing keys). Especially when using usual combos like Ctrl+F, I must now type more quickly than I am used to in some situations, otherwise the F is just missing, even if no secondary key follows...

This is interesting. If lowering the delay works for you to get rid of unwanted activations, you can get rid of the second problem by adding the following at the end of the macro. :

$ifNotInterrupted write d

Furthermore, you could limit it by a timeout. For instance $ifNotInterrupted ifNotPlaytime 350 write d.

Suggested solution:

Your suggestion is exactly how resolveSecondary is implemented. If the thing is misbehaving with long delay time, it is because you are releasing the keys in wrong order.

The only differences are:

There is a way to further improve the behaviour by adding a second delay after first release. I.e., when the secondary key release happens before primary key release, we wait for some more time and if the primary key is released in that time, we further behave as if the primary key was released first. The problem comes if the secondary key is pressed second time within the delay - the postponing system would have to remember and replay both the key taps, which becomes a bit hard to realize on the firmware level.

kareltucek commented 5 years ago

But congratulations, you've got me to think about that damned thing again. I'm going to dig into it further.

kareltucek commented 5 years ago

Here we go. The new release contains the second delay mechanism. resolveSecondary 350 350 1 3 should be much more reliable.

orthoceros commented 5 years ago

Thank you very much! A key ingredient for me was the "$ifNotInterrupted write x" at the end of each makro code block for the secondary actions: It has helped with missing keys due to too slowly releasing the primary key; a big step forward for me in terms of usability!

I have now configured "$resolveSecondary 200 0 1 3" makros on keys erdfcv. Triggering secondary actions via rocker gestures is not perfectly snappy with 200ms, but snappy enough as a tradeoff. However, with these 200ms, I still sometimes get missing keys when typing fast: For example, when writing "seldom" fast, I get "selm", because d->o triggers MAK.mod.o (which maps to nothing). This is equivalent to unwanted actions if MAK.mod.x maps to something.

So, I tried increasing the new second delay, but I am not sure if it works as I hoped: For example, even with a large secondary timeout $resolveSecondary 200 1000 1 3 on the c key, when I then enter "press c, press v, clearly release c within the 1000ms window and before releasing v, release v", I still get MAK.mod.v=Ctrl+V, even already on pressDownV. With a sufficiently high second delay parameter, I would expect that any key released after the time window for secondary actions has been closed by releasing the primary key, should be played afterwards. And if none was released within this time window of the pressed primary key, we are back at $ifNotInterrupted. I.e. in the above example, I would just expect "cv". In other words, I would like that only keys pressed and released within the time window from primary key press to primary key release (plus/minus tuning delta in ms) are mapped to secondary actions. Otherwise, their normal actions/keys should be played. Is this possible? Does it make sense or am I missing something?

kareltucek commented 5 years ago

Does it make sense or am I missing something?

Yes, you are entirely missing the point of the first argument. When the timer given by the first argument runs out, the resolution ends and is decided in favor of secondary role. By setting it low, you are making it end your resolutions prematurely.

The second delay (the (plus/minus tuning delta in ms) as you call it) acts only within the span of the first timer, so there's no way it could have helped the situation.

In other words, I would like that only keys pressed and released within the time window from primary key press to primary key release (plus/minus tuning delta in ms) are mapped to secondary actions.

The thing you ask for is exactly what the command does (and did before), it just does the thing only within the span of the timer (i.e., the first argument). If you set it to a large number, you will get what you ask for.

So, please, first try what $resolveSecondary 5000 0 1 3 does. Then try to use it in your standard workflow for some time. Then realize that if the thing is misbehaving, it is because your fingers release things in wrong order, and not because the algorithm is broken. Finally, increase the second argument to a reasonable value - e.g., 100.

If you still cannot figure out the purpose of the first argument, try to answer the following question: "How do I activate mouse movement via secondary role in the scheme I (you) have just proposed?"

orthoceros commented 5 years ago

Okay, thanks for the explanation. Got it, I think:

kareltucek commented 5 years ago

I see what difference it would make. The problem with this thing is that it contains a lot of tiny distinctions like whether or not we are querying for a key which is active or pressed but postponed. Honestly I do not feel very comfortably about the idea of implementing such a thing and maintaining it.

But what do you think of such a focused exception from normal operation? Would it work for the quick rocker gestures described above?

Well, I am not sure. Even if the condition was implemented, you would still have the pressed "c" in the postponing queue. There would have to be a way how to remove it from the queue again... I think that implementing it the way you propos would end up in a terribly overengineered solution.

But there's still the register mechanism. I didn't mention it at first, because it seemed as an overkill at first. But you can use the registers to indicate the state of the keys. Are you comfortable enough with the language to try yourself, or should I look into it? (I won't have time during the weekend.)

kareltucek commented 5 years ago

Also, I think that it might be a good idea to consider learning the conscious "roll-in V->C roll-out C->V" - because that is a generic scheme which can be used for unlimited number of additional shortcuts and does not cost you generality of your keyboard.

orthoceros commented 5 years ago

Yes, the roll in/out gesture is excellent in terms of generality and I will probably use it for other actions, but here I want to speed up the usual Ctrl+{xcvzy} from fast to very fast... Okay, I have thought about engineering this down using registers. I am currently thinking in these pseudocode lines:

kareltucek commented 5 years ago

What about:

      $ifNotRegEq 21 1 goTo  4
      $write A
      $setReg 21 0
      $break
      $setReg 20 1
      $delayUntilRelease
      $ifRegEq 20 1 write a
      $setReg 20 0

and

      $ifNotRegEq 20 1 goTo  4
      $write B
      $setReg 20 0
      $break
      $setReg 21 1
      $delayUntilRelease
      $ifRegEq 21 1 write b
      $setReg 21 0

(They are symmetrical - 20 <-> 21 and A <-> B.)

For better understanding assume just the following two:

Primary key:

      $setReg 20 1
      $delayUntilRelease
      $ifRegEq 20 1 write a
      $setReg 20 0

Secondary key:

      $ifNotRegEq 20 1 goTo  4
      $write A
      $setReg 20 0
      $break
      #default behaviour
kareltucek commented 5 years ago

(If you insist on hardcoded timeouts, you can use delayUntilReleaseMax, or use the ifPlaytime condition.)

kareltucek commented 5 years ago

I am a bit afraid that having either version of the rocker macro trigger on release or after delay will interfere with normal writing... In that case I guess I could introduce a new modifier it which will postpone all key activations except macros.

E: But that would interfere with other rocker gestures.

kareltucek commented 5 years ago

Any idea how to identify an arbitrary key? I mean, the originally proposed condition does not work if the target key is a macro because it is not a normal keystroke whose equality to scancode could be decided. If there was a condition like $ifNextKeyIs <something> , what would be the <something>? I could do such lookup easily, but how do I identify the key? [SLOT_COUNT][MAX_KEY_COUNT_PER_MODULE] indices could be used, but they don't seem as a very user-friendly option.

orthoceros commented 5 years ago

Thanks; I especially found $delayUntilReleaseMax interesting. I have extended your symmetric logic with a logic for arbitrary (rockerSource,rockerTargt) key pairs by storing their ASCII values in a register ("nLastPressedKey"). It works, but as you expected, I got a new form of unwanted interference with normal writing: If I write "comment" too fast, I get "ocmment" now, as c still waits at "$delayUntilReleaseMax 100" and o has the default $tap o action assigned. I.e. I miss a proper postponing logic... So, $ifNextKeyIs with such a queuing logic would definitely be helpful! See next post for a suggestion to uniquely identify keys. For reference, here is the pseudo code I am currently using for the c key (other rocker start/end keys have analogous macros):

//// UHK macro: Key c with rocker gestures:

//Test for rocker gestures *->c that have just been completed by this key press:
    $ifRegEq 20 118 goTo rocker_vc  //detect v->c gesture: if nLastPressedRockerSourceKey==ascii(v) goTo rocker_vc
    $ifRegEq 20 64 goTo rocker_dc   //detect d->c gesture: if nLastPressedRockerSourceKey==ascii(d) goTo rocker_dc

//Listen for rocker gestures c->* with this as primary/starting key:
    $setReg 20 99           //notify other macros by setting nLastPressedRockerSourceKey = ascii(this key) = ascii(c)
    $delayUntilReleaseMax 100   //wait until release or timeout (important: to avoid interference with normal typing, only a narrow time window is allowed, corresponding to fast "two-fingers-hit-simultanously-at-an-angle" gestures)
    $ifRegEq 20 0 break         //quit if any other rocker macro of has taken over control and consumed this primary key press elsewhere.
    $setReg 20 0            //If no rocker gesture was performed, close the listening time window for any secondary key press by resetting nLastPressedRockerSourceKey = 0.

//Perform the default typing action of this key and quit:
    $write v
    $break

//// Sub functions: Implement rocker gestures *->c

rocker_vc: //Map v->c to copy:
    $setReg 20 0            //consume the primary key press of this rocker gesture by resetting nLastPressedRockerSourceKey to 0
    $press LCtrl
    $write c
    $release LCtrl
    $break

rocker_dc: //Map d->c to navigate to next code cell:
    $setReg 20 0            //consume the primary key press of this rocker gesture by resetting nLastPressedRockerSourceKey to 0
    $press LAlt
    $tap PgDown
    $release LAtl
    $break
orthoceros commented 5 years ago

If there was a condition like $ifNextKeyIs something, what would be the something?

  • From a user prespective, I would like to stick with the same unique key names already known from the scancode drop-down list in Agent, irrespective of their assignment. E.g. $ifNextKeyIs C, $tap NextTrack, $press /?, $tap PageDown, $tap Space, $release 9( and so on.
  • To avoid having a long string translation table and actual translations in firmware, I would suggest using a macro postprocessor in Agent: It could translate each of the above examples into a 3-argument equivalent like "$tap SLOTCOUNT4PageDown MAXKEYCOUNTPERMODULE4PageDown PageDown", i.e. the firmware gets the numbers it needs in arg1 and arg2, but the user can conveniently enter key names that are retained in arg3. So no need for back-translation when loading the config from keyboard. On a macro code update, the user still only needs to care about arg3, as the post-processor will update/overwrite arg1 and arg2 again on "save macro".
  • Such a postprocessor could also become useful for other situations. For example, a user could write just "$goTo rocker_cv" and Agent replaces this on "save macro" by a "$goTo lineStartingWith([whitespace]rocker_vc:) rocker_vc". Again, the firmware directly gets the line number it needs in arg1, but the user has the convenience of names. If no line starts with [whitespace]rocker_vc:, then the Agent post-processor could display an error already on "save macro", before "save to keyboard".
  • Grammar-wise, $release 4$ and $tap 3# might need special treatment because of your #regNumber rule. I guess $ is already only active, if it is the first non-whitespace char on the code line, so no problem. Maybe, rename #regNumber to @@regNumber? See next remark on grammar updates with backwards compatibility.
  • Generally, such a post-processor could also pave the way for (hopefully seldom) grammar updates in the firmware, should the need arise, without breaking backwards compatibility: For example, if we want to change comments from # to // in order to also enable inline comments that do not conflict with the #regNumber rule, like "$command arg1 arg2 //comment", then the post processor could replace the first # of each old comment by // on "save macro". Alternatively, it could replace all #regNumber with @@regNumber when saving to a firmware with correspondingly updated language grammar. Of course, language updates breaking backwards compatibility should be avoided in general, especially later when the macro user base has grown. (But at this point, I would vote for comments at the end of code lines and for indentation support, i.e. whitespace before $ or comments in order to improve code legibility like in the above pseudo code...)
kareltucek commented 5 years ago

To avoid having a long string translation table and actual translations in firmware, I would suggest using a macro postprocessor in Agent

No way. First, having to use the modified agent along modifier firmware would be pain from both user perspective and developer perspective. Second, I am just not going to waste my time on hacking the agent.

Such a postprocessor could also become useful for other situations.

No, I don't want to employ any automatic "recompilation" system, no matter on which side. Also, I don't feel much need to maintain backward compatibility since the number of users using this firmware could be counted on fingers of one hand (of a not-very-skilled carpenter),.

kareltucek commented 5 years ago

Now to the thing which matters:

From a user prespective, I would like to stick with the same unique key names already known from the scancode drop-down list in Agent, irrespective of their assignment.

Well, I do understand that, but it is pretty impossible. What if the user uses the keyboard in a configuration which is different from the default one? Using any localized keymap would make the feature pain to use. If a user moves modifiers (which I imagine lots of people do), it will break the idea too.

The best solution I can see so far is to use a numeric id and add another macro which will write out the identifier when tapped.

orthoceros commented 5 years ago

The best solution I can see so far is to use a numeric id and add another macro which will write out the identifier when tapped.

If I understand you correctly, I would then assign, say, Mod+P to $printIdOfNextSentScancode. In order to get $press scancodeOfPgDown in the code editor, I would then simply write $press, then Mod+P, PgDown to get the right ID inserted. If PgDown was reassigned by the user, he would even get what he expects and sees in his keymap, similar to capturing keys in Agent. Sounds like a very good/general solution from my point of view!

Also, I don't feel much need to maintain backward compatibility since the number of users using this firmware could be counted on fingers of one hand (of a not-very-skilled carpenter),.

In this case and especially in context of the above scancode printer, I would really like to be able to append a comment like $press 46579 //press PgDown. I.e., could I please have?: BODY = whitespace*//COMMENT | whitespace*$COMMAND[//COMMENT]?

kareltucek commented 5 years ago

Yep, something like that.

Well, I did not say I wanted to make it messier than absolutely necessary :-). But I guess that it is a reasonable request. But one thing at a time.

orthoceros commented 5 years ago

Okay, cool! :) PS: My coming week will be very dense, too, but I hope to get back to UHK optimization next weekend...

kareltucek commented 5 years ago

Here we go, please read the new "Postponing mechanism" section of readme and the newly added (last) example.

I am closing this ticket since I've implemented the commands mentioned in the original post. Of course follow-up here on the topic of rocker gestures.

orthoceros commented 5 years ago

I can confirm that the added example for rocker c->v works (and enters V); thanks! But $resolveNextKeyId always returns 000 on my UHK (using v8.5.4.kt.9) and replacing the 90 (i.e. the second arg of $resolveNextKeyEq) with any char results in ERR.... I guess, it is not implemented, yet?

I already did some dry-coding in my head with the new postponing syntax, but could not yet figure out how to implement the gestures c->v and c->x simultaneously... Is it possible with just one macro on the starting key c, or do we need additional macros on landing keys (even if these do not need to be gesture starting keys, like x in my case)?

kareltucek commented 5 years ago

I guess, it is not implemented, yet?

Just a minor bug... should be working now.

I already did some dry-coding in my head with the new postponing syntax, but could not yet figure out how to implement the gestures c->v and c->x simultaneously...

The thing is basically an "if/else" construct. You can always put another "if/else" on the "else" branch. The only thing you need to avoid is consuming the postponed keys.

orthoceros commented 5 years ago

I can confirm that $resolveNextKeyId works well now; thanks. The single gesture c->v is also snappy and reliable now, if you set the the delay low enough (100ms in my case).

However, some problems still prevent it from becoming a productive UHK addition:

  1. modifier release events are not postponed: modifiers like Shift or Ctrl that are active at the beginning of a macro are not saved/postponed until the default case $tapKey c, causing a perfect sequence shift.press + c.press + c.release + shift.release to enter c instead of C, if it is completed in less than the resolveNextKeyEq delay. The intended Shift+c=C is only entered, if the Shift key is still being hold down after the delay of resolveNextKeyEq has ended. This is counter-intuitive, especially with traditional Ctrl+C for copy actions. Is there a way to save all pressed modifiers at macro entry, or even better, default-postpone all modifier.release events until after the default action $tapKey c has been executed by the macro?
  2. the delay is not interrupted by releasing the primary key: A sequence c.press + c.release + v.press + v.release can still trigger rocker c->v, if the v.press happens within the delay. I understand why this is happening, but find it counter-intuitive. I tried to insert $ifNotInterrupted goTo :default between resolveNextKeyEq and consumePending to prevent this, but this did not work and caused another problem:
  3. macro sequences are interrupted in unforeseeable ways (non-atomic exeuction): If consumePending is not the first/immediate command at the eq-goTo-target of resolveNextKeyEq, consuming does not work. It seems, as if most macro commands are allowed to be "interrupted between macro lines" by key events (from the user or the postponing queue), even a non-command like #comment line. I would expect that all macro commands are processed in uninterrupted/atomic sequence, unless they are delay commands. As macros cannot know/handle/foresee user key interrupts, I would default-postpone any queued event or user key event from the start of a macro until its completion or until the macro explicitly opens a time window, like by calling a command with a delay arg.
  4. How to achieve resolveNextKey else ifs without typing side effects?: I managed to implement three simultaneous rocker gestures c->{v,x,z} by "elseif via goTo". Essentially, I stack resolveNextKeyEq as follows: $resolveNextKeyEq 0 v 100ms :rocker_cv :test4rocker_cx and at line :test4rocker_cx another $resolveNextKeyEq 0 x 100ms :rocker_cx :test4rocker_cz etc. Maybe this is the completely wrong way to achieve this? While I can now trigger actions for c->v, c->x and c->z, this "3-branch elseif" construct causes typing side effects again: For example, I get "ocmmand" again instead of "command" when typing (this was not the case with just a single unstacked resolveNextKeyEq). Viewed from macro code flow perspective, the postponing is never interrupted, but "o" is inserted before "c"... Maybe it has the same root cause as point 3 above, i.e. the non-atomic processing of sequences of atomic macro commands?

(Sorry for the over-long post. I hope, I was able to describe it comprehensibly.)

kareltucek commented 5 years ago
  1. modifier release events are not postponed

Yep, it is planned. I have encountered similar problem before, but its occurences were extremely rare so I did not consider the issue urgent.

  1. the delay is not interrupted by releasing the primary key

This is true, and I will fix it. But still, this cannot be a problem if you have the timeout set to 100ms.

tried to insert $ifNotInterrupted goTo :default between resolveNextKeyEq and consumePending to prevent this, but this did not work and caused another problem:

I don't understand what you expected this to achieve. The Interrupted condition becomes active once another key is activated, but during the execution of the macro, all other key activations are postponed, so it will be always true. The only result is that the postponed key is be activated before the consumePending has a chance to consume it.

I know that interrupted keyword is not very descriptive, but I didn't have any better idea - if you have better one, please share :-).

  1. macro sequences are interrupted in unforeseeable ways (non-atomic exeuction)

The eventloop does not work that way. Per one update cycle, every macro is activated once, which specifically means that one action of the macro is activated. Then the macro returns either true or false, which indicates whether the action finished. If the action did not finish, it will be activated again in the next update cycle. If the action finished, next action is parsed and will be activated in the next cycle.

Postponing keys automatically on non-delay commands might work, but I don't like the idea of coupling the postponing mechanism so tightly to the macro player. Besides, one could easily write "active wait" loops, which would then unexpectedly make the keyboard hang.

How to achieve resolveNextKey else ifs without typing side effects?

Could you post the entire macro? (A screenshot will suffice if it's simpler for you.)

orthoceros commented 5 years ago

Thanks for the good explanation!

Could you post the entire macro?

The macro screenshot is attached below, but I already solved it with your new info on the event loop: My goTo line numbers in the last args of the $resolveNextKeyEqs pointed to my function/comment headers. I.e. to line 15 instead of line 16 for the default case...so one noOp cycle too much and the o got inserted before the c. By setting the goTos to the commands after the comment lines directly, I now can reliably type "command/comment" instead of "ocmmand/ocmment". I increased the timeout a bit as a safety margin and it looks good; I will test it in production/coding in the coming days! image

I know that interrupted keyword is not very descriptive, but I didn't have any better idea - if you have better one, please share :-).

I think it is well-named; I just interpreted it via a time window on the event series like: interrupted :<=> a (possibly still postponed) event has been received after pressing the macro trigger key and before its release.

Yep, it is planned.

Okay, great, thanks! :-) This is really the only problematic thing that I currently notice while typing. Especially capital c does reliably not work (should be Capital C), and holding LShift 200ms extra to make it work feels really long/slow... I will try to check&report back next Sunday.

kareltucek commented 5 years ago
  • Btw, how are async active wait loops (like noOp; goTo 0) currently detected and terminated?

They are not. Whoever wants an infinite loop shall get an infinite loop.

  • What do you think of an explicit modifier like atomicExecution or sync that would cause the event loop to directly execute the next macro action? (For comment lines, this should be implicitly enabled; see below.)

I do not see a motivation to have them atm. They do effectively the same as the postponeKeys modifier and postponeNext <n> command, except for already-running macros, and if they were implemented, it would be again via postponing.

Anyway, thanks for bringing up the topic, I would not have noticed it otherwise.

kareltucek commented 5 years ago

Here we go, postponing extended to key releases.

Also, I've realized that the timeout was ignoring key release on purpose - e.g., for implementation of "loose" gestures, like vim's gt gT gg etc.. So you can now specify untilRelease instead of the time.

orthoceros commented 5 years ago

Hi, thank you very much! I have now defined several rockers starting at c or v for copy&paste and related things. It works very well so far and without any typing interference (probably, because there are few words containing cv, vc, vx...). With respect to the trigger macro, I combined it with keymap/layer switches to source out the implementation of actual rocker actions to Agent. It is quite cool that you see the implementing keymap in the LED and that you can stack macros in this way. For example, I have now c->d mapped to "duplicate current line" (Home,Shift+End,Ctrl+C,End,Enter,Ctrl+V). Similar to resolveSecondary, I also found a way to not receive a "c" if I think again and want to abort the paste action: it now behaves like a mod key after 500ms. Finally, I restored the default OS behavior pressAndHoldC=>cccccc... after a safety delay. For reference, the semi-generic macro for the trigger key is below. It can be copied to new rocker trigger keys with minimal editing (define the new allowed secondary key IDs, route them to their implementing keymap/layer and update the default action). Btw., it would be useful to get press/tap/releaseKey <key ID by resolveNextKeyID> for non-ascii keys and a reflection to the macro trigger key, i.e. for example $tapkey thisKey; then we could implement the default action generically. I also got another maybe more important idea (next post/later...). image [edit: I just noticed that this image shows a slightly outdated version of the trigger macro: You have to insert a $postponeKeys goTo 13 to jump over the comment header of the default action. Otherwise the implicit noOp can destroy the order of near-simultaneously pressed keys. For example, I often got "nextkey" when actually typing "nextKey" after rocker-enabling the k key. I.e., the macro must not break the postponing time window before it executes the default press action of the key; currently, comment lines still do this...]

orthoceros commented 5 years ago

The (probably more important) idea relates to both rocker and to usual resolveSecondary gestures: I think, currently the main advantage of rockers compared to resolveSecondary (besides being very snappy and not release-order-sensitive) is the significantly reduced "attack surface" for typing interference, simply because only few key pairs of (triggerKey, allowedSecondaries) can deviate from normal typing, instead of all (triggerKey, *) combinations.

I think we could leverage this idea by making use of the "none" action in Agent for both gesture resolution types: First, what do you think of a $resolveIsNextKeyDefinedIn R4L mod :goToSecondary :goToPrimary? It would work just like $resolveNextkeyEq, only that you can conveniently specify all allowed secondary keys in Agent, simply by not assigning them to the none action in the target keymap layer R4L.mod. Actually, I made a key map template in Agent with completely empty layers and just copy it, whenever I need a new layer for implementing actions of a rocker trigger key. The keys having defined actions in this layer are identical to the keys queried by the $resolveNextKeyEq lines in the respective trigger macro. I.e. I could replace all lines under the first comment header by one $resolveIsNextKeyDefinedIn ....

Normal secondaries could also profit from the same idea to greatly reduce the attack surface for potential typing interference. I think of a $resolveSecondaryIfNextKeyIsDefinedIn R4L mod ... In some situations, I would prefer this over the rocker resolution type. (For example, rockers l->u and l->i caused typing interference when fast-writing things like "like", "live" or "resolution"; so I disabled rockers on l again. Here, I would prefer the resolution logic by release-order, if it would allow to restrict allowed secondaries...

I also thought about merging both resolution types into one command, as the rocker gesture seems to be just another OR condition towards goTo secondary. To get usual resolveSecondary behavior, one could simply set the rocker time window to zero. I think in the following command line to combine both gestures with the idea of reduced attack surface: $resolveSecondaryIfDefinedIn targetKeymap targetLayer timeWindow4triggerByFastSecondaryKeyPress duration4triggerByHoldingPrimaryKey safetyDelay4triggerByReleaseOrder lineIndex4confirmedReceiptOfSecondaryKey lineIndex4primaryKeyDefaultAction. Do you think it is possible to converge/streamline it like this?

kareltucek commented 5 years ago

Too many discussion points to keep track about, please next time think twice about your proposals and ideally split them into new tickets if they don't relate to the old thread...


I think that you are overengineering it.

The problem is that with any normal layer different from base, one wants it to contain a significant number of keys, which implies that it contains interfering pairs. The only reliable way to prevent unwanted key activations is abstraction on the level of activation mechanism - not an abstraction on the level of discrimination of keys depending on their mapping.

I also thought about merging both resolution types into one command, as the rocker gesture seems to be just another OR condition towards goTo secondary. To get usual resolveSecondary behavior, one could simply set the rocker time window to zero.

No, you could not. They are totally different resolutions. Rocker is activated on key presses. Secondaries are activated via analysis of releases.

I don't understand what the three timeout arguments in your proposal should do, but I am pretty sure that trying to combine the two mechanics into one is a bad idea. In other words, trying to heuristically decrease probability of bad activation of keys is wrong, because the result will never prevent all incorrect activations You should construct your mappings and learn to use the keyboard so that it is 100% reliable. The combined switch is simply not reliable because the assumption that the "attack surface" has zero intersection with real life usecases just simply does not work.


Second thing is that implementing IsNextKeyDefinedIn efficiently is not possible. If we want to determine action of another keymap, the keymap has to be fully parsed. I am not going to make a command parse entire keymap just for sake of resolving a branching condition.


Btw., it would be useful to get press/tap/releaseKey for non-ascii keys

I plan to implement a parser which would allow refer to non-ascii scancodes by reasonable ids (like "space", "pgDown", etc) ( #19). I don't wan't to allow "hardware" activation of keys at the moment, but if you insist, feel free to create a separate ticket for that. I am not sure if you understand that, but the thing you are proposing would mean exactly: "Do press that other key no matter what action it's mapped to" and not "send scancode corresponding to the letter which is printed on the keykap of that other key".

If you wish, you can help with implementation of the parser - for instance writing down the keys which are not covered by current mechanism alongside with their scancodes (by abbreviations which you can find in the firmware, not raw scancodes!) and types of scancodes (basic/media/system/mouse) into a table would be helpful .

reflection to the macro trigger key, i.e. for example $tapkey thisKey;

How would that work? A key which has assigned macro action has assigned macro action. You can't have both "default scancode" and "overriding macro" informations at the same time. There's no other information about what key it is apart from the macro.

In my local version of firmware, I now allow referring to thisKey by a "virtual register" #key, which is usefull for generic identification of the key (E.g., for the runtime macro engine), but that's it.

I would love if the Agent allowed to assign additional metadata to a key apart from the macro itself (like "macro arguments"), but it does no such thing, and the UHK team is not going to implement such exotic features.

kareltucek commented 5 years ago

Regarding the parser, nevermind. I will do it myself in some reasonable time.

kareltucek commented 5 years ago

@orthoceros please see:

I think they should greatly simplify your life :-).

orthoceros commented 5 years ago

Thanks, looks promising! :) Especially the relative goTos will save time when writing macros!

... so that it is 100% reliable. ... because the assumption that the "attack surface" has zero intersection with real life usecases just simply does not work.

I think you are right: In the meantime I went back from rockers to secondaries because of obscure words/commands/passwords that resulted in unexpected behavior with rockers (you always have to think of the defined rocker key pairs when typing...). Back with secondaries, I got the old problem again that cv overwrites selected text instead of pasting the clipboard... So I gave up for a few days and went back to Ctrl+c/v/x.

The only reliable way to prevent unwanted key activations is abstraction on the level of activation mechanism...

I agree. I still would like to achieve fast&reliable shortcuts/modifiers without having to move the fingers from near the base row. So I gave this another thought. I came up with the idea of a "2-finger-mod-key" concept. I will try to implement it via the new ifShortcut or other commands and maybe write about this in a new issue in the coming days.

... the keymap has to be fully parsed.

Do you mean parsing the keymap JSON from Agent into some binary mapping "hardware keys => entry points of actions/macros"? If so, I agree that it would be very inefficient for each branching condition (or any other non-delay runtime action). However, I wonder if it is possible to parse each keymap just once, directly after a config upload? Then, changing the current keymap would just need to load its pre-parsed binary mapping structure. Ideally, if there is enough RAM to hold all parsed binary keymaps simultanously(?), changing the current keymap could become as efficient as a "pointer switch": I.e., if the hardware key "x" gets pressed (and we are not currently in postponing mode), a goTo currentKeymap.currentLayer.x is executed, where x is essentially just another goTo target pointing to the action/macro implementing this key for the current layer of the current keymap.

How would that work? A key which has assigned macro action has assigned macro action. You can't have both "default scancode" and "overriding macro" informations at the same time.

A possible solution might be $callKeymapLayerAction QWC base thisKeyHardwareID, where QWC is a copy of the vanilla QWY keymap, resolving the key to the intended scan code. Of course, this can only be efficiently processed at runtime, if it is feasible to parse keymaps in advance. In this case, the same command could also be utilized to call macros from other macros, like action sub functions called from listener/resolver macros. That would be quite flexible, but requires implementing a calling stack.

kareltucek commented 5 years ago

Do you mean parsing the keymap JSON from Agent into some binary mapping "hardware keys => entry points of actions/macros"?

Well, there are the following representations:

Currently, on keymap switch, a keymap is parsed from the binary representation into the runtime representation.

Regarding memory, lets see:

I.e., no, there's no way to fit all keymaps into RAM. Looking the actions up in the eeprom is not impossible (although inefficient), but I don't feel comfortable about it and I don't see a sufficiently good motivation.

A possible solution might be $callKeymapLayerAction QWC base thisKeyHardwareID,

Yeah, I have though about this, but it again boils down to the above problem.

In this case, the same command could also be utilized to call macros from other macros

Macros are not stored within keymaps. Macros are stored in a separate container and the keymaps only reference these macros by ids. Therefore, when one macro is called from multiple different places, it is not duplicated in the config.

That would be quite flexible, but requires implementing a calling stack.

Implementing full "call & return & continue where left" macro execution would be complex and demanding on memory.

However, I am quite open to having "exec" on macros - i.e., replacing the current macro by another one. That should be quite easy, except that there is (again) no ideal way of identifying the macros which are to be run. But at the moment, I dont see much motivation for it. At least as long as registers can hold neither scancodes nor layer/keymap ids.

kareltucek commented 5 years ago

Also one more thing: key actions and macro actions are two separate things. Totally distinct data types even though they describe the same thing.

kareltucek commented 5 years ago

But yeah, at least within the same keymap, activating action of another key of the same or different layer is possible, except for a few problems:

orthoceros commented 5 years ago

I see; thanks for the good explanation. Just a few thoughts:

While this callAction would be nice to have, I currently worry more about the harder mutex problem reported in the other issue: If this cannot be solved, any gesture involving fast successive key presses could never be implemented reliably...

kareltucek commented 5 years ago

I read your other idea about "Selective import of layers or keymaps"...

Yeah... but it sounds overengineered too. Besides, the idea would work terribly badly in combination with the holdKeymapLayer mechanism. Currently, I use a system which consists of 14 layers, so I cannot really abandon it.

am not sure if I understand what you mean by querying the key state.

The problem is that the code which interprets those things uses information from the keystate matrix. I.e., if the code is to be reused, it has to be given a piece of memory which either is part of the keystate matrix or looks like it.