Is there a user-friendly editor for Karabiner complex modifications?
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)