123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- #!/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()
|