Initial commit

This commit is contained in:
Sebastian Lohff 2019-03-17 02:30:56 +01:00
commit 4833016d47
7 changed files with 304 additions and 0 deletions

.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@

61 Normal file
View File

@ -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.
* freely configure ALL the buttons
* quickly change between different button profiles
* change button mappings based on wheel led status
## Installation
$ pip install git+
## 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
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`:
ExecStartPre=/bin/sleep 2
ExecStart=path/to/fynncom -c path/to/fynncomm/fynncom.yaml
Don't forget to reload systemd with `systemctl daemon-reload`.

fynncom.yaml.example Normal file
View File

@ -0,0 +1,60 @@
# 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
# profile named default
# all Wacom devices with "pen" in the name
# 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)
# 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
- 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
base_profile: 'default'

fynncom/ Normal file
View File

fynncom/ Executable file
View File

@ -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
def validate_config(self):
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',):
# check recursion
except KeyError as e:
raise click.ClickException("Key missing in config: {}".format(e))
def get_current_profile(self):
with open(self.profile_file) as pf:
except IOError:
return self.config['device']['default_profile']
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(
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):
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'):
# 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():
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)
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))
with open(self.profile_file, 'w') as f:
f.write(profile + "\n")
@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["which", cmd], stdout=subprocess.DEVNULL) != 0:
raise click.ClickException("Could not find `{}`, please install it".format(cmd))
# load config
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()
print("Switched to profile {} with weel-state {}".format(profile, fynncom.wheel_state))
if __name__ == '__main__':

requirements.txt Normal file
View File

@ -0,0 +1,2 @@

18 Normal file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env python
from distutils.core import setup
description='Wacom tablet button configuration tool',
author='Sebastian Lohff',
install_requires=['click', 'pyyaml'],
'console_scripts': [
'fynncom = fynncom.fynncom:cli'