Created
May 3, 2026 07:09
-
-
Save trungnt13/1a9a6f27dade759d3bc642cf00d52c4d to your computer and use it in GitHub Desktop.
convert_cursor_keybindings_windows.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment