ModifierLayers plugin and Programmer Dvorak layout

Plugin name: ModifierLayers
Author: Nikita Kitaev
Source URL: https://github.com/nikitakit/Kaleidoscope-ModifierLayers

Description:

A modifier key like Shift applies the modifier to all other keys pressed, while a key marked as ShiftToLayer changes the current keymap. This plugin allows a single key to do both: it can activate an overlay layer where the keys defined there get used directly, but any transparent keys fall through to the base layers and simultaneously have the modifier applied to them.

One of my use-cases was to set up a hardware implementation of Programmer Dvorak, a layout that completely changes the number row of the keyboard and makes symbols accessible without pressing shift. Originally I had used the macro-based implementation by @Benji, but when I wanted to do similar things with other layers and modifiers I thought that it would be better to use layer definitions rather than continuously growing a gigantic switch statement.

I couldn’t find an existing plugin that offered the range of functionality I wanted, so I wrote this one.

5 Likes

As someone who’s got a similar plugin on the back burner, what’s your strategy for overlap/rollover in typing keys that want different shift states?

Right, you can’t do true rollover with keys that have different shift states.

My approach is that when you hold down a modifier that also toggles a layer, the rest of the keys get partitioned into three sets: the ones that must use the modifier (e.g. the letter “a” when shift is pressed), the ones that must avoid using the modifier (e.g. the number row in Programmer Dvorak), and the ones that don’t care (e.g. other modifiers and macro keys). Then for example when a must-avoid-holding category key is pressed, the opposite category gets masked out and can’t trigger any key-down events until all keys in the must-avoid-holding category have been released. And vice versa.

The masking behavior was the most challenging part of writing this plugin. I may still need to tweak the three-category classification if I find that keys classified to the don’t-care category actually shouldn’t be.

I thought about the alternative of just combining shift with the numbers, but that’s undesirable for text-editing usecase because it will cause keys to be typed that you didn’t input. I also considered splitting up into multiple HID reports, but that would cause thrashing which is again not good if you’re editing text.

1 Like

Have you found a good way to deal with the problem of compatibility with other plugins that modify the shift state without holding keys (e.g. OneShot) or holding keys that are injecting modifiers (e.g. Qukeys)?

That’s actually an interesting idea. What’s the failure case you envision?

I don’t understand what the idea here would be. Sending two reports, one with each modifier state? You’ve got to use some sort of masking to make this work and not end up with very fast repeats of characters for the rollover keys as you alternate between keycodes being on and off. My plan is to mask keycodes in the HID report on the output side, rather than masking keyswitches on the input side to accomplish this. Either one works if the plugin is used in isolation, but once it has to interact with other plugins, things become much more difficult. This is why I want a true pre-report event hook, called before a report is sent, even if that report is injected by another plugin from some other hook.

I’ll be experimenting with the new proposed plugin interface soon, and a re-written main loop with an alternate set of hooks, and see if I can get it to work with other plugins, as well.

Not if you add a second keyboard device…

2 Likes

Oh! Now I get it. That is a very interesting idea! How can I do that?

1 Like

It requires some pretty deep hacking of KeyboardioHID and would require writing new HID descriptors.

That would seriously simply things on the plugin side, though. I’m very interested in pursuing that idea now.

Actually, upon further reflection, it may not simplify things as much as I thought. It might even complicate things more in the corner cases.

I haven’t really looked into compatibility with Oneshot or Qukeys because I don’t use these plugins. I’ve only had my keyboard for a little while, and so far I’ve just wanted to move existing software keybindings I’m used to into hardware.

Compatibility issues should only be a concern if you’re trying to triple-purpose the same modifier key to have it input a letter when tapped, apply a modifier when held with certain keys, and apply a layer when held with other keys. If you apply ModifierLayers to modifier keys that don’t have other special behavior enabled, everything should work fine.

Compatibility with Oneshot should be doable, because the way it’s injecting keys is by calling back to the event handlers. Qukeys would be much harder because it’s directly calling out to the HID layer. On that note, I don’t really think masking keycodes on the HID report side is the right thing to do – it interferes with other plugins by bypassing event hooks, and it also only has access to the raw keypresses and not the keyswitch location that caused them. I do a little bit of HID report modification to disable modifiers that shouldn’t be active (though perhaps I should call out to event hooks for that, too), but for keyswitches I just use the event hook.

Having multiple HID keyboard devices is a really neat idea, I hadn’t thought of that! Maybe someone will try it at one point. Though for me personally I’m using this plugin for multiple modifiers throughout my keymap, which would raise the possibility of needing more than 1 additional HID device.

The real problem is that plugins can inject keypresses and send additional keyboard HID reports at any time, so if you detect a keypress, change the shift state, and wait for the normal report to be sent by Kaleidoscope, some other plugin can send a different HID report before that, invalidating your assumptions. Conversely, if you send the report immediately, you can end up invalidating the assumptions of some other plugin that acted before you. It’s nothing to do with overloading individual keys; the compatibility problem is more fundamental.

The OneShot compatibility issue I see is that it is trying to keep the modifier state applied after the modifier key is released, so you have to check the HID report to see if the modifiers are present, not the keyswitch.

It’s not perfect, but it’s by far the best solution I’ve come up with. If we assume that a HID keycode is in the report because of a key being active, then it will have the same effect as masking the keys (and just like keyswitch masking, we automatically release the mask as soon as the keycode becomes inactive on the input side). Even if we don’t assume that a keycode’s presence is due to a key being depressed, but generalize the ultimate cause to “user intent”, it still works the same way. The real problem is that it’s possible (but not necessarily likely or useful) for two keys (or other causes) to be mapped to the same keycode, in which case there could be rollover between those keys, one of which was pressed before the key that caused the modifier state change and the keycode masking.

Either way, it can’t be made invisible to the user: press and hold a “normal” key, and a key that has modifiers applied, and either one of them gets masked (either at the keyswitch or keycode level) or one of them gets overridden. My choice is to mask the earlier keypress until either it or the modifier-swapped key is released, as closely as possible, and especially while typing.

I plan to use all three types of plugins (Qukeys, OneShot (or something like it), and something that allows modification of keycodes based on modifier state (though I’m really only concerned about shift, which is fundamentally different from the others from the user’s point of view).

1 Like

I am following your discussion for a while now and I find it very interesting. Unfortunately, I am not as deep in the material concerning the mapping of key states to keyboard HID reports. Nevertheless, I have some questions about it, partially with respect to implementation changes that would make this more safe for plugins.

On an abstraction level, I understand the problem as follows: A data set A, the keys that are currently active/inactive, or rather their changes are mapped to a data set B, the HID report. Problems arise because all plugins’ mappings possibly interfere, leading to an inconsistent B.

What if plugins can have their own copy of B. The actutal reports C that are send would be the union of all registered B’s. This could possibly be made more efficient by allowing plugins to explicitly request flushing of their B’s. Thus not all Bs need to be considered every time a C is generated. And a C is only generated when there are any updated Bs, only at one well defined point in the program execution. By this means, the conflicts between plugins and also possible hook call order problems could be eliminated.

I would expect the overall behavior roughly to be that of several keyboards with different firmware configuration being attached to the same computer and with the same key sequence being entered.

I am just not sure if such unified keyboard HID reports would make sense or if they violate the HID protocol. Does every activation of a key need to be matched by a deactivation in the HID report?

FWIW, I just tried this, kinda: I pressed Shift on my laptop’s built-in keyboard, then a on my ErgoDox EZ, and I got A. There are two completely separate devices. Not just different nodes of the same device, but physically different too. But at least Linux appears to treat Shift on one device to apply to another. Which does make sense when you consider that you can use Shift+Click, to augment a mouse. You can use a keyboard to augment another.

(I also checked, the EZ does not include Shift in the report it sends during this.)

1 Like

I just tried this on my Macbook using three keyboards: the built-in one, my MS Natural 4000, and my Model01. The Mac does not apply a shift on one keyboard to the keys typed on another.

1 Like

Fun times! It appears this is OS dependent then. That’s perhaps even worse :confused:

1 Like

However, it does make the decision about what to do very easy. Thanks for thinking to test this out!

yeah. i was not thinking. this would only work on OS x, which doesn’t merge modifiers between keyboards.

Sounds interesting with the Model01 posing as two keyboards.

My use case is that I want a mostly US layout keyboard to also type danish characters.

So one way would be to have one virtual keyboard use US layout and another to use DA layout. Of course that would depend if the language setting is global or keyboard specific in the OS (mac and windows).

I guess you would need this feature too, if you have a regular keyboard along with a dvorak keyboard attached to the same computer?

Do you know if the layout is global or not?