Key History and Macros#1507
Conversation
| if (S->ms.oneShot == 1) { | ||
| return true; | ||
| } | ||
| if (S->ms.currentMacroKey == NULL) { | ||
| return S->ms.oneShot == 1; | ||
| return false; | ||
| } | ||
| if (S->ms.currentMacroKey->activationId != S->ms.keyActivationId) { | ||
| return false; | ||
| } | ||
| if (!KeyState_Active(S->ms.currentMacroKey)) { | ||
| return false; | ||
| } | ||
| if (isCurrentMacroPostponing()) { | ||
| bool isSameActivation = (S->ms.currentMacroKey->activationId == S->ms.keyActivationId); | ||
| bool keyIsActive = (KeyState_Active(S->ms.currentMacroKey) && !PostponerQuery_IsKeyReleased(S->ms.currentMacroKey)); | ||
| return (isSameActivation && keyIsActive) || S->ms.oneShot == 1; | ||
| } else { | ||
| bool isSameActivation = (S->ms.currentMacroKey->activationId == S->ms.keyActivationId); | ||
| bool keyIsActive = KeyState_Active(S->ms.currentMacroKey); | ||
| return (isSameActivation && keyIsActive) || S->ms.oneShot == 1; | ||
| return !PostponerQuery_IsKeyReleased(S->ms.currentMacroKey); | ||
| } | ||
| return true; |
There was a problem hiding this comment.
I think this spells out the logic more, and it should also be more performant to exit as soon as any logic allows it?
There was a problem hiding this comment.
Well, I think that the refactor logic is correct, but I don't think that the result is more readable.
For me, this unrolled variant feels very alien since in order to understand how it behaves in real life, I have to hold the entire function in my mind and evaluate it, while in the original, I see the return on a single line: (isSameActivation && keyIsActive) || S->ms.oneShot == 1, and if needed, I can backtrack from there to see how keyIsActive is evaluated.
I have a strong preference to not include this Macros_CurrentMacroKeyIsActive refactor.
|
I like both |
|
But why do they have to live in the macrostate? Wouldn't this be global info? Assume a macro runs for a longer time, and other keys are pressed while it is running. Would |
|
They are meant to be previous key from thisKey, not last key, which would generally be thisKey. So either I expand history enough to be reasonably sure that it still has the relevant event, which, fair enough, would be 4-8 events, or I do this caching and become certain I have the info. |
|
Yes, RAM concerns I have. @kareltucek made sure I don't store macroArgs in the macro state but use a global pool across all macros. Balance for keeping memory consumption compact. But key history size: hold shift key and type A LONGER SENTENCE ALL IN CAPITALS. How would that hold up regarding key history and a macro bound to the shift key? I guess macro may need to cache the info at start of macro to avoid running out of 4-8 key history, e.g. Not sure I like this solution. Lock certain history items as "don't delete" because they were active when a macro started and just refer to the history index of that item to retrieve keyId and time when needed by the macro? Release the lock when macro ends? Exiting times. |
"Exiting times" are always exciting. I'll show myself out... |
|
Well it depends on how we would feel about a world in which a macro may loose access to key press information if it lives long enough. I'm thinking we don't feel too good about it, even if long-lived macros are discouraged. Sure, individual macros can mitigate by using variables, but it would have to be communicated very clearly in documentation, and going from certainty to near-certainty is a big leap. I could make key history persist as long as a macro has reference to it, but it adds a lot of complexity and increases RAM usage in that history now realistically needs to be long enough that each macro slot can hold a reference on an individual key event and it's predecessor. Finally, in a language like C without scoped deconstruction, I do not like such patterns. Further, I don't currently don't see the value in extending key history except having lazy fetching of some information in some macro commands/variables. |
|
One thing I have really appreciated about the UHK over the years, and since I started working in the codebase, is the lack of jank-by-design. Even if it limits the available options, and bugs notwithstanding, one can be reasonably sure that things work as intended, robustly, even in edge cases. |
That's what I did with macroArgs. Each argument in the pool stores the macro state slot index as its owner, and when the macro ends, I clean up the pool and remove the arguments owned by that macro. Memory consumption is 8 bits * pool size (32), vs. storing pointers or even complete arguments in the macro state (size * maximum number of concurrent macros (16)). Clean up is in core.c/endMacro(). |
|
Having thought about it some more, this is where I'm at: Either we accept that macros may loose access to information if they live long enough, or we don't. First, let's presume that we don't accept that: The information has to live somewhere. Having it live in macro state allows history to be only 2 long but increases macro state size. Having it live in macro state allows history to be only 2 long but increases macro state size. It can live in macro state or in history, but it has to live. As such, there is no fundamental argument RAM-wise to be made for storing it in one place or the other. Here are the arguments against having it live in history:
And the arguments for having it live in history:
Now, let's say we accept the possibility that history leaves a macro behind and they may lose access to data: I will not start making that choice, but I would be happy to implement it if it was made. |
|
I realise in the comparison with Thanks for the conversation! |
|
I am mot sure we actually need these. Afaik, the only person that ever requested these was Max, and it was for his own macro-HRM implementation, which is a usecase that is kind of not supported on purpose. But assume we actually want them. Then keeping the info in the macro state is indeed the only reasonable choice.
Indeed. Let's not go that way.
Macro arguments are a very different case. My bottom line: I definitely pick the macro state. Current implementation I believe. I will have to look the code over yet though. |
|
Don't yet. |
kareltucek
left a comment
There was a problem hiding this comment.
A couple change requests, but otherwise, seems solid.
| static uint32_t lastPressTime; | ||
|
|
||
| #define POS(idx) ((bufferPosition + POSTPONER_BUFFER_SIZE + (idx)) % POSTPONER_BUFFER_SIZE) | ||
| #define POS(idx) ((bufferPosition + (idx)) % POSTPONER_BUFFER_SIZE) |
There was a problem hiding this comment.
This will fail with negative indexes. I am not sure how much illegal they are, but would prefer to leave the original version in place.
There was a problem hiding this comment.
Aha, that's why it was there! Will it fail with negative indexes though? I guess that depends on what type the compiler chooses to calculate in, but if it calculates in unsigned integers, the buffer will underflow, and since the modulo is a power of two, it will all work out.
Will put it back the way it was, of course. :)
| if (S->ms.oneShot == 1) { | ||
| return true; | ||
| } | ||
| if (S->ms.currentMacroKey == NULL) { | ||
| return S->ms.oneShot == 1; | ||
| return false; | ||
| } | ||
| if (S->ms.currentMacroKey->activationId != S->ms.keyActivationId) { | ||
| return false; | ||
| } | ||
| if (!KeyState_Active(S->ms.currentMacroKey)) { | ||
| return false; | ||
| } | ||
| if (isCurrentMacroPostponing()) { | ||
| bool isSameActivation = (S->ms.currentMacroKey->activationId == S->ms.keyActivationId); | ||
| bool keyIsActive = (KeyState_Active(S->ms.currentMacroKey) && !PostponerQuery_IsKeyReleased(S->ms.currentMacroKey)); | ||
| return (isSameActivation && keyIsActive) || S->ms.oneShot == 1; | ||
| } else { | ||
| bool isSameActivation = (S->ms.currentMacroKey->activationId == S->ms.keyActivationId); | ||
| bool keyIsActive = KeyState_Active(S->ms.currentMacroKey); | ||
| return (isSameActivation && keyIsActive) || S->ms.oneShot == 1; | ||
| return !PostponerQuery_IsKeyReleased(S->ms.currentMacroKey); | ||
| } | ||
| return true; |
There was a problem hiding this comment.
Well, I think that the refactor logic is correct, but I don't think that the result is more readable.
For me, this unrolled variant feels very alien since in order to understand how it behaves in real life, I have to hold the entire function in my mind and evaluate it, while in the original, I see the return on a single line: (isSameActivation && keyIsActive) || S->ms.oneShot == 1, and if needed, I can backtrack from there to see how keyIsActive is evaluated.
I have a strong preference to not include this Macros_CurrentMacroKeyIsActive refactor.
| const key_press_event_t * KeyHistory_GetPreceedingPress(const key_state_t *keyState, uint8_t activationId); | ||
|
|
||
| // Querying specific info | ||
| uint8_t KeyHistory_GetMultitapCount(const key_state_t *keyState, uint8_t activationId); |
There was a problem hiding this comment.
Is there any chance that any of these two functions gets called with any but the last pressed keyState?
If yes, then I suspect that history size 2 might be insufficient.
(Just thinking out loud. I don't think it is the case.)
There was a problem hiding this comment.
Currently, there is not, no. I just put the interface in for good measure. I think you mentioned it when I originally made the history.
|
Funny how preferences are different. I much prefer the other factorization of Also, as I mentioned somewhere, there is one more thing I want to do before this PR is done: I want child macros to inherit more state. Also also, there has been talk of a repeat key, which can be easily implemented with |
|
And even for the repeat key it may make sense to have timing information since that last keypress, e.g. only trigger repeat if that previous keypress was within the last x milliseconds... |
Did some more key history work and exposed it in macros:
ifDoubletaphas support for detecting a series of taps any number long, within reason, such as triple and so on.Added
$previousKeyIdwhich can be used to create a repeat key, which I have seen requested.Added
$previousKeyPressTimewhich can be used to detect idle time before the key was pressed. I know that has also been requested.Also did a couple of small refactors where I ran across code which I thought could be optimized.
I want to do one more small refactor in macros: Macros started using
call,forkandexecshould inherit more variables from their parents thancurrentMacroKeyandkeyActivationId.I am okay with any part of this being rejected, I mostly did this for fun.