Skip to content

Instantly share code, notes, and snippets.

@jrbergen
Last active September 6, 2022 17:23
Show Gist options
  • Save jrbergen/1e30d311ef395a1b1949e8d4737e6532 to your computer and use it in GitHub Desktop.
Save jrbergen/1e30d311ef395a1b1949e8d4737e6532 to your computer and use it in GitHub Desktop.
Script to change monitor config when using 4 specific monitors in a .:. configuration in x11 (terrible code)
"""Badly coded script to change monitor config when using 4 specific monitors in a .:. configuration in x11"""
from __future__ import annotations
import functools
import os
import re
import sys
import subprocess
import time
import warnings
from dataclasses import dataclass
from typing import Optional, TypeAlias, NamedTuple
try:
from rich import print
except ImportError:
pass
try:
from rich.markup import escape
except ImportError:
def escape(s: str) -> str:
return s
class MonitorNames:
LG_GL850: str = 'LG Electronics 27GL850'
SAMSUNG_C24FG7X: str = 'Samsung Electric Company C24FG7x'
ACER_G247HL: str = 'Acer Technologies G247HL'
ACER_V243HL: str = 'Acer Technologies V243HL'
PHYSICAL_LOCS: dict[str, str] = {
'LEFT': MonitorNames.ACER_G247HL,
'TOP': MonitorNames.ACER_V243HL,
'MID': MonitorNames.LG_GL850,
'RIGHT': MonitorNames.SAMSUNG_C24FG7X,
}
DEFAULT_CONFIG: str = 'all'
MonitorLocations: TypeAlias = tuple[
tuple[None, Optional[str], None],
tuple[Optional[str], Optional[str], Optional[str]]
]
CONFIGS: dict[str, MonitorLocations] = {
'off': (
(None, None, None),
(None, None, None)
),
'all': (
(None, 'TOP', None),
('LEFT', 'MID', 'RIGHT')
),
'3x1': (
(None, None, None),
('LEFT', 'MID', 'RIGHT')
),
'top': (
(None, 'TOP', None),
(None, None, None)
),
'2x1r': (
(None, None, None),
(None, 'MID', 'RIGHT')
),
'2x1l': (
(None, None, None),
('LEFT', 'MID', None)
),
'2x2r': (
(None, 'TOP', None),
(None, 'MID', 'RIGHT')
),
'2x2l': (
(None, 'TOP', None),
('LEFT', 'MID', None)
),
'1': (
(None, None, None),
(None, 'MID', None)
),
'1l': (
(None, None, None),
('LEFT', None, None)
),
'1r': (
(None, None, None),
(None, None, 'RIGHT')
),
'1x2': (
(None, 'TOP', None),
(None, 'MID', None)
),
}
CONFIGS['3x2'] = CONFIGS['all']
CONFIGS['0x2'] = CONFIGS['top']
CONFIGS['mid'] = CONFIGS['1']
CONFIGS['left'] = CONFIGS['1l']
CONFIGS['right'] = CONFIGS['1r']
CONFIGS['m'] = CONFIGS['1']
CONFIGS['l'] = CONFIGS['1l']
CONFIGS['r'] = CONFIGS['1r']
CONFIGS['t'] = CONFIGS['top']
CONFIGS['2r'] = CONFIGS['2x1r']
CONFIGS['2l'] = CONFIGS['2x1l']
CONFIGS['2x1'] = CONFIGS['2x1r']
CONFIGS['2x2'] = CONFIGS['2x2r']
CONFIGS['on'] = CONFIGS['all']
CONFIGS['ml'] = CONFIGS['lm'] = CONFIGS['2l']
CONFIGS['mr'] = CONFIGS['rm'] = CONFIGS['2r']
CONFIGS['tm'] = CONFIGS['mt'] = CONFIGS['1x2']
CONFIGS['lmr'] = CONFIGS['lrm'] = CONFIGS['mlr'] = CONFIGS['mrl'] = CONFIGS['rlm'] = CONFIGS['rml'] = CONFIGS['3x1']
CONFIGS['t'] = CONFIGS['top']
if DEFAULT_CONFIG not in CONFIGS:
raise RuntimeError(f"DEFAULT_CONFIG {DEFAULT_CONFIG!r} was not found in CONFIGS...")
for val in CONFIGS.values():
if not val[0][0] is None:
raise RuntimeError("Upper-left monitor not present")
if not val[0][2] is None:
raise RuntimeError("Upper-right monitor not present")
REX_DFP_ONLY: re.Pattern = re.compile(r'DFP-\d{1}')
REX_DFP_END: re.Pattern = re.compile(r'(?P<name>.+) \((?P<number>DFP-\d{1})\)')
REX_XRANDR: re.Pattern = re.compile(
(
r'.*(?P<xrandrname>DP-\d{1,2}|HDMI-\d{1,2}|USB-C-\d{1,2}|VGA\d{1,2}) connected.*\s*'
r'Identifier: (?P<id>.+)(?:[\s\S]*?)'
r'EDID:(?:[\s\S]*?)\s+(?P<edid>[\s|\S]*?)'
r'(?:CTM|BorderDimensions)(?:[\s\S]*?)'
r'ConnectorType: (?P<connector>.*)\s+'
r'ConnectorNumber: (?P<conn_num>\d{1,2})(?:[\s\S]*?)'
r'(?P<resx>\d{1,5})x(?P<res>\d{1,5}[\s|\S]*?)'
)
)
class Resolution(str):
pass
class RefreshRate(str):
pass
class Resolutions:
VGA: Resolution = "640x480"
SVGA: Resolution = "800x600"
XGA: Resolution = "1024x768"
HD720P: Resolution = "1280x720"
LAPTOP_1366 = "1366X768"
WSXGA_PLUS_1680: Resolution = "1680x1050"
FULLHD: Resolution = "1920x1080"
WQHD: Resolution = "2560x1440"
UHD: Resolution = "3840x2160"
class RefreshRates:
HZ60: RefreshRate = "60"
HZ144: RefreshRate = "144"
HZ120: RefreshRate = "120"
class EDID(str):
pass
EDIDS: dict[str, tuple[RefreshRate, EDID]] = {
MonitorNames.LG_GL850: (
RefreshRate('144.00'),
EDID(
"00ffffffffffff001e6d7f5b37b50000"
"061e0104b53c22789f8cb5af4f43ab26"
"0e5054254b007140818081c0a9c0b300"
"d1c08100d1cf28de0050a0a038500830"
"080455502100001a000000fd003090e6"
"e63c010a202020202020000000fc0032"
"37474c3835300a2020202020000000ff"
"003030364e54475931433339310a0128"
"02031a7123090607e305c000e6060501"
"60592846100403011f13565e00a0a0a0"
"29503020350055502100001a909b0050"
"a0a046500820880c555021000000b8bc"
"0050a0a055500838f80c55502100001a"
"00000000000000000000000000000000"
"00000000000000000000000000000000"
"0000000000000000000000000000001a"
)
),
MonitorNames.SAMSUNG_C24FG7X: (
RefreshRate('144.00'),
EDID(
"00ffffffffffff004c2d430e50475230"
"2f1c0104a5351e783b1c05b04e47a826"
"105054bfef8081c0810081809500a9c0"
"b300714f01015a8780a070384d403020"
"350014302100001a000000fd005a90a8"
"a823010a202020202020000000fc0043"
"3234464737780a2020202020000000ff"
"004854484b4230323935360a20200131"
"020315f148903f401f04130312230907"
"07830100000474801871382d40582c45"
"0014302100001e047480d072382d4010"
"2c458014302100001e023a801871382d"
"40582c450014302100001e0000000000"
"00000000000000000000000000000000"
"00000000000000000000000000000000"
"0000000000000000000000000000008a"
)
),
MonitorNames.ACER_G247HL: (
RefreshRate('60.00'),
EDID(
"00ffffffffffff000472fa0337210143"
"1e18010380351e78ca3f60a455529e27"
"0d5054bfef80714f8140818081c08100"
"9500b300d1c0023a801871382d40582c"
"4500132b2100001e000000fd00384c1f"
"5011000a202020202020000000fc0047"
"323437484c0a202020202020000000ff"
"005430564545303031383534300a01f4"
"020324f14f9002030405060701111213"
"1415161f230907078301000067030c00"
"1000382d023a801871382d40582c4500"
"132b2100001f011d8018711c1620582c"
"2500132b2100009f011d007251d01e20"
"6e285500132b2100001e8c0ad08a20e0"
"2d10103e9600132b2100001800000000"
"0000000000000000000000000000007e"
)
),
MonitorNames.ACER_V243HL: (
RefreshRate('60.00'),
EDID(
"00ffffffffffff000472be00438e6004"
"2e14010308351d78ea6085a6564a9c25"
"125054b30c00714f81009500d1c08180"
"010101010101023a801871382d40582c"
"4500132a2100001a000000fc00563234"
"33484c0a202020202020000000fd0037"
"4b1e5012000a202020202020000000ff"
"004c474b3043303136343032420a00c6"
)
),
}
@functools.lru_cache(maxsize=8)
def name_from_edid(edid: EDID | str) -> str:
for k, v in EDIDS.items():
if edid == v[1]:
return k
raise RuntimeError(f"Couldn't find name associated with EDID: {edid!r}")
@functools.lru_cache(maxsize=8)
def edid_from_name(name: str) -> EDID:
try:
return EDIDS[name][1]
except KeyError:
raise RuntimeError(f"Couldn't find EDID associated with name {name!r}")
@functools.lru_cache(maxsize=8)
def refresh_rate_from_edid(edid: EDID | str) -> RefreshRate:
for k, v in EDIDS.items():
if edid == v[1]:
return RefreshRate(v[0])
raise RuntimeError("Couldn't find refresh rate...")
@dataclass
class Monitor:
name: str
identifier: str
dp: str
connector_type: str
connector_num: str
refresh_rate: RefreshRate
resx: str
resy: str
@classmethod
def from_xrandr_tup(cls, xrandr_tup: tuple[str, str, EDID, str, str, str, str]) -> Monitor:
dp_, id_, edid_, conn_type, conn_num, resx_, resy_ = xrandr_tup
edid_ = edid_.replace('\n', '').replace('\t', '').replace(' ', '')
return Monitor(
name=name_from_edid(edid=edid_),
identifier=id_,
dp=dp_,
connector_type=conn_type,
connector_num=conn_num,
refresh_rate=refresh_rate_from_edid(edid=edid_),
resx=resx_,
resy=resy_,
)
@property
def edid(self, ) -> EDID:
return edid_from_name(name=self.name)
def model(self) -> str:
return self.name.split(' ')[-1]
def __post_init__(self):
self.refresh_rate = RefreshRate(f"{float(self.refresh_rate):.2f}")
def monitors_from_xrandr_output() -> list[Monitor]:
xrandr_raw: str = subprocess.check_output(["xrandr", "--verbose"]).decode('utf8')
monitor_rex = REX_XRANDR.findall(xrandr_raw)
monitors = []
for monitor_tup in monitor_rex:
monitors.append(Monitor.from_xrandr_tup(monitor_tup))
return monitors
class MonitorConfigTup(NamedTuple):
config: MonitorLocations
primary_mon: str
def parse_args(args) -> MonitorConfigTup:
primary: str = 'MID'
try:
config_keyw = args[0]
if len(args) >= 2:
primary = args[1]
except IndexError:
config_keyw = DEFAULT_CONFIG
warnings.warn(f"No monitor config provided; defaulting to {config_keyw!r}", UserWarning)
try:
config = CONFIGS[config_keyw]
except KeyError as err:
raise KeyError(f"Unknown config keyword/ID: {config_keyw!r}.") from err
return MonitorConfigTup(config, primary)
def get_xrandr_cmd(
mon_locs: MonitorLocations,
primary: str,
monitors: list[Monitor],
print_cmd: bool = True,
) -> list[str]:
cmd: list[str] = ['xrandr']
mon_l, mon_m, mon_r, mon_t = [None] * 4
if mon_locs[1][0]:
mon_l = [mon for mon in monitors if mon.name == PHYSICAL_LOCS['LEFT']]
mon_l = mon_l[0] if mon_l else None
if mon_locs[1][1]:
mon_m = [mon for mon in monitors if mon.name == PHYSICAL_LOCS['MID']]
mon_m = mon_m[0] if mon_m else None
if mon_locs[1][2]:
mon_r = [mon for mon in monitors if mon.name == PHYSICAL_LOCS['RIGHT']]
mon_r = mon_r[0] if mon_r else None
if mon_locs[0][1]:
mon_t = [mon for mon in monitors if mon.name == PHYSICAL_LOCS['TOP']]
mon_t = mon_t[0] if mon_t else None
mon_t_offset_y = 0
if mon_m and mon_t:
mon_t_offset_y = -int(mon_t.resy)
elif any((mon_l, mon_r)):
mon_t_offset_y = max(
-int(mon_l.resy) if mon_l is not None else 0,
-int(mon_r.resy) if mon_r is not None else 0
)
cur_x_offset = 0
mon_t_offset_x = 0
for iimon, (mon, loc) in enumerate(zip([mon_l, mon_m, mon_r], ['LEFT', 'MID', 'RIGHT'])):
if (mon_m and iimon == 1) or not mon_m:
if not mon_m:
if mon_l:
mon_t_offset_x = int(mon_l.resx)
elif mon_r:
mon_t_offset_x = int(mon_r.resx)
else:
mon_t_offset_x = 0
else:
mon_t_offset_x = int(cur_x_offset)
if mon is None:
continue
# Offset to make screens at sides align with bottom of neighboring heigher screen.
flanking_mon_offset_y = 0
if mon_m:
if loc == 'LEFT' and mon_l:
flanking_mon_offset_y += round(int(mon_m.resy) - int(mon_l.resy))
elif loc == 'RIGHT' and mon_r:
flanking_mon_offset_y += round(int(mon_m.resy) - int(mon_r.resy))
cur_moncmd = [
"--output", mon.dp,
"--mode", f"{mon.resx}x{mon.resy}",
"--pos", f"{cur_x_offset}x{flanking_mon_offset_y}",
"--rotate", "normal",
"-r", f"{mon.refresh_rate!r}",
]
if loc == primary:
cur_moncmd.append("--primary")
cur_x_offset += int(mon.resx)
cmd.extend(cur_moncmd)
if mon_t:
if mon_m:
# Align top monitor to middle of top screen.
mon_t_offset_x += round((int(mon_m.resx) - int(mon_t.resx)) / 2)
cur_moncmd = [
"--output", mon_t.dp,
"--mode", f"{mon_t.resx}x{mon_t.resy}",
"--pos", f"{mon_t_offset_x}x{mon_t_offset_y}",
"--rotate", "normal",
"-r", f"{mon_t.refresh_rate!r}",
]
cmd.extend(cur_moncmd)
for loc1, mon in zip(('LEFT', 'MID', 'RIGHT', 'TOP'), (mon_l, mon_m, mon_r, mon_t)):
if mon is None:
try:
new_mon = [mon for mon in monitors if mon.name == PHYSICAL_LOCS[loc1]][0]
except IndexError:
print(f"Monitor at position {loc1} is probably unplugged. Skipping its command.")
continue
cmd.extend(["--output", new_mon.dp, "--off"])
if print_cmd:
print(escape(f"Command: \n\t {' '.join(cmd)}"))
if len(cmd) <= 1:
raise RuntimeError("No xrandr args could be generated somehow...")
return cmd
def main(args: list[str]):
mon_locs_, primary_ = parse_args(args=args)
monitors_: list[Monitor] = monitors_from_xrandr_output()
try:
print(f"Changing monitor setup to {sys.argv[1]!r}")
except IndexError:
print(f"Changing monitor setup to default: {DEFAULT_CONFIG!r}")
cmdlist = get_xrandr_cmd(mon_locs=mon_locs_, primary=primary_, monitors=monitors_)
cmd = ' '.join(cmdlist)
os.system(cmd)
def run_tests(wait_between_sec: int = 8):
start_time = time.time()
#PRIMARIES = ('LEFT', 'MID', 'RIGHT', 'TOP')
PRIMARIES = ('MID',)
monitors_: list[Monitor] = monitors_from_xrandr_output()
print(f"Testing {len(CONFIGS)} configurations with {len(PRIMARIES)} primaries...")
print(f"Total no. configurations: {len(CONFIGS)*len(PRIMARIES)}")
print(f"Tests will take approx. {len(CONFIGS) * len(PRIMARIES) * wait_between_sec} seconds...")
time.sleep(4)
print("waiting 4 seconds before starting test...")
for iip, primary_ in enumerate(PRIMARIES):
for iia, arg in enumerate(CONFIGS):
if arg != 'off':
args = [arg, primary_]
mon_locs_, primary_ = parse_args(args)
print(f"Testing config {arg!r} with primary {primary_!r}...")
else:
args = [arg]
mon_locs_, primary_ = parse_args(args)
print(f"Testing config {arg!r} with NO primary...")
try:
print(f"Changing monitor setup to {arg!r}")
except IndexError:
print(f"Changing monitor setup to default: {DEFAULT_CONFIG!r}")
cmdlist = get_xrandr_cmd(mon_locs=mon_locs_, primary=primary_, monitors=monitors_)
cmd = ' '.join(cmdlist)
os.system(cmd)
if (iip+1)*(iia+1) != len(PRIMARIES) * len(CONFIGS):
print(f"Waiting {wait_between_sec} seconds...")
time.sleep(wait_between_sec)
print("Tests completed! Returning to state with all monitors enabled!")
args = ["all", "MID"]
mon_locs_, primary_ = parse_args(args)
cmdlist = get_xrandr_cmd(mon_locs=mon_locs_, primary=primary_, monitors=monitors_)
cmd = ' '.join(cmdlist)
os.system(cmd)
print(f"All tests including reset to 'all' state took {time.time()-start_time} seconds.")
if __name__ == '__main__':
DEFAULT_TEST_WAIT: int = 15
try:
if sys.argv[1].lower() in ('help', '--help', '-help'):
print("Tool for changing monitor configurations for 4 specific monitors in a .:. configuration.\n\n")
print("monitor [CONFIG_KEY] [PRIMARY_MONITOR]\n\n")
print("Config key values:\n")
print(f"\t {', '.join((f'{x!r}' for x in CONFIGS))}")
print("\nPRIMARY_MONITOR values:\n ")
print(f"\t {', '.join((f'{x!r}' for x in ('LEFT', 'MID', 'RIGHT', 'TOP')))}\n")
sys.exit(0)
elif sys.argv[1] == 'test':
if len(sys.argv) > 2:
wait_between_sec_ = int(sys.argv[2])
else:
wait_between_sec_ = DEFAULT_TEST_WAIT
run_tests(wait_between_sec=wait_between_sec_)
else:
main(args=sys.argv[1:])
except KeyboardInterrupt:
# Reset monitors to mid
main(args=['mid', 'MID'])
except Exception as err:
print(f"ERROR OCCURED; attempting restore to 'mid'... (error: {str(err)!r})")
try:
main(args=['mid', 'MID'])
except Exception as err:
print(f"ERROR occured trying to restore; exiting...")
raise Exception(f"ERROR occured trying to restore; exiting...") from err
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment