From c35ac6f8e56f042f0188a284d85a4700ccf37773 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Sat, 11 Feb 2023 19:47:01 +0100 Subject: Make it a debian package Signed-off-by: Olivier Gayot --- debian/changelog | 5 ++ debian/control | 16 +++++ debian/copyright | 32 ++++++++++ debian/rules | 4 ++ debian/source/format | 1 + monitor-menu.py | 148 ----------------------------------------------- monitor_menu/__init__.py | 0 monitor_menu/__main__.py | 148 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 + setup.cfg | 21 +++++++ 10 files changed, 230 insertions(+), 148 deletions(-) create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format delete mode 100755 monitor-menu.py create mode 100644 monitor_menu/__init__.py create mode 100644 monitor_menu/__main__.py create mode 100644 pyproject.toml create mode 100644 setup.cfg diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..d5c989e --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +monitor-menu (0.1) lunar; urgency=medium + + * Initial release. + + -- Olivier Gayot Sat, 11 Feb 2023 18:25:48 +0100 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..099a01b --- /dev/null +++ b/debian/control @@ -0,0 +1,16 @@ +Source: monitor-menu +Maintainer: Olivier Gayot +Section: python +Priority: optional +Build-Depends: debhelper-compat (= 13), + dh-python, + pybuild-plugin-pyproject, + python3-setuptools, + python3-all-dev, +Standards-Version: 4.5.1 + +Package: monitor-menu +Architecture: all +Pre-Depends: ${misc:Pre-Depends} +Depends: ${misc:Depends}, ${python3:Depends}, python3-dialog +Description: TUI application to manage monitor profiles diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..167257a --- /dev/null +++ b/debian/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: monitor-menu +Upstream-Contact: Olivier Gayot +Source: https://git.sigexec.com/cgit.cgi/monitor-menu.git + +Files: * +Copyright: Copyright (c) 2023 Olivier Gayot +License: BSD-3-clause + +License: BSD-3-clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + . + 1. Redistributions of source code must retain the copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..772500e --- /dev/null +++ b/debian/rules @@ -0,0 +1,4 @@ +#!/usr/bin/make -f + +%: + dh $@ --with python3 --buildsystem pybuild diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/monitor-menu.py b/monitor-menu.py deleted file mode 100755 index d450ba8..0000000 --- a/monitor-menu.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/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() diff --git a/monitor_menu/__init__.py b/monitor_menu/__init__.py new file mode 100644 index 0000000..e69de29 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() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fed528d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..828c630 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,21 @@ +[metadata] +name = monitor_menu +version = 0.1 +author = Olivier Gayot +author_email = olivier.gayot@sigexec.com + +description = TUI interface to manage monitor profiles +url = https://git.sigexec.com/cgit.cgi/monitor_menu.git +classifiers = + Programming Language :: Python :: 3 + License :: BSD-3 + Operating System :: POSIX :: Linux + +[options] +packages = find: +python_requires = >=3.10 + +[options.entry_points] +console_scripts = + monitor-menu = monitor_menu.__main__:main + -- cgit v1.2.3