python tool to manage wacom tablet keybindings
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

fynncom.py 5.7KB

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