Blog

Emulating the Compose key on macOS

On my GNU/Linux system, the Compose key is my favourite method for typing any kind of “special” (or not-so-special) characters, such as typographically correct punctuation marks (e.g., dashes, quotes, thin spaces…), diacritics, or characters from foreign languages (e.g. greek letters). It works independently of the application I am using (contrary to, say, Vim’s digraphs) and independently of the keyboard layout; also, compose key sequences are generally quite “intuitive” (e.g., Compose + hypen + hypen + hypen for an em dash, Compose + hyphen + hypen + full stop for an en dash) and can easily be customised as needed.

While there is no such thing as a Compose key mechanism on macOS, it is quite easy to emulate it using macOS’s built-in Key Bindings system.

Key Bindings on macOS

All that is needed is a ~/Library/KeyBindings/DefaultKeyBinding.dict file (to be created if such a file does not already exist). This file allows to associate an arbitrary action to a key or to a combination of key. For example, the following configuration associates the hyphen key to an action that inserts an em dash (do not use that example: it would prevent you from ever being able to type a normal hypen again! this is just to illustrate the principle):

{
    "-" = ("insertText:", "\U2014");
}

This other example illustrates how to associate actions to key combinations instead of single keys:

{
    "-" = {
        "-" = ("insertText:", "\U2014");
        "." = ("insertText:", "\U2013");
    };
}

Here, typing the hypen key twice will insert an em dash (U2014), while typing successively the hyphen key and the full stop key will insert an en dash (U2013).

Compose emulation

From the above, it is easy to define key bindings that emulate the behaviour of a compose key. All that is needed is to choose a key that will act as the Compose key, and then to define a series of combinations that all start with that key.

On my professional Apple machine (which I use with a PC-type keyboard, not an Apple keyboard), I have chosen the left-side “menu” key, which has the key code \U0010 (found by trial-and-error). I then creted the following DefaultKeyBinding.dict file, defining the Compose sequences that I use the most:

{
    "\U0010" = { /* Compose-like key */

        "-" = {
            "m" = ("insertText:", "\U2212"); /* MINUS SIGN */
            "+" = ("insertText:", "\U2213"); /* MINUS-OR-PLUS SIGN */
            "-" = {
                "." = ("insertText:", "\U2013"); /* EN DASH */
                "-" = ("insertText:", "\U2014"); /* EM DASH */
            };
            "0" = ("insertText:", "\U2080"); /* SUBSCRIPT 0 */
            "1" = ("insertText:", "\U2081"); /* SUBSCRIPT 1 */
            "2" = ("insertText:", "\U2082"); /* SUBSCRIPT 2 */
            "3" = ("insertText:", "\U2083"); /* SUBSCRIPT 3 */
            "4" = ("insertText:", "\U2084"); /* SUBSCRIPT 4 */
            "5" = ("insertText:", "\U2085"); /* SUBSCRIPT 5 */
            "6" = ("insertText:", "\U2086"); /* SUBSCRIPT 6 */
            "7" = ("insertText:", "\U2087"); /* SUBSCRIPT 7 */
            "8" = ("insertText:", "\U2088"); /* SUBSCRIPT 8 */
            "9" = ("insertText:", "\U2089"); /* SUBSCRIPT 9 */
        };

        "_" = {
            "0" = ("insertText:", "\U2070"); /* SUPERSCRIPT 0 */
            "1" = ("insertText:", "\U2071"); /* SUPERSCRIPT 1 */
            "2" = ("insertText:", "\U00B2"); /* SUPERSCRIPT 2 */
            "3" = ("insertText:", "\U00B3"); /* SUPERSCRIPT 3 */
            "4" = ("insertText:", "\U2074"); /* SUPERSCRIPT 4 */
            "5" = ("insertText:", "\U2075"); /* SUPERSCRIPT 5 */
            "6" = ("insertText:", "\U2076"); /* SUPERSCRIPT 6 */
            "7" = ("insertText:", "\U2077"); /* SUPERSCRIPT 7 */
            "8" = ("insertText:", "\U2078"); /* SUPERSCRIPT 8 */
            "9" = ("insertText:", "\U2079"); /* SUPERSCRIPT 9 */
        };

        "g" = { /* Fly pushing stuff */
            "m" = ("insertText:", "\U2642"); /* MALE */
            "f" = ("insertText:", "\U2640"); /* FEMALE */
            "F" = ("insertText:", "\U263F"); /* VIRGIN FEMALE */
            "y" = ("insertText:", "\U21C1"); /* Y CHROMOSOME */
        };

        "G" = { /* Greek letters */
            "A" = ("insertText:", "\U0391");    /* GREEK CAPITAL LETTER ALPHA */
            "B" = ("insertText:", "\U0392");    /* GREEK CAPITAL LETTER BETA */
            "G" = ("insertText:", "\U0393");    /* GREEK CAPITAL LETTER GAMMA */
            "D" = ("insertText:", "\U0394");    /* GREEK CAPITAL LETTER DELTA */
            "E" = ("insertText:", "\U0395");    /* GREEK CAPITAL LETTER EPSILON */
            "Z" = ("insertText:", "\U0396");    /* GREEK CAPITAL LETTER ZETA */
            "Y" = ("insertText:", "\U0397");    /* GREEK CAPITAL LETTER ETA */
            "H" = ("insertText:", "\U0398");    /* GREEK CAPITAL LETTER THETA */
            "I" = ("insertText:", "\U0399");    /* GREEK CAPITAL LETTER IOTA */
            "K" = ("insertText:", "\U039A");    /* GREEK CAPITAL LETTER KAPPA */
            "L" = ("insertText:", "\U039B");    /* GREEK CAPITAL LETTER LAMBDA */
            "M" = ("insertText:", "\U039C");    /* GREEK CAPITAL LETTER MU */
            "N" = ("insertText:", "\U039D");    /* GREEK CAPITAL LETTER NU */
            "C" = ("insertText:", "\U039E");    /* GREEK CAPITAL LETTER XI */
            "O" = ("insertText:", "\U039F");    /* GREEK CAPITAL LETTER OMICRON */
            "P" = ("insertText:", "\U03A0");    /* GREEK CAPITAL LETTER PI */
            "R" = ("insertText:", "\U03A1");    /* GREEK CAPITAL LETTER RHO */
            "S" = ("insertText:", "\U03A3");    /* GREEK CAPITAL LETTER SIGMA */
            "T" = ("insertText:", "\U03A4");    /* GREEK CAPITAL LETTER TAU */
            "U" = ("insertText:", "\U03A5");    /* GREEK CAPITAL LETTER UPSILON */
            "F" = ("insertText:", "\U03A6");    /* GREEK CAPITAL LETTER PHI */
            "X" = ("insertText:", "\U03A7");    /* GREEK CAPITAL LETTER CHI */
            "Q" = ("insertText:", "\U03A8");    /* GREEK CAPITAL LETTER PSI */
            "W" = ("insertText:", "\U03A9");    /* GREEK CAPITAL LETTER OMEGA */

            "a" = ("insertText:", "\U03B1");    /* GREEK SMALL LETTER ALPHA */
            "b" = ("insertText:", "\U03B2");    /* GREEK SMALL LETTER BETA */
            "g" = ("insertText:", "\U03B3");    /* GREEK SMALL LETTER GAMMA */
            "d" = ("insertText:", "\U03B4");    /* GREEK SMALL LETTER DELTA */
            "e" = ("insertText:", "\U03B5");    /* GREEK SMALL LETTER EPSILON */
            "z" = ("insertText:", "\U03B6");    /* GREEK SMALL LETTER ZETA */
            "y" = ("insertText:", "\U03B7");    /* GREEK SMALL LETTER ETA */
            "h" = ("insertText:", "\U03B8");    /* GREEK SMALL LETTER THETA */
            "i" = ("insertText:", "\U03B9");    /* GREEK SMALL LETTER IOTA */
            "k" = ("insertText:", "\U03BA");    /* GREEK SMALL LETTER KAPPA */
            "l" = ("insertText:", "\U03BB");    /* GREEK SMALL LETTER LAMBDA */
            "m" = ("insertText:", "\U03BC");    /* GREEK SMALL LETTER MU */
            "n" = ("insertText:", "\U03BD");    /* GREEK SMALL LETTER NU */
            "c" = ("insertText:", "\U03BE");    /* GREEK SMALL LETTER XI */
            "o" = ("insertText:", "\U03BF");    /* GREEK SMALL LETTER OMICRON */
            "p" = ("insertText:", "\U03C0");    /* GREEK SMALL LETTER PI */
            "r" = ("insertText:", "\U03C1");    /* GREEK SMALL LETTER RHO */
            "v" = ("insertText:", "\U03C2");    /* GREEK SMALL LETTER FINAL SIGMA */
            "s" = ("insertText:", "\U03C3");    /* GREEK SMALL LETTER SIGMA */
            "t" = ("insertText:", "\U03C4");    /* GREEK SMALL LETTER TAU */
            "u" = ("insertText:", "\U03C5");    /* GREEK SMALL LETTER UPSILON */
            "f" = ("insertText:", "\U03C6");    /* GREEK SMALL LETTER PHI */
            "x" = ("insertText:", "\U03C7");    /* GREEK SMALL LETTER CHI */
            "q" = ("insertText:", "\U03C8");    /* GREEK SMALL LETTER PSI */
            "w" = ("insertText:", "\U03C9");    /* GREEK SMALL LETTER OMEGA */
        };

        "?" = {
            "?" = ("insertText:", "\U2E2E");    /* IRONY MARK */
            "-" = ("insertText:", "\U2243");    /* ASYMPTOTICALLY EQUAL TO */
            "=" = ("insertText:", "\U2248");    /* ALMOST EQUAL TO */
        };

        "d" = {
            "d" = ("insertText:", "\U00B0");    /* DEGREE SIGN */
            "C" = ("insertText:", "\U2103");    /* CELSIUS DEGREE */
            "F" = ("insertText:", "\U2109");    /* FARENHEIT DEGREE */
        };

        "x" = {
            "x" = ("insertText:", "\U2A09");    /* N-ARY TIMES OPERATOR */
        };

        "\U0020" = {
            "\U0020" = ("insertText:", "\U00A0");   /* NO-BREAK SPACE */
            "." = ("insertText:", "\U2008");    /* PUNCTUATION SPACE */
            "," = ("insertText:", "\U202F");    /* NARROW NO-BREAK SPACE */
            "!" = ("insertText:", "\U200B");    /* ZERO WIDTH SPACE */
        };

        "c" = {
            "o" = ("insertText:", "\U00A9"); /* COPYRIGHT SIGN */
        };
    };
}

Bonus: giving the “Home” and ”End” keys a sane behaviour

Unrelated to the Compose emulation, the same DefaultKeyBinding.dict file can be used to fix the very annoying default behaviour of the Home and End keys on macOS. Those keys move the cursor to the beginning and end of the viewport, instead of moving it to the beginning and end of the current line. This is not what I expect from those keys, so I use the following additions to the DefaultKeyBinding.dict file:

{
    /* Sane behavior for Home and End keys. */
    "\UF729" = "moveToBeginningOfLine:";
    "\UF72B" = "moveToEndOfLine:";
    "$\UF729" = "moveToBeginningOfLineAndModifySelection:";
    "$\UF72B" = "moveToEndOfLineAndModifySelection:";
    "^\UF729" = "moveToBeginningOfDocument:";
    "^\UF72B" = "moveToEndOfDocument:";
    "^$\UF729" = "moveToBeginningOfDocumentAndModifySelection:";
    "^$\UF72B" = "moveToEndOfDocumentAndModifySelection:";
}

Now, the Home key will move the cursor to the beginning of the line; only when it is combined with the Ctrl key, will it move the cursor to the beginning of the viewport; combined with the Shift key, it will move the cursor and select the text on the way. Likewise for the End key and the end of the line or end of the viewport.

You can add a comment by replying to this message on the Fediverse.