1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
#!/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)
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")
parser.add_argument("--index", type=int, required=False)
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]
if args.index is None:
MonitorMenu(profiles).run()
else:
try:
profiles[args.index].apply()
except IndexError:
raise NoMatchingProfile from None
if __name__ == '__main__':
main()
|