Created
April 29, 2026 09:10
-
-
Save kolibril13/7b09017c6dce1bf2e7c8d706ecbbc66a to your computer and use it in GitHub Desktop.
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 | |
| # macOS only | |
| """ | |
| DaVinci Resolve keyboard binding editor. | |
| Usage: | |
| python resolve_keybind.py list [filter] | |
| python resolve_keybind.py get <command> | |
| python resolve_keybind.py set <command> <key> [modifiers] | |
| key: single char (x) or special (delete, backspace, f1..f12, etc.) | |
| modifiers: comma-separated ctrl,shift,alt,meta | |
| Examples: | |
| python resolve_keybind.py get editBackspace | |
| python resolve_keybind.py set editBackspace x | |
| python resolve_keybind.py set editRippleCut x ctrl,shift | |
| python resolve_keybind.py list delete | |
| """ | |
| import re | |
| import shutil | |
| import struct | |
| import sys | |
| import uuid | |
| from pathlib import Path | |
| PRESET_PATH = Path.home() / "Library/Preferences/Blackmagic Design/DaVinci Resolve/keyboard.preset.xml" | |
| MODIFIERS = {"ctrl": 0x01, "shift": 0x02, "alt": 0x04, "meta": 0x08} | |
| SPECIAL_KEYS = { | |
| "backspace": 0x01000003, "delete": 0x01000007, "escape": 0x01000000, | |
| "return": 0x01000004, "tab": 0x01000001, | |
| "left": 0x01000012, "right": 0x01000014, | |
| "up": 0x01000013, "down": 0x01000015, | |
| "home": 0x01000010, "end": 0x01000011, | |
| "pageup": 0x01000016, "pagedown": 0x01000017, | |
| **{f"f{i}": 0x0100002F + i for i in range(1, 13)}, | |
| } | |
| _SPECIAL_BY_CODE = {v & 0x00FFFFFF: k.capitalize() for k, v in SPECIAL_KEYS.items()} | |
| _U16 = struct.Struct(">H") | |
| _U32 = struct.Struct(">I") | |
| def _scan_strings(data): | |
| """Yield (end_offset, string) for each UTF-16 BE ASCII run of length >= 3.""" | |
| i, n = 0, len(data) - 1 | |
| while i < n: | |
| chars, j = [], i | |
| while j < n: | |
| (cp,) = _U16.unpack_from(data, j) | |
| if 32 <= cp <= 126: | |
| chars.append(chr(cp)) | |
| j += 2 | |
| else: | |
| break | |
| if len(chars) >= 3: | |
| yield j, "".join(chars) | |
| i = j | |
| else: | |
| i += 1 | |
| def _parse_bindings(data, offset): | |
| """Return list of (byte_offset, key_field) for bindings starting at offset.""" | |
| if offset + 4 > len(data): | |
| return [] | |
| (n,) = _U32.unpack_from(data, offset) | |
| if not (0 < n <= 20): | |
| return [] | |
| result = [] | |
| for i in range(n): | |
| off = offset + 4 + i * 8 | |
| if off + 8 > len(data): | |
| break | |
| (key_field,) = _U32.unpack_from(data, off + 4) | |
| result.append((off, key_field)) | |
| return result | |
| def _decode_key(key_field): | |
| mod, key = (key_field >> 24) & 0xFF, key_field & 0x00FFFFFF | |
| parts = [name.capitalize() for name, bit in MODIFIERS.items() if mod & bit] | |
| parts.append(_SPECIAL_BY_CODE.get(key) or (chr(key) if 32 <= key <= 126 else f"0x{key:06x}")) | |
| return "+".join(parts) | |
| def _encode_key(key_str, mods=()): | |
| mod = 0 | |
| for m in mods: | |
| if m not in MODIFIERS: | |
| raise ValueError(f"unknown modifier '{m}'. Use: {', '.join(MODIFIERS)}") | |
| mod |= MODIFIERS[m] | |
| k = key_str.lower() | |
| if k in SPECIAL_KEYS: | |
| key_code = SPECIAL_KEYS[k] & 0x00FFFFFF | |
| elif len(key_str) == 1: | |
| key_code = ord(key_str.upper()) | |
| else: | |
| raise ValueError(f"unknown key '{key_str}'") | |
| return (mod << 24) | key_code | |
| def _read_blob(): | |
| text = PRESET_PATH.read_text(encoding="utf-8") | |
| m = re.search(r"<PresetListBA>([0-9a-f]+)</PresetListBA>", text) | |
| if not m: | |
| raise ValueError("PresetListBA not found in preset file") | |
| return text, bytes.fromhex(m.group(1)) | |
| def _write_blob(text, blob): | |
| backup = PRESET_PATH.with_suffix(".xml.bak") | |
| shutil.copy2(PRESET_PATH, backup) | |
| print(f"Backup → {backup}") | |
| new_text = text.replace( | |
| re.search(r"<PresetListBA>[0-9a-f]+</PresetListBA>", text).group(), | |
| f"<PresetListBA>{blob.hex()}</PresetListBA>", | |
| ) | |
| PRESET_PATH.write_text(new_text, encoding="utf-8") | |
| print(f"Saved → {PRESET_PATH}") | |
| def _build_blob(command, key_field, preset_name="Custom"): | |
| name_utf16 = preset_name.encode("utf-16-be") | |
| cmd_utf16 = command.encode("utf-16-be") | |
| entry = _U16.pack(len(cmd_utf16)) + cmd_utf16 + _U32.pack(1) + _U32.pack(1) + _U32.pack(key_field) | |
| body = _U32.pack(1) + _U32.pack(1) + entry | |
| return _U32.pack(1) + _U32.pack(1) + _U32.pack(len(name_utf16)) + name_utf16 + _U32.pack(len(body)) + body | |
| def _create_preset(command, key_field): | |
| blob = _build_blob(command, key_field) | |
| xml = ( | |
| f'<?xml version="1.0" encoding="UTF-8"?>\n' | |
| f'<SmKeyboardPresetList DbId="{uuid.uuid4()}">\n' | |
| f' <FieldsBlob/>\n' | |
| f' <PresetListBA>{blob.hex()}</PresetListBA>\n' | |
| f'</SmKeyboardPresetList>\n' | |
| ) | |
| PRESET_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| PRESET_PATH.write_text(xml, encoding="utf-8") | |
| print(f"Created → {PRESET_PATH}") | |
| def cmd_list(filt=None): | |
| try: | |
| _, blob = _read_blob() | |
| except (FileNotFoundError, ValueError) as e: | |
| sys.exit(f"Error: {e}") | |
| for end, name in _scan_strings(blob): | |
| if filt and filt.lower() not in name.lower(): | |
| continue | |
| for _, key_field in _parse_bindings(blob, end): | |
| print(f"{name:<55} {_decode_key(key_field)}") | |
| def cmd_get(command): | |
| try: | |
| _, blob = _read_blob() | |
| except (FileNotFoundError, ValueError) as e: | |
| sys.exit(f"Error: {e}") | |
| found = False | |
| for end, name in _scan_strings(blob): | |
| if name != command: | |
| continue | |
| found = True | |
| bindings = _parse_bindings(blob, end) | |
| if not bindings: | |
| print(f"{name}: (no bindings)") | |
| for i, (off, key_field) in enumerate(bindings): | |
| print(f"{name} [{i}]: {_decode_key(key_field)} (raw @ {off}: {blob[off:off+8].hex()})") | |
| if not found: | |
| print(f"Command '{command}' not found.") | |
| def cmd_set(command, key_field): | |
| if not PRESET_PATH.exists(): | |
| _create_preset(command, key_field) | |
| print(f"Set {command} → {_decode_key(key_field)}") | |
| return | |
| try: | |
| text, blob = _read_blob() | |
| except ValueError as e: | |
| sys.exit(f"Error: {e}") | |
| new_blob = bytearray(blob) | |
| for end, name in _scan_strings(blob): | |
| if name != command: | |
| continue | |
| bindings = _parse_bindings(blob, end) | |
| if not bindings: | |
| sys.exit(f"Command '{command}' has no bindings — cannot patch") | |
| off, _ = bindings[0] | |
| new_blob[off + 4:off + 8] = _U32.pack(key_field) | |
| print(f"Set {command} → {_decode_key(key_field)} (raw @ {off}: {new_blob[off:off+8].hex()})") | |
| break | |
| else: | |
| sys.exit(f"Command '{command}' not found in preset") | |
| _write_blob(text, bytes(new_blob)) | |
| def main(): | |
| args = sys.argv[1:] | |
| if not args: | |
| sys.exit(__doc__) | |
| verb, rest = args[0], args[1:] | |
| if verb == "list": | |
| cmd_list(rest[0] if rest else None) | |
| elif verb == "get": | |
| if not rest: | |
| sys.exit("Usage: get <command>") | |
| cmd_get(rest[0]) | |
| elif verb == "set": | |
| if len(rest) < 2: | |
| sys.exit("Usage: set <command> <key> [modifiers]") | |
| mods = [m.strip() for m in rest[2].split(",")] if len(rest) > 2 else [] | |
| try: | |
| key_field = _encode_key(rest[1], mods) | |
| except ValueError as e: | |
| sys.exit(str(e)) | |
| cmd_set(rest[0], key_field) | |
| else: | |
| sys.exit(__doc__) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment