|
@@ -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()
|