5

I have a large list of Karabiner complex modifications and it is hard to keep track of them all including conflicts, excluded applications, etc. From what I can tell the user is expected to edit ~/.config/karabiner/karabiner.json by hand. However this isn't practical at all, since each modification definition ends up being quite verbose and I can only see 1-2 per screen. Is there some more efficient method that I'm missing? Or should I just write my own program to generate the karabiner.json file?

1

Not an editor, but an alternative, more concise and human friendly syntax for Karabiner is provided by Goku.

Goku is a tool that lets you manage your Karabiner configuration with ease, leveraging on EDN file format.

1
  • I actually found Goku but it was even more confusing than the JSON, so it didn't work for me. But since this technically answers my question, I'll accept it in case it helps others. Thank you!
    – Donentolon
    Apr 30 '20 at 23:28
3

I found Goku (see answer by furins) and a few other tools but none seemed any easier than editing the JSON. In the end I wrote my own scripts which I'll share here.

There are two Python files:

  • decompose.py takes a karabiner JSON and breaks it up into a many files and directories, so you can edit one rule at a time.
  • compose.py takes these directories and rolls them into a single JSON that karabiner will accept.

I keep both the single JSON and the decomposed directories (as well as the script) in my dotfiles repo. When I want to alter a keybind, I create a directory for it (easiest method is to copy an existing one and modify) then do a roundtrip encode/decode. Eg.: python compose.py; python decompose.py; python compose.py.

After this my first step is to check git diff to see if my new rule has some error that broke the whole thing. If so, I have many options to revert the changes via git and try again. When done, I test the keybind with Karabiner-EventViewer and its intended use case, and commit.

The scripts are obviously limited and hacky, because they are for personal use (and I think the "proper" solution to this would be to just not use a Mac, har har). I suggest starting with a known-working JSON such as PC-style shortcuts so you can see how the existing rules work. They do work reasonably well, but some caveats:

  • Each rule goes into a directory. The directory name is built from a number (to keep the rules from changing order and confusing git) and the name of the rule. Obviously rule names can have special characters, but directories cannot, so there's some funny normalization I had to do with that. It can be a bit fragile if you do fancy things with the rule name.
  • I use PyCharm which has its own keyboard shortcut interceptor. Because of this, all rules are blacklisted from PyCharm at the composer/decomposer stage.

compose.py:

import json
from pathlib import Path

p_decomposed = Path('decomposed/')

# Load scaffold
p_scaffold = p_decomposed / 'scaffold.json'
with p_scaffold.open() as f:
    scaffold = json.load(f)

# Load rules
p_rules = p_decomposed / 'rules'
for p_rule in sorted(p_rules.iterdir()):
    if p_rule.stem.startswith('.'):
        continue
    print(p_rule)

    p_rule_json = p_rule / 'rule.json'
    with p_rule_json.open() as f:
        rule = json.load(f)

    p_manipulators = p_rule / 'manipulators'
    for p_manipulator in sorted(p_manipulators.iterdir()):
        with p_manipulator.open() as f:
            j = json.load(f)

        rule['manipulators'].append(j)

    profiles = scaffold['profiles']
    first_prof = profiles[0]
    complex_mods = first_prof['complex_modifications']
    rules = complex_mods['rules']
    rules.append(rule)

p_composed = Path('karabiner.json')
with p_composed.open('w') as f:
json.dump(scaffold, f, indent=4)

decompose.py:

import json
from pathlib import Path

with open('karabiner.json') as f:
    j = json.load(f)

profiles = j['profiles']
first_prof = profiles[0]
complex_mods = first_prof['complex_modifications']
rules = complex_mods['rules']

# Dump everything except the rules into a "scaffold file"
complex_mods['rules'] = []
with open('decomposed/scaffold.json', 'w') as f:
    json.dump(j, f, indent=4)


def normalize_rule_name(raw_name):
    """
    Normalize rule name by removing special characters, to make it suitable
    for use as a file name.
    """
    lowered = raw_name.lower()

    filtered = ''
    for c in lowered:
        if c.isalnum():
            filtered += c
        else:
            filtered += '-'

    while '--' in filtered:
        filtered = filtered.replace('--', '-')

    if filtered.endswith('-'):
        filtered = filtered[:-1]

    return filtered


def blacklist_pycharm(manipulator):
    pattern = "^com\\.jetbrains\\."

    if 'conditions' not in manipulator:
        return

    for c in manipulator['conditions']:
        if c.get('type', '') != 'frontmost_application_unless':
            continue

        if pattern not in c['bundle_identifiers']:
            c['bundle_identifiers'].append(pattern)


def process_manipulator(manipulator):
    """
    Gets applied to every manipulator before dumping it to a file.
    """
    result = dict(manipulator)

    blacklist_pycharm(result)

    return result


# Dump each rule to a separate file
for n, rule in enumerate(rules):
    # Normalize name
    desc = rule['description']
    desc_norm = normalize_rule_name(desc)

    # Pull out manipulators and save the rest
    manipulators = rule['manipulators']
    rule['manipulators'] = []

    p_rule = Path('decomposed/rules') / f'{n:03d}_{desc_norm}' / 'rule.json'
    p_rule.parent.mkdir(exist_ok=True)

    with p_rule.open('w') as f:
        json.dump(rule, f, indent=4)

    # Dump each manipulator
    p_manipulators = p_rule.parent / 'manipulators'
    p_manipulators.mkdir(exist_ok=True)
    for i, manipulator in enumerate(manipulators):
        p_m = p_manipulators / f'{i+1}.json'
        with p_m.open('w') as f:
            json.dump(process_manipulator(manipulator), f, indent=4)
4
  • Thank you for sharing your solution! I agree that Goku helps reducing the size of the document but not its complexity. By breaking the configuration file into smaller parts your solution helps a lot in keeping things simple. Correct me if I'm wrong but it seems that you can also leave e.g. a txt file in a decomposed folder to store some notes about the rule, that may be extremely useful.
    – furins
    May 1 '20 at 8:54
  • @furins No problem - wish I had time to actually clean up and maintain this.
    – Donentolon
    May 1 '20 at 21:15
  • You are correct - it will only look at rule.json in the rule directory. However in the manipulators subdir, it will load everything - so if you put a non-json file in there, it will crash above rule['manipulators'].append(j).
    – Donentolon
    May 1 '20 at 21:18
  • Thank you for the clarification. I've missed that line!
    – furins
    May 2 '20 at 7:03
3

There's a simple web editor these days courtesy @genesy: https://genesy.github.io/karabiner-complex-rules-generator/

1
  • Welcome to Ask Different! We're trying to find the best answers and those answers will provide supporting info as to why they're the best. Answers should be self-contained so explain why you think the answer you provided will solve the problem or is better/more complete than the others already provided. See How to Answer on how to provide a quality answer. - From Review
    – fsb
    Apr 1 at 13:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .