#!/usr/bin/env python3 import argparse import json from pathlib import Path DEFAULT_SOURCE = Path("/Users/trungnt13/Library/Application Support/Cursor/User/keybindings.json") DEFAULT_OUTPUT = Path("/Users/trungnt13/Downloads/keybindings.windows.json") MODIFIER_MAP = { "cmd": "ctrl", "ctrl": "alt", } MODIFIER_ORDER = { "ctrl": 0, "alt": 1, "shift": 2, "win": 3, "meta": 4, "cmd": 5, } def normalize_chord(chord: str) -> tuple[str, bool]: tokens = [token.strip().lower() for token in chord.split("+") if token.strip()] remapped = [MODIFIER_MAP.get(token, token) for token in tokens] modifiers = [] primary = [] duplicate_removed = False seen_modifiers = set() for token in remapped: if token in MODIFIER_ORDER: if token in seen_modifiers: duplicate_removed = True continue seen_modifiers.add(token) modifiers.append(token) else: primary.append(token) modifiers.sort(key=lambda token: MODIFIER_ORDER[token]) normalized = "+".join(modifiers + primary) return normalized, duplicate_removed def convert_key(key: str) -> tuple[str, bool]: chords = key.split(" ") converted = [] duplicate_removed = False for chord in chords: normalized, chord_duplicate_removed = normalize_chord(chord) converted.append(normalized) duplicate_removed = duplicate_removed or chord_duplicate_removed return " ".join(converted), duplicate_removed def convert_bindings(bindings: list[dict]) -> tuple[list[dict], int, int]: converted_bindings = [] changed_keys = 0 duplicate_removed_count = 0 for binding in bindings: converted_binding = dict(binding) key = binding.get("key") if isinstance(key, str): converted_key, duplicate_removed = convert_key(key) converted_binding["key"] = converted_key if converted_key != key: changed_keys += 1 if duplicate_removed: duplicate_removed_count += 1 converted_bindings.append(converted_binding) return converted_bindings, changed_keys, duplicate_removed_count def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Convert Cursor macOS keybindings into a Windows-oriented variant." ) parser.add_argument( "--source", type=Path, default=DEFAULT_SOURCE, help=f"Source keybindings JSON file. Default: {DEFAULT_SOURCE}", ) parser.add_argument( "--output", type=Path, default=DEFAULT_OUTPUT, help=f"Output JSON file. Default: {DEFAULT_OUTPUT}", ) return parser.parse_args() def main() -> int: args = parse_args() bindings = json.loads(args.source.read_text()) converted_bindings, changed_keys, duplicate_removed_count = convert_bindings(bindings) args.output.write_text(json.dumps(converted_bindings, indent=2) + "\n") print(f"source: {args.source}") print(f"output: {args.output}") print(f"entries: {len(converted_bindings)}") print(f"changed keys: {changed_keys}") print(f"duplicate modifiers removed: {duplicate_removed_count}") return 0 if __name__ == "__main__": raise SystemExit(main())