Add utility to set the lights on a QMK keyboard
This is tested for the KeyChron K3 Max.
This commit is contained in:
605
qmk-lights.py
Executable file
605
qmk-lights.py
Executable file
@@ -0,0 +1,605 @@
|
||||
#!/usr/bin/python3
|
||||
"""qmk-lights.py — QMK/VIA backlight controller for Keychron K3 Max RGB."""
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import dbus
|
||||
import dbus.mainloop.glib
|
||||
import gi.repository.GLib # type: ignore[import-untyped]
|
||||
import pprint
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Must preload libhidapi-hidraw before importing hid.
|
||||
# The Debian default (libhidapi-libusb) does not populate usage_page/usage,
|
||||
# which are required to identify the VIA Raw HID interface.
|
||||
_hidraw = ctypes.util.find_library("hidapi-hidraw")
|
||||
if _hidraw:
|
||||
ctypes.CDLL(_hidraw, mode=ctypes.RTLD_GLOBAL)
|
||||
|
||||
import hid # noqa: E402 # type: ignore[import-untyped]
|
||||
|
||||
# ── USB / HID ──────────────────────────────────────────────────────────────────
|
||||
VID = 0x3434
|
||||
PID = 0x0A30
|
||||
RAW_HID_USAGE_PAGE = 0xFF60 # VIA Raw HID interface
|
||||
RAW_HID_USAGE = 0x61
|
||||
REPORT_SIZE = 32 # payload bytes (firmware sees these)
|
||||
|
||||
# ── VIA protocol command IDs (quantum/via.h) ───────────────────────────────────
|
||||
VIA_CUSTOM_SET_VALUE = 0x07 # id_custom_set_value
|
||||
VIA_CUSTOM_GET_VALUE = 0x08 # id_custom_get_value
|
||||
VIA_CUSTOM_SAVE = 0x09 # id_custom_save
|
||||
|
||||
# ── VIA channel IDs (via.h: via_channel_id enum) ──────────────────────────────
|
||||
RGB_MATRIX_CHANNEL = 0x03 # id_qmk_rgb_matrix_channel
|
||||
|
||||
# ── QMK RGB Matrix value IDs (via.h: via_qmk_rgb_matrix_value enum) ───────────
|
||||
QMK_BRIGHTNESS = 0x01 # id_qmk_rgb_matrix_brightness
|
||||
QMK_EFFECT = 0x02 # id_qmk_rgb_matrix_effect
|
||||
QMK_SPEED = 0x03 # id_qmk_rgb_matrix_effect_speed
|
||||
QMK_COLOR = 0x04 # id_qmk_rgb_matrix_color (hue byte, saturation byte)
|
||||
|
||||
# ── Preset defaults ────────────────────────────────────────────────────────────
|
||||
# --on : solid white
|
||||
ON_EFFECT = 1 # RGB_MATRIX_SOLID_COLOR
|
||||
ON_BRIGHTNESS = 128
|
||||
ON_HUE = 0 # irrelevant when saturation = 0
|
||||
ON_SATURATION = 0 # 0 = white; 255 = fully saturated colour
|
||||
ON_SPEED = 128 # mid speed (unused for solid, but set anyway)
|
||||
|
||||
# --per-key : per_key_rgb — effect 23 (confirmed via VIA web app effect list).
|
||||
PERKEY_EFFECT = 23 # per_key_rgb
|
||||
PERKEY_BRIGHTNESS = 127
|
||||
PERKEY_HUE = 0
|
||||
PERKEY_SATURATION = 0
|
||||
PERKEY_SPEED = 128
|
||||
|
||||
# --program : reactive_multiwide — effect 19 (confirmed via VIA web app effect list).
|
||||
PROGRAMMED_EFFECT = 19 # reactive_multiwide
|
||||
PROGRAMMED_BRIGHTNESS = 255
|
||||
PROGRAMMED_HUE = 0
|
||||
PROGRAMMED_SATURATION = 0 # full saturation makes reactive effects visible
|
||||
PROGRAMMED_SPEED = 128
|
||||
|
||||
# ── Effect name table — confirmed from VIA web app effect list ─────────────────
|
||||
EFFECT_NAMES = {
|
||||
0: "none",
|
||||
1: "solid_color",
|
||||
2: "breathing",
|
||||
3: "band_spiral_val",
|
||||
4: "cycle_all",
|
||||
5: "cycle_left_right",
|
||||
6: "cycle_up_down",
|
||||
7: "rainbow_moving_chevron",
|
||||
8: "cycle_out_in",
|
||||
9: "cycle_out_in_dual",
|
||||
10: "cycle_pinwheel",
|
||||
11: "cycle_spiral",
|
||||
12: "dual_beacon",
|
||||
13: "rainbow_beacon",
|
||||
14: "jellybean_raindrops",
|
||||
15: "pixel_rain",
|
||||
16: "typing_heatmap",
|
||||
17: "digital_rain",
|
||||
18: "reactive_simple",
|
||||
19: "reactive_multiwide",
|
||||
20: "reactive_multinexus",
|
||||
21: "splash",
|
||||
22: "solid_splash",
|
||||
23: "per_key_rgb",
|
||||
24: "mix_pgb",
|
||||
}
|
||||
|
||||
# ── Screensaver DBus interfaces ────────────────────────────────────────────────
|
||||
SCREENSAVER_INTERFACES = [
|
||||
"org.gnome.ScreenSaver",
|
||||
"org.cinnamon.ScreenSaver",
|
||||
"org.kde.screensaver",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
]
|
||||
|
||||
HELP_EPILOG = """\
|
||||
━━━ Effect IDs ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
All IDs confirmed from the VIA web app effect list for this firmware.
|
||||
|
||||
ID Name
|
||||
─── ──────────────────────────────────────────────────────
|
||||
0 none All LEDs off
|
||||
1 solid_color Solid colour (see --hue, --saturation)
|
||||
2 breathing Whole-board pulsing brightness
|
||||
3 band_spiral_val Spiral brightness band
|
||||
4 cycle_all All keys cycle through hues together
|
||||
5 cycle_left_right Hue sweeps left → right
|
||||
6 cycle_up_down Hue sweeps top → bottom
|
||||
7 rainbow_moving_chevron Chevron-shaped rainbow wave
|
||||
8 cycle_out_in Hue cycle from edges inward
|
||||
9 cycle_out_in_dual Dual cycle from edges inward
|
||||
10 cycle_pinwheel Pinwheel of cycling hues
|
||||
11 cycle_spiral Spiral of cycling hues
|
||||
12 dual_beacon Two-colour spinning beacon
|
||||
13 rainbow_beacon Rainbow spinning beacon
|
||||
14 jellybean_raindrops Coloured jellybean drops
|
||||
15 pixel_rain Pixel colour rain
|
||||
16 typing_heatmap Keys glow where you type most
|
||||
17 digital_rain Matrix-style digital rain
|
||||
18 reactive_simple Solid + flash on keypress
|
||||
19 reactive_multiwide Wide ripple on keypress
|
||||
20 reactive_multinexus Nexus ripple on keypress
|
||||
21 splash Colour splash on keypress
|
||||
22 solid_splash Solid + colour splash on keypress
|
||||
23 per_key_rgb Per-key RGB (stored layout)
|
||||
24 mix_pgb Mixed PGB pattern
|
||||
|
||||
━━━ Colour (HSV) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
--hue 0–255 maps to the colour wheel:
|
||||
0=red 21=orange 43=yellow 64=chartreuse 85=green
|
||||
106=spring_green 128=cyan 149=azure 170=blue 192=violet
|
||||
213=magenta 234=rose 255=red (wraps)
|
||||
|
||||
--saturation: 0 = white (no colour), 255 = fully saturated colour.
|
||||
|
||||
Note: solid white is hue=<any>, saturation=0.
|
||||
To get solid blue: --effect 1 --hue 170 --saturation 255
|
||||
|
||||
━━━ Examples ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
qmk-lights.py --on # solid white at full brightness
|
||||
qmk-lights.py --on --brightness 128 # solid white at half brightness
|
||||
qmk-lights.py --on --hue 170 --saturation 255 # solid blue
|
||||
qmk-lights.py --effect 13 # cycle_left_right (if compiled in)
|
||||
qmk-lights.py --brightness 200 --speed 64 # dim and slow, keep current effect
|
||||
qmk-lights.py --off # turn off, saved to EEPROM
|
||||
qmk-lights.py --query # read current settings from keyboard
|
||||
qmk-lights.py --screensaver # auto off/on with screen lock
|
||||
"""
|
||||
|
||||
debug = 0
|
||||
|
||||
|
||||
# ── Device access ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def find_device():
|
||||
"""Enumerate HID devices and return an opened handle to the VIA Raw HID interface."""
|
||||
for info in hid.enumerate(VID, 0):
|
||||
if info["product_id"] != PID:
|
||||
continue
|
||||
if info.get("usage_page") != RAW_HID_USAGE_PAGE:
|
||||
continue
|
||||
if info.get("usage") != RAW_HID_USAGE:
|
||||
continue
|
||||
if debug > 0:
|
||||
print(
|
||||
"Found: {} (usage_page=0x{:04x} usage=0x{:02x})".format(
|
||||
info.get("path"), info.get("usage_page"), info.get("usage")
|
||||
)
|
||||
)
|
||||
kbd = hid.device()
|
||||
kbd.open_path(info["path"])
|
||||
if debug > 0:
|
||||
print("Manufacturer: {}".format(kbd.get_manufacturer_string()))
|
||||
print("Product: {}".format(kbd.get_product_string()))
|
||||
return kbd
|
||||
raise ValueError(
|
||||
"QMK keyboard not found (VID=0x{:04x} PID=0x{:04x}). "
|
||||
"Is it connected and in USB mode?".format(VID, PID)
|
||||
)
|
||||
|
||||
|
||||
# ── VIA communication ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def send_via_command(kbd, command_id, channel_id, value_id=0x00, *extra_bytes):
|
||||
"""Send one VIA command (33 bytes: report-ID 0x00 + 32-byte payload).
|
||||
|
||||
VIA protocol v12 layout: [command_id, channel_id, value_id, value_data...]
|
||||
The report-ID byte (buf[0] = 0x00) is stripped by the HID layer before
|
||||
the firmware receives the remaining 32 bytes.
|
||||
"""
|
||||
buf = bytearray(REPORT_SIZE + 1)
|
||||
buf[0] = 0x00 # report ID
|
||||
buf[1] = command_id
|
||||
buf[2] = channel_id
|
||||
buf[3] = value_id
|
||||
for i, b in enumerate(extra_bytes):
|
||||
buf[4 + i] = b
|
||||
if debug > 1:
|
||||
print("HID write: {}".format(buf[: max(6, 4 + len(extra_bytes) + 1)].hex()))
|
||||
kbd.write(bytes(buf))
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def get_via_value(kbd, channel_id, value_id, response_bytes=1):
|
||||
"""Query one VIA value; returns a list of response_bytes ints, or None on error."""
|
||||
buf = bytearray(REPORT_SIZE + 1)
|
||||
buf[0] = 0x00
|
||||
buf[1] = VIA_CUSTOM_GET_VALUE
|
||||
buf[2] = channel_id
|
||||
buf[3] = value_id
|
||||
if debug > 1:
|
||||
print("HID get query: {}".format(buf[:6].hex()))
|
||||
kbd.write(bytes(buf))
|
||||
time.sleep(0.05)
|
||||
resp = kbd.read(REPORT_SIZE, timeout_ms=500)
|
||||
if debug > 1:
|
||||
print("HID get resp: {}".format(bytes(resp).hex() if resp else "(none)"))
|
||||
if resp and resp[0] != 0xFF:
|
||||
return list(resp[3 : 3 + response_bytes])
|
||||
return None
|
||||
|
||||
|
||||
# ── High-level lighting functions ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def lights_off(kbd):
|
||||
"""Set effect to 0 (RGB_MATRIX_NONE) and save — proper off, not just dim."""
|
||||
send_via_command(kbd, VIA_CUSTOM_SET_VALUE, RGB_MATRIX_CHANNEL, QMK_EFFECT, 0x00)
|
||||
send_via_command(kbd, VIA_CUSTOM_SAVE, RGB_MATRIX_CHANNEL)
|
||||
if debug > 0:
|
||||
print("Lights off")
|
||||
|
||||
|
||||
def apply_settings(
|
||||
kbd, *, effect=None, brightness=None, hue=None, saturation=None, speed=None
|
||||
):
|
||||
"""Send whichever settings are not None, then save to EEPROM.
|
||||
|
||||
If only one of hue/saturation is supplied, the other is read from the
|
||||
keyboard so the colour command always sends both bytes.
|
||||
"""
|
||||
if effect is not None:
|
||||
send_via_command(
|
||||
kbd, VIA_CUSTOM_SET_VALUE, RGB_MATRIX_CHANNEL, QMK_EFFECT, effect
|
||||
)
|
||||
if brightness is not None:
|
||||
send_via_command(
|
||||
kbd, VIA_CUSTOM_SET_VALUE, RGB_MATRIX_CHANNEL, QMK_BRIGHTNESS, brightness
|
||||
)
|
||||
if hue is not None or saturation is not None:
|
||||
if hue is None or saturation is None:
|
||||
current = get_via_value(
|
||||
kbd, RGB_MATRIX_CHANNEL, QMK_COLOR, response_bytes=2
|
||||
)
|
||||
if current:
|
||||
if hue is None:
|
||||
hue = current[0]
|
||||
if saturation is None:
|
||||
saturation = current[1]
|
||||
else:
|
||||
if hue is None:
|
||||
hue = 0
|
||||
if saturation is None:
|
||||
saturation = 0
|
||||
send_via_command(
|
||||
kbd, VIA_CUSTOM_SET_VALUE, RGB_MATRIX_CHANNEL, QMK_COLOR, hue, saturation
|
||||
)
|
||||
if speed is not None:
|
||||
send_via_command(
|
||||
kbd, VIA_CUSTOM_SET_VALUE, RGB_MATRIX_CHANNEL, QMK_SPEED, speed
|
||||
)
|
||||
send_via_command(kbd, VIA_CUSTOM_SAVE, RGB_MATRIX_CHANNEL)
|
||||
if debug > 0:
|
||||
applied = {
|
||||
k: v
|
||||
for k, v in dict(
|
||||
effect=effect,
|
||||
brightness=brightness,
|
||||
hue=hue,
|
||||
saturation=saturation,
|
||||
speed=speed,
|
||||
).items()
|
||||
if v is not None
|
||||
}
|
||||
print("Applied: {}".format(applied))
|
||||
|
||||
|
||||
def lights_on(kbd):
|
||||
"""Solid white preset (effect=1, brightness=255, saturation=0)."""
|
||||
apply_settings(
|
||||
kbd,
|
||||
effect=ON_EFFECT,
|
||||
brightness=ON_BRIGHTNESS,
|
||||
hue=ON_HUE,
|
||||
saturation=ON_SATURATION,
|
||||
speed=ON_SPEED,
|
||||
)
|
||||
|
||||
|
||||
def lights_per_key(kbd):
|
||||
"""Per-key RGB preset — per_key_rgb (effect 23, uses PERKEY_* constants)."""
|
||||
apply_settings(
|
||||
kbd,
|
||||
effect=PERKEY_EFFECT,
|
||||
brightness=PERKEY_BRIGHTNESS,
|
||||
hue=PERKEY_HUE,
|
||||
saturation=PERKEY_SATURATION,
|
||||
speed=PERKEY_SPEED,
|
||||
)
|
||||
|
||||
|
||||
def lights_programmed(kbd):
|
||||
"""Programmed preset — reactive_multiwide (effect 19, uses PROGRAMMED_* constants)."""
|
||||
apply_settings(
|
||||
kbd,
|
||||
effect=PROGRAMMED_EFFECT,
|
||||
brightness=PROGRAMMED_BRIGHTNESS,
|
||||
hue=PROGRAMMED_HUE,
|
||||
saturation=PROGRAMMED_SATURATION,
|
||||
speed=PROGRAMMED_SPEED,
|
||||
)
|
||||
|
||||
|
||||
def lights_query(kbd):
|
||||
"""Read and display current RGB matrix settings from the keyboard."""
|
||||
effect = get_via_value(kbd, RGB_MATRIX_CHANNEL, QMK_EFFECT)
|
||||
brightness = get_via_value(kbd, RGB_MATRIX_CHANNEL, QMK_BRIGHTNESS)
|
||||
speed = get_via_value(kbd, RGB_MATRIX_CHANNEL, QMK_SPEED)
|
||||
color = get_via_value(kbd, RGB_MATRIX_CHANNEL, QMK_COLOR, response_bytes=2)
|
||||
|
||||
if effect:
|
||||
eff_name = EFFECT_NAMES.get(effect[0], "(firmware-specific — see --help)")
|
||||
print("Effect: {} — {}".format(effect[0], eff_name))
|
||||
else:
|
||||
print("Effect: ?")
|
||||
print("Brightness: {}".format(brightness[0] if brightness else "?"))
|
||||
if color:
|
||||
sat_desc = (
|
||||
"white" if color[1] == 0 else "fully saturated" if color[1] == 255 else ""
|
||||
)
|
||||
sat_str = "{}{}".format(color[1], " ({})".format(sat_desc) if sat_desc else "")
|
||||
print("Hue: {}".format(color[0]))
|
||||
print("Saturation: {}".format(sat_str))
|
||||
else:
|
||||
print("Hue: ?")
|
||||
print("Saturation: ?")
|
||||
print("Speed: {}".format(speed[0] if speed else "?"))
|
||||
|
||||
|
||||
# ── Screensaver integration ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def screensaver_message_callback(session, message):
|
||||
global debug
|
||||
for screensaver in SCREENSAVER_INTERFACES:
|
||||
if message.get_interface() != screensaver:
|
||||
continue
|
||||
if debug > 2:
|
||||
pprint.pprint(message)
|
||||
if message.get_member() in ("ActiveChanged", "WakeUpScreen"):
|
||||
try:
|
||||
if debug > 1:
|
||||
print(
|
||||
"{} ActiveChanged/WakeUpScreen: args={}".format(
|
||||
screensaver, message.get_args_list()
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Wrap individually so a non-supporting screensaver doesn't
|
||||
# block one that does support GetActive.
|
||||
try:
|
||||
path = "/{}".format(screensaver.replace(".", "/"))
|
||||
obj = session.get_object(screensaver, path)
|
||||
iface = dbus.Interface(obj, screensaver)
|
||||
time.sleep(0.25)
|
||||
active = bool(iface.GetActive())
|
||||
if debug > 1:
|
||||
print("{} GetActive → {}".format(screensaver, active))
|
||||
if active:
|
||||
kbd = find_device()
|
||||
lights_off(kbd)
|
||||
kbd.close()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if message.get_member() == "WakeUpScreen":
|
||||
try:
|
||||
time.sleep(0.25)
|
||||
if debug > 1:
|
||||
print("{} WakeUpScreen → restore lights".format(screensaver))
|
||||
kbd = find_device()
|
||||
lights_on(kbd)
|
||||
kbd.close()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
# ── Argument parsing and main ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def byte_value(s):
|
||||
"""argparse type: accept decimal or 0x hex, validate 0–255."""
|
||||
n = int(s, 0)
|
||||
if not 0 <= n <= 255:
|
||||
raise argparse.ArgumentTypeError("value must be 0–255, got {}".format(n))
|
||||
return n
|
||||
|
||||
|
||||
def main():
|
||||
global debug
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Control RGB backlight on Keychron K3 Max (QMK/VIA protocol v12)",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=HELP_EPILOG,
|
||||
)
|
||||
|
||||
# ── Mode flags (mutually exclusive) ───────────────────────────────────────
|
||||
mode_group = parser.add_mutually_exclusive_group()
|
||||
mode_group.add_argument(
|
||||
"--on",
|
||||
"-1",
|
||||
dest="mode",
|
||||
action="store_const",
|
||||
const="on",
|
||||
help="Preset: solid white (effect=1, brightness=255, hue=0, saturation=0)",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--off",
|
||||
"-0",
|
||||
dest="mode",
|
||||
action="store_const",
|
||||
const="off",
|
||||
help="Turn backlight off (sets effect=0, saves to EEPROM)",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--per-key",
|
||||
"-k",
|
||||
dest="mode",
|
||||
action="store_const",
|
||||
const="per_key",
|
||||
help="Preset: per_key_rgb (effect 23, uses PERKEY_* constants)",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--program",
|
||||
"-p",
|
||||
dest="mode",
|
||||
action="store_const",
|
||||
const="program",
|
||||
help="Preset: reactive_multiwide (effect 19, uses PROGRAMMED_* constants)",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--query",
|
||||
"-q",
|
||||
dest="mode",
|
||||
action="store_const",
|
||||
const="query",
|
||||
help="Read and display current effect, brightness, hue, saturation and speed",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--screensaver",
|
||||
"-s",
|
||||
dest="mode",
|
||||
action="store_const",
|
||||
const="screensaver",
|
||||
help="Watch screensaver DBus signals: off on lock, solid white on wake",
|
||||
)
|
||||
|
||||
# ── Individual settings (can augment a mode or stand alone) ───────────────
|
||||
parser.add_argument(
|
||||
"--effect",
|
||||
"-e",
|
||||
type=byte_value,
|
||||
metavar="0-255",
|
||||
help=(
|
||||
"Set effect by ID. 0=off, 1=solid_color; higher IDs are "
|
||||
"firmware-specific (see effect table in --help)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--brightness",
|
||||
"-b",
|
||||
type=byte_value,
|
||||
metavar="0-255",
|
||||
help="LED brightness. 0=off, 128=half, 255=full.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hue",
|
||||
type=byte_value,
|
||||
metavar="0-255",
|
||||
help=(
|
||||
"Colour hue on the HSV wheel. "
|
||||
"0=red 43=yellow 85=green 128=cyan 170=blue 213=magenta. "
|
||||
"Has no visible effect when --saturation is 0 (white)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--saturation",
|
||||
"--sat",
|
||||
type=byte_value,
|
||||
metavar="0-255",
|
||||
help="Colour saturation. 0=white, 255=fully saturated colour.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speed",
|
||||
type=byte_value,
|
||||
metavar="0-255",
|
||||
help="Animation speed. 0=slowest, 128=mid, 255=fastest.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
"-v",
|
||||
action="count",
|
||||
default=0,
|
||||
help="Increase verbosity (repeat for more detail: -v, -vv, -vvv).",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
individual = dict(
|
||||
effect=args.effect,
|
||||
brightness=args.brightness,
|
||||
hue=args.hue,
|
||||
saturation=args.saturation,
|
||||
speed=args.speed,
|
||||
)
|
||||
has_individual = any(v is not None for v in individual.values())
|
||||
|
||||
if args.mode is None and not has_individual:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
debug = args.verbose
|
||||
|
||||
# ── Screensaver mode: long-running DBus loop ───────────────────────────────
|
||||
if args.mode == "screensaver":
|
||||
dbus_loop = dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||
session = dbus.SessionBus(mainloop=dbus_loop)
|
||||
for iface in SCREENSAVER_INTERFACES:
|
||||
session.add_match_string_non_blocking("interface='{}'".format(iface))
|
||||
session.add_message_filter(screensaver_message_callback)
|
||||
gi.repository.GLib.MainLoop().run()
|
||||
return
|
||||
|
||||
# ── All other modes: open device, act, close ───────────────────────────────
|
||||
kbd = find_device()
|
||||
|
||||
if args.mode == "query":
|
||||
lights_query(kbd)
|
||||
|
||||
elif args.mode == "off":
|
||||
lights_off(kbd)
|
||||
|
||||
else:
|
||||
# Start with profile defaults, then override with explicit args.
|
||||
settings = {}
|
||||
if args.mode == "on":
|
||||
settings = dict(
|
||||
effect=ON_EFFECT,
|
||||
brightness=ON_BRIGHTNESS,
|
||||
hue=ON_HUE,
|
||||
saturation=ON_SATURATION,
|
||||
speed=ON_SPEED,
|
||||
)
|
||||
elif args.mode == "per_key":
|
||||
settings = dict(
|
||||
effect=PERKEY_EFFECT,
|
||||
brightness=PERKEY_BRIGHTNESS,
|
||||
hue=PERKEY_HUE,
|
||||
saturation=PERKEY_SATURATION,
|
||||
speed=PERKEY_SPEED,
|
||||
)
|
||||
elif args.mode == "program":
|
||||
settings = dict(
|
||||
effect=PROGRAMMED_EFFECT,
|
||||
brightness=PROGRAMMED_BRIGHTNESS,
|
||||
hue=PROGRAMMED_HUE,
|
||||
saturation=PROGRAMMED_SATURATION,
|
||||
speed=PROGRAMMED_SPEED,
|
||||
)
|
||||
# Individual args override profile defaults.
|
||||
for key, val in individual.items():
|
||||
if val is not None:
|
||||
settings[key] = val
|
||||
apply_settings(kbd, **settings)
|
||||
|
||||
kbd.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user