9c661d605a
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
4.2 KiB
Python
120 lines
4.2 KiB
Python
"""Add or replace a PowerToys Keyboard Manager global shortcut."""
|
|
|
|
import json
|
|
import os
|
|
|
|
# Windows Virtual Key codes — key name (lowercase) → VK int
|
|
VK_CODES: dict[str, int] = {
|
|
"lwin": 91, "rwin": 92,
|
|
"lshift": 160, "rshift": 161, "shift": 160,
|
|
"lctrl": 162, "rctrl": 163, "ctrl": 162,
|
|
"lalt": 164, "ralt": 165, "alt": 164,
|
|
"space": 32, "enter": 13, "tab": 9, "esc": 27,
|
|
"win": 91,
|
|
# F-keys
|
|
"f1": 112, "f2": 113, "f3": 114, "f4": 115,
|
|
"f5": 116, "f6": 117, "f7": 118, "f8": 119,
|
|
"f9": 120, "f10": 121, "f11": 122, "f12": 123,
|
|
# Digits
|
|
"0": 48, "1": 49, "2": 50, "3": 51, "4": 52,
|
|
"5": 53, "6": 54, "7": 55, "8": 56, "9": 57,
|
|
}
|
|
# Letters A-Z
|
|
for _ch in "abcdefghijklmnopqrstuvwxyz":
|
|
VK_CODES[_ch] = ord(_ch.upper())
|
|
|
|
|
|
def _keys_to_vk_string(keys: list[str]) -> str:
|
|
"""Convert a list of key names to a semicolon-separated VK code string.
|
|
|
|
Key names are case-insensitive. Letters are matched as lowercase.
|
|
Raises ValueError for unknown key names.
|
|
"""
|
|
codes = []
|
|
for key in keys:
|
|
normalized = key.lower()
|
|
if normalized not in VK_CODES:
|
|
raise ValueError(
|
|
f"Unknown key name: '{key}'. "
|
|
f"Use names like 'ctrl', 'alt', 'lctrl', 'lalt', 'shift', 't', 'f1', etc."
|
|
)
|
|
codes.append(str(VK_CODES[normalized]))
|
|
return ";".join(codes)
|
|
|
|
|
|
def _default_config_path() -> str:
|
|
override = os.environ.get("POWERTOYS_CONFIG")
|
|
if override:
|
|
return override
|
|
user = os.environ.get("USER") or os.environ.get("USERNAME", "")
|
|
return (
|
|
f"/mnt/c/Users/{user}/AppData/Local/Microsoft/PowerToys"
|
|
f"/Keyboard Manager/default.json"
|
|
)
|
|
|
|
|
|
def powertoys_shortcut_add(
|
|
keys: list[str],
|
|
target_path: str,
|
|
args: str = "",
|
|
elevated: bool = False,
|
|
exact_match: bool = False,
|
|
start_in_dir: str = "",
|
|
config_path: str | None = None,
|
|
) -> None:
|
|
"""Add or replace a global shortcut in PowerToys Keyboard Manager config.
|
|
|
|
Converts the human-readable key list to VK code string and writes the entry
|
|
into remapShortcuts.global. If an entry with the same originalKeys already
|
|
exists, it is replaced. Writes JSON in compact (single-line) format to
|
|
maintain compatibility with the format PowerToys uses.
|
|
|
|
Args:
|
|
keys: List of key names (case-insensitive). E.g. ["lctrl", "lalt", "t"].
|
|
Supported modifiers: lctrl/rctrl/ctrl, lalt/ralt/alt, lshift/rshift/shift,
|
|
lwin/rwin/win. Letters: a-z. Digits: 0-9. F-keys: f1-f12.
|
|
Special: space, enter, tab, esc.
|
|
target_path: Windows path to the executable. E.g. "C:\\Windows\\System32\\wt.exe".
|
|
args: Command-line arguments for the program (default "").
|
|
elevated: Whether to run the program elevated (default False).
|
|
exact_match: Whether the shortcut requires an exact key match (default False).
|
|
start_in_dir: Working directory for the program. Defaults to "" (empty string).
|
|
config_path: Path to default.json. If None, uses $POWERTOYS_CONFIG or WSL default.
|
|
"""
|
|
if config_path is None:
|
|
config_path = _default_config_path()
|
|
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
original_keys_str = _keys_to_vk_string(keys)
|
|
|
|
new_entry = {
|
|
"originalKeys": original_keys_str,
|
|
"exactMatch": exact_match,
|
|
"runProgramElevationLevel": 1 if elevated else 0,
|
|
"operationType": 1,
|
|
"runProgramAlreadyRunningAction": 1,
|
|
"runProgramStartWindowType": 0,
|
|
"runProgramFilePath": target_path,
|
|
"runProgramArgs": args,
|
|
"runProgramStartInDir": start_in_dir,
|
|
"unicodeText": "*Unsupported*",
|
|
}
|
|
|
|
global_list: list[dict] = data.setdefault("remapShortcuts", {}).setdefault("global", [])
|
|
|
|
# Replace if same originalKeys already exists
|
|
replaced = False
|
|
for i, entry in enumerate(global_list):
|
|
if entry.get("originalKeys") == original_keys_str:
|
|
global_list[i] = new_entry
|
|
replaced = True
|
|
break
|
|
|
|
if not replaced:
|
|
global_list.append(new_entry)
|
|
|
|
with open(config_path, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, separators=(",", ":"), ensure_ascii=False)
|