#!/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 import re from ratbagd import Ratbagd, RatbagdDevice, RatbagdProfile, RatbagdMacro, RatbagdResolution, RatbagdButton, RatbagdLed, RatbagdUnavailable, 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: {}, duration: {}, brightness: {}".format(l.index, types[l.type], depths[l.colordepth], leds[l.mode], l.effect_duration, l.brightness)) elif l.mode == RatbagdLed.MODE_BREATHING: print(" " * level + "LED: {} type: {}, depth: {}, mode: {}, color: {:02x}{:02x}{:02x}, duration: {}, brightness: {}".format(l.index, types[l.type], depths[l.colordepth], leds[l.mode], l.color[0], l.color[1], l.color[2], l.effect_duration, 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 + "{}: ".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] svg_name = 'unknown' try: with d.get_svg_fd('gnome') as fd: svg = fd.read() match = re.search('sodipodi:docname=".*"\n', svg) if match: svg_name = match.group(0).split('"')[1] except FileNotFoundError: pass print(" " * level + "{} - {}".format(d.id, d.name)) print(" " * level + " SVG: {}".format(svg_name)) 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: duration = args.duration except AttributeError: pass else: l.effect_duration = duration try: brightness = args.brightness except AttributeError: pass else: l.brightness = brightness commit(d, args) 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) if not d.CAP_BUTTON in d.capabilities: raise RatbagErrorCapability("assigning a button is not supported on this device") b.mapping = args.target_button commit(d, args) def func_button_action_set_special(r, args): b, p, d = find_button(r, args) if not d.CAP_BUTTON in d.capabilities: raise RatbagErrorCapability("assigning a button is not supported on this device") try: special = args.target_special except AttributeError: pass else: b.special = button_specials_strmap[special] commit(d, args) def func_button_action_set_macro(r, args): b, p, d = find_button(r, args) if not d.CAP_BUTTON_MACROS in d.capabilities: raise RatbagErrorCapability("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: 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 commit(d, args) 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]) commit(d, args) 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() commit(d, args) 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() commit(d, args) 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 commit(d, args) 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() commit(d, args) def func_profile_enable(r, args): p, d = find_profile(r, args) p.enabled = True commit(d, args) def func_profile_disable(r, args): p, d = find_profile(r, args) p.enabled = False commit(d, args) 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 commit(device, args): if args.nocommit: return device.commit(commit_cb) 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: '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 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) 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 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 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") 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(): return RatbagParserRoot(parser_def) def open_ratbagd(ratbagd_process=None, verbose=0): try: r = Ratbagd() r.verbose = verbose except RatbagdUnavailable: print("Unable to connect to ratbagd") 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: try: f(r, cmd) except RatbagErrorCapability as e: print("Error: {}".format(e), file=sys.stderr) if __name__ == "__main__": main(sys.argv[1:])