forked from drath/libratbag
1975 lines
65 KiB
Python
Executable File
1975 lines
65 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright 2016 Red Hat, Inc.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
# copy of this software and associated documentation files (the "Software"),
|
|
# to deal in the Software without restriction, including without limitation
|
|
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
# and/or sell copies of the Software, and to permit persons to whom the
|
|
# Software is furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice (including the next
|
|
# paragraph) shall be included in all copies or substantial portions of the
|
|
# Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
# DEALINGS IN THE SOFTWARE.
|
|
|
|
import evdev
|
|
import subprocess
|
|
import sys
|
|
import argparse
|
|
import textwrap
|
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
|
|
# This must be on a single line, as we replace it using merge_ratbagd.py while building.
|
|
# fmt: off
|
|
from ratbagd import Ratbagd, RatbagdDevice, RatbagdProfile, RatbagdMacro, RatbagdResolution, RatbagdButton, RatbagdLed, RatbagdUnavailableError, RatbagCapabilityError, evcode_to_str # NOQA
|
|
# fmt: on
|
|
|
|
|
|
RATBAGD_API_VERSION = int("@RATBAGD_API_VERSION@")
|
|
|
|
|
|
def humanize(string: str) -> str:
|
|
return string.lower().replace("_", "-")
|
|
|
|
|
|
button_special_names = [
|
|
humanize(e.name) for e in RatbagdButton.ActionSpecial if e.name != "INVALID"
|
|
]
|
|
led_mode_names = [humanize(e.name) for e in RatbagdLed.Mode]
|
|
|
|
button_specials_strmap = {
|
|
**{e: e.name.lower().replace("_", "-") for e in RatbagdButton.ActionSpecial},
|
|
**{e.name.lower().replace("_", "-"): e for e in RatbagdButton.ActionSpecial},
|
|
}
|
|
|
|
|
|
def convert_str_to_evcode(s: str) -> int:
|
|
if not s.startswith("KEY_") and not s.startswith("BTN_"):
|
|
msg = f"Don't know how to convert {s}"
|
|
raise ValueError(msg)
|
|
|
|
try:
|
|
return evdev.ecodes.ecodes[s]
|
|
except KeyError as e:
|
|
msg = f"No such button or key `{s}`"
|
|
raise ValueError(msg) from e
|
|
|
|
|
|
def list_devices(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
if not ratbagd.devices:
|
|
print("No devices available.")
|
|
|
|
for device in ratbagd.devices:
|
|
print("{:20s} {:32s}".format(device.id + ":", device.name))
|
|
|
|
|
|
def find_device(ratbagd: Ratbagd, args: argparse.Namespace) -> RatbagdDevice:
|
|
device = ratbagd[args.device]
|
|
if device is None:
|
|
for device in ratbagd.devices:
|
|
if args.device in device.name:
|
|
return device
|
|
print(f"Unable to find device {args.device}")
|
|
sys.exit(1)
|
|
return device
|
|
|
|
|
|
def find_profile(
|
|
ratbagd: Ratbagd, args: argparse.Namespace
|
|
) -> Tuple[RatbagdProfile, RatbagdDevice]:
|
|
device = find_device(ratbagd, args)
|
|
try:
|
|
profile = device.profiles[args.profile_n]
|
|
except IndexError:
|
|
print(f"Invalid profile index {args.profile_n}")
|
|
sys.exit(1)
|
|
except AttributeError:
|
|
profile = device.active_profile
|
|
if ratbagd is None:
|
|
print("The device has no active profile")
|
|
sys.exit(1)
|
|
return profile, device
|
|
|
|
|
|
def find_resolution(
|
|
ratbagd: Ratbagd, args: argparse.Namespace
|
|
) -> Tuple[RatbagdResolution, RatbagdProfile, RatbagdDevice]:
|
|
profile, device = find_profile(ratbagd, args)
|
|
try:
|
|
resolution = profile.resolutions[args.resolution_n]
|
|
except IndexError:
|
|
print(f"Invalid resolution index {args.resolution_n}")
|
|
sys.exit(1)
|
|
except AttributeError:
|
|
resolution = profile.active_resolution
|
|
if resolution is None:
|
|
print("The device has no active resolution")
|
|
sys.exit(1)
|
|
return resolution, profile, device
|
|
|
|
|
|
def find_button(
|
|
ratbagd: Ratbagd, args: argparse.Namespace
|
|
) -> Tuple[RatbagdButton, RatbagdProfile, RatbagdDevice]:
|
|
profile, device = find_profile(ratbagd, args)
|
|
try:
|
|
button = profile.buttons[args.button_n]
|
|
except IndexError:
|
|
print(f"Invalid button index {args.button_n}")
|
|
sys.exit(1)
|
|
return button, profile, device
|
|
|
|
|
|
def find_led(
|
|
ratbagd: Ratbagd, args: argparse.Namespace
|
|
) -> Tuple[RatbagdLed, RatbagdProfile, RatbagdDevice]:
|
|
profile, device = find_profile(ratbagd, args)
|
|
try:
|
|
led = profile.leds[args.led_n]
|
|
except IndexError:
|
|
print(f"Invalid LED index {args.led_n}")
|
|
sys.exit(1)
|
|
return led, profile, device
|
|
|
|
|
|
def print_led(
|
|
_device: RatbagdDevice, _profile: RatbagdProfile, led: RatbagdLed, level: int
|
|
) -> None:
|
|
leds = {
|
|
RatbagdLed.Mode.BREATHING: "breathing",
|
|
RatbagdLed.Mode.CYCLE: "cycle",
|
|
RatbagdLed.Mode.OFF: "off",
|
|
RatbagdLed.Mode.ON: "on",
|
|
}
|
|
depths = {
|
|
RatbagdLed.ColorDepth.MONOCHROME: "monochrome",
|
|
RatbagdLed.ColorDepth.RGB_888: "rgb",
|
|
RatbagdLed.ColorDepth.RGB_111: "rgb111",
|
|
}
|
|
if led.mode == RatbagdLed.Mode.OFF:
|
|
print(
|
|
" " * level
|
|
+ f"LED: {led.index}, depth: {depths[led.colordepth]}, mode: {leds[led.mode]}"
|
|
)
|
|
elif led.mode == RatbagdLed.Mode.ON:
|
|
print(
|
|
" " * level
|
|
+ f"LED: {led.index}, depth: {depths[led.colordepth]}, mode: {leds[led.mode]}, color: {led.color[0]:02x}{led.color[1]:02x}{led.color[2]:02x}"
|
|
)
|
|
elif led.mode == RatbagdLed.Mode.CYCLE:
|
|
print(
|
|
" " * level
|
|
+ f"LED: {led.index}, depth: {depths[led.colordepth]}, mode: {leds[led.mode]}, duration: {led.effect_duration}, brightness: {led.brightness}"
|
|
)
|
|
elif led.mode == RatbagdLed.Mode.BREATHING:
|
|
print(
|
|
" " * level
|
|
+ f"LED: {led.index}, depth: {depths[led.colordepth]}, mode: {leds[led.mode]}, color: {led.color[0]:02x}{led.color[1]:02x}{led.color[2]:02x}, duration: {led.effect_duration},"
|
|
f" brightness: {led.brightness}"
|
|
)
|
|
|
|
|
|
def print_led_caps(
|
|
_device: RatbagdDevice, _profile: RatbagdProfile, led: RatbagdLed, level: int
|
|
) -> None:
|
|
leds = {
|
|
RatbagdLed.Mode.BREATHING: "breathing",
|
|
RatbagdLed.Mode.CYCLE: "cycle",
|
|
RatbagdLed.Mode.OFF: "off",
|
|
RatbagdLed.Mode.ON: "on",
|
|
}
|
|
supported = sorted([v for k, v in leds.items() if k in led.modes])
|
|
print(" " * level + "Modes: {}".format(", ".join(supported)))
|
|
|
|
|
|
def print_button(
|
|
_device: RatbagdDevice, _profile: RatbagdProfile, button: RatbagdButton, level: int
|
|
) -> None:
|
|
header = " " * level + f"Button: {button.index} is mapped to "
|
|
|
|
if button.action_type == RatbagdButton.ActionType.BUTTON:
|
|
print(f"{header}'button {button.mapping}'")
|
|
elif button.action_type == RatbagdButton.ActionType.SPECIAL:
|
|
print(f"{header}'{button_specials_strmap[button.special]}'")
|
|
elif button.action_type == RatbagdButton.ActionType.KEY:
|
|
key_name = evcode_to_str(button.key)
|
|
print(f"{header}key '{key_name}'")
|
|
elif button.action_type == RatbagdButton.ActionType.MACRO:
|
|
print(f"{header}macro '{str(button.macro)}'")
|
|
elif button.action_type == RatbagdButton.ActionType.NONE:
|
|
print(f"{header}none")
|
|
else:
|
|
print(f"{header}UNKNOWN")
|
|
|
|
|
|
def print_resolution(
|
|
_device: RatbagdDevice,
|
|
_profile: RatbagdProfile,
|
|
resolution: RatbagdResolution,
|
|
level: int,
|
|
) -> None:
|
|
if resolution.resolution == (0, 0):
|
|
print(" " * level + f"{resolution.index}: <disabled>")
|
|
return
|
|
if len(resolution.resolution) == 2:
|
|
dpi = f"{resolution.resolution[0]}x{resolution.resolution[1]}"
|
|
else:
|
|
dpi = f"{resolution.resolution[0]}"
|
|
|
|
print(
|
|
" " * level
|
|
+ "{}: {}dpi{}{}{}".format(
|
|
resolution.index,
|
|
dpi,
|
|
" (active)" if resolution.is_active else "",
|
|
" (default)" if resolution.is_default else "",
|
|
" (disabled)" if resolution.is_disabled else "",
|
|
)
|
|
)
|
|
|
|
|
|
def print_profile(device: RatbagdDevice, profile: RatbagdProfile, level: int) -> None:
|
|
print(
|
|
" " * (level - 2)
|
|
+ "Profile {}:{}{}{}".format(
|
|
profile.index,
|
|
" (disabled)" if profile.disabled else "",
|
|
" (active)" if profile.is_active else "",
|
|
" (dirty)" if profile.dirty else "",
|
|
)
|
|
)
|
|
|
|
if not profile.disabled:
|
|
print(" " * level + "Name: {}".format(profile.name or "n/a"))
|
|
if profile.report_rate:
|
|
print(" " * level + f"Report Rate: {profile.report_rate}Hz")
|
|
if profile.resolutions:
|
|
print(" " * level + "Resolutions:")
|
|
for resolution in profile.resolutions:
|
|
print_resolution(device, profile, resolution, level + 2)
|
|
if profile.angle_snapping != -1:
|
|
print(" " * level + f"Angle Snapping: {bool(profile.angle_snapping)}")
|
|
if profile.debounce != -1:
|
|
print(" " * level + f"Debounce time: {profile.debounce}ms")
|
|
if profile.buttons:
|
|
for button in profile.buttons:
|
|
print_button(device, profile, button, level)
|
|
if profile.leds:
|
|
for led in profile.leds:
|
|
print_led(device, profile, led, level)
|
|
|
|
|
|
def print_device(device: RatbagdDevice, level: int) -> None:
|
|
profile = device.profiles[0] # there should be always one
|
|
|
|
print(" " * level + f"{device.id} - {device.name}")
|
|
print(" " * level + f" Model: {device.model}")
|
|
print(" " * level + f" Device Type: {device.device_type.name.capitalize()}")
|
|
if device.firmware_version:
|
|
print(" " * level + f" Firmware version: {device.firmware_version}")
|
|
print(" " * level + f" Number of Buttons: {len(profile.buttons)}")
|
|
print(" " * level + f" Number of Leds: {len(profile.leds)}")
|
|
print(" " * level + f"Number of Profiles: {len(device.profiles)}")
|
|
for profile in device.profiles:
|
|
print_profile(device, profile, level + 2)
|
|
|
|
|
|
def show_device(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
device = find_device(ratbagd, args)
|
|
print_device(device, 0)
|
|
|
|
|
|
def show_profile(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
print(f"Profile {args.profile} on {device.id} ({device.name})")
|
|
print_profile(device, profile, 0)
|
|
|
|
|
|
def show_resolution(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, p, d = find_resolution(ratbagd, args)
|
|
print(
|
|
f"Resolution {args.resolution} on Profile {args.profile} on {d.id} ({d.name})"
|
|
)
|
|
print_resolution(d, p, resolution, 0)
|
|
caps = {
|
|
RatbagdResolution.CAP_INDIVIDUAL_REPORT_RATE: "individual-report-rate",
|
|
RatbagdResolution.CAP_SEPARATE_XY_RESOLUTION: "separate-xy-resolution",
|
|
}
|
|
capabilities = [caps[c] for c in resolution.capabilities]
|
|
print(" Capabilities: {}".format(", ".join(capabilities)))
|
|
|
|
|
|
def show_button(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
button, profile, device = find_button(ratbagd, args)
|
|
print(
|
|
f"Button {args.button} on Profile {args.profile} on {device.id} ({device.name})"
|
|
)
|
|
print_button(device, profile, button, 0)
|
|
|
|
|
|
def func_led_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
led, profile, device = find_led(ratbagd, args)
|
|
print_led(device, profile, led, 0)
|
|
|
|
|
|
def func_led_caps(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
led, profile, device = find_led(ratbagd, args)
|
|
print_led_caps(device, profile, led, 0)
|
|
|
|
|
|
def func_led_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
led, _profile, device = find_led(ratbagd, args)
|
|
try:
|
|
mode = args.mode
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
leds = {
|
|
"breathing": RatbagdLed.Mode.BREATHING,
|
|
"cycle": RatbagdLed.Mode.CYCLE,
|
|
"off": RatbagdLed.Mode.OFF,
|
|
"on": RatbagdLed.Mode.ON,
|
|
}
|
|
led.mode = leds[mode]
|
|
try:
|
|
color = args.color
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
led.color = color
|
|
try:
|
|
duration = args.duration
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
led.effect_duration = duration
|
|
try:
|
|
brightness = args.brightness
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
led.brightness = brightness
|
|
commit(device, args)
|
|
|
|
|
|
def func_led_get_all(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
for led in profile.leds:
|
|
print_led(device, profile, led, 0)
|
|
|
|
|
|
def func_button_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
button, profile, _device = find_button(ratbagd, args)
|
|
print_button(button, profile, button, 0)
|
|
|
|
|
|
def func_button_action_set_none(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
button, _profile, device = find_button(ratbagd, args)
|
|
if button.ActionType.NONE not in button.action_types:
|
|
raise RatbagCapabilityError("disabling buttons is not supported on this device")
|
|
|
|
button.disable()
|
|
commit(device, args)
|
|
|
|
|
|
def func_button_action_set_button(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
button, _profile, device = find_button(ratbagd, args)
|
|
button.mapping = args.target_button
|
|
commit(device, args)
|
|
|
|
|
|
def func_button_action_set_special(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
button, _profile, device = find_button(ratbagd, args)
|
|
try:
|
|
special = args.target_special
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
button.special = button_specials_strmap[special]
|
|
commit(device, args)
|
|
|
|
|
|
def func_button_action_set_key(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
button, _profile, device = find_button(ratbagd, args)
|
|
if button.ActionType.KEY not in button.action_types:
|
|
raise RatbagCapabilityError("assigning a key is not supported on this device")
|
|
|
|
try:
|
|
key = args.target_key
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
button.key = convert_str_to_evcode(key)
|
|
commit(device, args)
|
|
|
|
|
|
def func_button_action_set_macro(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
button, _profile, device = find_button(ratbagd, args)
|
|
if button.ActionType.MACRO not in button.action_types:
|
|
raise RatbagCapabilityError("assigning a macro is not supported on this device")
|
|
|
|
macro_keys = args.target_macro
|
|
macro = RatbagdMacro()
|
|
for s in macro_keys:
|
|
is_press = True
|
|
is_release = True
|
|
is_timeout = False
|
|
|
|
s = s.upper()
|
|
if s[0] == "T":
|
|
is_timeout = True
|
|
is_press = False
|
|
is_release = False
|
|
elif s[0] == "+":
|
|
is_release = False
|
|
s = s[1:]
|
|
elif s[0] == "-":
|
|
is_press = False
|
|
s = s[1:]
|
|
|
|
if is_timeout:
|
|
t = int(s[1:])
|
|
macro.append(RatbagdButton.Macro.WAIT, t)
|
|
else:
|
|
code = convert_str_to_evcode(s)
|
|
if is_press:
|
|
macro.append(RatbagdButton.Macro.KEY_PRESS, code)
|
|
if is_release:
|
|
macro.append(RatbagdButton.Macro.KEY_RELEASE, code)
|
|
|
|
button.macro = macro
|
|
commit(device, args)
|
|
|
|
|
|
def func_button_count(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
print(len(profile.buttons))
|
|
|
|
|
|
def func_dpi_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
ratbagd, _profile, _device = find_resolution(ratbagd, args)
|
|
if len(ratbagd.resolution) == 2:
|
|
print(f"{ratbagd.resolution[0]}x{ratbagd.resolution[1]}dpi")
|
|
else:
|
|
print(f"{ratbagd.resolution[0]}dpi")
|
|
|
|
|
|
def func_dpi_get_all(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, _profile, _device = find_resolution(ratbagd, args)
|
|
dpis = resolution.resolutions
|
|
print(" ".join([str(x) for x in dpis]))
|
|
|
|
|
|
def func_dpi_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, _profile, device = find_resolution(ratbagd, args)
|
|
dpi = args.dpi_n
|
|
if len(resolution.resolution) > len(dpi):
|
|
dpi = (dpi[0], dpi[0])
|
|
resolution.resolution = dpi
|
|
commit(device, args)
|
|
|
|
|
|
def func_report_rate_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
print(profile.report_rate)
|
|
|
|
|
|
def func_report_rate_get_all(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
rates = profile.report_rates
|
|
print(" ".join([str(x) for x in rates]))
|
|
|
|
|
|
def func_report_rate_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
profile.report_rate = args.rate_n
|
|
commit(device, args)
|
|
|
|
|
|
def func_angle_snapping_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
if profile.angle_snapping == -1:
|
|
return
|
|
print(bool(profile.angle_snapping))
|
|
|
|
|
|
def func_angle_snapping_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
profile.angle_snapping = 1 if args.angle_snapping_n else 0
|
|
commit(device, args)
|
|
|
|
|
|
def func_debounce_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
if profile.debounce == -1:
|
|
return
|
|
print(profile.debounce)
|
|
|
|
|
|
def func_debounce_get_all(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
values = profile.debounces
|
|
print(" ".join([str(x) for x in values]))
|
|
|
|
|
|
def func_debounce_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
profile.debounce = args.debounce_n
|
|
commit(device, args)
|
|
|
|
|
|
def func_resolution_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, profile, device = find_resolution(ratbagd, args)
|
|
print_resolution(device, profile, resolution, 0)
|
|
|
|
|
|
def func_resolution_active_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
print(profile.active_resolution.index)
|
|
|
|
|
|
def func_resolution_active_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, _profile, device = find_resolution(ratbagd, args)
|
|
resolution.set_active()
|
|
commit(device, args)
|
|
|
|
|
|
def func_resolution_default_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
resolution = None
|
|
for resolution in profile.resolutions:
|
|
if resolution.is_default:
|
|
break
|
|
|
|
if resolution is None:
|
|
print("The device has no default resolution")
|
|
sys.exit(1)
|
|
|
|
print(resolution.index)
|
|
|
|
|
|
def func_default_resolution_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
# FIXME: capabilities check?
|
|
resolution, _profile, device = find_resolution(ratbagd, args)
|
|
resolution.set_default()
|
|
commit(device, args)
|
|
|
|
|
|
def func_resolution_disabled_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, _profile, _device = find_resolution(ratbagd, args)
|
|
print(resolution.is_disabled)
|
|
|
|
|
|
def func_resolution_enabled_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, _profile, _device = find_resolution(ratbagd, args)
|
|
print(not resolution.is_disabled)
|
|
|
|
|
|
def func_resolution_disabled_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, _profile, device = find_resolution(ratbagd, args)
|
|
resolution.set_disabled(True)
|
|
commit(device, args)
|
|
|
|
|
|
def func_resolution_enabled_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
resolution, _profile, device = find_resolution(ratbagd, args)
|
|
resolution.set_disabled(False)
|
|
commit(device, args)
|
|
|
|
|
|
def func_profile_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
print_profile(device, profile, 0)
|
|
|
|
|
|
def func_profile_name_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, _device = find_profile(ratbagd, args)
|
|
# See https://github.com/libratbag/libratbag/issues/617
|
|
# ratbag converts to ascii, so this has no real effect there, but
|
|
# ratbag-command may still have a non-ascii string.
|
|
string = bytes(profile.name, "utf-8", "ignore")
|
|
print(string.decode("utf-8"))
|
|
|
|
|
|
def func_profile_name_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
if not profile.name:
|
|
raise RatbagCapabilityError(
|
|
"assigning a profile name is not supported on this profile"
|
|
)
|
|
profile.name = args.name
|
|
commit(device, args)
|
|
|
|
|
|
def func_profile_active_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
device = find_device(ratbagd, args)
|
|
profile = device.active_profile
|
|
if profile is None:
|
|
print("The device has no active profile")
|
|
sys.exit(1)
|
|
print(profile.index)
|
|
|
|
|
|
def func_profile_active_set(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
profile.set_active()
|
|
commit(device, args)
|
|
|
|
|
|
def func_profile_enable(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
profile.disabled = False
|
|
commit(device, args)
|
|
|
|
|
|
def func_profile_disable(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
profile, device = find_profile(ratbagd, args)
|
|
profile.disabled = True
|
|
commit(device, args)
|
|
|
|
|
|
def func_device_name_get(ratbagd: Ratbagd, args: argparse.Namespace) -> None:
|
|
device = find_device(ratbagd, args)
|
|
print(device.name)
|
|
|
|
|
|
################################################################################
|
|
# these are definitions to be reused in the dict that defines our language
|
|
|
|
# key elements
|
|
"""the type of the element (see 'types' below)"""
|
|
of_type = "type"
|
|
"""the name of the element, it'll be the one matching the args on the CLI"""
|
|
name = "name"
|
|
"""the group to logically associate commands while printing the help"""
|
|
group = "group"
|
|
"""list of positional arguments for the given command"""
|
|
pos_args = "pos_args"
|
|
"""a tag that we can refer latrer in an element of type 'link'"""
|
|
tag = "tag"
|
|
"""the element pointed to in an element of type 'link'"""
|
|
dest = "dest"
|
|
"""the function to associate to the switch or command"""
|
|
func = "func"
|
|
"""this is a particular command that is an integer, but not an terminating argument.
|
|
example:
|
|
profile active get
|
|
profile **2** button 3 get
|
|
- "profile" needs to be a switch
|
|
- "2" needs to be translated as a N_access, given it is a requirement to be able to call 'button'
|
|
"""
|
|
N_access = "N_access"
|
|
|
|
# argparse.add_argument parameters (forwarded as such)
|
|
"""'type' of the argument"""
|
|
arg_type = "arg_type"
|
|
"""'metavar' of the argument"""
|
|
metavar = "metavar"
|
|
"""'help' of the argument"""
|
|
help_str = "help"
|
|
"""'nargs' of the argument"""
|
|
nargs = "nargs"
|
|
"""'choices' of the argument"""
|
|
choices = "choices"
|
|
|
|
# types
|
|
"""an option to interpret as a command (example 'list', 'info')"""
|
|
command = "command"
|
|
"""an argument that is required for the given command arguments are leaf nodes
|
|
and can not have children
|
|
"""
|
|
argument = "argument"
|
|
"""provides a list of choice of commands for instance, a switch of [A, B] means
|
|
we can have A or B only when parsing the command line
|
|
"""
|
|
switch = "switch"
|
|
"""same as list, except we can loop inside the list for instance, a set of
|
|
[A, B] means we can have A and B (and A, ...) one after the other, no matter
|
|
the order
|
|
"""
|
|
set = "set"
|
|
"""a reference to any other element in the tree marked with a tag"""
|
|
link = "link"
|
|
|
|
################################################################################
|
|
|
|
|
|
def commit(device: RatbagdDevice, args: argparse.Namespace) -> None:
|
|
if args.nocommit:
|
|
return
|
|
device.commit()
|
|
|
|
|
|
def color(string: str) -> Tuple[int, int, int]:
|
|
try:
|
|
int_value = int(string, 16)
|
|
except ValueError as e:
|
|
msg = f"{string!r} is not a color in hex format"
|
|
raise ValueError(msg) from e
|
|
r = (int_value >> 16) & 0xFF
|
|
g = (int_value >> 8) & 0xFF
|
|
b = (int_value >> 0) & 0xFF
|
|
return (r, g, b)
|
|
|
|
|
|
def u8(string: str) -> int:
|
|
int_value = int(string)
|
|
msg = f"{string!r} is not a single byte"
|
|
if int_value < 0 or int_value > 255:
|
|
raise ValueError(msg)
|
|
return int_value
|
|
|
|
|
|
def dpi(string: str) -> Union[Tuple[int, int], Tuple[int,]]:
|
|
try:
|
|
int_value = int(string)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
return (int_value,)
|
|
if string.endswith("dpi"):
|
|
string = string[:-3]
|
|
x, y = string.split("x")
|
|
try:
|
|
int_x = int(x)
|
|
int_y = int(y)
|
|
except ValueError as e:
|
|
raise ValueError(f"{string!r} is not a valid dpi") from e
|
|
return (int_x, int_y)
|
|
|
|
|
|
RatbagParserSchemaElement = Dict[str, Any]
|
|
|
|
# TODO: make it easier to introspect this.
|
|
# note: 'hidrawX' is assumed before each command
|
|
parser_def: List[RatbagParserSchemaElement] = [
|
|
{
|
|
of_type: command,
|
|
name: "info",
|
|
help_str: "Show device information",
|
|
func: show_device,
|
|
group: "Device",
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "name",
|
|
help_str: "Returns the device name",
|
|
func: func_device_name_get,
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "profile",
|
|
help_str: "Access profile information",
|
|
tag: "profile",
|
|
group: "Profile",
|
|
switch: [
|
|
{
|
|
of_type: switch,
|
|
name: "active",
|
|
help_str: "access active profile information",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current active profile",
|
|
func: func_profile_active_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set current active profile",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "profile_n",
|
|
metavar: "N",
|
|
help_str: "The profile to set as current",
|
|
arg_type: int,
|
|
},
|
|
],
|
|
func: func_profile_active_set,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
N_access: {
|
|
of_type: N_access,
|
|
name: "profile_n",
|
|
metavar: "N",
|
|
help_str: "The profile to act on",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show selected profile information",
|
|
func: func_profile_get,
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "name",
|
|
help_str: "access profile name information",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show the name of the profile",
|
|
func: func_profile_name_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set the name of the profile",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "name",
|
|
metavar: "blah",
|
|
help_str: "The name to set",
|
|
},
|
|
],
|
|
func: func_profile_name_set,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "enable",
|
|
help_str: "Enable a profile",
|
|
func: func_profile_enable,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "disable",
|
|
help_str: "Disable a profile",
|
|
func: func_profile_disable,
|
|
},
|
|
{
|
|
of_type: link,
|
|
dest: "resolution",
|
|
},
|
|
{
|
|
of_type: link,
|
|
dest: "dpi",
|
|
},
|
|
{
|
|
of_type: link,
|
|
dest: "rate",
|
|
},
|
|
{
|
|
of_type: link,
|
|
dest: "angle_snapping",
|
|
},
|
|
{
|
|
of_type: link,
|
|
dest: "debounce",
|
|
},
|
|
{
|
|
of_type: link,
|
|
dest: "button",
|
|
},
|
|
{
|
|
of_type: link,
|
|
dest: "led",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "resolution",
|
|
help_str: """Access resolution information
|
|
|
|
Resolution commands work on the given profile, or on the
|
|
active profile if none is given.""",
|
|
tag: "resolution",
|
|
group: "Resolution",
|
|
switch: [
|
|
{
|
|
of_type: switch,
|
|
name: "active",
|
|
help_str: "access active resolution information",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current active resolution",
|
|
func: func_resolution_active_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set current active resolution",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "resolution_n",
|
|
metavar: "N",
|
|
help_str: "The resolution to set as current",
|
|
arg_type: int,
|
|
},
|
|
],
|
|
func: func_resolution_active_set,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "default",
|
|
help_str: "access default resolution information",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current default resolution",
|
|
func: func_resolution_default_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set current default resolution",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "resolution_n",
|
|
metavar: "N",
|
|
help_str: "The resolution to set as default",
|
|
arg_type: int,
|
|
},
|
|
],
|
|
func: func_default_resolution_set,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
N_access: {
|
|
of_type: N_access,
|
|
name: "resolution_n",
|
|
metavar: "N",
|
|
help_str: "The resolution to act on",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show selected resolution",
|
|
func: func_resolution_get,
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "disabled",
|
|
help_str: "access disabled resolution information",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show if resolution is disabled",
|
|
func: func_resolution_disabled_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set resolution as disabled",
|
|
func: func_resolution_disabled_set,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "enabled",
|
|
help_str: "access enabled resolution information",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show if resolution is enabled",
|
|
func: func_resolution_enabled_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set resolution as enabled",
|
|
func: func_resolution_enabled_set,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: link,
|
|
dest: "dpi",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "dpi",
|
|
help_str: """Access DPI information
|
|
|
|
DPI commands work on the given profile and resolution, or on the
|
|
active resolution of the active profile if none are given.""",
|
|
tag: "dpi",
|
|
group: "DPI",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current DPI value",
|
|
func: func_dpi_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "get-all",
|
|
help_str: "Show all available DPIs",
|
|
func: func_dpi_get_all,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set the DPI value to N",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "dpi_n",
|
|
metavar: "N",
|
|
help_str: "The resolution to set as current",
|
|
arg_type: dpi,
|
|
},
|
|
],
|
|
func: func_dpi_set,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "rate",
|
|
help_str: """Access report rate information
|
|
|
|
Rate commands work on the given profile, or on the active profile if none is given.""",
|
|
tag: "rate",
|
|
group: "Rate",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current report rate",
|
|
func: func_report_rate_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "get-all",
|
|
help_str: "Show all available report rates",
|
|
func: func_report_rate_get_all,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set the report rate to N",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "rate_n",
|
|
metavar: "N",
|
|
help_str: "The report rate to set as current",
|
|
arg_type: int,
|
|
},
|
|
],
|
|
func: func_report_rate_set,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "angle_snapping",
|
|
help_str: """Angle snapping information
|
|
|
|
Angle snapping commands work on the given profile, or on the active profile if none is given.""",
|
|
tag: "angle_snapping",
|
|
group: "AngleSnapping",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current angle snapping",
|
|
func: func_angle_snapping_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set the current angle snapping to N",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "angle_snapping_n",
|
|
metavar: "N",
|
|
help_str: "The angle snapping to set as current",
|
|
arg_type: int,
|
|
},
|
|
],
|
|
func: func_angle_snapping_set,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "debounce",
|
|
help_str: """Access debounce time information
|
|
|
|
Debounce commands work on the given profile, or on the active profile if none is given.""",
|
|
tag: "debounce",
|
|
group: "Debounce",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current debounce time",
|
|
func: func_debounce_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "get-all",
|
|
help_str: "Show all available debounce times",
|
|
func: func_debounce_get_all,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "set",
|
|
help_str: "Set the current debounce time to N",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "debounce_n",
|
|
metavar: "N",
|
|
help_str: "The debounce time to set as current",
|
|
arg_type: int,
|
|
},
|
|
],
|
|
func: func_debounce_set,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "button",
|
|
help_str: """Access Button information
|
|
|
|
Button commands work on the given profile, or on the
|
|
active profile if none is given.""",
|
|
tag: "button",
|
|
group: "Button",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "count",
|
|
help_str: "Print the number of buttons",
|
|
func: func_button_count,
|
|
},
|
|
],
|
|
N_access: {
|
|
of_type: N_access,
|
|
name: "button_n",
|
|
metavar: "N",
|
|
help_str: "The button to act on",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show selected button",
|
|
func: func_button_get,
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "action",
|
|
help_str: "Act on the selected button",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Print the button action",
|
|
func: func_button_get,
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "set",
|
|
help_str: "Set an action on the selected button",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "button",
|
|
help_str: "Set the button action to button B",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "target_button",
|
|
metavar: "B",
|
|
help_str: "The new button value to assign",
|
|
arg_type: int,
|
|
},
|
|
],
|
|
func: func_button_action_set_button,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "special",
|
|
help_str: (
|
|
"Set the button action to special action S"
|
|
),
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "target_special",
|
|
metavar: "S",
|
|
help_str: "The new special value to assign",
|
|
choices: button_special_names,
|
|
},
|
|
],
|
|
func: func_button_action_set_special,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "key",
|
|
help_str: "Set the button action to key",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "target_key",
|
|
metavar: "S",
|
|
help_str: "The new key value to assign",
|
|
},
|
|
],
|
|
func: func_button_action_set_key,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "macro",
|
|
help_str: """Set the button action to the given macro
|
|
|
|
Macro syntax:
|
|
A macro is a series of key events or waiting periods.
|
|
Keys must be specified in linux/input-event-codes.h key names.
|
|
KEY_A Press and release 'a'
|
|
+KEY_A Press 'a'
|
|
-KEY_A Release 'a'
|
|
t300 Wait 300ms""",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "target_macro",
|
|
metavar: "...",
|
|
help_str: "The new macro to assign",
|
|
nargs: argparse.REMAINDER,
|
|
},
|
|
],
|
|
func: func_button_action_set_macro,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "disabled",
|
|
help_str: "Disable this button",
|
|
pos_args: [],
|
|
func: func_button_action_set_none,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
of_type: switch,
|
|
name: "led",
|
|
help_str: """Access LED information
|
|
|
|
LED commands work on the given profile, or on the
|
|
active profile if none is given.""",
|
|
tag: "led",
|
|
group: "LED",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current LED value",
|
|
func: func_led_get_all,
|
|
},
|
|
],
|
|
N_access: {
|
|
of_type: N_access,
|
|
name: "led_n",
|
|
metavar: "N",
|
|
help_str: "The LED to act on",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "get",
|
|
help_str: "Show current LED value",
|
|
func: func_led_get,
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "capabilities",
|
|
help_str: "Show LED capabilities",
|
|
func: func_led_caps,
|
|
},
|
|
{
|
|
of_type: set,
|
|
name: "set",
|
|
help_str: "Act on the selected LED",
|
|
switch: [
|
|
{
|
|
of_type: command,
|
|
name: "mode",
|
|
help_str: "The mode to set as current",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "mode",
|
|
metavar: "mode",
|
|
help_str: "The mode to set as current",
|
|
choices: led_mode_names,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "color",
|
|
help_str: "The color to set as current",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "color",
|
|
metavar: "RRGGBB",
|
|
help_str: (
|
|
"The color in hex format to set as current"
|
|
),
|
|
arg_type: color,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "duration",
|
|
help_str: "The duration to set as current",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "duration",
|
|
metavar: "R",
|
|
help_str: "The duration in ms to set as current",
|
|
arg_type: int,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
of_type: command,
|
|
name: "brightness",
|
|
help_str: "The brightness to set as current",
|
|
pos_args: [
|
|
{
|
|
of_type: argument,
|
|
name: "brightness",
|
|
metavar: "B",
|
|
help_str: "The brightness to set as current",
|
|
arg_type: u8,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
func: func_led_set,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]
|
|
|
|
|
|
class ParseError(Exception):
|
|
pass
|
|
|
|
|
|
RatbagParserFunction = Callable[[Ratbagd, argparse.Namespace], None]
|
|
|
|
|
|
class RatbagParser:
|
|
tagged: Dict[str, "RatbagParser"] = {}
|
|
|
|
def __init__(
|
|
self,
|
|
type: str,
|
|
name: str,
|
|
group: Optional[str] = None,
|
|
tag: Optional[str] = None,
|
|
func: Optional[RatbagParserFunction] = None,
|
|
help: Optional[str] = None,
|
|
) -> None:
|
|
self.type = type
|
|
self.name = name
|
|
self.tag = tag
|
|
self.group = group
|
|
if tag is not None:
|
|
RatbagParser.tagged[tag] = self
|
|
self.func = func
|
|
self.help = help
|
|
|
|
def repr_args(self) -> str:
|
|
return f"name='{self.name}', tag='{self.tag}', func='{self.func}', help='{self.help}'"
|
|
|
|
def __repr__(self) -> str:
|
|
return f"{type(self)}({self.repr_args()})"
|
|
|
|
def store_function(self, parser: argparse.ArgumentParser) -> None:
|
|
if self.func is not None:
|
|
parser.set_defaults(func=self.func)
|
|
|
|
def _add_to_subparsers(self, parent: Any) -> None:
|
|
raise ParseError(f"please implement _add_to_subparsers on {type(self)}")
|
|
|
|
def add_to_subparsers(self, parent: Any) -> None:
|
|
self._add_to_subparsers(parent)
|
|
|
|
def _sub_parse(
|
|
self, input_string: List[str], ns: argparse.Namespace
|
|
) -> argparse.ArgumentParser:
|
|
raise ParseError(f"please implement _sub_parse on {type(self)}")
|
|
|
|
def sub_parse(
|
|
self, input_string: List[str], ns: argparse.Namespace
|
|
) -> argparse.ArgumentParser:
|
|
r = self._sub_parse(input_string, ns)
|
|
return r # noqa: RET504
|
|
|
|
def print_help(self, group: Optional[str], prefix: str = "") -> None:
|
|
if self.group is not None:
|
|
print(f"\n{self.group} Commands:")
|
|
self._print_help(prefix)
|
|
|
|
def _print_help(self, prefix: str) -> None:
|
|
raise ParseError(f"please implement _print_help on {type(self)}")
|
|
|
|
|
|
class RatbagParserSwitch(RatbagParser):
|
|
def __init__(
|
|
self,
|
|
type: str,
|
|
name: str,
|
|
group: Optional[str] = None,
|
|
switch: Optional[List[RatbagParserSchemaElement]] = None,
|
|
N_access: Optional[RatbagParserSchemaElement] = None,
|
|
tag: Optional[str] = None,
|
|
func: Optional[RatbagParserFunction] = None,
|
|
help: Optional[str] = None,
|
|
) -> None:
|
|
super().__init__(type, name, group, tag, func, help)
|
|
self.switch: List[RatbagParser] = (
|
|
[classes[obj[of_type]](**obj) for obj in switch]
|
|
if switch is not None
|
|
else []
|
|
)
|
|
self.N_access = (
|
|
RatbagParserNAccess(**N_access) if N_access is not None else None
|
|
)
|
|
|
|
def repr_args(self) -> str:
|
|
return f"""switch='{[repr(o) for o in self.switch]}', N_access='{self.N_access}', {RatbagParser.repr_args(self)}"""
|
|
|
|
def _add_to_subparsers(self, parent: Any) -> None:
|
|
parser = parent.add_parser(self.name, help=self.help)
|
|
parser.set_defaults(subparse=self.sub_parse)
|
|
|
|
def _sub_parse(
|
|
self, input_string: List[str], ns: argparse.Namespace
|
|
) -> argparse.ArgumentParser:
|
|
if input_string and self.N_access is not None:
|
|
# retrieve first numbered element if any
|
|
try:
|
|
int(input_string[0])
|
|
except ValueError:
|
|
# there are arguments, but they look like commands
|
|
pass
|
|
else:
|
|
# we have a single int as first argument, switch to the
|
|
# N_access subtree of the command
|
|
return self.N_access.parse_parent(self, input_string, ns)
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog=f"{sys.argv[0]} <device> {self.name}",
|
|
description=self.help,
|
|
add_help=False,
|
|
)
|
|
|
|
# create a new subparser to handle all commands
|
|
subs = parser.add_subparsers(title="COMMANDS", help=None)
|
|
for e in self.switch:
|
|
e.add_to_subparsers(subs)
|
|
|
|
return parser
|
|
|
|
def _print_help(self, prefix: str) -> None:
|
|
if self.help and self.group is not None:
|
|
print(textwrap.indent(self.help, " "), "\n")
|
|
for e in self.switch:
|
|
e.print_help(None, f"{prefix}{self.name} ")
|
|
if self.N_access is not None:
|
|
self.N_access.print_help(None, self.name + " ")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"switch({self.repr_args()})"
|
|
|
|
|
|
class RatbagParserNAccess(RatbagParserSwitch):
|
|
def __init__(
|
|
self,
|
|
type: str,
|
|
name: str,
|
|
group: Optional[str] = None,
|
|
switch: Optional[List[RatbagParserSchemaElement]] = None,
|
|
metavar: Optional[str] = None,
|
|
tag: Optional[str] = None,
|
|
func: Optional[RatbagParserFunction] = None,
|
|
help: Optional[str] = None,
|
|
) -> None:
|
|
super().__init__(type, name, group, switch, None, tag, func, help)
|
|
self.metavar = metavar
|
|
|
|
def parse_parent(
|
|
self, parent: Any, input_string: List[str], ns: argparse.Namespace
|
|
) -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog=f"{sys.argv[0]} <device> {parent.name}", add_help=False
|
|
)
|
|
parser.add_argument(self.name, help=self.help, type=int)
|
|
# create a new subparser to handle all commands
|
|
subs = parser.add_subparsers(title="COMMANDS", help=None)
|
|
for e in self.switch:
|
|
e.add_to_subparsers(subs)
|
|
return parser
|
|
|
|
def repr_args(self) -> str:
|
|
return f"""switch='{[repr(o) for o in self.switch]}', metavar = '{self.metavar}', {RatbagParser.repr_args(self)}"""
|
|
|
|
def __repr__(self) -> str:
|
|
return f"N_Access({self.repr_args()})"
|
|
|
|
def _print_help(self, prefix: str) -> None:
|
|
for e in self.switch:
|
|
e.print_help(None, f"{prefix}N ")
|
|
|
|
|
|
class RatbagParserSet(RatbagParserSwitch):
|
|
def __init__(
|
|
self,
|
|
type: str,
|
|
name: str,
|
|
group: Optional[str] = None,
|
|
switch: Optional[List[RatbagParserSchemaElement]] = None,
|
|
N_access: Optional[RatbagParserSchemaElement] = None,
|
|
tag: Optional[str] = None,
|
|
func: Optional[RatbagParserFunction] = None,
|
|
help: Optional[str] = None,
|
|
) -> None:
|
|
super().__init__(type, name, group, switch, N_access, tag, func, help)
|
|
|
|
def _add_to_subparsers(self, parent: Any) -> None:
|
|
parser = parent.add_parser(self.name, help=self.help)
|
|
parser.set_defaults(subparse=self.sub_parse)
|
|
|
|
def _sub_parse(
|
|
self, input_string: List[str], ns: argparse.Namespace
|
|
) -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog=f"{sys.argv[0]} <device> {self.name}", add_help=False
|
|
)
|
|
# create a new subparser to handle all commands
|
|
subs = parser.add_subparsers(title="COMMANDS", help=None)
|
|
for e in self.switch:
|
|
e.add_to_subparsers(subs)
|
|
if len(input_string) == 2:
|
|
self.store_function(parser)
|
|
else:
|
|
parser.set_defaults(subparse=self.sub_parse)
|
|
return parser
|
|
|
|
def __repr__(self) -> str:
|
|
return f"set({self.repr_args()})"
|
|
|
|
def _print_help(self, prefix: str) -> None:
|
|
command = prefix + "{COMMAND} ..."
|
|
print(" {:<36}{}".format(command, self.help if self.help else ""))
|
|
for e in self.switch:
|
|
e.print_help(None, " " * len(prefix))
|
|
|
|
|
|
class RatbagParserCommand(RatbagParser):
|
|
def __init__(
|
|
self,
|
|
type: str,
|
|
name: str,
|
|
group: Optional[str] = None,
|
|
pos_args: Optional[List[RatbagParserSchemaElement]] = None,
|
|
tag: Optional[str] = None,
|
|
func: Optional[RatbagParserFunction] = None,
|
|
help: Optional[str] = None,
|
|
) -> None:
|
|
super().__init__(type, name, group, tag, func, help)
|
|
self.pos_args: List[RatbagParserArgument] = (
|
|
[classes[obj[of_type]](**obj) for obj in pos_args]
|
|
if pos_args is not None
|
|
else []
|
|
)
|
|
|
|
def _add_to_subparsers(self, parent: Any) -> None:
|
|
parser = parent.add_parser(self.name, help=self.help)
|
|
for a in self.pos_args:
|
|
a.add_to_subparsers(parser)
|
|
self.store_function(parser)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"command({self.repr_args()})"
|
|
|
|
def _print_help(self, prefix: str) -> None:
|
|
command = prefix + self.name
|
|
for a in self.pos_args:
|
|
if a.choices is None or len(a.choices) > 5:
|
|
command += f" {a.metavar}"
|
|
else:
|
|
command += " [{}]".format("|".join(a.choices))
|
|
print(" {:<36}{}".format(command, self.help if self.help else ""))
|
|
|
|
|
|
class RatbagParserArgument(RatbagParser):
|
|
def __init__(
|
|
self,
|
|
type: str,
|
|
name: str,
|
|
group: Optional[str] = None,
|
|
arg_type: Optional[object] = None,
|
|
metavar: Optional[str] = None,
|
|
nargs: Optional[int] = None,
|
|
choices: Optional[List] = None,
|
|
tag: Optional[str] = None,
|
|
func: Optional[RatbagParserFunction] = None,
|
|
help: Optional[str] = None,
|
|
) -> None:
|
|
super().__init__(type, name, group, tag, func, help)
|
|
self.arg_type = arg_type
|
|
self.metavar = metavar
|
|
self.nargs = nargs
|
|
self.choices = choices
|
|
|
|
def _add_to_subparsers(self, parent: Any) -> None:
|
|
parent.add_argument(
|
|
self.name,
|
|
metavar=self.metavar,
|
|
help=self.help,
|
|
type=self.arg_type,
|
|
nargs=self.nargs,
|
|
choices=self.choices,
|
|
)
|
|
|
|
|
|
class RatbagParserLink(RatbagParser):
|
|
def __init__(
|
|
self,
|
|
type: str,
|
|
dest: str,
|
|
group: Optional[str] = None,
|
|
tag: Optional[str] = None,
|
|
func: Optional[RatbagParserFunction] = None,
|
|
help: Optional[str] = None,
|
|
) -> None:
|
|
super().__init__(type, dest, group, tag, func, help)
|
|
self.dest = dest
|
|
|
|
def get_dest(self) -> RatbagParser:
|
|
try:
|
|
dest = RatbagParser.tagged[self.dest]
|
|
except KeyError as e:
|
|
raise ParseError(f"link '{self.dest}' points to nothing") from e
|
|
return dest
|
|
|
|
def _add_to_subparsers(self, parent: Any) -> None:
|
|
dest = self.get_dest()
|
|
dest.add_to_subparsers(parent)
|
|
|
|
def _print_help(self, prefix: str) -> None:
|
|
dest = self.get_dest()
|
|
print(
|
|
" {:<36}Use {}for '{} Commands'".format(
|
|
prefix + self.dest + " ...", prefix, dest.group
|
|
)
|
|
)
|
|
|
|
|
|
classes = {
|
|
switch: RatbagParserSwitch,
|
|
set: RatbagParserSet,
|
|
command: RatbagParserCommand,
|
|
argument: RatbagParserArgument,
|
|
link: RatbagParserLink,
|
|
N_access: RatbagParserNAccess,
|
|
}
|
|
|
|
|
|
class RatbagParserRoot:
|
|
def __init__(self, commands: List[RatbagParserSchemaElement]) -> None:
|
|
self.children: List[RatbagParser] = [
|
|
classes[def_parser[of_type]](**def_parser) for def_parser in commands
|
|
]
|
|
self.want_keepalive = False
|
|
|
|
def parse(self, input_string: List[str]) -> argparse.Namespace:
|
|
self.parser = argparse.ArgumentParser(
|
|
description="Inspect and modify a configurable device", add_help=False
|
|
)
|
|
self.parser.add_argument(
|
|
"-V", "--version", action="version", version="@version@"
|
|
)
|
|
self.parser.add_argument("--verbose", "-v", action="count", default=0)
|
|
self.parser.add_argument("--help", "-h", action="store_true", default=False)
|
|
self.parser.add_argument("--nocommit", action="store_true", default=False)
|
|
if self.want_keepalive:
|
|
self.parser.add_argument("--keepalive", action="store_true", default=False)
|
|
|
|
# retrieve the global options now and remove them from the processing
|
|
ns, rest = self.parser.parse_known_args(input_string)
|
|
|
|
if ns.help:
|
|
return ns
|
|
|
|
# retrieve the device and remove it from the command processing
|
|
self.parser.add_argument("device_or_list", action="store")
|
|
ns, rest = self.parser.parse_known_args(rest, namespace=ns)
|
|
|
|
if ns.device_or_list == "list":
|
|
if rest:
|
|
self.parser.error("extra arguments: '{}'".format(" ".join(rest)))
|
|
ns.func = list_devices
|
|
return ns
|
|
|
|
ns.device = ns.device_or_list
|
|
|
|
# we need a new parser or 'device_or_list' will eat all of our commands
|
|
command_parser = argparse.ArgumentParser(
|
|
description="command parser",
|
|
prog=f"{sys.argv[0]} <device>",
|
|
add_help=False,
|
|
)
|
|
|
|
subs = command_parser.add_subparsers(title="COMMANDS")
|
|
|
|
subparser = command_parser
|
|
|
|
for child in self.children:
|
|
child.add_to_subparsers(subs)
|
|
|
|
ns.subparse = None
|
|
|
|
while rest and subparser:
|
|
old_rest = rest
|
|
ns, rest = subparser.parse_known_args(rest, namespace=ns)
|
|
if hasattr(ns, func):
|
|
break
|
|
if old_rest == rest:
|
|
break
|
|
if ns.subparse:
|
|
subparser = ns.subparse(rest, ns)
|
|
|
|
if rest:
|
|
self.parser.error("extra arguments: '{}'".format(" ".join(rest)))
|
|
|
|
return ns
|
|
|
|
def print_help(self) -> None:
|
|
print(f"usage: {self.parser.prog} [OPTIONS] list")
|
|
print(f" {self.parser.prog} [OPTIONS] <device> {{COMMAND}} ...\n")
|
|
print(self.parser.description)
|
|
print(
|
|
"""
|
|
Common options:
|
|
--version -V show program's version number and exit
|
|
--verbose, -v increase verbosity level
|
|
--nocommit Do not immediately write the settings to the mouse
|
|
--help, -h show this help and exit"""
|
|
)
|
|
if self.want_keepalive:
|
|
print(
|
|
" --keepalive do not terminate ratbagd after the"
|
|
" processing"
|
|
)
|
|
print(
|
|
"""
|
|
General Commands:
|
|
list List supported devices (does not take a device argument)"""
|
|
)
|
|
for c in self.children:
|
|
c.print_help(None)
|
|
print(
|
|
"""
|
|
Examples:
|
|
{0} profile active get
|
|
{0} profile 0 resolution active set 4
|
|
{0} profile 0 resolution 1 dpi get
|
|
{0} resolution 4 rate get
|
|
{0} dpi set 800
|
|
{0} profile 0 led 0 set mode on
|
|
{0} profile 0 led 0 set color ff00ff
|
|
{0} profile 0 led 0 set duration 50
|
|
|
|
Exit codes:
|
|
0 Success
|
|
1 Unsupported feature, index out of available range or invalid device
|
|
2 Commandline arguments are invalid
|
|
3 A command failed on the device
|
|
""".format( # noqa: UP032
|
|
self.parser.prog
|
|
)
|
|
)
|
|
|
|
|
|
def get_parser() -> RatbagParserRoot:
|
|
return RatbagParserRoot(parser_def)
|
|
|
|
|
|
def on_device_added(_ratbagd: Ratbagd, device: RatbagdDevice) -> None:
|
|
device_names = [
|
|
"mara",
|
|
"capybara",
|
|
"porcupine",
|
|
"paca",
|
|
"vole",
|
|
"woodrat",
|
|
"gerbil",
|
|
"shrew",
|
|
"hutia",
|
|
"beaver",
|
|
"squirrel",
|
|
"chinchilla",
|
|
"rabbit",
|
|
"viscacha",
|
|
"hare",
|
|
"degu",
|
|
"gundi",
|
|
"acouchy",
|
|
"nutria",
|
|
"paca",
|
|
"hamster",
|
|
"zokor",
|
|
"chipmunk",
|
|
"gopher",
|
|
"marmot",
|
|
"groundhog",
|
|
"suslik",
|
|
"agouti",
|
|
"blesmol",
|
|
]
|
|
|
|
device_attr = [
|
|
"sobbing",
|
|
"whooping",
|
|
"barking",
|
|
"yapping",
|
|
"howling",
|
|
"squawking",
|
|
"cheering",
|
|
"warbling",
|
|
"thundering",
|
|
"booming",
|
|
"blustering",
|
|
"humming",
|
|
"crying",
|
|
"bawling",
|
|
"roaring",
|
|
"raging",
|
|
"chanting",
|
|
"crooning",
|
|
"murmuring",
|
|
"bellowing",
|
|
"wailing",
|
|
"weeping",
|
|
"screaming",
|
|
"yelling",
|
|
"yodeling",
|
|
"singing",
|
|
"honking",
|
|
"hooting",
|
|
"whispering",
|
|
"hollering",
|
|
]
|
|
|
|
# Let's convert the sha into something not boring. This takes the first
|
|
# 4 characters, creates two different indices from it to generate a
|
|
# name. The rest is hope hope that never get a collision here but it's
|
|
# unlikely enough.
|
|
name = device_names[int(device.id[0:2], 16) % len(device_names)]
|
|
attr = device_attr[int(device.id[2:4], 16) % len(device_attr)]
|
|
device.id = "-".join([attr, name])
|
|
|
|
|
|
def open_ratbagd(
|
|
ratbagd_process: Optional[subprocess.Popen] = None, verbose: int = 0
|
|
) -> Optional[Ratbagd]:
|
|
try:
|
|
ratbagd = Ratbagd(RATBAGD_API_VERSION)
|
|
ratbagd.verbose = verbose
|
|
try:
|
|
ratbagd.connect("device-added", on_device_added)
|
|
for device in ratbagd.devices:
|
|
on_device_added(ratbagd, device)
|
|
except AttributeError:
|
|
pass # the ratbag-command case
|
|
|
|
except RatbagdUnavailableError as e:
|
|
print(f"Unable to connect to ratbagd: {e}")
|
|
return None
|
|
|
|
if ratbagd_process is not None:
|
|
# if some old version of ratbagd is still running, ratbagd_process may
|
|
# have never started but our DBus bindings may succeed. Check for the
|
|
# return code here, this also gives ratbagd enough time to start and
|
|
# die. If we check immediately we may not have terminated yet.
|
|
ratbagd_process.poll()
|
|
assert ratbagd_process.returncode is None
|
|
|
|
return ratbagd
|
|
|
|
|
|
def main(argv: List[str]) -> int:
|
|
if not argv:
|
|
argv = ["list"]
|
|
|
|
parser = get_parser()
|
|
cmd = parser.parse(argv)
|
|
if cmd.help:
|
|
parser.print_help()
|
|
return 0
|
|
|
|
_r = open_ratbagd(verbose=cmd.verbose)
|
|
if _r is not None:
|
|
with _r as r:
|
|
try:
|
|
f = cmd.func
|
|
except AttributeError:
|
|
parser.print_help()
|
|
return 2
|
|
else:
|
|
try:
|
|
f(r, cmd)
|
|
except RatbagCapabilityError as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
return 1
|
|
except ValueError as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ret = main(sys.argv[1:])
|
|
sys.exit(ret)
|