Files
libratbag/tools/ratbagctl.body.py.in
2023-03-07 00:31:10 +03:00

2003 lines
66 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, RatbagError, RatbagErrorCode, RatbagCapabilityError, RatbagDeviceType # 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
+ "LED: {}, depth: {}, mode: {}".format(
led.index, depths[led.colordepth], leds[led.mode]
)
)
elif led.mode == RatbagdLed.Mode.ON:
print(
" " * level
+ "LED: {}, depth: {}, mode: {}, color: {:02x}{:02x}{:02x}".format(
led.index,
depths[led.colordepth],
leds[led.mode],
led.color[0],
led.color[1],
led.color[2],
)
)
elif led.mode == RatbagdLed.Mode.CYCLE:
print(
" " * level
+ "LED: {}, depth: {}, mode: {}, duration: {}, brightness: {}".format(
led.index,
depths[led.colordepth],
leds[led.mode],
led.effect_duration,
led.brightness,
)
)
elif led.mode == RatbagdLed.Mode.BREATHING:
print(
" " * level
+ "LED: {}, depth: {}, mode: {}, color: {:02x}{:02x}{:02x}, duration: {},"
" brightness: {}".format(
led.index,
depths[led.colordepth],
leds[led.mode],
led.color[0],
led.color[1],
led.color[2],
led.effect_duration,
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 = evdev.ecodes.KEY[button.key][RatbagdMacro._PREFIX_LEN :]
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(
"Resolution {} on Profile {} on {} ({})".format(
args.resolution, args.profile, 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(
"Button {} on Profile {} on {} ({})".format(
args.button, args.profile, 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 = "%r is not a color in hex format" % string
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 = "%r is not a single byte" % string
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("%r is not a valid dpi" % string) 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.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 "name='{}', tag='{}', func='{}', help='{}'".format(
self.name, self.tag, self.func, 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 """switch='{}', N_access='{}', {}""".format(
[repr(o) for o in self.switch], 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 """switch='{}', metavar = '{}', {}""".format(
[repr(o) for o in self.switch], 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(
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]) -> None:
if not argv:
argv = ["list"]
parser = get_parser()
cmd = parser.parse(argv)
if cmd.help:
parser.print_help()
return
_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
else:
try:
f(r, cmd)
except RatbagCapabilityError as e:
print(f"Error: {e}", file=sys.stderr)
if __name__ == "__main__":
main(sys.argv[1:])