#!/usr/bin/env python3 """Interactive menu for PH18 HID RGB controls.""" from __future__ import annotations import re import subprocess import sys from pathlib import Path HEX_COLOR_RE = re.compile(r"^[0-9a-fA-F]{6}$") CSV_COLOR_RE = re.compile(r"^\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*$") SCRIPT_DIR = Path(__file__).resolve().parent HID_RGB_SCRIPT = SCRIPT_DIR / "ph18_hid_rgb.py" def prompt_menu_choice() -> str: """Prompt for the main menu action.""" print("\nPH18 RGB Menu") print("1. List detected HID devices and mappings") print("2. Set keyboard single static color") print("3. Set keyboard 4-zone static colors") print("4. Switch keyboard to dynamic mode") print("5. Set rear/logo/bar static color") print("6. Turn rear/logo/bar off") print("7. Set rear/logo/bar progressbar effect") print("0. Exit") return input("Choose an option: ").strip() def prompt_yes_no(question: str, *, default_yes: bool = True) -> bool: """Prompt for a yes/no answer.""" suffix = " [Y/n]: " if default_yes else " [y/N]: " raw = input(question + suffix).strip().lower() if not raw: return default_yes return raw in {"y", "yes"} def _valid_csv_color(value: str) -> bool: if not CSV_COLOR_RE.match(value): return False try: parts = [int(part.strip()) for part in value.split(",")] except ValueError: return False return len(parts) == 3 and all(0 <= part <= 255 for part in parts) def prompt_color(question: str) -> str: """Prompt for a color in hex (RRGGBB) or CSV (R,G,B).""" while True: raw = input(f"{question} (RRGGBB or R,G,B): ").strip() if HEX_COLOR_RE.match(raw): return raw.lower() if _valid_csv_color(raw): return raw print("Invalid color format.") def prompt_target() -> str: """Prompt for zone target.""" options = {"1": "rear", "2": "logo", "3": "bar", "4": "all"} print("\nTarget:") print("1. rear") print("2. logo") print("3. bar") print("4. all") while True: choice = input("Choose target: ").strip() target = options.get(choice) if target: return target print("Invalid choice.") def prompt_brightness() -> str: """Prompt for brightness byte.""" while True: raw = input("Brightness [0-255] (default 25): ").strip() if not raw: return "25" try: value = int(raw, 10) except ValueError: print("Invalid number.") continue if 0 <= value <= 255: return str(value) print("Brightness must be 0..255.") def run_hid_rgb(args: list[str]) -> int: """Run ph18_hid_rgb.py with provided arguments.""" cmd = [sys.executable, str(HID_RGB_SCRIPT), *args] print(f"\n$ {' '.join(cmd)}") result = subprocess.run(cmd, check=False) if result.returncode != 0: print(f"Command failed with exit code {result.returncode}") return result.returncode def action_keyboard_static() -> None: color = prompt_color("Keyboard color") args = ["keyboard", "--color", color] if not prompt_yes_no("Apply MagKey/WASD overlay color too?", default_yes=True): args.append("--no-magkeys") run_hid_rgb(args) def action_keyboard_zones() -> None: z1 = prompt_color("Zone 1 color (left)") z2 = prompt_color("Zone 2 color") z3 = prompt_color("Zone 3 color") z4 = prompt_color("Zone 4 color (right)") args = ["keyboard-zones", "--z1", z1, "--z2", z2, "--z3", z3, "--z4", z4] if not prompt_yes_no("Apply MagKey/WASD overlay from zone1 color?", default_yes=True): args.append("--no-magkeys") run_hid_rgb(args) def action_keyboard_dynamic() -> None: repeats = input("Dynamic repeats (default 2): ").strip() or "2" run_hid_rgb(["keyboard-dynamic", "--repeats", repeats]) def action_zone_static() -> None: target = prompt_target() color = prompt_color("Zone color") brightness = prompt_brightness() run_hid_rgb(["zone", target, "static", "--color", color, "--brightness", brightness]) def action_zone_off() -> None: target = prompt_target() run_hid_rgb(["zone", target, "off"]) def action_zone_progressbar() -> None: target = prompt_target() run_hid_rgb(["zone", target, "progressbar"]) def main() -> int: """Run the interactive RGB menu.""" if not HID_RGB_SCRIPT.exists(): print(f"Missing script: {HID_RGB_SCRIPT}") return 1 if hasattr(sys, "getuid") and sys.getuid() != 0: print("Tip: run with sudo for write operations (e.g. `sudo python3 ph18_rgb_menu.py`).") actions = { "1": lambda: run_hid_rgb(["list"]), "2": action_keyboard_static, "3": action_keyboard_zones, "4": action_keyboard_dynamic, "5": action_zone_static, "6": action_zone_off, "7": action_zone_progressbar, } while True: choice = prompt_menu_choice() if choice == "0": print("Bye.") return 0 action = actions.get(choice) if action is None: print("Invalid option.") continue action() if __name__ == "__main__": raise SystemExit(main())