191 lines
7.1 KiB
Python
191 lines
7.1 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 _state_for(device):
|
|
"""The shadow state of the pad that is plugged in, or the default one."""
|
|
if device is None:
|
|
return core.State()
|
|
state = core.State(device_id=f"{device.vid}:{device.pid}")
|
|
if not state.path.exists():
|
|
known = core.KNOWN_LAYOUTS.get((device.vid, device.pid))
|
|
if known:
|
|
state.layout = known
|
|
return state
|
|
|
|
|
|
def cmd_state(_):
|
|
s = _state_for(core.diagnose(keyd=False).device)
|
|
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):
|
|
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 = _state_for(d.device)
|
|
if a.control not in state.layout.control_ids:
|
|
print(f"Unknown control. One of: {', '.join(state.layout.control_ids)}")
|
|
return 1
|
|
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())
|