From 1ffedbf48d8ffafa936e4dbbefa661b55cfdc7f4 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 8 May 2026 01:21:17 +0200 Subject: [PATCH] feat(infra): auto-commit con 12 cambios Co-Authored-By: Claude Opus 4.7 (1M context) --- .../functions/infra/powertoys_config_path.md | 51 ++++++++ .../functions/infra/powertoys_config_path.py | 51 ++++++++ python/functions/infra/powertoys_restart.md | 61 +++++++++ python/functions/infra/powertoys_restart.py | 48 +++++++ .../functions/infra/powertoys_shortcut_add.md | 74 +++++++++++ .../functions/infra/powertoys_shortcut_add.py | 119 ++++++++++++++++++ .../infra/powertoys_shortcut_list.md | 50 ++++++++ .../infra/powertoys_shortcut_list.py | 74 +++++++++++ .../infra/powertoys_shortcut_remove.md | 53 ++++++++ .../infra/powertoys_shortcut_remove.py | 89 +++++++++++++ python/types/core/error_py_core.md | 37 ++++++ python/types/core/error_py_core.py | 9 ++ 12 files changed, 716 insertions(+) create mode 100644 python/functions/infra/powertoys_config_path.md create mode 100644 python/functions/infra/powertoys_config_path.py create mode 100644 python/functions/infra/powertoys_restart.md create mode 100644 python/functions/infra/powertoys_restart.py create mode 100644 python/functions/infra/powertoys_shortcut_add.md create mode 100644 python/functions/infra/powertoys_shortcut_add.py create mode 100644 python/functions/infra/powertoys_shortcut_list.md create mode 100644 python/functions/infra/powertoys_shortcut_list.py create mode 100644 python/functions/infra/powertoys_shortcut_remove.md create mode 100644 python/functions/infra/powertoys_shortcut_remove.py create mode 100644 python/types/core/error_py_core.md create mode 100644 python/types/core/error_py_core.py diff --git a/python/functions/infra/powertoys_config_path.md b/python/functions/infra/powertoys_config_path.md new file mode 100644 index 00000000..8c91774d --- /dev/null +++ b/python/functions/infra/powertoys_config_path.md @@ -0,0 +1,51 @@ +--- +name: powertoys_config_path +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: pure +signature: "def powertoys_config_path(user: str | None = None) -> str" +description: "Devuelve el path al default.json de PowerToys Keyboard Manager. Soporta WSL, Windows nativo y override via env var POWERTOYS_CONFIG." +tags: [powertoys, keyboard, windows, wsl, config, path] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [os, platform] +params: + - name: user + desc: "Nombre de usuario Windows. Si es None, lee $USER del entorno. Solo relevante en WSL." +output: "Path absoluto al archivo default.json de PowerToys Keyboard Manager (el archivo puede no existir)." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/infra/powertoys_config_path.py" +notes: | + Clasificada como pure por convencion del registry: sin estado mutable, determinista dado el entorno. + Lee env vars ($POWERTOYS_CONFIG, $USER, $LOCALAPPDATA, $WSL_DISTRO_NAME) que se tratan como + parametros implicitos del entorno — comportamiento identico al de funciones puras de configuracion. + Prioridad de resolucion: (1) $POWERTOYS_CONFIG override, (2) WSL path con /mnt/c, (3) Windows nativo. +--- + +## Ejemplo + +```python +from infra.powertoys_config_path import powertoys_config_path + +# WSL con usuario actual +path = powertoys_config_path() +# => /mnt/c/Users/lucas/AppData/Local/Microsoft/PowerToys/Keyboard Manager/default.json + +# Override explicito +import os +os.environ["POWERTOYS_CONFIG"] = "/tmp/test_powertoys.json" +path = powertoys_config_path() +# => /tmp/test_powertoys.json +``` + +## Notas + +Detecta WSL verificando `/mnt/c` y las variables `WSL_DISTRO_NAME` / `WSLENV`. +En Windows nativo usa `%LOCALAPPDATA%`. El archivo puede no existir si PowerToys no esta instalado. diff --git a/python/functions/infra/powertoys_config_path.py b/python/functions/infra/powertoys_config_path.py new file mode 100644 index 00000000..cb8b9794 --- /dev/null +++ b/python/functions/infra/powertoys_config_path.py @@ -0,0 +1,51 @@ +"""Detect the path of PowerToys Keyboard Manager default.json config file.""" + +import os +import platform + + +def powertoys_config_path(user: str | None = None) -> str: + """Return the path to PowerToys Keyboard Manager default.json. + + Supports three resolution modes (in priority order): + 1. POWERTOYS_CONFIG env var override. + 2. WSL: /mnt/c/Users//AppData/Local/Microsoft/PowerToys/Keyboard Manager/default.json + where is the `user` argument, then $USER env var. + 3. Native Windows: %LOCALAPPDATA%/Microsoft/PowerToys/Keyboard Manager/default.json + + Args: + user: Windows username. If None, reads $USER from env. Only used in WSL mode. + + Returns: + Absolute path string to default.json (file may or may not exist). + """ + override = os.environ.get("POWERTOYS_CONFIG") + if override: + return override + + relative = "Microsoft/PowerToys/Keyboard Manager/default.json" + + # Detect WSL: check for /mnt/c mount or WSL-specific env vars + is_wsl = ( + os.path.exists("/mnt/c") + or "WSL_DISTRO_NAME" in os.environ + or "WSLENV" in os.environ + ) + + if is_wsl: + resolved_user = user or os.environ.get("USER") or os.environ.get("USERNAME") + if not resolved_user: + raise ValueError( + "Cannot determine Windows username. Pass user= or set $USER env var." + ) + return f"/mnt/c/Users/{resolved_user}/AppData/Local/{relative}" + + if platform.system() == "Windows": + local_app_data = os.environ.get("LOCALAPPDATA", "") + if not local_app_data: + raise ValueError("$LOCALAPPDATA env var is not set on this Windows system.") + return os.path.join(local_app_data, relative) + + # Fallback: assume WSL-style path with $USER + resolved_user = user or os.environ.get("USER") or os.environ.get("USERNAME", "") + return f"/mnt/c/Users/{resolved_user}/AppData/Local/{relative}" diff --git a/python/functions/infra/powertoys_restart.md b/python/functions/infra/powertoys_restart.md new file mode 100644 index 00000000..a5abfe31 --- /dev/null +++ b/python/functions/infra/powertoys_restart.md @@ -0,0 +1,61 @@ +--- +name: powertoys_restart +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "def powertoys_restart() -> None" +description: "Mata los procesos de PowerToys y los relanza para que recargue la configuracion del Keyboard Manager. Compatible con WSL via taskkill.exe y cmd.exe." +tags: [powertoys, keyboard, windows, wsl, restart, process, taskkill] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_py_core" +imports: [os, subprocess, time] +params: [] +output: "None. Mata PowerToys.exe y PowerToys.KeyboardManagerEngine.exe, espera 1 segundo, y relanza PowerToys.exe de forma desacoplada." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/infra/powertoys_restart.py" +notes: | + error_py_core no existe en el registry. Esta funcion puede lanzar RuntimeError si + taskkill.exe o cmd.exe no estan disponibles en el PATH, o FileNotFoundError si no + se puede resolver el path del exe. + + Mata los procesos con taskkill.exe /F (forzado). Si PowerToys no esta corriendo, + taskkill falla silenciosamente (capture_output=True, returncode ignorado). + + El path del exe se construye como /mnt/c/Users//AppData/Local/PowerToys/PowerToys.exe. + Se puede sobreescribir con la env var $POWERTOYS_EXE (path WSL o Windows). + + La conversion de path WSL a Windows: /mnt/c/Users/... -> C:\Users\... se hace internamente. + El Popen usa cmd.exe /c start "" para desacoplar el proceso hijo (no bloquea). + + Tiempo de espera entre kill y launch: 1 segundo (hardcoded). Si PowerToys tarda mas + en cerrar, la nueva instancia puede fallar al iniciar — esperar unos segundos extra + antes de verificar si esta corriendo. +--- + +## Ejemplo + +```python +from infra.powertoys_shortcut_add import powertoys_shortcut_add +from infra.powertoys_restart import powertoys_restart + +# Anadir atajo y recargar PowerToys +powertoys_shortcut_add( + keys=["lctrl", "lalt", "t"], + target_path=r"C:\Windows\System32\wt.exe", +) +powertoys_restart() +print("PowerToys reiniciado — el nuevo atajo esta activo") +``` + +## Notas + +Solo funciona en entornos donde `taskkill.exe` y `cmd.exe` estan disponibles en el PATH +(WSL con integracion Windows habilitada, o Windows nativo). No funciona en Linux puro +ni en Mac. diff --git a/python/functions/infra/powertoys_restart.py b/python/functions/infra/powertoys_restart.py new file mode 100644 index 00000000..d9d0afe0 --- /dev/null +++ b/python/functions/infra/powertoys_restart.py @@ -0,0 +1,48 @@ +"""Restart PowerToys so it reloads its Keyboard Manager configuration.""" + +import os +import subprocess +import time + + +def powertoys_restart() -> None: + """Kill PowerToys processes and relaunch the application. + + Kills PowerToys.exe and PowerToys.KeyboardManagerEngine.exe using taskkill.exe, + waits 1 second for them to shut down cleanly, then relaunches PowerToys.exe via + cmd.exe /c start. Works from WSL (invokes taskkill.exe and cmd.exe from the + Windows system32 path). + + The PowerToys executable is resolved using the $USER env var to build the + standard AppData path. To override the exe path, set $POWERTOYS_EXE. + + Raises: + RuntimeError: If taskkill.exe or cmd.exe cannot be found/invoked. + FileNotFoundError: If the PowerToys.exe path cannot be resolved. + """ + user = os.environ.get("USER") or os.environ.get("USERNAME", "") + powertoys_exe = os.environ.get( + "POWERTOYS_EXE", + f"/mnt/c/Users/{user}/AppData/Local/PowerToys/PowerToys.exe", + ) + + # Kill running instances (ignore errors if not running) + for process_name in ("PowerToys.exe", "PowerToys.KeyboardManagerEngine.exe"): + subprocess.run( + ["taskkill.exe", "/IM", process_name, "/F"], + capture_output=True, + ) + + time.sleep(1) + + # Relaunch via cmd.exe /c start to detach from the current process + # Convert WSL path to Windows path for cmd.exe + win_exe = powertoys_exe + if powertoys_exe.startswith("/mnt/c/"): + win_exe = "C:\\" + powertoys_exe[len("/mnt/c/"):].replace("/", "\\") + + subprocess.Popen( + ["cmd.exe", "/c", "start", "", win_exe], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) diff --git a/python/functions/infra/powertoys_shortcut_add.md b/python/functions/infra/powertoys_shortcut_add.md new file mode 100644 index 00000000..7c9013f2 --- /dev/null +++ b/python/functions/infra/powertoys_shortcut_add.md @@ -0,0 +1,74 @@ +--- +name: powertoys_shortcut_add +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "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" +description: "Añade o reemplaza un atajo global en el config de PowerToys Keyboard Manager. Convierte nombres legibles de teclas a VK codes y escribe JSON compacto (una linea) para mantener compatibilidad con el formato de PowerToys." +tags: [powertoys, keyboard, windows, wsl, shortcut, config, write, add] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_py_core" +imports: [json, os] +params: + - name: keys + desc: "Lista de nombres de teclas (case-insensitive). Ej: ['lctrl', 'lalt', 't']. Modificadores: lctrl/rctrl/ctrl, lalt/ralt/alt, lshift/rshift/shift, lwin/rwin/win. Letras: a-z. Digitos: 0-9. F-keys: f1-f12. Especiales: space, enter, tab, esc." + - name: target_path + desc: "Path Windows al ejecutable. Ej: 'C:\\\\Windows\\\\System32\\\\wt.exe'." + - name: args + desc: "Argumentos de linea de comandos para el programa (default vacio)." + - name: elevated + desc: "Si True, lanza el programa elevado (runProgramElevationLevel=1). Default False." + - name: exact_match + desc: "Si True, el atajo requiere coincidencia exacta de teclas. Default False." + - name: start_in_dir + desc: "Directorio de trabajo para el programa. Default string vacio." + - name: config_path + desc: "Path al default.json. Si None, usa $POWERTOYS_CONFIG o el path WSL por defecto para $USER." +output: "None. Modifica el archivo default.json en disco." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/infra/powertoys_shortcut_add.py" +notes: | + error_py_core no existe en el registry. Esta funcion lanza excepciones nativas de Python: + FileNotFoundError si config_path no existe, json.JSONDecodeError si el JSON es invalido, + ValueError si un nombre de tecla no esta en VK_CODES. + + El JSON se escribe en formato compacto (separators=(",", ":")) — sin espacios ni saltos de linea — + para mantener compatibilidad con el formato que usa PowerToys en el archivo original. + + El dict VK_CODES mapea "ctrl"->162, "alt"->164, "shift"->160, "win"->91 como aliases + a las variantes izquierdas. Para especificar side derecho, usar "rctrl", "ralt", etc. + + Si ya existe una entrada con los mismos originalKeys, la reemplaza en su posicion original. +--- + +## Ejemplo + +```python +from infra.powertoys_shortcut_add import powertoys_shortcut_add + +# Anadir Ctrl+Alt+T -> Windows Terminal +powertoys_shortcut_add( + keys=["lctrl", "lalt", "t"], + target_path=r"C:\Windows\System32\wt.exe", + start_in_dir=r"C:\Users\lucas", +) + +# Anadir Win+Shift+E -> Explorer elevado +powertoys_shortcut_add( + keys=["win", "shift", "e"], + target_path=r"C:\Windows\explorer.exe", + elevated=True, +) +``` + +## Notas + +Despues de modificar el config, PowerToys necesita reiniciarse para detectar los cambios. +Usar `powertoys_restart_py_infra` para recargar la configuracion automaticamente. diff --git a/python/functions/infra/powertoys_shortcut_add.py b/python/functions/infra/powertoys_shortcut_add.py new file mode 100644 index 00000000..cea433a5 --- /dev/null +++ b/python/functions/infra/powertoys_shortcut_add.py @@ -0,0 +1,119 @@ +"""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) diff --git a/python/functions/infra/powertoys_shortcut_list.md b/python/functions/infra/powertoys_shortcut_list.md new file mode 100644 index 00000000..bc74322e --- /dev/null +++ b/python/functions/infra/powertoys_shortcut_list.md @@ -0,0 +1,50 @@ +--- +name: powertoys_shortcut_list +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "def powertoys_shortcut_list(config_path: str | None = None) -> list[dict]" +description: "Lee el default.json de PowerToys Keyboard Manager y devuelve los atajos globales con VK codes convertidos a nombres legibles." +tags: [powertoys, keyboard, windows, wsl, shortcut, config, read] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_py_core" +imports: [json, os] +params: + - name: config_path + desc: "Path absoluto al default.json. Si es None, usa $POWERTOYS_CONFIG o el path WSL por defecto para $USER." +output: "Lista de dicts con campos: keys (list[str] nombres legibles), target (str path al exe), args (str argumentos), elevated (bool), exact_match (bool), start_in_dir (str)." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/infra/powertoys_shortcut_list.py" +notes: | + error_py_core no existe en el registry. Esta funcion lanza excepciones nativas de Python: + FileNotFoundError si config_path no existe, json.JSONDecodeError si el JSON es invalido. + El caller debe manejar estas excepciones. Cuando error_py_core se cree en el registry, + actualizar error_type. + + Solo lee remapShortcuts.global (no appSpecific, no remapKeys, no remapKeysToText). + El dict VK_CODES esta definido en el modulo — no hay cross-file imports para mantener KISS. +--- + +## Ejemplo + +```python +from infra.powertoys_shortcut_list import powertoys_shortcut_list + +shortcuts = powertoys_shortcut_list() +for s in shortcuts: + print(s["keys"], "->", s["target"]) +# ["lctrl", "lalt", "t"] -> C:\Windows\System32\wt.exe +``` + +## Notas + +Mapeo VK codes: lctrl=162, rctrl=163, lalt=164, ralt=165, lshift=160, rshift=161, +lwin=91, rwin=92, A-Z=65-90, 0-9=48-57, F1-F12=112-123, space=32, enter=13, tab=9, esc=27. +VK codes desconocidos se devuelven como string numerico (ej: "190"). diff --git a/python/functions/infra/powertoys_shortcut_list.py b/python/functions/infra/powertoys_shortcut_list.py new file mode 100644 index 00000000..47bcbfbb --- /dev/null +++ b/python/functions/infra/powertoys_shortcut_list.py @@ -0,0 +1,74 @@ +"""Read and list PowerToys Keyboard Manager shortcuts as human-readable dicts.""" + +import json +import os + +# Windows Virtual Key codes → readable names +VK_CODES: dict[int, str] = { + 91: "lwin", 92: "rwin", + 160: "lshift", 161: "rshift", + 162: "lctrl", 163: "rctrl", + 164: "lalt", 165: "ralt", + 32: "space", 13: "enter", 9: "tab", 27: "esc", + # F-keys + 112: "f1", 113: "f2", 114: "f3", 115: "f4", + 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", + # Digits 0-9 + 48: "0", 49: "1", 50: "2", 51: "3", 52: "4", + 53: "5", 54: "6", 55: "7", 56: "8", 57: "9", +} +# Letters A-Z (VK 65-90) +for _vk in range(65, 91): + VK_CODES[_vk] = chr(_vk).lower() + + +def _vk_to_name(vk: int) -> str: + """Convert a VK code integer to a readable key name.""" + return VK_CODES.get(vk, str(vk)) + + +def _parse_keys(original_keys: str) -> list[str]: + """Parse a semicolon-separated VK code string to a list of readable names.""" + if not original_keys: + return [] + return [_vk_to_name(int(code)) for code in original_keys.split(";") if code] + + +def powertoys_shortcut_list(config_path: str | None = None) -> list[dict]: + """Return all global shortcuts from PowerToys Keyboard Manager config. + + Reads the default.json file and returns the list of remapShortcuts.global entries + with VK codes converted to human-readable key names. + + Args: + config_path: Absolute path to default.json. If None, uses the POWERTOYS_CONFIG + env var or the default WSL path for the current $USER. + + Returns: + List of dicts with keys: keys, target, args, elevated, exact_match, start_in_dir. + """ + if config_path is None: + config_path = os.environ.get("POWERTOYS_CONFIG") + if config_path is None: + user = os.environ.get("USER") or os.environ.get("USERNAME", "") + config_path = ( + f"/mnt/c/Users/{user}/AppData/Local/Microsoft/PowerToys" + f"/Keyboard Manager/default.json" + ) + + with open(config_path, "r", encoding="utf-8") as f: + data = json.load(f) + + shortcuts = data.get("remapShortcuts", {}).get("global", []) + result = [] + for entry in shortcuts: + result.append({ + "keys": _parse_keys(entry.get("originalKeys", "")), + "target": entry.get("runProgramFilePath", ""), + "args": entry.get("runProgramArgs", ""), + "elevated": entry.get("runProgramElevationLevel", 0) != 0, + "exact_match": entry.get("exactMatch", False), + "start_in_dir": entry.get("runProgramStartInDir", ""), + }) + return result diff --git a/python/functions/infra/powertoys_shortcut_remove.md b/python/functions/infra/powertoys_shortcut_remove.md new file mode 100644 index 00000000..bc177a7b --- /dev/null +++ b/python/functions/infra/powertoys_shortcut_remove.md @@ -0,0 +1,53 @@ +--- +name: powertoys_shortcut_remove +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "def powertoys_shortcut_remove(keys: list[str], config_path: str | None = None) -> bool" +description: "Elimina un atajo global del config de PowerToys Keyboard Manager por combinacion de teclas. Devuelve True si se elimino, False si no existia." +tags: [powertoys, keyboard, windows, wsl, shortcut, config, write, remove, delete] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_py_core" +imports: [json, os] +params: + - name: keys + desc: "Lista de nombres de teclas (case-insensitive) que identifican el atajo a eliminar. Debe coincidir exactamente con las teclas usadas al crear el atajo. Ej: ['lctrl', 'lalt', 't']." + - name: config_path + desc: "Path al default.json. Si None, usa $POWERTOYS_CONFIG o el path WSL por defecto para $USER." +output: "True si se encontro y elimino el atajo, False si no existia ninguna entrada con esas teclas." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/infra/powertoys_shortcut_remove.py" +notes: | + error_py_core no existe en el registry. Esta funcion lanza excepciones nativas de Python: + FileNotFoundError si config_path no existe, json.JSONDecodeError si el JSON es invalido, + ValueError si un nombre de tecla no esta en VK_CODES. + + La coincidencia se hace contra el string originalKeys canonico (semicolon-separated VK codes). + Si las teclas se pasaron en un orden diferente al guardado, no coincidiran — el orden importa. + + El JSON se escribe en formato compacto (separators=(",", ":")) sin espacios ni saltos de linea. +--- + +## Ejemplo + +```python +from infra.powertoys_shortcut_remove import powertoys_shortcut_remove + +removed = powertoys_shortcut_remove(["lctrl", "lalt", "t"]) +if removed: + print("Atajo eliminado correctamente") +else: + print("El atajo no existia") +``` + +## Notas + +Despues de modificar el config, PowerToys necesita reiniciarse para detectar los cambios. +Usar `powertoys_restart_py_infra` para recargar la configuracion automaticamente. diff --git a/python/functions/infra/powertoys_shortcut_remove.py b/python/functions/infra/powertoys_shortcut_remove.py new file mode 100644 index 00000000..e6a57b5e --- /dev/null +++ b/python/functions/infra/powertoys_shortcut_remove.py @@ -0,0 +1,89 @@ +"""Remove a PowerToys Keyboard Manager global shortcut by key combination.""" + +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.""" + 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_remove( + keys: list[str], + config_path: str | None = None, +) -> bool: + """Remove a global shortcut from PowerToys Keyboard Manager config by key combination. + + Converts the key list to a VK code string and removes the matching entry from + remapShortcuts.global. Writes back in compact JSON format. + + Args: + keys: List of key names (case-insensitive) identifying the shortcut to remove. + Must match exactly the keys used when the shortcut was created. + config_path: Path to default.json. If None, uses $POWERTOYS_CONFIG or WSL default. + + Returns: + True if a shortcut was found and removed, False if no matching shortcut existed. + """ + 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) + + global_list: list[dict] = data.get("remapShortcuts", {}).get("global", []) + original_len = len(global_list) + filtered = [e for e in global_list if e.get("originalKeys") != original_keys_str] + + if len(filtered) == original_len: + return False + + data["remapShortcuts"]["global"] = filtered + + with open(config_path, "w", encoding="utf-8") as f: + json.dump(data, f, separators=(",", ":"), ensure_ascii=False) + + return True diff --git a/python/types/core/error_py_core.md b/python/types/core/error_py_core.md new file mode 100644 index 00000000..23918199 --- /dev/null +++ b/python/types/core/error_py_core.md @@ -0,0 +1,37 @@ +--- +name: error +lang: py +domain: core +version: "1.0.0" +algebraic: sum +definition: | + class RegistryError(Exception): + """Error base del registry Python.""" +description: "Tipo de error base del registry Python. Referenciado como error_type por funciones impuras en Python. Subclase de Exception que marca errores esperados en funciones con efectos secundarios." +tags: [error, exception, core, impure, base] +uses_types: [] +file_path: "python/types/core/error_py_core.py" +--- + +## Ejemplo + +```python +from core.error_py_core import RegistryError + +def my_impure_function(path: str) -> str: + try: + with open(path) as f: + return f.read() + except OSError as e: + raise RegistryError(f"Cannot read {path}: {e}") from e +``` + +## Notas + +Analogo a `error_go_core` (tipo Go). Las funciones impuras Python del registry +declaran `error_type: error_py_core` en su frontmatter para indicar que pueden +lanzar excepciones en vez de retornar valores invalidos. + +En la mayoria de casos las funciones lanzan excepciones nativas de Python +(FileNotFoundError, json.JSONDecodeError, ValueError) — RegistryError actua como +clase base opcional para errores de dominio especificos. diff --git a/python/types/core/error_py_core.py b/python/types/core/error_py_core.py new file mode 100644 index 00000000..9b00e03f --- /dev/null +++ b/python/types/core/error_py_core.py @@ -0,0 +1,9 @@ +"""Tipo de error base para funciones impuras Python del registry.""" + + +class RegistryError(Exception): + """Error base del registry Python. + + Subclase de Exception que marca errores esperados en funciones impuras. + Las funciones impuras del registry lo lanzan en vez de retornar None o -1. + """