Browse Source

Initial commit

Sebastian Lohff 8 months ago
commit
4833016d47
7 changed files with 304 additions and 0 deletions
  1. 5
    0
      .gitignore
  2. 61
    0
      README.md
  3. 60
    0
      fynncom.yaml.example
  4. 0
    0
      fynncom/__init__.py
  5. 158
    0
      fynncom/fynncom.py
  6. 2
    0
      requirements.txt
  7. 18
    0
      setup.py

+ 5
- 0
.gitignore View File

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

+ 61
- 0
README.md View File

@@ -0,0 +1,61 @@
1
+# Fynncomm - Wacom configuration for Fynn's tablet
2
+This is a quick and dirty wacon configuration tool - basically a glorified
3
+shellscript with a config in python.
4
+
5
+Features:
6
+ * freely configure ALL the buttons
7
+ * quickly change between different button profiles
8
+ * change button mappings based on wheel led status
9
+
10
+## Installation
11
+
12
+```shell
13
+$ pip install git+https://git.someserver.de/seba/fynnwacom/
14
+```
15
+
16
+## Configuration
17
+An example configuration can be found in the git repository.
18
+
19
+For Fynncom to find the correct device a name and a usb id has to be configured.
20
+The name can be found in `xinput list` and is just the part of the device wihout
21
+a Pen/Pad suffix, e.g. for `Wacom Intuos5 touch M Pen stylus` the name would be
22
+`Wacom Intuos5 touch M`.
23
+The usbid can be found with `lsusb | grep -i wacom` and looks something like
24
+`056a:0027`.
25
+
26
+Available buttons can be found with `xev`. In the mappings section of the config
27
+multiple profiles can be defined. Inside a profile a device that matches any of
28
+the existing wacom inputs has to be defined, e.g. `pen`, `pad` or `touch`. Inside
29
+this "subinterface" a mapping from button codes to keycodes can be defined. Single
30
+characters are automatically prefixed with "key".
31
+
32
+To get the wheel to change functionality (effectively applying the wheel mappings
33
+from the configuration file), button 1 should be mapped to a shortcut for your
34
+windowmanager to launch `fynncom`.
35
+
36
+## Autoconfig on login
37
+The easiest way is to call `fynncom` from the commandline after the tablet has
38
+been plugged in.
39
+
40
+Another possibility would be to create a systemd oneshot service that is triggered
41
+by a Udev rule.
42
+
43
+Udev rule to trigger the service in `/etc/udev/rules.d/81-fynncom.rules`:
44
+```
45
+ACTION=="add", SUBSYSTEM=="hid", ATTRS{idVendor}=="056a", ATTRS{idProduct}=="0027", \
46
+    TAG+="systemd", ENV{SYSTEMD_WANTS}="fynncom.service"
47
+```
48
+
49
+Systemd service in `/etc/systemd/system/fynncom.service`:
50
+```
51
+[Service]
52
+Type=oneshot
53
+Environment="DISPLAY=:0"
54
+ExecStartPre=/bin/sleep 2
55
+User=YOUR_USER
56
+Group=YOUR_USERS_GROUP
57
+ExecStart=path/to/fynncom -c path/to/fynncomm/fynncom.yaml
58
+```
59
+
60
+Don't forget to reload systemd with `systemctl daemon-reload`.
61
+

+ 60
- 0
fynncom.yaml.example View File

@@ -0,0 +1,60 @@
1
+device:
2
+  # name as found in `xinput list` (without Pen/Pad/Finger)
3
+  # Example: Wacom Intuos5 touch M
4
+  name: Wacom Intuos5 touch M
5
+
6
+  # usbid, as found in `lsusb`
7
+  # Example: 056a:0027
8
+  usbid: 056a:0027
9
+
10
+  default_profile: default
11
+  profile_file: ~/.config/fynncom/last_profile
12
+
13
+mappings:
14
+  # profile named default
15
+  default:
16
+    # all Wacom devices with "pen" in the name
17
+    pen:
18
+      # make pen absolute to this screen (can be found with xrandr)
19
+      maptooutput: 1920x1200+1920+0
20
+
21
+    # all Wacom devices with pad in name (usually just one)
22
+    pad:
23
+      # upper four buttons
24
+      2: '1'
25
+      3: '2'
26
+      8: '3'
27
+      9: '4'
28
+
29
+      # wheel + button
30
+      # to get the wheel to work, this should be a shortcut for your
31
+      # window manager launching fynncom to refresh the mappings
32
+      1: 'key ctrl alt shift c' # middle
33
+      # This setting will be overwritten by the wheel config lower
34
+      AbsWheelDown: '5'
35
+      AbsWheelUp: '7'
36
+
37
+      # lower four buttons
38
+      10: '8'
39
+      11: '9'
40
+      12: '0'
41
+      13: 'A'
42
+
43
+    # this configuration is applied depending upon the tablet's wheel state
44
+    wheel:
45
+      - pad:
46
+          AbsWheelUp: 'B'
47
+          AbsWheelDown: 'C'
48
+      - pad:
49
+          AbsWheelUp: 'D'
50
+          AbsWheelDown: 'E'
51
+      - pad:
52
+          AbsWheelUp: 'F'
53
+          AbsWheelDown: 'G'
54
+      - pad:
55
+          AbsWheelUp: 'H'
56
+          AbsWheelDown: 'I'
57
+
58
+  # profile named gimp inheriting from default
59
+  gimp:
60
+    base_profile: 'default'

+ 0
- 0
fynncom/__init__.py View File


+ 158
- 0
fynncom/fynncom.py View File

@@ -0,0 +1,158 @@
1
+#!/usr/bin/env python3
2
+from collections import defaultdict
3
+import os
4
+import subprocess
5
+
6
+import click
7
+import yaml
8
+
9
+
10
+class FynnWacom:
11
+    def __init__(self, config, verbose=False):
12
+        self.config = config
13
+        self.verbose = verbose
14
+        self.validate_config()
15
+
16
+    def validate_config(self):
17
+        try:
18
+            self.usbid = self.config['device']['usbid'].lower()
19
+            self.device_name = self.config['device']['name']
20
+            self.profile_file = os.path.expanduser(self.config['device']['profile_file'])
21
+
22
+            # check default profile
23
+            default_profile = self.config['device']['default_profile']
24
+            if default_profile not in self.config['mappings']:
25
+                raise click.ClickException("Profile {} missing from mappigns".format(default_profile))
26
+
27
+            # check mappings
28
+            for name, mapping in self.config['mappings'].items():
29
+                # does the baseprofile exist?
30
+                if 'base_profile' in mapping and mapping['base_profile'] not in self.config['mappings']:
31
+                    raise click.ClickException("Base profile {} not found for profile {}"
32
+                                               "".format(mapping['base_profile'], name))
33
+
34
+                for key in mapping:
35
+                    if key in ('base_profile',):
36
+                        continue
37
+
38
+                    pass
39
+
40
+                # check recursion
41
+                pass
42
+        except KeyError as e:
43
+            raise click.ClickException("Key missing in config: {}".format(e))
44
+
45
+    def get_current_profile(self):
46
+        try:
47
+            with open(self.profile_file) as pf:
48
+                return pf.read().strip()
49
+        except IOError:
50
+            return self.config['device']['default_profile']
51
+
52
+    @property
53
+    def wheel_state(self):
54
+        """Read the wacom tablet wheel status from /sys, int in [0, 3]"""
55
+        basepath = '/sys/bus/hid/drivers/wacom/'
56
+        wacom_dirs = [d for d in os.listdir(basepath) if self.usbid in d.lower()]
57
+        if not wacom_dirs:
58
+            raise click.ClickException("Could not find usbid {} in {}".format(self.usbid, basepath))
59
+
60
+        ledpath = os.path.join(basepath, sorted(wacom_dirs)[0], 'wacom_led/status_led0_select')
61
+
62
+        with open(ledpath) as ledfile:
63
+            return int(ledfile.read())
64
+
65
+    @staticmethod
66
+    def _call(cmd):
67
+        # print(" >> Calling: ", " ".join(cmd))
68
+        return subprocess.check_output(cmd, encoding='utf-8')
69
+
70
+    def guess_devices(self, subname):
71
+        found = False
72
+        for device in self._call(["xinput", "list", "--name-only"]).split("\n"):
73
+            if not device.startswith(self.device_name):
74
+                continue
75
+            rest = device.replace(self.device_name, "")
76
+            if subname.lower() in rest.lower():
77
+                yield device
78
+                found = True
79
+
80
+        if not found:
81
+            raise click.ClickException("Subdevice {} for {} not found".format(subname, self.device_name))
82
+
83
+    def build_mapping(self, profile, wheel_state):
84
+        conf = self.config['mappings'][profile]
85
+        mapping = defaultdict(dict)
86
+        if 'base_profile' in conf:
87
+            mapping = self.build_mapping(conf['base_profile'], wheel_state)
88
+
89
+        # copy mappings into mapping dict
90
+        for key, keymap in conf.items():
91
+            if key in ('base_profile', 'wheel'):
92
+                continue
93
+            mapping[key].update(keymap)
94
+
95
+        # handle wheel, if present and in wheel_state range
96
+        if 'wheel' in conf and len(conf['wheel']) > wheel_state and conf['wheel'][wheel_state]:
97
+            for device, key_mapping in conf['wheel'][wheel_state].items():
98
+                mapping[device].update(key_mapping)
99
+
100
+        return mapping
101
+
102
+    def configure(self, profile):
103
+        if profile not in self.config['mappings']:
104
+            raise click.ClickException("Profile {} does not exist".format(profile))
105
+
106
+        mapping = self.build_mapping(profile, self.wheel_state)
107
+
108
+        if self.verbose:
109
+            print("Current mapping for profile {}: {}".format(profile, mapping))
110
+
111
+        for subdevice, keymap in mapping.items():
112
+            for button, keysym in keymap.items():
113
+                if len(keysym) == 1:
114
+                    keysym = "key {}".format(keysym)
115
+                try:
116
+                    btn = ["Button", str(int(button))]
117
+                except ValueError:
118
+                    btn = [button]
119
+
120
+                for device in self.guess_devices(subdevice):
121
+                    cmd = ["xsetwacom", "set", device] + btn + [keysym]
122
+                    if self.verbose:
123
+                        print(" >> xsetwacom set '{}' '{}' '{}'".format(device, btn, keysym))
124
+                    self._call(cmd)
125
+
126
+        with open(self.profile_file, 'w') as f:
127
+            f.write(profile + "\n")
128
+
129
+
130
+@click.command()
131
+@click.option('-c', '--config', default='~/.config/fynncom/fynncom.yaml',
132
+              help='Path to config file')
133
+@click.option('-p', '--profile', default=None,
134
+              help='Profile to switch to')
135
+@click.option('-v', '--verbose', is_flag=True, default=False,
136
+              help='Be more verbose')
137
+def cli(config, profile, verbose):
138
+    # check for xinput / xsetwacom
139
+    for cmd in ('xinput', 'xsetwacom'):
140
+        if subprocess.call(["which", cmd], stdout=subprocess.DEVNULL) != 0:
141
+            raise click.ClickException("Could not find `{}`, please install it".format(cmd))
142
+
143
+    # load config
144
+    try:
145
+        with open(os.path.expanduser(config)) as f:
146
+            config = yaml.safe_load(f)
147
+    except IOError as e:
148
+        raise click.ClickException("Could not load config: {}".format(e))
149
+
150
+    fynncom = FynnWacom(config, verbose)
151
+    profile = profile or fynncom.get_current_profile()
152
+    fynncom.configure(profile)
153
+
154
+    print("Switched to profile {} with weel-state {}".format(profile, fynncom.wheel_state))
155
+
156
+
157
+if __name__ == '__main__':
158
+    cli()

+ 2
- 0
requirements.txt View File

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

+ 18
- 0
setup.py View File

@@ -0,0 +1,18 @@
1
+#!/usr/bin/env python
2
+
3
+from distutils.core import setup
4
+
5
+setup(name='fynncom',
6
+      version='0.1.0',
7
+      description='Wacom tablet button configuration tool',
8
+      author='Sebastian Lohff',
9
+      author_email='seba@someserver.de',
10
+      url='https://git.someserver.de/seba/fynnwacom',
11
+      packages=['fynncom'],
12
+      install_requires=['click', 'pyyaml'],
13
+      entry_points={
14
+        'console_scripts': [
15
+            'fynncom = fynncom.fynncom:cli'
16
+        ]
17
+      },
18
+      )

Loading…
Cancel
Save