Files
libratbag/tools/ratbagctl.in
Thomas Hindoe Paaboel Andersen 790c5e90bd add RATBAG_LED_TYPE_SCROLL_WHEEL
2017-10-18 00:19:12 +02:00

1539 lines
54 KiB
Python
Executable File

#!/usr/bin/env python3
#
# vim: set expandtab shiftwidth=4 tabstop=4:
#
# 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 sys
import argparse
from ratbagd import Ratbagd, RatbagdDevice, RatbagdProfile, RatbagdMacro, RatbagdResolution, RatbagdButton, RatbagdLed, RatbagdDBusUnavailable, RatbagError, RatbagErrorCode, RatbagErrorCapability # NOQA
button_specials_strmap = {
RatbagdButton.ACTION_SPECIAL_UNKNOWN: "unknown",
RatbagdButton.ACTION_SPECIAL_DOUBLECLICK: "doubleclick",
RatbagdButton.ACTION_SPECIAL_WHEEL_LEFT: "wheel-left",
RatbagdButton.ACTION_SPECIAL_WHEEL_RIGHT: "wheel-right",
RatbagdButton.ACTION_SPECIAL_WHEEL_UP: "wheel-up",
RatbagdButton.ACTION_SPECIAL_WHEEL_DOWN: "wheel-down",
RatbagdButton.ACTION_SPECIAL_RATCHET_MODE_SWITCH: "ratchet-mode-switch",
RatbagdButton.ACTION_SPECIAL_RESOLUTION_CYCLE_UP: "resolution-cycle-up",
RatbagdButton.ACTION_SPECIAL_RESOLUTION_CYCLE_DOWN: "resolution-cycle-down",
RatbagdButton.ACTION_SPECIAL_RESOLUTION_UP: "resolution-up",
RatbagdButton.ACTION_SPECIAL_RESOLUTION_DOWN: "resolution-down",
RatbagdButton.ACTION_SPECIAL_RESOLUTION_ALTERNATE: "resolution-alternate",
RatbagdButton.ACTION_SPECIAL_RESOLUTION_DEFAULT: "resolution-default",
RatbagdButton.ACTION_SPECIAL_PROFILE_CYCLE_UP: "profile-cycle-up",
RatbagdButton.ACTION_SPECIAL_PROFILE_CYCLE_DOWN: "profile-cycle-down",
RatbagdButton.ACTION_SPECIAL_PROFILE_UP: "profile-up",
RatbagdButton.ACTION_SPECIAL_PROFILE_DOWN: "profile-down",
RatbagdButton.ACTION_SPECIAL_SECOND_MODE: "second-mode",
RatbagdButton.ACTION_SPECIAL_BATTERY_LEVEL: "battery-level",
}
for k, v in [(v, k) for k, v in button_specials_strmap.items()]:
button_specials_strmap[k] = v
def list_devices(r, args):
if not r.devices:
print("No devices available.")
for d in r.devices:
print("{:10s} {:32s}".format(d.id + ":", d.name))
def find_device(r, args):
dev = r[args.device]
if dev is None:
print("Unable to find device {}".format(args.device))
sys.exit(1)
return dev
def find_profile(r, args):
d = find_device(r, args)
try:
p = d.profiles[args.profile_n]
except IndexError:
print("Invalid profile index {}".format(args.profile_n))
sys.exit(1)
except AttributeError:
p = d.active_profile
return p, d
def find_resolution(r, args):
p, d = find_profile(r, args)
try:
r = p.resolutions[args.resolution_n]
except IndexError:
print("Invalid resolution index {}".format(args.resolution_n))
sys.exit(1)
except AttributeError:
r = p.active_resolution
return r, p, d
def find_button(r, args):
p, d = find_profile(r, args)
try:
b = p.buttons[args.button_n]
except IndexError:
print("Invalid button index {}".format(args.button_n))
sys.exit(1)
return b, p, d
def find_led(r, args):
p, d = find_profile(r, args)
try:
l = p.leds[args.led_n]
except IndexError:
print("Invalid LED index {}".format(args.led_n))
sys.exit(1)
return l, p, d
def print_led(d, p, l, level):
leds = {
RatbagdLed.MODE_BREATHING: "breathing",
RatbagdLed.MODE_CYCLE: "cycle",
RatbagdLed.MODE_OFF: "off",
RatbagdLed.MODE_ON: "on",
}
types = {
RatbagdLed.TYPE_LOGO: "logo",
RatbagdLed.TYPE_SIDE: "side",
RatbagdLed.TYPE_BATTERY: "battery",
RatbagdLed.TYPE_DPI: "dpi",
RatbagdLed.TYPE_WHEEL: "wheel",
}
depths = {
RatbagdLed.COLORDEPTH_MONOCHROME: "monochrome",
RatbagdLed.COLORDEPTH_RGB_888: "rgb",
RatbagdLed.COLORDEPTH_RGB_111: "rgb111",
}
if l.mode == RatbagdLed.MODE_OFF:
print(" " * level + "LED: {} type: {}, depth: {}, mode: {}".format(l.index,
types[l.type],
depths[l.colordepth],
leds[l.mode]))
elif l.mode == RatbagdLed.MODE_ON:
print(" " * level + "LED: {} type: {}, depth: {}, mode: {}, color: {:02x}{:02x}{:02x}".format(l.index,
types[l.type],
depths[l.colordepth],
leds[l.mode],
l.color[0],
l.color[1],
l.color[2]))
elif l.mode == RatbagdLed.MODE_CYCLE:
print(" " * level + "LED: {} type: {}, depth: {}, mode: {}, rate: {}, brightness: {}".format(l.index,
types[l.type],
depths[l.colordepth],
leds[l.mode],
l.effect_rate,
l.brightness))
elif l.mode == RatbagdLed.MODE_BREATHING:
print(" " * level + "LED: {} type: {}, depth: {}, mode: {}, color: {:02x}{:02x}{:02x}, rate: {}, brightness: {}".format(l.index,
types[l.type],
depths[l.colordepth],
leds[l.mode],
l.color[0],
l.color[1],
l.color[2],
l.effect_rate,
l.brightness))
def print_led_caps(d, p, l, level):
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 l.modes])
print(" " * level + "Modes: {}".format(", ".join(supported)))
def print_button(d, p, b, level):
types = {
RatbagdButton.TYPE_UNKNOWN: "unknown",
RatbagdButton.TYPE_LEFT: "left",
RatbagdButton.TYPE_MIDDLE: "middle",
RatbagdButton.TYPE_RIGHT: "right",
RatbagdButton.TYPE_THUMB: "thumb",
RatbagdButton.TYPE_THUMB2: "thumb2",
RatbagdButton.TYPE_THUMB3: "thumb3",
RatbagdButton.TYPE_THUMB4: "thumb4",
RatbagdButton.TYPE_WHEEL_LEFT: "wheel-left",
RatbagdButton.TYPE_WHEEL_RIGHT: "wheel-right",
RatbagdButton.TYPE_WHEEL_CLICK: "wheel-click",
RatbagdButton.TYPE_WHEEL_UP: "wheel-up",
RatbagdButton.TYPE_WHEEL_DOWN: "wheel-down",
RatbagdButton.TYPE_WHEEL_RATCHET_MODE_SHIFT: "wheel-ratchet-mode-shift",
RatbagdButton.TYPE_EXTRA: "extra",
RatbagdButton.TYPE_SIDE: "side",
RatbagdButton.TYPE_PINKIE: "pinkie",
RatbagdButton.TYPE_PINKIE2: "pinkie2",
RatbagdButton.TYPE_RESOLUTION_CYCLE_UP: "resolution-cycle-up",
RatbagdButton.TYPE_RESOLUTION_UP: "resolution-up",
RatbagdButton.TYPE_RESOLUTION_DOWN: "resolution-down",
RatbagdButton.TYPE_PROFILE_CYCLE_UP: "profile-cycle-up",
RatbagdButton.TYPE_PROFILE_UP: "profile-up",
RatbagdButton.TYPE_PROFILE_DOWN: "profile-down",
}
t = types[b.type]
header = " " * level + "Button: {} type {} is mapped to ".format(b.index, t)
if b.action_type == RatbagdButton.ACTION_TYPE_BUTTON:
print("{}'button {}'".format(header, b.mapping))
elif b.action_type == RatbagdButton.ACTION_TYPE_SPECIAL:
print("{}'{}'".format(header, button_specials_strmap[b.special]))
elif b.action_type == RatbagdButton.ACTION_TYPE_MACRO:
print("{}macro '{}'".format(header, str(b.macro)))
elif b.action_type == RatbagdButton.ACTION_TYPE_NONE:
print("{}none".format(header))
else:
print("{}UNKNOWN".format(header))
def print_resolution(d, p, r, level):
if r.resolution == (0, 0):
print(" " * level + "{}: <disabled>".format(r.index))
return
if RatbagdResolution.CAP_SEPARATE_XY_RESOLUTION in r.capabilities:
dpi = "{}x{}".format(r.resolution[0], r.resolution[1])
else:
dpi = "{}".format(r.resolution[0])
print(" " * level + "{}: {}dpi @ {}Hz{}{}".format(r.index,
dpi,
r.report_rate,
" (active)" if r.is_active else "",
" (default)" if r.is_default else "",
))
def print_profile(d, p, level):
print(" " * (level - 2) + "Profile {}:{}{}".format(p.index,
" (disabled)" if not p.enabled else "",
" (active)" if p.is_active else ""))
if p.enabled:
print(" " * level + "Name : '{}'".format(p.name))
print(" " * level + "Resolutions:")
for r in p.resolutions:
print_resolution(d, p, r, level + 2)
for b in p.buttons:
print_button(d, p, b, level)
for l in p.leds:
print_led(d, p, l, level)
def print_device(d, level):
p = d.profiles[0] # there should be always one
caps = {RatbagdDevice.CAP_RESOLUTION: "resolution",
RatbagdDevice.CAP_PROFILE: "profile",
RatbagdDevice.CAP_BUTTON: "button",
RatbagdDevice.CAP_SWITCHABLE_RESOLUTION: "switchable-resolution",
RatbagdDevice.CAP_SWITCHABLE_PROFILE: "switchable-profile",
RatbagdDevice.CAP_BUTTON_KEY: "button-keys",
RatbagdDevice.CAP_BUTTON_MACROS: "button-macros",
RatbagdDevice.CAP_DEFAULT_PROFILE: "default-profile",
RatbagdDevice.CAP_QUERY_CONFIGURATION: "query-configuration",
RatbagdDevice.CAP_DISABLE_PROFILE: "disable-profile",
RatbagdDevice.CAP_LED: "led"}
capabilities = [caps[c] for c in d.capabilities]
print(" " * level + "{} - {}".format(d.id, d.name))
print(" " * level + " SVG: {}".format(d.get_svg('gnome')))
print(" " * level + " Capabilities: {}".format(", ".join(capabilities)))
print(" " * level + " Number of Buttons: {}".format(len(p.buttons)))
print(" " * level + " Number of Leds: {}".format(len(p.leds)))
print(" " * level + "Number of Profiles: {}".format(len(d.profiles)))
for p in d.profiles:
print_profile(d, p, level + 2)
def show_device(r, args):
d = find_device(r, args)
print_device(d, 0)
def show_profile(r, args):
p, d = find_profile(r, args)
print("Profile {} on {} ({})".format(args.profile, d.id, d.name))
print_profile(d, p, 0)
def show_resolution(r, args):
r, p, d = find_resolution(r, args)
print("Resolution {} on Profile {} on {} ({})".format(args.resolution,
args.profile,
d.id,
d.name))
print_resolution(d, p, r, 0)
caps = {RatbagdResolution.CAP_INDIVIDUAL_REPORT_RATE: "individual-report-rate",
RatbagdResolution.CAP_SEPARATE_XY_RESOLUTION: "separate-xy-resolution"}
capabilities = [caps[c] for c in r.capabilities]
print(" Capabilities: {}".format(", ".join(capabilities)))
def show_button(r, args):
b, p, d = find_button(r, args)
print("Button {} on Profile {} on {} ({})".format(args.button,
args.profile,
d.id,
d.name))
print_button(d, p, b, 0)
def func_led_get(r, args):
l, p, d = find_led(r, args)
print_led(d, p, l, 0)
def func_led_caps(r, args):
l, p, d = find_led(r, args)
print_led_caps(d, p, l, 0)
def func_led_set(r, args):
l, p, d = find_led(r, 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,
}
l.mode = leds[mode]
try:
color = args.color
except AttributeError:
pass
else:
l.color = color
try:
rate = args.rate
except AttributeError:
pass
else:
l.effect_rate = rate
try:
brightness = args.brightness
except AttributeError:
pass
else:
l.brightness = brightness
d.commit(commit_cb)
def func_led_get_all(r, args):
p, d = find_profile(r, args)
for l in p.leds:
print_led(d, p, l, 0)
def func_button_get(r, args):
b, p, d = find_button(r, args)
print_button(b, p, b, 0)
def func_button_action_set_button(r, args):
b, p, d = find_button(r, args)
b.mapping = args.target_button
d.commit(commit_cb)
def func_button_action_set_special(r, args):
b, p, d = find_button(r, args)
try:
special = args.target_special
except AttributeError:
pass
else:
b.special = button_specials_strmap[special]
d.commit(commit_cb)
def func_button_action_set_macro(r, args):
b, p, d = find_button(r, args)
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:
if not s.startswith("KEY_") and not s.startswith("BTN_"):
msg = "Don't know how to convert {}".format(s)
raise argparse.ArgumentTypeError(msg)
code = evdev.ecodes.ecodes[s]
if is_press:
macro.append(RatbagdButton.MACRO_KEY_PRESS, code)
if is_release:
macro.append(RatbagdButton.MACRO_KEY_RELEASE, code)
b.macro = macro
d.commit(commit_cb)
def func_button_count(r, args):
p, d = find_profile(r, args)
print(len(p.buttons))
def func_dpi_get(r, args):
r, p, d = find_resolution(r, args)
if RatbagdResolution.CAP_SEPARATE_XY_RESOLUTION in r.capabilities:
print("{}x{}dpi".format(r.resolution[0], r.resolution[1]))
else:
print("{}dpi".format(r.resolution[0]))
def func_dpi_get_all(r, args):
r, p, d = find_resolution(r, args)
dpis = r.resolutions
print(" ".join([str(x) for x in dpis]))
def func_dpi_set(r, args):
r, p, d = find_resolution(r, args)
r.resolution = (args.dpi_n[0], args.dpi_n[1])
d.commit(commit_cb)
def func_report_rate_get(r, args):
r, p, d = find_resolution(r, args)
print(r.report_rate)
def func_report_rate_get_all(r, args):
r, p, d = find_resolution(r, args)
rates = r.report_rates
print(" ".join([str(x) for x in rates]))
def func_report_rate_set(r, args):
r, p, d = find_resolution(r, args)
r.report_rate = args.rate_n
def func_resolution_get(r, args):
r, p, d = find_resolution(r, args)
print_resolution(d, p, r, 0)
def func_resolution_active_get(r, args):
p, d = find_profile(r, args)
print(p.active_resolution.index)
def func_resolution_active_set(r, args):
r, p, d = find_resolution(r, args)
r.set_active()
d.commit(commit_cb)
def func_resolution_default_get(r, args):
p, d = find_profile(r, args)
for r in p.resolutions:
if r.is_default:
break
else:
r = None
print(r.index)
def func_default_resolution_set(r, args):
# FIXME: capabilities check?
r, p, d = find_resolution(r, args)
r.set_default()
d.commit(commit_cb)
def func_profile_get(r, args):
p, d = find_profile(r, args)
print_profile(d, p, 0)
def func_profile_name_get(r, args):
p, d = find_profile(r, args)
print(p.name)
def func_profile_name_set(r, args):
p, d = find_profile(r, args)
if RatbagdProfile.CAP_WRITABLE_NAME not in p.capabilities:
raise RatbagErrorCapability("assigning a profile name is not supported on this profile")
p.name = args.name
d.commit(commit_cb)
def func_profile_active_get(r, args):
d = find_device(r, args)
print(d.active_profile.index)
def func_profile_active_set(r, args):
p, d = find_profile(r, args)
p.set_active()
d.commit(commit_cb)
def func_profile_enable(r, args):
p, d = find_profile(r, args)
p.enabled = True
d.commit(commit_cb)
def func_profile_disable(r, args):
p, d = find_profile(r, args)
p.enabled = False
d.commit(commit_cb)
def func_device_name_get(r, args):
d = find_device(r, args)
print(d.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 interprete 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_cb(status):
if not status == RatbagErrorCode.RATBAG_SUCCESS:
print("Could not commit changes to the device")
def color(string):
try:
int_value = int(string, 16)
except ValueError:
msg = "%r is not a color in hex format" % string
raise argparse.ArgumentTypeError(msg)
r = (int_value >> 16) & 0xff
g = (int_value >> 8) & 0xff
b = (int_value >> 0) & 0xff
return (r, g, b)
def u8(string):
int_value = int(string)
msg = "%r is not a single byte" % string
if int_value < 0 or int_value > 255:
raise argparse.ArgumentTypeError(msg)
return int_value
def dpi(string):
try:
int_value = int(string)
except ValueError:
pass
else:
return (int_value, 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:
raise argparse.ArgumentTypeError("%r is not a valid dpi" % string)
else:
return (int_x, int_y)
# note: 'eventX' is assumed at the end of each command
parser_def = [
{
of_type: command,
name: 'list',
help_str: 'List supported devices (does not take a device argument)',
func: list_devices,
group: 'General',
},
{
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: '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: link,
dest: 'dpi',
},
{
of_type: link,
dest: 'rate',
},
],
},
},
{
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 and resolution, or on the
active resolution of the active profile if none are 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: '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: [
"unknown",
"doubleclick",
"wheel-left",
"wheel-right",
"wheel-up",
"wheel-down",
"ratchet-mode-switch",
"resolution-cycle-up",
"resolution-cycle-down",
"resolution-up",
"resolution-down",
"resolution-alternate",
"resolution-default",
"profile-cycle-up",
"profile-cycle-down",
"profile-up",
"profile-down",
"second-mode",
"battery-level",
]
},
],
func: func_button_action_set_special,
},
{
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: 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: ['on', 'off', 'cycle', 'breathing'],
},
],
},
{
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: 'rate',
help_str: 'The rate to set as current',
pos_args: [
{
of_type: argument,
name: 'rate',
metavar: 'R',
help_str: 'The rate in Hz 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
class RatbagParser(object):
tagged = {}
def __init__(self, type, name, group=None, tag=None, func=None, help=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):
return "name='{}', tag='{}', func='{}', help='{}'".format(self.name, self.tag, self.func, self.help)
def __repr__(self):
return "{}({})".format(type(self), self.repr_args())
def store_function(self, parser):
if self.func is not None:
parser.set_defaults(func=self.func)
if 'list' not in self.name:
parser.add_argument("device", metavar='eventX')
def _add_to_subparsers(self, parent, input_string, ns):
raise ParseError("please implement _add_to_subparsers on {}".format(type(self)))
def add_to_subparsers(self, parent):
self._add_to_subparsers(parent)
def _sub_parse(self, input_string, ns):
raise ParseError("please implement _sub_parse on {}".format(type(self)))
def sub_parse(self, input_string, ns):
r = self._sub_parse(input_string, ns)
return r
def build_cmd_args_name(self):
"""uniquely tag the arguments of the command in the namespace"""
return "{}_args_{}".format(self.name)
def print_help(self, group, prefix=""):
if self.group is not None:
print("\n{} Commands:".format(self.group))
self._print_help(prefix)
def _print_help(self, prefix):
raise ParseError("please implement _print_help on {}".format(type(self)))
class RatbagParserSwitch(RatbagParser):
def __init__(self, type, name, group=None, switch=[], N_access=None, tag=None, func=None, help=None):
super(RatbagParserSwitch, self).__init__(type, name, group, tag, func, help)
self.switch = [classes[obj[of_type]](**obj) for obj in switch]
if N_access is not None:
self.N_access = RatbagParserNAccess(**N_access)
else:
self.N_access = None
def repr_args(self):
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):
parser = parent.add_parser(self.name, help=self.help)
parser.set_defaults(subparse=self.sub_parse)
def _sub_parse(self, input_string, ns):
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._sub_parse(self, input_string, ns)
parser = argparse.ArgumentParser(prog="{} {}".format(sys.argv[0], 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):
if self.help and self.group is not None:
string = self.help.split('\n')
string = "\n ".join(string)
print(" ", string, '\n')
for e in self.switch:
e.print_help(None, "{}{} ".format(prefix, self.name))
if self.N_access is not None:
self.N_access.print_help(None, self.name + " ")
def __repr__(self):
return "switch({})".format(self.repr_args())
class RatbagParserNAccess(RatbagParserSwitch):
def __init__(self, type, name, group=None, switch=[], metavar=None, tag=None, func=None, help=None):
super(RatbagParserNAccess, self).__init__(type, name, group, switch, None, tag, func, help)
self.metavar = metavar
def _sub_parse(self, parent, input_string, ns):
parser = argparse.ArgumentParser(prog="{} {}".format(sys.argv[0], 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):
return """switch='{}', metavar = '{}', {}""".format([repr(o) for o in self.switch], self.metavar, RatbagParser.repr_args(self))
def __repr__(self):
return "N_Access({})".format(self.repr_args())
def _print_help(self, prefix):
for e in self.switch:
e.print_help(None, "{}N ".format(prefix))
class RatbagParserSet(RatbagParserSwitch):
def __init__(self, type, name, group=None, switch=[], N_access=None, tag=None, func=None, help=None):
super(RatbagParserSet, self).__init__(type, name, group, switch, N_access, tag, func, help)
def _add_to_subparsers(self, parent):
parser = parent.add_parser(self.name, help=self.help)
parser.set_defaults(subparse=self.sub_parse)
def _sub_parse(self, input_string, ns):
parser = argparse.ArgumentParser(prog="{} {}".format(sys.argv[0], 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) == 3:
self.store_function(parser)
elif len(input_string) == 2:
# missing device argument
parser.add_argument("device", metavar='eventX')
else:
parser.set_defaults(subparse=self.sub_parse)
return parser
def __repr__(self):
return "set({})".format(self.repr_args())
def _print_help(self, prefix):
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, name, group=None, pos_args=[], tag=None, func=None, help=None):
super(RatbagParserCommand, self).__init__(type, name, group, tag, func, help)
self.pos_args = [classes[obj[of_type]](**obj) for obj in pos_args]
def _add_to_subparsers(self, parent):
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):
return "command({})".format(self.repr_args())
def _print_help(self, prefix):
command = prefix + self.name
for a in self.pos_args:
if a.choices is None or len(a.choices) > 5:
command += " {}".format(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, name, group=None, arg_type=None, metavar=None, nargs=None, choices=None, tag=None, func=None, help=None):
super(RatbagParserArgument, self).__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):
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, dest=None, group=None, tag=None, func=None, help=None):
super(RatbagParserLink, self).__init__(type, dest, group, tag, func, help)
self.dest = dest
def get_dest(self):
try:
dest = RatbagParser.tagged[self.dest]
except KeyError:
raise ParseError("link '{}' points to nothing".format(self.dest))
return dest
def _add_to_subparsers(self, parent):
dest = self.get_dest()
dest.add_to_subparsers(parent)
def _print_help(self, prefix):
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(object):
def __init__(self, commands):
self.children = [classes[def_parser[of_type]](**def_parser) for def_parser in commands]
self.want_keepalive = False
def parse(self, input_string):
# we do not care about case, use lower case for the input string
for i, string in enumerate(input_string):
if not string.startswith('-'):
input_string[i] = string.lower()
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)
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
subs = self.parser.add_subparsers(title="COMMANDS")
subparser = self.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 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):
print("usage: {} [OPTIONS] {{COMMAND}} ... /path/to/device\n".format(self.parser.prog))
print(self.parser.description)
print("""
Common options:
--version -V show program's version number and exit
--verbose, -v
--help, -h show this help and exit""")
if self.want_keepalive:
print(" --keepalive do not terminate ratbagd after the processing")
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 rate 20
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():
return RatbagParserRoot(parser_def)
def open_ratbagd(ratbagd_process=None, verbose=0):
try:
r = Ratbagd()
r.verbose = verbose
except RatbagdDBusUnavailable:
print("Unable to connect to ratbagd over dbus")
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 r
def main(argv):
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:
f(r, cmd)
if __name__ == "__main__":
main(sys.argv[1:])