#!/usr/bin/env python3 import json import logging import os import sys import time from argparse import ArgumentParser from subprocess import Popen import evdev INPUTDEV_BASEDIR = "/dev/input/" # TODO: handle sighup # TODO: prevent event to do other things def listall(): for f in os.listdir(INPUTDEV_BASEDIR): try: print(f, evdev.InputDevice(os.path.join(INPUTDEV_BASEDIR, f))) except: pass def find_dev_by_name(name): for f in os.listdir(INPUTDEV_BASEDIR): try: ev = evdev.InputDevice(os.path.join(INPUTDEV_BASEDIR, f)) except Exception: continue if ev.name == name: return os.path.join(INPUTDEV_BASEDIR, f) raise ValueError("device with such name not found") class KeymapTable: def __init__(self): self.table = {} @classmethod def from_config_file(cls, data): kt = cls() kt.table = {int(key): value for key, value in data.items()} kt.validate() return kt def validate(self): errors = 0 for key in self.table: if type(key) is not int: logging.error("Keycode %r is not of valid type", key) errors += 1 if type(self.table[key]) is str: # Autofixing key key self.table[key] = { "short": self.table[key], "long": self.table[key], } if type(self.table[key]) is not dict: logging.error("Keycode %r is not of valid type", key) errors += 1 if errors: raise ValueError("%d validation errors found" % errors) def __contains__(self, x): code, pressure_type = x return code in self.table and pressure_type in self.table[code] def __getitem__(self, x): code, pressure_type = x return self.table[code][pressure_type] def loop(device, args): pressed = None for event in device.read_loop(): if event.type != evdev.ecodes.EV_KEY: continue # for attr in dir(event): # if not attr.startswith("_"): # logging.debug(" ", attr, str(getattr(event, attr))) logging.debug("received %s", str(evdev.categorize(event))) if event.value == 1: # keydown pressed = {"code": event.code, "ts": event.timestamp()} if event.value == 0 and pressed is not None: pressure_time = event.timestamp() - pressed["ts"] if pressure_time * 1000 <= args.short_threshold: pressure_type = "short" else: pressure_type = "long" yield (pressed["code"], pressure_type) def main(): p = ArgumentParser() p.add_argument("--config", type=open) input_p = p.add_argument_group("input device options") input_p.add_argument("-d", "--device") input_p.add_argument( "-n", "--name", help="Look at `xinput list` to have an idea" ) input_p.add_argument( "--short-threshold", default=300, help="Threshold to distinguish a short and a long press, " "in milliseconds", ) output_p = p.add_argument_group("output options") output_p.add_argument( "-v", "--verbose", dest="logging_level", action="store_const", const=logging.INFO, ) output_p.add_argument( "--debug", dest="logging_level", action="store_const", const=logging.DEBUG, ) args = p.parse_args() logging.basicConfig(level=args.logging_level) if args.config: config = json.load(args.config) else: config = {} keymaps = KeymapTable.from_config_file(config.get("keys", {})) if args.name: if args.device: print("Cannot specify both -d and -n", file=sys.stderr) sys.exit(1) while True: try: args.device = find_dev_by_name(args.name) except ValueError: logging.debug("Could not find device; trying again") time.sleep(2) continue else: logging.info("Found device: %s", args.device) break device = evdev.InputDevice(args.device) logging.debug("%s", device) logging.info("start loop") for highlevel_event in loop(device, args): logging.debug(highlevel_event) if highlevel_event not in keymaps: continue cmd = keymaps[highlevel_event] if type(cmd) is str: cmd = cmd.split() cmd[0] = os.path.expanduser(cmd[0]) try: Popen(cmd) except Exception as exc: logging.error("Error running %s: %s" % (cmd, str(exc))) if __name__ == "__main__": # listall() main()