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 | ++++++++++++++ |
| A | Makefile | | | 14 | ++++++++++++++ |
| A | README | | | 65 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | layerkey.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);
+}