WIP: AccentLayerPlugin, accented char build onto Unicode plugin

Hi,

TL;DR;

I’m seeking feedback on my first plugin code before making it better for sharing, see code sample below

Long Story

I own a model 100 since a few months, and I discover the world of split keyboards and layer.

Since I type both in english and french (and c++ and other programming languages), I need to have efficient way to type accented characters.

Firstly I setted up macro for compose key combination, within Chrysalis, but while the key to macro id mapping is exported, the macro content is not (I have one kbd at home, another at work and I want to share the configuration).

So I think the solution is to setup my “accent layer” on the firmware side, which I’m currently doing.

To this end I have done a few tests, and I’m writing here to have feedback.

I have developped a plugin that leverage Unicode plugin to type accent char (which seems to be portable accros OSes, not tested yet).

Sample code

First I define key codes

constexpr Key Key_A_CIRC{kaleidoscope::ranges::SAFE_START};
constexpr Key Key_A_ACUTE{Key_A_CIRC.getRaw() + 1};
// ...

Then I created a table key code to unicode char


struct KeyToUnicode {
  Key key;
  uint32_t unicode;
};

static const KeyToUnicode keyToUnicodeTable[] = {
  {Key_A_CIRC, U'â'},
  {Key_A_ACUTE, U'á'},
// ...

Finally I setted up the plugin as (temporary name, “Weur” inspired by https://altgr-weur.eu/altgr-intl.html)


class WeurPlugin : public Plugin {
 public:
  static constexpr uint32_t upperOffset = 0x00E0 - 0x00C0;

  bool isShiftKeyHeld(Key &which) {
    for (Key key : kaleidoscope::live_keys.all()) {
      if (key.isKeyboardShift()) {
        which = key;
        return true;
      }
    }
    return false;
  }

  EventHandlerResult onKeyEvent(KeyEvent &event) {

    uint32_t converted = keyToUnicode(event.key);

    if (converted == 0) return EventHandlerResult::OK;

    if (keyToggledOn(event.state)) {
      Key shiftKey;
      if (isShiftKeyHeld(shiftKey)) {
// I'm not sure if its the right way to do that,
// having shift pressed while using Unicode.type produces strange results
        kaleidoscope::Runtime.hid().keyboard().releaseRawKey(shiftKey);
        kaleidoscope::Runtime.hid().keyboard().sendReport();
        ::Unicode.type(converted - upperOffset);
// upperOffset works most of the time, need better managment
        kaleidoscope::Runtime.hid().keyboard().pressRawKey(shiftKey);
        kaleidoscope::Runtime.hid().keyboard().sendReport();
      } else {
        ::Unicode.type(converted);
      }
    }
    return EventHandlerResult::ABORT;
  }
};

In the layer definition, I can use Key_A_CIRC where ever I want e.g. as Key_A
It supports helded shift to produce uppercase accented character.
The main drawback is a visual feedback on screen, I can quickly see the typed sequence for unicode char (that was also the case with macro and compose key in chrysalis).

Any comment welcome to improve the approach.

++dlyr

  1. In general, I would advise against directly interacting with HID reports in this way, but the Unicode plugin is already doing it, so I feel like it can be justified in this case. It could also be argued that the existing Unicode plugin should be rewritten.

  2. It is possible to hold more than one shift key at a time, but your code assumes only one. Perhaps it would be better to use an onAddToReport() handler to suppress modifiers (shift isn’t the only one that can interfere and cause unexpected output). See the ShiftBlocker example for this trick.

Thanks for the feedback.
Indeed I first try using ShiftBlocker, but as I understand it is closely link to macro behavior and not compatible with Unicode plugin internals.
I will try to update Unicode plugin using macro key press/release and save/clear/restore modifiers state in the callback.

After a second read, ShiftBlocker seems to remove shift from the current live key to the macro temp key state, but also remove shift keys activated by macro code.
What I need to achieve is to save modifiers state, then send a sequence for the unicode output, and restore state.
I’m not sure how to achieve this … any clue welcome.

The current version of Unicode.type() (which I’m thinking more and more should be rewritten, but that’s a separate issue) just uses the current HID report, modifies it, and sends reports immediately. If you want to guarantee that any shift modifier bits are removed from the HID report, you should just unconditionally remove both of them with releaseRawKey() rather than recording the first one you find (and note that isKeyboardShift() will not return true for all keys that cause a shift modifier bit to get set) and only unsetting that one. You still need to test for a held shift key, but you can drop the which parameter because it’s not needed. There’s also no need to restore shift bits that you removed from the HID report if you don’t return ABORT: they’ll automatically get restored:

    if (keyToggledOn(event.state)) {
      kaleidoscope::Runtime.hid().keyboard().releaseRawKey(Key_LeftShift);
      kaleidoscope::Runtime.hid().keyboard().releaseRawKey(Key_RightShift);
      kaleidoscope::Runtime.hid().keyboard().sendReport();
      
      if (isShiftKeyHeld()) {
        converted -= upperOffset;
        // upperOffset works most of the time, need better managment
      }
      ::Unicode.type(converted);
    }
    return EventHandlerResult::OK;
  }

In fact, you might want to clear all the modifier bits in the HID report, not just the shift keys, to prevent shortcuts from being triggered. This simplifies the code even more:

      kaleidoscope::Runtime.hid().keyboard().clearModifiers();
      kaleidoscope::Runtime.hid().keyboard().sendReport();

It’s not really linked to Macros (though it’s raison d’être was a Macro that someone wanted); it’s written to work the way most plugins do. Unicode is exceptional in sending HID reports directly rather than calling Runtime.handleKeyEvent(), like most plugins do. Even then, ShiftBlocker could be used to suppress keys before calling Unicode.type() by forcing a report to get sent the “normal” way, but calling Runtime.prepareKeyboardReport(), then Runtime.sendKeyboardReport().

Thanks for the reply,
Indeed the new version of onKeyEvent works as expected.
As far as I understand, with the current version of Unicode plugin, I have to use hid() to clearModifiers.
Also my understanding of shiftBlocker and how report works is weak. I understand it blocks shifts during onAddToReport, which is called for each active key to generate a report, later send to the host.

Since I’m new to hardware and keyboard programming, I surely need to have a “big picture” introduction on how it works, wath is a report, and how they are managed. Any reference welcome.

I also need to figure out tests, my current version is available here GitHub - dlyr/Kaleidoscope at dlyr

++ dlyr.

Think of the HID report as a set of keycodes that are currently active (“pressed”). Functions like pressRawKey() and clearModifiers() just add or remove keycodes from that set (setting or unsetting a bit in a bitfield). sendReport() actually causes a report to be sent over USB. Your plugin and the Unicode plugin call these functions directly, but for most plugins, Kaleidoscope handles all of the HID reporting functions behind another layer of abstraction. When a key event is received, each plugin’s onKeyEvent() handler is called, then Kaleidoscope makes any corresponding updates to the live_keys array, and generates a new report from that (calling any onAddToReport() handlers in the process).

You might find it helpful to look at the lower level of how HID reports are handled in the KeyboardioHID repository. Kaleidoscope code itself gets impenetrable as there are many layers of redirection to follow.