summaryrefslogtreecommitdiff
path: root/monitor_menu
diff options
context:
space:
mode:
Diffstat (limited to 'monitor_menu')
-rw-r--r--monitor_menu/__init__.py0
-rw-r--r--monitor_menu/__main__.py148
2 files changed, 148 insertions, 0 deletions
diff --git a/monitor_menu/__init__.py b/monitor_menu/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/monitor_menu/__init__.py
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()