From 4833016d47cabedc78131f732d0f11a9808a4ecc Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sun, 17 Mar 2019 02:30:56 +0100 Subject: [PATCH] Initial commit --- .gitignore | 5 ++ README.md | 61 +++++++++++++++++ fynncom.yaml.example | 60 ++++++++++++++++ fynncom/__init__.py | 0 fynncom/fynncom.py | 158 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + setup.py | 18 +++++ 7 files changed, 304 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 fynncom.yaml.example create mode 100644 fynncom/__init__.py create mode 100755 fynncom/fynncom.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b9c7e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +fynncom.egg-info +fynncom.yaml +*.swp +__pycache__ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5f561a --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Fynncomm - Wacom configuration for Fynn's tablet +This is a quick and dirty wacon configuration tool - basically a glorified +shellscript with a config in python. + +Features: + * freely configure ALL the buttons + * quickly change between different button profiles + * change button mappings based on wheel led status + +## Installation + +```shell +$ pip install git+https://git.someserver.de/seba/fynnwacom/ +``` + +## Configuration +An example configuration can be found in the git repository. + +For Fynncom to find the correct device a name and a usb id has to be configured. +The name can be found in `xinput list` and is just the part of the device wihout +a Pen/Pad suffix, e.g. for `Wacom Intuos5 touch M Pen stylus` the name would be +`Wacom Intuos5 touch M`. +The usbid can be found with `lsusb | grep -i wacom` and looks something like +`056a:0027`. + +Available buttons can be found with `xev`. In the mappings section of the config +multiple profiles can be defined. Inside a profile a device that matches any of +the existing wacom inputs has to be defined, e.g. `pen`, `pad` or `touch`. Inside +this "subinterface" a mapping from button codes to keycodes can be defined. Single +characters are automatically prefixed with "key". + +To get the wheel to change functionality (effectively applying the wheel mappings +from the configuration file), button 1 should be mapped to a shortcut for your +windowmanager to launch `fynncom`. + +## Autoconfig on login +The easiest way is to call `fynncom` from the commandline after the tablet has +been plugged in. + +Another possibility would be to create a systemd oneshot service that is triggered +by a Udev rule. + +Udev rule to trigger the service in `/etc/udev/rules.d/81-fynncom.rules`: +``` +ACTION=="add", SUBSYSTEM=="hid", ATTRS{idVendor}=="056a", ATTRS{idProduct}=="0027", \ + TAG+="systemd", ENV{SYSTEMD_WANTS}="fynncom.service" +``` + +Systemd service in `/etc/systemd/system/fynncom.service`: +``` +[Service] +Type=oneshot +Environment="DISPLAY=:0" +ExecStartPre=/bin/sleep 2 +User=YOUR_USER +Group=YOUR_USERS_GROUP +ExecStart=path/to/fynncom -c path/to/fynncomm/fynncom.yaml +``` + +Don't forget to reload systemd with `systemctl daemon-reload`. + diff --git a/fynncom.yaml.example b/fynncom.yaml.example new file mode 100644 index 0000000..90588f3 --- /dev/null +++ b/fynncom.yaml.example @@ -0,0 +1,60 @@ +device: + # name as found in `xinput list` (without Pen/Pad/Finger) + # Example: Wacom Intuos5 touch M + name: Wacom Intuos5 touch M + + # usbid, as found in `lsusb` + # Example: 056a:0027 + usbid: 056a:0027 + + default_profile: default + profile_file: ~/.config/fynncom/last_profile + +mappings: + # profile named default + default: + # all Wacom devices with "pen" in the name + pen: + # make pen absolute to this screen (can be found with xrandr) + maptooutput: 1920x1200+1920+0 + + # all Wacom devices with pad in name (usually just one) + pad: + # upper four buttons + 2: '1' + 3: '2' + 8: '3' + 9: '4' + + # wheel + button + # to get the wheel to work, this should be a shortcut for your + # window manager launching fynncom to refresh the mappings + 1: 'key ctrl alt shift c' # middle + # This setting will be overwritten by the wheel config lower + AbsWheelDown: '5' + AbsWheelUp: '7' + + # lower four buttons + 10: '8' + 11: '9' + 12: '0' + 13: 'A' + + # this configuration is applied depending upon the tablet's wheel state + wheel: + - pad: + AbsWheelUp: 'B' + AbsWheelDown: 'C' + - pad: + AbsWheelUp: 'D' + AbsWheelDown: 'E' + - pad: + AbsWheelUp: 'F' + AbsWheelDown: 'G' + - pad: + AbsWheelUp: 'H' + AbsWheelDown: 'I' + + # profile named gimp inheriting from default + gimp: + base_profile: 'default' diff --git a/fynncom/__init__.py b/fynncom/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fynncom/fynncom.py b/fynncom/fynncom.py new file mode 100755 index 0000000..bf21463 --- /dev/null +++ b/fynncom/fynncom.py @@ -0,0 +1,158 @@ +#!/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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1f95931 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +click +pyyaml diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7a1dd11 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='fynncom', + version='0.1.0', + description='Wacom tablet button configuration tool', + author='Sebastian Lohff', + author_email='seba@someserver.de', + url='https://git.someserver.de/seba/fynnwacom', + packages=['fynncom'], + install_requires=['click', 'pyyaml'], + entry_points={ + 'console_scripts': [ + 'fynncom = fynncom.fynncom:cli' + ] + }, + )