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

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
fynncom.egg-info
fynncom.yaml
*.swp
__pycache__
*.pyc

61
README.md 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.
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`.

60
fynncom.yaml.example Normal file
View File

@ -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'

0
fynncom/__init__.py Normal file
View File

158
fynncom/fynncom.py 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
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()

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
click
pyyaml

18
setup.py Normal file
View File

@ -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'
]
},
)