#!/usr/bin/env python3 from collections import defaultdict import os import subprocess import click import yaml class FynnWacom: def __init__(self, config, verbose=False): self.config = config self.verbose = verbose self.validate_config() def validate_config(self): try: self.usbid = self.config['device']['usbid'].lower() self.device_name = self.config['device']['name'] self.profile_file = os.path.expanduser(self.config['device']['profile_file']) # check default profile default_profile = self.config['device']['default_profile'] if default_profile not in self.config['mappings']: raise click.ClickException("Profile {} missing from mappigns".format(default_profile)) # check mappings for name, mapping in self.config['mappings'].items(): # does the baseprofile exist? if 'base_profile' in mapping and mapping['base_profile'] not in self.config['mappings']: raise click.ClickException("Base profile {} not found for profile {}" "".format(mapping['base_profile'], name)) for key in mapping: if key in ('base_profile',): continue pass # check recursion pass except KeyError as e: raise click.ClickException("Key missing in config: {}".format(e)) def get_current_profile(self): try: with open(self.profile_file) as pf: return pf.read().strip() except IOError: return self.config['device']['default_profile'] @property def wheel_state(self): """Read the wacom tablet wheel status from /sys, int in [0, 3]""" basepath = '/sys/bus/hid/drivers/wacom/' wacom_dirs = [d for d in os.listdir(basepath) if self.usbid in d.lower()] if not wacom_dirs: raise click.ClickException("Could not find usbid {} in {}".format(self.usbid, basepath)) ledpath = os.path.join(basepath, sorted(wacom_dirs)[0], 'wacom_led/status_led0_select') with open(ledpath) as ledfile: return int(ledfile.read()) @staticmethod def _call(cmd): # print(" >> Calling: ", " ".join(cmd)) return subprocess.check_output(cmd, encoding='utf-8') def guess_devices(self, subname): found = False for device in self._call(["xinput", "list", "--name-only"]).split("\n"): if not device.startswith(self.device_name): continue rest = device.replace(self.device_name, "") if subname.lower() in rest.lower(): yield device found = True if not found: raise click.ClickException("Subdevice {} for {} not found".format(subname, self.device_name)) def build_mapping(self, profile, wheel_state): conf = self.config['mappings'][profile] mapping = defaultdict(dict) if 'base_profile' in conf: mapping = self.build_mapping(conf['base_profile'], wheel_state) # copy mappings into mapping dict for key, keymap in conf.items(): if key in ('base_profile', 'wheel'): continue mapping[key].update(keymap) # handle wheel, if present and in wheel_state range if 'wheel' in conf and len(conf['wheel']) > wheel_state and conf['wheel'][wheel_state]: for device, key_mapping in conf['wheel'][wheel_state].items(): mapping[device].update(key_mapping) return mapping def configure(self, profile): if profile not in self.config['mappings']: raise click.ClickException("Profile {} does not exist".format(profile)) mapping = self.build_mapping(profile, self.wheel_state) if self.verbose: print("Current mapping for profile {}: {}".format(profile, mapping)) for subdevice, keymap in mapping.items(): for button, keysym in keymap.items(): if len(keysym) == 1: keysym = "key {}".format(keysym) try: btn = ["Button", str(int(button))] except ValueError: btn = [button] for device in self.guess_devices(subdevice): cmd = ["xsetwacom", "set", device] + btn + [keysym] if self.verbose: print(" >> xsetwacom set '{}' '{}' '{}'".format(device, btn, keysym)) self._call(cmd) with open(self.profile_file, 'w') as f: f.write(profile + "\n") @click.command() @click.option('-c', '--config', default='~/.config/fynncom/fynncom.yaml', help='Path to config file') @click.option('-p', '--profile', default=None, help='Profile to switch to') @click.option('-v', '--verbose', is_flag=True, default=False, help='Be more verbose') def cli(config, profile, verbose): # check for xinput / xsetwacom for cmd in ('xinput', 'xsetwacom'): if subprocess.call(["which", cmd], stdout=subprocess.DEVNULL) != 0: raise click.ClickException("Could not find `{}`, please install it".format(cmd)) # load config try: with open(os.path.expanduser(config)) as f: config = yaml.safe_load(f) except IOError as e: raise click.ClickException("Could not load config: {}".format(e)) fynncom = FynnWacom(config, verbose) profile = profile or fynncom.get_current_profile() fynncom.configure(profile) print("Switched to profile {} with weel-state {}".format(profile, fynncom.wheel_state)) if __name__ == '__main__': cli()