diff options
Diffstat (limited to 'monitor_menu/__main__.py')
-rw-r--r-- | monitor_menu/__main__.py | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/monitor_menu/__main__.py b/monitor_menu/__main__.py new file mode 100644 index 0000000..d450ba8 --- /dev/null +++ b/monitor_menu/__main__.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +import argparse +import dataclasses +import json +import logging +from os.path import expanduser +import subprocess +from typing import Any + +import dialog + + +class NoMatchingProfile(ValueError): + pass + + +class LogLevel: + def __init__(self, value: str) -> None: + try: + self.numerical_level = logging.getLevelNamesMapping()[value.upper()] + except KeyError: + raise ValueError("invalid value") + + +@dataclasses.dataclass +class Monitor: + output: str + + xrandr_opts: list[str] = dataclasses.field(default_factory=list) + position: str | None = None + background: str | None = None + primary: bool = False + + @classmethod + def from_json_dict(cls, json_dict: dict[str, Any]) -> "Monitor": + def convert_key_name(name: str) -> str: + return name.replace("-", "_") + + return cls(**{convert_key_name(key): value for key, value in json_dict.items()}) + + +@dataclasses.dataclass +class Profile: + name: str + monitors: list[Monitor] = dataclasses.field(default_factory=list) + + identifier: str | None = None + xrandr_opts: list[str] = dataclasses.field(default_factory=list) + + @classmethod + def from_json_dict(cls, json_dict: dict[str, Any]) -> "Profile": + def convert_key_name(name: str) -> str: + return name.replace("-", "_") + + kwargs = {} + for key, value in json_dict.items(): + key = convert_key_name(key) + if key == "monitors": + value = [Monitor.from_json_dict(item) for item in value] + kwargs[key] = value + return cls(**kwargs) + + def apply(self): + # We build the command line starting from just "xrandr" and adding + # arguments. + xrandr_cmd = ['xrandr'] + feh_cmd = ['feh', '--bg-fill'] + + try: + xrandr_cmd += self.xrandr_opts + except KeyError: + pass + + for monitor in self.monitors: + xrandr_cmd.extend(monitor.xrandr_opts) + + if monitor.background is not None: + feh_cmd.append(monitor.background) + + logging.debug("Executing: %s", xrandr_cmd) + subprocess.run(xrandr_cmd, check=False) + logging.debug("Executing: %s", feh_cmd) + subprocess.run(feh_cmd, check=False) + + +class MonitorMenu: + def __init__(self, profiles: list[Profile]) -> None: + self.profiles = profiles + self.d = dialog.Dialog(autowidgetsize=True) + + def run(self): + choices = [] + + i = 0 + for p in self.profiles: + choices.append((str(i), p.name)) + i += 1 + + code, profile_idx = self.d.menu( + 'Select the profile you want to use.', + choices=choices) + + if code in (self.d.ESC, self.d.CANCEL): + return + + try: + self.profiles[int(profile_idx)].apply() + except IndexError: + raise NoMatchingProfile from None + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--log-level", "--loglevel", type=LogLevel, default="info") + subparser = parser.add_subparsers(dest="command") + subparser.add_parser("run") + apply_parser = subparser.add_parser("apply") + identifier_group = apply_parser.add_mutually_exclusive_group(required=True) + identifier_group.add_argument("--index", type=int) + identifier_group.add_argument("--id", type=str) + + args = parser.parse_args() + logging.basicConfig(level=args.log_level.numerical_level) + + config_file = '~/.config/monitor-profiles.json' + with open(expanduser(config_file)) as fh: + data = json.load(fh) + profiles = [Profile.from_json_dict(item) for item in data] + + match args.command: + case "run" | None: + MonitorMenu(profiles).run() + case "apply": + if args.index is not None: + try: + profiles[args.index].apply() + except IndexError: + raise NoMatchingProfile from None + else: + try: + next(filter(lambda p: p.identifier == args.id, profiles)).apply() + except StopIteration: + raise NoMatchingProfile from None + + +if __name__ == '__main__': + main() |