From 6135fa569cbc7c5843a8fa03f679249789fe7cb5 Mon Sep 17 00:00:00 2001 From: Daniel Langbein Date: Mon, 23 May 2022 12:40:17 +0200 Subject: [PATCH] json configuration --- example.cfg | 13 ---- example.json | 6 ++ rotate-screen.py | 185 +++++++++++++++++++++++++++++++++-------------- 3 files changed, 136 insertions(+), 68 deletions(-) delete mode 100644 example.cfg create mode 100644 example.json diff --git a/example.cfg b/example.cfg deleted file mode 100644 index 4e0ce05..0000000 --- a/example.cfg +++ /dev/null @@ -1,13 +0,0 @@ -[eDP] - -[HDMI-A-0] - -[eDP-1] - -1 = pointer:ELAN9038:00 04F3:2A1C -2 = pointer:ELAN9038:00 04F3:2A1C Stylus Pen (0) -3 = pointer:ELAN9038:00 04F3:2A1C Stylus Eraser (0) - -4 = pointer:ELAN9038:00 04F3:2A1C touch -5 = pointer:ELAN9038:00 04F3:2A1C stylus -6 = pointer:ELAN9038:00 04F3:2A1C eraser diff --git a/example.json b/example.json new file mode 100644 index 0000000..cdd3c26 --- /dev/null +++ b/example.json @@ -0,0 +1,6 @@ +{ + "screens": ["eDP", "HDMI-A-0", "eDP-1"], + "devices": [ + {"screen": "eDP-1", "name_contains": "ELAN9038:00"} + ] +} diff --git a/rotate-screen.py b/rotate-screen.py index 8dea8bc..f6b3464 100644 --- a/rotate-screen.py +++ b/rotate-screen.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import configparser +import json import subprocess import re from pathlib import Path @@ -10,6 +10,9 @@ orientations = ['normal', 'right', 'inverted', 'left'] class Screen(NamedTuple): + """ + The xrandr-name of a screen and a list of xinput-device-names that shall be mapped to the screen. + """ name: str devices: list[str] @@ -19,80 +22,152 @@ def main(): def rotate_clockwise(): - cfg = get_cfg() - - # screens from cfg that are connected - screens = [Screen(name=name, devices=list(cfg[name].values())) - for name in cfg - if name != cfg.default_section - and is_connected(name)] + # Screens from configuration that are connected. + screens = [screen for screen in Config().get_screens() + if Xrandr.is_connected(screen.name)] if len(screens) == 0: raise Exception('None of the configured screens are connected.') - current_orientation = get_current_orientation(screens[0].name) + current_orientation = Xrandr.get_orientation(screens[0].name) next_orientation = orientations[(orientations.index(current_orientation) + 1) % len(orientations)] for screen in screens: rotate(screen, next_orientation) -def get_cfg() -> configparser.ConfigParser: - config: configparser.ConfigParser = configparser.ConfigParser() - config.read(get_cfg_path()) - return config - - -def get_cfg_path() -> Path: - global_path = Path('/etc/rotate-screen.cfg') - if global_path.exists(): - return global_path - - local_path = Path('example.cfg') - if local_path.exists(): - return local_path - - raise Exception('No configuration file found.') - - def rotate(screen: Screen, orientation): - execute(['xrandr', '--output', screen.name, '--rotate', orientation]) + Xrandr.rotate(screen.name, orientation) for device in screen.devices: - execute(['xinput', '--map-to-output', device, screen.name]) + Xinput.map_to_output(device, screen.name) -def get_current_orientation(screen: str): +class Config: """ - @precond: is_connected(screen) = True + Json structure: - Example: - - stdout includes line: eDP connected primary 2880x1620+0+0 (0x55) normal (normal left inverted right x axis y axis) 344mm x 194mm - - screen: eDP - - returns: normal + { + # List of screen names from xrandr: + "screens": ["eDP", "HDMI-A-0", "eDP-1"] - Example: - - stdout includes line: eDP-1 connected 1920x1280+0+0 (0x46) normal (normal left inverted right x axis y axis) 222mm x 148mm - - screen: eDP-1 - - returns: normal + # List of devices from xinput that shall be mapped to a screen. + "devices": [ + # A device can be identified by its exact name through `name` ... + {"screen": "eDP-1", "name": "ELAN9038:00"}, + # ... or by `name_contains`. + {"screen": "eDP-1", "name_contains": "ELAN9038"} + ] + } """ - stdout = execute(['xrandr', '--query', '--verbose']) - # pattern = re.compile(rf'^{re.escape(screen)} .* \([^\)]+\) (\S+) \([^\)]+\) .*$', flags=re.MULTILINE) - pattern = re.compile(rf'^{re.escape(screen)} connected [^\(]+ \([^\)]+\) (\S+) \([^\)]+\) [^\(]+$', flags=re.MULTILINE) - match = pattern.search(stdout) - if match is None: raise Exception(f'Did not find screen {screen} in stdout:\n{stdout}') - return match.group(1) + + def __init__(self): + self.screens, self.devices = self.load_json() + + def get_screens(self) -> list[Screen]: + return [Screen(name=screen, devices=self.get_devices_for(screen)) for screen in self.screens] + + def get_devices_for(self, screen: str) -> list[str]: + device_names = [] + + for device in self.devices: + if device["screen"] != screen: + continue + if "name" in device: + device_names.append(device["name"]) + if "name_contains" in device: + for x_dev in Xinput.get_device_names(): + if device["name_contains"] in x_dev: + device_names.append(x_dev) + + return device_names + + def load_json(self) -> tuple[list[str], list]: + j = json.loads(self.get_cfg_path().read_text()) + if "screens" not in j: + raise Exception("'screens' array missing in cfg.") + screens = j["screens"] + + if "devices" not in j: + raise Exception("'devices' array missing in cfg.") + devices = j["devices"] + for device in devices: + if not "name" in device and not "name_contains" in device: + raise Exception("Device must have 'name' or 'name_contains'.") + + return screens, devices + + @classmethod + def get_cfg_path(cls) -> Path: + global_path = Path('/etc/rotate-screen.json') + if global_path.exists(): + return global_path + + local_path = Path('example.json') + if local_path.exists(): + return local_path + + raise Exception('No configuration file found.') -def is_connected(screen: str): - """ - Example: - - stdout includes line: eDP connected primary 2880x1620+0+0 (normal left inverted right x axis y axis) 344mm x 194mm - - returns: True - """ - stdout = execute(['xrandr']) - pattern = re.compile(rf'^({re.escape(screen)}\sconnected\s.*)$', flags=re.MULTILINE) - match = pattern.search(stdout) - return match is not None +class Xrandr: + @classmethod + def rotate(cls, screen: str, orientation: str): + execute(['xrandr', '--output', screen, '--rotate', orientation]) + + @classmethod + def get_orientation(cls, screen: str): + """ + @precond: is_connected(screen) = True + + Example: + - stdout includes line: eDP connected primary 2880x1620+0+0 (0x55) normal (normal left inverted right x axis y axis) 344mm x 194mm + - screen: eDP + - returns: normal + + Example: + - stdout includes line: eDP-1 connected 1920x1280+0+0 (0x46) normal (normal left inverted right x axis y axis) 222mm x 148mm + - screen: eDP-1 + - returns: normal + """ + stdout = execute(['xrandr', '--query', '--verbose']) + # pattern = re.compile(rf'^{re.escape(screen)} .* \([^\)]+\) (\S+) \([^\)]+\) .*$', flags=re.MULTILINE) + pattern = re.compile(rf'^{re.escape(screen)} connected [^\(]+ \([^\)]+\) (\S+) \([^\)]+\) [^\(]+$', + flags=re.MULTILINE) + match = pattern.search(stdout) + if match is None: raise Exception(f'Did not find screen {screen} in stdout:\n{stdout}') + return match.group(1) + + @classmethod + def is_connected(cls, screen: str): + """ + Example: + - stdout includes line: eDP connected primary 2880x1620+0+0 (normal left inverted right x axis y axis) 344mm x 194mm + - returns: True + """ + stdout = execute(['xrandr']) + pattern = re.compile(rf'^({re.escape(screen)} connected .*)$', flags=re.MULTILINE) + match = pattern.search(stdout) + return match is not None + + +class Xinput: + @classmethod + def map_to_output(cls, device: str, screen: str): + execute(['xinput', '--map-to-output', device, screen]) + + @classmethod + def get_device_tuples(cls) -> list[(int, str)]: + ids = cls.get_device_ids() + names = [execute(['xinput', 'list', '--name-only', id_]).strip() for id_ in ids] + return [(id_, name) for id_, name in zip(ids, names)] + + @classmethod + def get_device_names(cls) -> list[str]: + return execute(['xinput', 'list', '--name-only']).strip().splitlines() + + @classmethod + def get_device_ids(cls) -> list[int]: + return [int(id_) for id_ in execute(['xinput', 'list', '--id-only']).strip().splitlines()] def execute(command: list[str]) -> str: