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!