Files
macropad-itprotech/macropad_gui/__main__.py
Bjorn Blomberg 021f9f7fa1 Lighting as a macropad subcommand; knobless pads start in the flat view
Running the backend module directly made runpy warn, since the package
had already imported it. The lighting commands now sit with doctor, state
and write. A pad without knobs lies flat on the desk, so it is drawn flat
unless an orientation has been chosen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-23 19:26:46 +02:00

179 lines
6.7 KiB
Python

"""
python3 -m macropad_gui open the app
python3 -m macropad_gui doctor check the pad is connected and writable
The rest are for poking at the core from a terminal:
python3 -m macropad_gui state
python3 -m macropad_gui state
python3 -m macropad_gui write key1 "ctrl+c"
python3 -m macropad_gui write dial1-right --media volumeup
python3 -m macropad_gui lighting show ITPROTECH pads only
python3 -m macropad_gui lighting rgb "press: blue"
python3 -m macropad_gui lighting colour ff8800
"""
import argparse
import sys
from . import core
G = "\033[32m"; R = "\033[31m"; Y = "\033[33m"; B = "\033[1m"; DIM = "\033[2m"; O = "\033[0m"
def cmd_doctor(_):
d = core.diagnose()
colour = G if d.ready else (Y if d.status == "unplugged" else R)
print(f"{colour}{B}{d.headline}{O}")
for line in d.detail:
print(f" {line}")
if d.fix:
print(f"\n{B}Fix{O}")
for c in d.fix:
print(f" {DIM if c.startswith('#') else ''}{c}{O}")
for w in d.warnings:
print(f"\n{Y}Note:{O} {w}")
return 0 if d.ready else 1
def cmd_state(_):
s = core.State()
print(f"{DIM}{s.path}{'' if s.path.exists() else ' (not created yet)'}{O}\n")
print(f"{DIM}layout: {s.layout.describe()}{O}\n")
for c in s.layout.control_ids:
e = s.get(c)
byte = f"0x{core.action_byte(c):02x}"
if e.unknown:
print(f" {c:<12} {DIM}{byte}{O} {Y}unknown{O} {DIM}{e.reason}{O}")
else:
print(f" {c:<12} {DIM}{byte}{O} {e.binding.label():<24} "
f"{DIM}{e.written}{O}")
return 0
def cmd_detect(_):
d = core.diagnose(keyd=False)
if not d.device:
print(f"{R}No pad found at all.{O}")
return 1
print(f"{B}Asking {d.device.vid}:{d.device.pid} on {d.device.path}{O}")
print(f"{DIM}This only reads. Nothing is configured.{O}\n")
found = core.detect(d.device)
print(core.detection_report(d.device, found))
if found.speaks:
lay = found.layout
print(f"\n{G}It speaks this protocol.{O}")
print(f"Best guess at the shape: {lay.describe()}. The pad reports how "
f"many\nkeys it has, not how they're arranged, so check that "
f"against yours.")
return 0
print(f"\n{Y}It didn't answer properly, so the app won't write to it.{O}")
return 1
def cmd_actions(a):
"""The background listener. Normally started by the app or at login."""
from . import actions
return actions.listen(debug=a.debug)
def cmd_write(a):
if a.control not in core.CONTROL_IDS:
print(f"Unknown control. One of: {', '.join(core.CONTROL_IDS)}")
return 1
try:
b = core.Binding(keys=a.keys or "", media=a.media or "",
delay=a.delay).validate()
except ValueError as e:
print(f"{R}{e}{O}")
return 1
d = core.diagnose()
if not d.ready:
return cmd_doctor(None)
state = core.State()
try:
core.write_binding(d.device, a.control, b, state)
except core.WriteError as e:
print(f"{R}Write failed:{O} {e}")
print(" " + ("The pad was partly written; this control is now marked "
"unknown." if e.pad_touched else "Nothing reached the pad."))
return 1
print(f"{G}Written.{O} {a.control} -> {b.label()} {DIM}{state.path}{O}")
return 0
def cmd_lighting(a):
"""Lighting mode and custom colour on pads whose backend has them."""
d = core.diagnose(keyd=False)
backend = core.backend_for(d.device) if d.device else None
if not d.device or not hasattr(backend, "set_lighting"):
print(f"{R}No pad with lighting control found.{O} "
+ (d.headline if not d.device else f"{backend.name} has no lighting here."))
return 1
from .backends import itprotech_proto as wire
modes = ", ".join(f"{i} {m}" for i, m in enumerate(wire.RGB_MODES))
if a.what == "show":
mode, colour = backend.lighting(d.device)
name = wire.RGB_MODES[mode] if mode is not None and mode < len(wire.RGB_MODES) else "?"
hexcol = "#%02x%02x%02x" % colour if colour else "?"
print(f"lighting {mode} ({name}), custom colour {hexcol}")
return 0
if not d.ready:
return cmd_doctor(None)
if a.what == "rgb":
want = (a.value or "").lower()
if want.isdigit() and int(want) < len(wire.RGB_MODES):
mode = int(want)
elif want in wire.RGB_MODES:
mode = wire.RGB_MODES.index(want)
else:
print(f"Pick a mode: {modes}")
return 1
backend.set_lighting(d.device, mode)
print(f"{G}Lighting set to {mode} ({wire.RGB_MODES[mode]}).{O}")
return 0
hexcol = (a.value or "").lstrip("#")
if len(hexcol) != 6 or any(c not in "0123456789abcdefABCDEF" for c in hexcol):
print("Give the colour as RRGGBB, e.g. ff8800")
return 1
rgb = tuple(int(hexcol[i:i + 2], 16) for i in (0, 2, 4))
backend.set_lighting(d.device, wire.RGB_CUSTOM, rgb)
print(f"{G}Custom colour #{hexcol.lower()} set (mode {wire.RGB_CUSTOM}).{O}")
return 0
def main():
ap = argparse.ArgumentParser(prog="python3 -m macropad_gui")
sub = ap.add_subparsers(dest="cmd")
sub.add_parser("doctor", help="check the pad is connected and writable")
sub.add_parser("state", help="show what this tool believes is on the pad")
sub.add_parser("detect", help="ask an unrecognised pad what it is (read only)")
ac = sub.add_parser("actions", help="run the listener that makes action keys work")
ac.add_argument("--debug", action="store_true",
help="print every report the pad's keyboard sends")
w = sub.add_parser("write", help="write one binding and record it")
w.add_argument("control")
w.add_argument("keys", nargs="?", help="e.g. 'ctrl+c' or 'a,b,c'")
w.add_argument("--media", help="e.g. volumeup")
w.add_argument("--delay", type=int, default=0)
li = sub.add_parser("lighting", help="lighting mode and colour (ITPROTECH pads)")
li.add_argument("what", choices=("show", "rgb", "colour", "color"))
li.add_argument("value", nargs="?", help="a mode number or name, or RRGGBB")
a = ap.parse_args()
if a.cmd is None:
try:
from .app import run
except ImportError as e:
print(f"{R}Can't start the app:{O} {e}")
print("Install Qt for Python: sudo pacman -S pyside6")
return 1
return run()
return {"doctor": cmd_doctor, "state": cmd_state, "write": cmd_write,
"detect": cmd_detect, "actions": cmd_actions,
"lighting": cmd_lighting}[a.cmd](a)
if __name__ == "__main__":
sys.exit(main())