#!/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("{:20s} {:32s}".format(d.id + ":", d.name)) def find_device(r, args): dev = r[args.device] if dev is None: for d in r.devices: if args.device in d.name: return d 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", } depths = { RatbagdLed.COLORDEPTH_MONOCHROME: "monochrome", RatbagdLed.COLORDEPTH_RGB_888: "rgb", RatbagdLed.COLORDEPTH_RGB_111: "rgb111", } if l.mode == RatbagdLed.MODE_OFF: print(" " * level + "LED: {}, depth: {}, mode: {}".format(l.index, depths[l.colordepth], leds[l.mode])) elif l.mode == RatbagdLed.MODE_ON: print(" " * level + "LED: {}, depth: {}, mode: {}, color: {:02x}{:02x}{:02x}".format(l.index, depths[l.colordepth], leds[l.mode], l.color[0], l.color[1], l.color[2])) elif l.mode == RatbagdLed.MODE_CYCLE: print(" " * level + "LED: {}, depth: {}, mode: {}, duration: {}, brightness: {}".format(l.index, depths[l.colordepth], leds[l.mode], l.effect_duration, l.brightness)) elif l.mode == RatbagdLed.MODE_BREATHING: print(" " * level + "LED: {}, depth: {}, mode: {}, color: {:02x}{:02x}{:02x}, duration: {}, brightness: {}".format(l.index, 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): header = " " * level + "Button: {} is mapped to ".format(b.index) 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 len(r.resolution) == 2: dpi = "{}x{}".format(r.resolution[0], r.resolution[1]) else: dpi = "{}".format(r.resolution[0]) print(" " * level + "{}: {}dpi{}{}".format(r.index, dpi, " (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 or 'n/a')) print(" " * level + "Report Rate: {}Hz".format(p.report_rate)) 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 print(" " * level + "{} - {}".format(d.id, d.name)) 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) b.mapping = args.target_button commit(d, args) 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] commit(d, args) def func_button_action_set_macro(r, args): b, p, d = find_button(r, args) if not b.ACTION_TYPE_MACRO in b.action_types: 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 len(r.resolution) == 2: 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) dpi = args.dpi_n if len(r.resolution) > len(dpi): dpi = (dpi[0], dpi[0]) r.resolution = dpi commit(d, args) def func_report_rate_get(r, args): p, d = find_profile(r, args) print(p.report_rate) def func_report_rate_get_all(r, args): p, d = find_profile(r, args) rates = p.report_rates print(" ".join([str(x) for x in rates])) def func_report_rate_set(r, args): p, d = find_profile(r, args) p.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) # 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(p.name, 'utf-8', 'ignore') print(string.decode('utf-8')) def func_profile_name_set(r, args): p, d = find_profile(r, args) if not p.name: 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(device, args): if args.nocommit: return device.commit() 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, ) 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: 'hidrawX' is assumed before each command parser_def = [ { 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: 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: '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) 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) == 2: self.store_function(parser) 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): 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="{} ".format(sys.argv[0]), 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): print("usage: {} [OPTIONS] list".format(self.parser.prog)) print(" {} [OPTIONS] {{COMMAND}} ...\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") 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(): return RatbagParserRoot(parser_def) def on_device_added(ratbagd, device): 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=None, verbose=0): try: r = Ratbagd() r.verbose = verbose try: r.connect('device-added', on_device_added) for d in r.devices: on_device_added(r, d) except AttributeError: pass # the ratbag-command case except RatbagdUnavailable as e: print("Unable to connect to ratbagd: {}".format(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 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:])