layerkey

(WIP) A simple utility to add a second keyboard layer to your mac.
git clone https://github.com/michaelneuper/layerkey.git
README | Files | Log | Refs

commit 78b12073b405c26cb6b5dd2ede4f2bb251928959
Author: Michael Neuper <michael@michaelneuper.com>
Date:   Thu, 28 Aug 2025 20:29:35 +0200

initial commit

Diffstat:
A.clang-format | 23+++++++++++++++++++++++
A.editorconfig | 14++++++++++++++
AMakefile | 14++++++++++++++
AREADME | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alayerkey.c | 335+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 451 insertions(+), 0 deletions(-)

diff --git a/.clang-format b/.clang-format @@ -0,0 +1,23 @@ +# -*- mode: yaml -*- +BasedOnStyle: LLVM + +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterNamespace: false + BeforeElse: false + IndentBraces: false + +IndentCaseLabels: true +IndentPPDirectives: AfterHash +SeparateDefinitionBlocks: Always +IndentWidth: 4 +TabWidth: 4 +UseTab: ForIndentation +ColumnLimit: 80 +AlignConsecutiveDeclarations: true +AlignTrailingComments: true +AllowShortIfStatementsOnASingleLine: WithoutElse diff --git a/.editorconfig b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = tab +tab_width = 4 +max_line_length = 80 + +[*.{c,h}] +indent_style = tab +tab_width = 4 diff --git a/Makefile b/Makefile @@ -0,0 +1,14 @@ +CC = clang +CFLAGS = -Wall -Wextra +TARGET = layerkey + +all: $(TARGET) + +$(TARGET): layerkey.o + $(CC) $(CFLAGS) -o $(TARGET) layerkey.o + +layerkey.o: layerkey.c + $(CC) $(CFLAGS) -c layerkey.c + +clean: + rm -f $(TARGET) *.o diff --git a/README b/README @@ -0,0 +1,65 @@ +layerkey +======== + +A simple utility that allows you to easily configure a second keyboard layer +and generate a corresponding `.json` file to use with Karabiner-Elements. + + +Installation +------------ + +In the project root, run: + + make install + +This will install layerkey to `~/.local/bin/` and create a config directory +in `~/.config/layerkey/`. + +Usage +----- + +Edit the configuration file in `~/.config/layerkey/config` then run: + + layerkey <key code> + +where `<key code>` is the key code of the key that should activate the layer, +for example `grave_accent_and_tilde`. + +If you don't know the key code, open Karabiner-EventViewer and press the key. + +If the command is not found then `~/.local/bin/` might not be in your `$PATH`, +in that case, add it: + + export PATH="$PATH:$HOME/.local/bin" + +This will create a file `~/.config/layerkey/layer.json`. + +To add it to Karabiner-Elements: + +1. Open Karabiner-Elements +2. Navigate to 'Complex Modifications' +3. Select 'Add your own rule' +4. Copy and paste the contents of `~/.config/layerkey/layer.json` + + +Configuration +------------- + +Simply replace the character you want to remap with the character you want +to remap it to. Here is an example of what a config might look like: + + ,---,---,---,---,---,---,---,---,---,---,---,---,---,-------, + | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | | | | + |---'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-----| + | | q | < | > | - | + | ^ | { | } | o | p | | | | + |-----',--',--',--',--',--',--',--',--',--',--',--',--'-----| + | | ! | * | / | _ | = | h | ( | ) | l | | | | + |------'-,-'-,-'-,-'-+-'-,-'-,-'-+-'-,-'-,-'-,-'-,-'------,-' + | | z | x | c | v | b | [ | ] | | | | | + |---,----',--'--,'---'---'---'---'---',--'--,'---'------,-' + | | | | | | | | + '---'-----'-----'---------------------'-----'----'------' + +**NOTE**: DO NOT ADD OR REMOVE ANY WHITESPACE + +Unfortunately only alphanumeric keys can be remapped at the moment. diff --git a/layerkey.c b/layerkey.c @@ -0,0 +1,335 @@ +/** + * @file layerkey.c + * @brief A simple utility to add a second keyboard layer to your mac. + * @author Michael Neuper <michael at michaelneuper dot com> + * @date 2025-07-26 + * + * A simple mac utility that allows you to easily configure a second keyboard + * layer and generate a corresponding `.json` file to use with + * Karabiner-Elements. + */ + +#include <assert.h> +#include <ctype.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* --- preprocessor directives ---------------------------------------------- */ + +#define ALPHA_LEN 36 +#define ERR_EXIT(msg) \ + do { \ + fprintf(stderr, "%s\n", (msg)); \ + exit(EXIT_FAILURE); \ + } while (0) + +/* --- types ---------------------------------------------------------------- */ + +typedef struct { + int index; + char key; +} KbKey; + +typedef struct { + const char key; + const char *kcode; + const bool shift; +} Key; + +/* --- global variables ----------------------------------------------------- */ + +/** output file: `~/.config/layer.json` */ +FILE *outp; + +// TODO read keyboard from config file +const char *keyboard = + ",---,---,---,---,---,---,---,---,---,---,---,---,---,-------,\n" + "| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | | | |\n" + "|---'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-----|\n" + "| | q | w | e | r | t | y | u | i | o | p | | | |\n" + "|-----',--',--',--',--',--',--',--',--',--',--',--',--'-----|\n" + "| | a | s | d | f | g | h | j | k | l | | | |\n" + "|------'-,-'-,-'-,-'-+-'-,-'-,-'-+-'-,-'-,-'-,-'-,-'------,-'\n" + "| | z | x | c | v | b | n | m | | | | |\n" + "|---,----',--'--,'---'---'---'---'---',--'--,'---'------,-'\n" + "| | | | | | | |\n" + "'---'-----'-----'---------------------'-----'----'------'\n"; + +/** index of each key in keyboard */ +KbKey indices[ALPHA_LEN]; + +/** karabiner key codes sorted in lexicographic order */ +// TODO add custom field for length +const Key keys[] = { + {'!', "1", true}, + {'"', "quote", true}, + {'#', "3", true}, + {'$', "4", true}, + {'%', "5", true}, + {'&', "7", true}, + {'(', "9", true}, + {')', "0", true}, + {'*', "8", true}, + {'+', "equal_sign", true}, + {',', "comma", false}, + {'-', "hyphen", false}, + {'.', "period", false}, + {'/', "slash", false}, + {':', "semicolon", true}, + {';', "semicolon", false}, + {'<', "comma", true}, + {'=', "equal_sign", false}, + {'>', "period", true}, + {'?', "slash", true}, + {'@', "2", true}, + {'[', "open_bracket", false}, + {'\'', "quote", false}, + {'\\', "backslash", false}, + {']', "close_bracket", false}, + {'^', "6", true}, + {'_', "hyphen", true}, + {'`', "grave_accent_and_tilde", false}, + {'{', "open_bracket", true}, + {'|', "backslash", true}, + {'}', "close_bracket", true}, + {'~', "grave_accent_and_tilde", true}, +}; + +/* --- function prototypes -------------------------------------------------- */ + +/** + * Writes the karabiner rules to a buffer. + * + * @param keycode the key code of the key that activates the symbol layer + */ +void write_krules(const char *keycode); + +/** + * Writes the karabiner rule for an individual key. + * + * @param from the key that gets remapped, e.g., 'f' + * @param to the key that `from` should be remapped to + */ +void write_krule(const char from, const Key *to); + +/** + * Writes the karabiner rule for toggling the layer. + * + * @param keycode the key code of the key that activates the symbol layer + */ +void write_ktoggle(const char *keycode); + +/** builds the indices array */ +void build_key_indices(); + +/** print json to file */ +void print_json(const char *, ...); + +/** compares two key codes */ +int keycmp(const void *, const void *); + +/* --- main routine --------------------------------------------------------- */ + +int main(int argc, char **argv) +{ + if (argc != 2) { + ERR_EXIT("Usage: `layerkey <keycode>`\n" + "where `<keycode>` is the keycode of the key that should " + "activate the layer."); + } + + // TODO handle error + outp = fopen("layer.json", "w"); + + build_key_indices(); + + write_krules(argv[1]); + + return EXIT_SUCCESS; +} + +/* --- helper routines ------------------------------------------------------ */ + +void write_krules(const char *keycode) +{ + FILE *configp; + + print_json("{\n" + " \"description\": \"Symbol Layer\",\n" + " \"manipulators\": [\n"); + + write_ktoggle(keycode); + + // see + // <https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFPreferences/Tasks/UsingHighAPI.html> + configp = fopen("config", "r"); + + if (!configp) { + fprintf(stderr, "Unable to open config file."); + goto error; + } + + for (size_t i = 0; i < ALPHA_LEN; ++i) { + // TODO handle errors + fseek(configp, indices[i].index, SEEK_SET); + int ch = fgetc(configp); + + assert(isprint(ch)); + + // only create an entry if the key was changed + if (ch != indices[i].key) { + // FIXME magic number + // TODO handle errors + Key *key = bsearch(&ch, keys, 31, sizeof(Key), keycmp); + + write_krule(indices[i].key, key); + } + } + + // TODO remove final trailing comma + print_json(" ]\n" + "}\n"); + + fclose(configp); + return; + +error: + if (configp) fclose(configp); + exit(1); +} + +void write_krule(const char from, const Key *to) +{ + const char *json = + " {\n" + " \"conditions\": [\n" + " {\n" + " \"name\": \"layer\",\n" + " \"type\": \"variable_if\",\n" + " \"value\": 0\n" + " }\n" + " ],\n" + " \"from\": {\n" + " \"key_code\": \"%c\",\n" // from->key + " \"modifiers\": {\n" + " \"optional\": [\n" + " \"shift\",\n" + " \"command\",\n" + " \"control\",\n" + " \"option\",\n" + " \"fn\"\n" + " ]\n" + " }\n" + " },\n" + " },\n" + " \"to\": [\n" + " {\n" + " \"key_code\": \"%s\",\n" // to->kcode + " %s\n" // to->shift (modifier) + " }\n" + " ],\n" + " \"type\": \"basic\"\n" + " },\n"; + + const char *modifier = to->shift + ? "\"modifiers\": [\"shift\"]" + // FIXME remove trailing comma + : "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"; + + print_json(json, from, to->kcode, modifier); +} + +void write_ktoggle(const char *keycode) +{ + print_json( + " {\n" + " \"conditions\": [\n" + " {\n" + " \"identifiers\": [\n" + " {\n" + " \"description\": \"Built-in keyboard\",\n" + " \"is_built_in_keyboard\": true\n" + " }\n" + " ],\n" + " \"type\": \"device_if\"\n" + " },\n" + " {\n" + " \"name\": \"layer\",\n" + " \"type\": \"variable_if\",\n" + " \"value\": 0\n" + " }\n" + " ],\n" + " \"from\": {\n" + " \"key_code\": \"%s\",\n" // toggle key + " \"modifiers\": {\n" + " \"optional\": [\n" + " \"shift\",\n" + " \"command\",\n" + " \"control\",\n" + " \"option\",\n" + " \"fn\"\n" + " ]\n" + " }\n" + " },\n" + " \"to\": [\n" + " {\n" + " \"set_variable\": {\n" + " \"name\": \"layer\",\n" + " \"value\": 1\n" + " }\n" + " }\n" + " ],\n" + " \"to_after_key_up\": [\n" + " {\n" + " \"set_variable\": {\n" + " \"name\": \"layer\",\n" + " \"value\": 0\n" + " }\n" + " }\n" + " ],\n" + " \"type\": \"basic\"\n" + " },\n", + keycode); +} + +void build_key_indices() +{ + const size_t kblen = strlen(keyboard); + size_t k = ALPHA_LEN; + + // TODO skip whitespace + for (size_t i = 0; i < kblen; ++i) { + if (!isalnum(keyboard[i])) continue; + + indices[ALPHA_LEN - (k--)] = (KbKey){ + .index = i, + .key = keyboard[i], + }; + } + +#ifdef DEBUG + printf("key\tindex\n"); + for (int i = 0; i < ALPHA_LEN; ++i) { + printf("%d\t%c\n", indices[i].index, indices[i].key); + } +#endif +} + +void print_json(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + vfprintf(outp, fmt, args); + va_end(args); +} + +int keycmp(const void *keycode1p, const void *keycode2p) +{ + const int key1 = ((Key *)keycode1p)->key; + const int key2 = ((Key *)keycode2p)->key; + + return (key1 < key2) ? -1 : (key1 > key2); +}