EntriesAbout

Focus on the Mac

Unicode Input

Vexingly, there isn’t really a straight-forward way for a keyboard to send “👍”. The USB HID only defines a limited number of keycodes – basically ASCII plus control keys – so if we want to be able to send emoji directly from the keyboard, some extra step will be required.

The Kaleidoscope, the firmware of my beloved keyboardio Model 01, has a “Unicode” library which works by using operating system-specific shortcuts. For example, on Linux, it simulates pressing Control-Shift-u then inputing the hex codes of the number. This generally works – although you can see the little input popup thing briefly – but in my experience, the equivalent on macOS doesn’t really work. It requires setting the language to “Unicode Hex Input”, but even then I could never get emoji to input reliably.

I’m going to be using macOS quite a bit more soon though and I need the ability to type “😬” with a single keystroke – so what am I to do?

Keyboard

On the keyboard side, I made my own function that can wrap the default Unicode plugin but also use Focus if enabled. I have both options so that if the listener isn’t set up on the host, I still have an imperfect fall-back method.

FocusSerial (née Focus) is a Kaleidoscope library for (bi-directional) communication over the serial port. It isn’t really made for “end-users”, but for plugins to use to communicate with host applications. For instance, Chrysalis, the GUI configurator for Kaleidoscope, uses FocusSerial to sync configuration to and from the keyboard.

Using FocusSerial is very straightforward; my code looks like this:

namespace jamesnvc {
namespace UnicodeInput {
bool useNative = false;
}
}

static void UnicodeType(uint32_t letter) {
  if (jamesnvc::UnicodeInput::useNative) {
    Unicode.type(letter);
  } else {
    ::Focus.send(F("unicode_input"), letter, F("\n"));
  }
}

Then I replaced all the places that had been calling Unicode.type(x) with UnicodeType(x).

I also added a leader key binding to toggle the useNative boolean:

static void leaderToggleUnicodeInputMethod(uint8_t seq_index) {
  jamesnvc::UnicodeInput::useNative = !jamesnvc::UnicodeInput::useNative;
}

static const kaleidoscope::plugin::Leader::dictionary_t leader_dict[] PROGMEM =
  LEADER_DICT(/* ...a bunch of other stuff ... */
              { LEADER_SEQ(LEAD(0), Key_U), leaderToggleUnicodeInputMethod }
              );

Pretty simple on the keyboard side!

Host

On the “host” side, I just have a little shell script. Perhaps a program in a proper language would be better, but for now not much is really required of this program – just has to read and call Applescript, so why bother? Bash is already there, no dependencies.

The script looks like this:

#!/usr/bin/env bash

set -euo pipefail

input_codepoint(){
    local char=$(perl -C -e "print chr ${1}")
    osascript <<-EOF
    tell application "System Events"
      set tmp to the clipboard
      set the clipboard to "${char}"
      keystroke "v" using command down
      delay 0.2
      set the clipboard to tmp
    end tell
EOF
}

listen() {
    SERIAL_DEVICES=(/dev/cu.usbmodem*)
    SERIAL_DEVICE="${SERIAL_DEVICES[0]}"

    echo "Listening on ${SERIAL_DEVICE}" >&2

    while IFS= read -r -d $'\n' line
    do
        IFS=' ' read -ra command <<< "$line"
        case "${command[0]}" in
            unicode_input)
                input_codepoint "${command[1]}";;
            *) echo "Unknown event ${command[0]}";;
       esac
    done < "${SERIAL_DEVICE}"
}

listen

Let’s break down how this works a little.

The listen function uses the first read in a while-loop to read in whole lines of input at a time from the serial port. The second read splits the line on spaces into an array (command), then checks what the “argument” was. I’m only using unicode_input now (but maybe there will be more later?), with the second argument being the numeric value of the code-point to enter.

The second function, input_codepoint, first uses a perl one-liner to get the corresponding glyph for the codepoint, then uses AppleScript to input it. As best I can tell, there’s still to direct way to input a character, so it puts it in the clipboard and simulates hitting command-v to paste it in. It also does a little dance to restore the previous value of the clipboard, just to be polite.

It does rely on being able to paste with the default shortcut, but I think that’s a fairly reasonable assumption. In practice, it works very smoothly.

LaunchAgent

Finally, I want to keep my script running, so I wrote a little “launch agent” to do so:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.occasionallycogent.keyboardiofocuslistener</string>
    <key>ProgramArguments</key>
    <array>
      <string>/Users/james/dotfiles/focus_input_listener/listener.sh</string>
    </array>
    <key>KeepAlive</key>
    <true/>
    <key>EnvironmentVariables</key>
    <dict>
      <key>LC_ALL</key>
      <string>en_US.UTF-8</string>
    </dict>
  </dict>
</plist>

Pretty straight-forward; created the file, put it in to ~/Library/LaunchAgents, then did a lauchctl load <path to plist> to get it going.

The one twist was the LC_ALL environment variable. Before adding that, things appeared to work properly for emoji, but accented characters I input with the same method were getting mangled. I’d recently seen a similiar issue when making a custom LaunchBar action, where it was running with a different environment, so I tried setting the locale. Indeed, running the script with LC_ALL=C would show the same mangled characters that I was getting, so I added a UTF-8 locale in the launch agent’s environ, and it all works perfectly!

Next Steps?

I was very pleased how easy it was to do something useful with Focus. I knew such a thing was possible for years, but had never bothered; it always seemed like too much of a pain. Now that I see how easy it can be though, I’m very curious what else I could do… Using Emacs to communicate with the keyboard, having it behave differently based on the front-most app…many possibilities present themselves!