Last active
July 5, 2022 23:05
-
-
Save Staninna/85e7fee88cfeb98932b83f5fbffb3850 to your computer and use it in GitHub Desktop.
`autoxrandr` is the main script `_get_xrandr_command` is a helper script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# Modified from https://gitlab.com/arandr/arandr and put all necessary things in 1 file | |
# Still recommended to use arandr to set the configurations to save/load | |
from math import pi | |
import os | |
import subprocess | |
from functools import reduce | |
import warnings | |
class Size(tuple): | |
"""2-tuple of width and height that can be created from a '<width>x<height>' string""" | |
def __new__(cls, arg): | |
if isinstance(arg, str): | |
arg = [int(x) for x in arg.split("x")] | |
arg = tuple(arg) | |
assert len(arg) == 2 | |
return super(Size, cls).__new__(cls, arg) | |
width = property(lambda self: self[0]) | |
height = property(lambda self: self[1]) | |
def __str__(self): | |
return "%dx%d" % self | |
class NamedSize: | |
"""Object that behaves like a size, but has an additional name attribute""" | |
def __init__(self, size, name): | |
self._size = size | |
self.name = name | |
width = property(lambda self: self[0]) | |
height = property(lambda self: self[1]) | |
def __str__(self): | |
if "%dx%d" % (self.width, self.height) in self.name: | |
return self.name | |
return "%s (%dx%d)" % (self.name, self.width, self.height) | |
def __iter__(self): | |
return self._size.__iter__() | |
def __getitem__(self, i): | |
return self._size[i] | |
def __len__(self): | |
return 2 | |
class Position(tuple): | |
"""2-tuple of left and top that can be created from a '<left>x<top>' string""" | |
def __new__(cls, arg): | |
if isinstance(arg, str): | |
arg = [int(x) for x in arg.split("x")] | |
arg = tuple(arg) | |
assert len(arg) == 2 | |
return super(Position, cls).__new__(cls, arg) | |
left = property(lambda self: self[0]) | |
top = property(lambda self: self[1]) | |
def __str__(self): | |
return "%dx%d" % self | |
class Geometry(tuple): | |
"""4-tuple of width, height, left and top that can be created from an XParseGeometry style string""" | |
# FIXME: use XParseGeometry instead of an own incomplete implementation | |
def __new__(cls, width, height=None, left=None, top=None): | |
if isinstance(width, str): | |
width, rest = width.split("x") | |
height, left, top = rest.split("+") | |
return super(Geometry, cls).__new__(cls, (int(width), int(height), int(left), int(top))) | |
def __str__(self): | |
return "%dx%d+%d+%d" % self | |
width = property(lambda self: self[0]) | |
height = property(lambda self: self[1]) | |
left = property(lambda self: self[2]) | |
top = property(lambda self: self[3]) | |
position = property(lambda self: Position(self[2:4])) | |
size = property(lambda self: Size(self[0:2])) | |
class Rotation(str): | |
"""String that represents a rotation by a multiple of 90 degree""" | |
def __init__(self, _original_me): | |
super().__init__() | |
if self not in ('left', 'right', 'normal', 'inverted'): | |
raise Exception("No know rotation.") | |
is_odd = property(lambda self: self in ('left', 'right')) | |
_angles = {'left': pi / 2, 'inverted': pi, 'right': 3 * pi / 2, 'normal': 0} | |
angle = property(lambda self: Rotation._angles[self]) | |
def __repr__(self): | |
return '<Rotation %s>' % self | |
class Feature: | |
PRIMARY = 1 | |
class XRandR: | |
configuration = None | |
state = None | |
def __init__(self, display=None, force_version=False): | |
"""Create proxy object and check for xrandr at `display`. Fail with | |
untested versions unless `force_version` is True.""" | |
self.environ = dict(os.environ) | |
if display: | |
self.environ['DISPLAY'] = display | |
version_output = self._output("--version") | |
supported_versions = ["1.2", "1.3", "1.4", "1.5"] | |
if not any(x in version_output for x in supported_versions) and not force_version: | |
raise Exception("XRandR %s required." % | |
"/".join(supported_versions)) | |
self.features = set() | |
if " 1.2" not in version_output: | |
self.features.add(Feature.PRIMARY) | |
#################### calling xrandr #################### | |
def _output(self, *args): | |
proc = subprocess.Popen( | |
("xrandr",) + args, | |
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.environ | |
) | |
ret, err = proc.communicate() | |
status = proc.wait() | |
if status != 0: | |
raise Exception("XRandR returned error code %d: %s" % | |
(status, err)) | |
if err: | |
warnings.warn( | |
"XRandR wrote to stderr, but did not report an error (Message was: %r)" % err) | |
return ret.decode('utf-8') | |
#################### loading #################### | |
def load_from_x(self): # FIXME -- use a library | |
self.configuration = self.Configuration(self) | |
self.state = self.State() | |
screenline, items = self._load_raw_lines() | |
self._load_parse_screenline(screenline) | |
for headline, details in items: | |
if headline.startswith(" "): | |
continue # a currently disconnected part of the screen i can't currently get any info out of | |
if headline == "": | |
continue # noise | |
headline = headline.replace( | |
'unknown connection', 'unknown-connection') | |
hsplit = headline.split(" ") | |
output = self.state.Output(hsplit[0]) | |
assert hsplit[1] in ( | |
"connected", "disconnected", 'unknown-connection') | |
output.connected = (hsplit[1] in ('connected', 'unknown-connection')) | |
primary = False | |
if 'primary' in hsplit: | |
if Feature.PRIMARY in self.features: | |
primary = True | |
hsplit.remove('primary') | |
if not hsplit[2].startswith("("): | |
active = True | |
geometry = Geometry(hsplit[2]) | |
# modeid = hsplit[3].strip("()") | |
if hsplit[4] in ROTATIONS: | |
current_rotation = Rotation(hsplit[4]) | |
else: | |
current_rotation = NORMAL | |
else: | |
active = False | |
geometry = None | |
# modeid = None | |
current_rotation = None | |
output.rotations = set() | |
for rotation in ROTATIONS: | |
if rotation in headline: | |
output.rotations.add(rotation) | |
currentname = None | |
for detail, w, h in details: | |
name, _mode_raw = detail[0:2] | |
mode_id = _mode_raw.strip("()") | |
try: | |
size = Size([int(w), int(h)]) | |
except ValueError: | |
raise Exception( | |
"Output %s parse error: modename %s modeid %s." % (output.name, name, mode_id) | |
) | |
if "*current" in detail: | |
currentname = name | |
for x in ["+preferred", "*current"]: | |
if x in detail: | |
detail.remove(x) | |
for old_mode in output.modes: | |
if old_mode.name == name: | |
if tuple(old_mode) != tuple(size): | |
warnings.warn(( | |
"Supressing duplicate mode %s even " | |
"though it has different resolutions (%s, %s)." | |
) % (name, size, old_mode)) | |
break | |
else: | |
# the mode is really new | |
output.modes.append(NamedSize(size, name=name)) | |
self.state.outputs[output.name] = output | |
self.configuration.outputs[output.name] = self.configuration.OutputConfiguration( | |
active, primary, geometry, current_rotation, currentname | |
) | |
def _load_raw_lines(self): | |
output = self._output("--verbose") | |
items = [] | |
screenline = None | |
for line in output.split('\n'): | |
if line.startswith("Screen "): | |
assert screenline is None | |
screenline = line | |
elif line.startswith('\t'): | |
continue | |
elif line.startswith(2 * ' '): # [mode, width, height] | |
line = line.strip() | |
if reduce(bool.__or__, [line.startswith(x + ':') for x in "hv"]): | |
line = line[-len(line):line.index(" start") - len(line)] | |
items[-1][1][-1].append(line[line.rindex(' '):]) | |
else: # mode | |
items[-1][1].append([line.split()]) | |
else: | |
items.append([line, []]) | |
return screenline, items | |
def _load_parse_screenline(self, screenline): | |
assert screenline is not None | |
ssplit = screenline.split(" ") | |
ssplit_expect = ["Screen", None, "minimum", None, "x", None, | |
"current", None, "x", None, "maximum", None, "x", None] | |
assert all(a == b for (a, b) in zip( | |
ssplit, ssplit_expect) if b is not None) | |
self.state.virtual = self.state.Virtual( | |
min_mode=Size((int(ssplit[3]), int(ssplit[5][:-1]))), | |
max_mode=Size((int(ssplit[11]), int(ssplit[13]))) | |
) | |
self.configuration.virtual = Size( | |
(int(ssplit[7]), int(ssplit[9][:-1])) | |
) | |
#################### saving #################### | |
def save_to_shellscript_string(self, template=None, additional=None): | |
""" | |
Return a shellscript that will set the current configuration. | |
Output can be parsed by load_from_string. | |
You may specify a template, which must contain a %(xrandr)s parameter | |
and optionally others, which will be filled from the additional dictionary. | |
""" | |
if not template: | |
template = self.DEFAULTTEMPLATE | |
template = '\n'.join(template) + '\n' | |
data = { | |
'xrandr': "xrandr " + " ".join(self.configuration.commandlineargs()) | |
} | |
if additional: | |
data.update(additional) | |
return template % data | |
#################### sub objects #################### | |
class State: | |
"""Represents everything that can not be set by xrandr.""" | |
virtual = None | |
def __init__(self): | |
self.outputs = {} | |
class Virtual: | |
def __init__(self, min_mode, max_mode): | |
self.min = min_mode | |
self.max = max_mode | |
class Output: | |
rotations = None | |
connected = None | |
def __init__(self, name): | |
self.name = name | |
self.modes = [] | |
def __repr__(self): | |
return '<%s %r (%d modes)>' % (type(self).__name__, self.name, len(self.modes)) | |
class Configuration: | |
""" | |
Represents everything that can be set by xrandr | |
(and is therefore subject to saving and loading from files) | |
""" | |
virtual = None | |
def __init__(self, xrandr): | |
self.outputs = {} | |
self._xrandr = xrandr | |
def __repr__(self): | |
return '<%s for %d Outputs, %d active>' % ( | |
type(self).__name__, len(self.outputs), | |
len([x for x in self.outputs.values() if x.active]) | |
) | |
def commandlineargs(self): | |
args = [] | |
for output_name, output in self.outputs.items(): | |
args.append("--output") | |
args.append(output_name) | |
if not output.active: | |
args.append("--off") | |
else: | |
if Feature.PRIMARY in self._xrandr.features: | |
if output.primary: | |
args.append("--primary") | |
args.append("--mode") | |
args.append(str(output.mode.name)) | |
args.append("--pos") | |
args.append(str(output.position)) | |
args.append("--rotate") | |
args.append(output.rotation) | |
return args | |
class OutputConfiguration: | |
def __init__(self, active, primary, geometry, rotation, modename): | |
self.active = active | |
self.primary = primary | |
if active: | |
self.position = geometry.position | |
self.rotation = rotation | |
if rotation.is_odd: | |
self.mode = NamedSize( | |
Size(reversed(geometry.size)), name=modename) | |
else: | |
self.mode = NamedSize(geometry.size, name=modename) | |
size = property(lambda self: NamedSize( | |
Size(reversed(self.mode)), name=self.mode.name | |
) if self.rotation.is_odd else self.mode) | |
LEFT = Rotation('left') | |
RIGHT = Rotation('right') | |
INVERTED = Rotation('inverted') | |
NORMAL = Rotation('normal') | |
ROTATIONS = (NORMAL, RIGHT, INVERTED, LEFT) | |
current = XRandR() | |
current.load_from_x() | |
print(current.save_to_shellscript_string(["%(xrandr)s"]).strip()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Set some variables | |
config_dir="$HOME/.config/autoxrandr" | |
config_file="$config_dir/screenlayouts" | |
sep="{!!}" | |
# Check and make config files | |
[ ! -d "$config_dir" ] && mkdir "$config_dir" | |
[ ! -f "$config_file" ] && touch "$config_file" | |
# Get the setup indentefier | |
get_monitors_id() { | |
monitors_id="" | |
local print="false" | |
# Loop over all lines in the command output | |
for line in $(xrandr --prop); do | |
# Disable collecting | |
if [[ "$line" =~ [[:upper:]] ]]; then | |
print="false" | |
fi | |
# Collect indentify data | |
if [[ $print == "true" ]]; then | |
monitors_id+="$line" | |
fi | |
# Start collecting | |
if [[ "$line" == "EDID:" ]]; then | |
print="true" | |
fi | |
done | |
} | |
# Get current xrandr command using extern python script | |
get_current_command() { | |
xrandr_command="$(python "$(dirname "${BASH_SOURCE[0]}")"/_get_xrandr_command)" | |
} | |
# Save current xrandr config to name | |
if [[ "$1" == "--save" ]]; then | |
if [[ $# -eq 2 ]]; then | |
# Get current configeration | |
get_monitors_id | |
get_current_command | |
# Check for dupes in the config file | |
conflict="false" | |
while read -r line; do | |
# Find monitors and names in file | |
found_id=$(echo "$line" | awk -F $sep '{printf "%s", $2}') | |
found_name=$(echo "$line" | awk -F $sep '{printf "%s", $1}') | |
# Check monitors configeration is already in config | |
if [[ "$found_id" == "$monitors_id" ]]; then | |
echo "This monitor configeration is already saved under $found_name" | |
conflict="true" | |
fi | |
# Check name is already in config | |
if [[ "$found_name" == "$2" ]]; then | |
echo "This config name is already used please use a diffrent one" | |
conflict="true" | |
fi | |
done <"$config_file" | |
# Exit is variables conflict with each other | |
if [[ "$conflict" == "true" ]]; then | |
exit 1 | |
fi | |
# Save to config file | |
printf "%s%s%s%s%s\n" "$2" "$sep" "$monitors_id" "$sep" "$xrandr_command" >>"$config_file" | |
# Didn't receive a config name | |
else | |
echo "Supply a config name as an argument" | |
echo "For example \`./auto_xrandr.sh --save example\`" | |
fi | |
# Automattomaticly change xrandr | |
elif [[ "$1" == "--auto" ]]; then | |
get_monitors_id | |
while read -r line; do | |
# Find monitors in file | |
found_id=$(echo "$line" | awk -F $sep '{printf "%s", $2}') | |
# Check monitors name with found monitors | |
if [[ "$found_id" == "$monitors_id" ]]; then | |
# Show dectected profile | |
found_name=$(echo "$line" | awk -F $sep '{printf "%s", $1}') | |
echo "Loaded $found_name..." | |
# Loads xrandr command | |
eval "$(echo "$line" | awk -F $sep '{printf "%s", $3}')" | |
exit 0 | |
fi | |
done <"$config_file" | |
# Couldn't find current monitors | |
echo "Couldn't find monitor configuration" | |
# Load config | |
elif [[ "$1" == "--load" ]]; then | |
if [[ $# -eq 2 ]]; then | |
while read -r line; do | |
# Find config name in file | |
found_name=$(echo "$line" | awk -F $sep '{printf "%s", $1}') | |
# Check config name with config name in file | |
if [[ "$found_name" == "$2" ]]; then | |
# Loads xrandr command | |
echo "Loaded $found_name..." | |
eval "$(echo "$line" | awk -F $sep '{printf "%s", $3}')" | |
exit 0 | |
fi | |
done <"$config_file" | |
# Couldn't find given config name | |
echo "Couldn't find configuration name" | |
# Didn't receive a config name | |
else | |
echo "Supply a config name as an argument" | |
echo "For example \`./auto_xrandr.sh --load example\`" | |
fi | |
# Remove item from the config file | |
elif [[ "$1" == "--remove" ]]; then | |
if [[ $# -eq 2 ]]; then | |
delete="" | |
while read -r line; do | |
# Find config name in file | |
found_name=$(echo "$line" | awk -F $sep '{printf "%s", $1}') | |
# Check config name with config name in file | |
if [[ "$found_name" == "$2" ]]; then | |
# Set line to delete | |
delete="$line" | |
break | |
fi | |
done <"$config_file" | |
# Remove name from the config file | |
file_id="$(date +%s)" | |
if [[ ! "$delete" == "" ]]; then | |
while read -r line; do | |
if [[ ! $line == "$delete" ]]; then | |
echo "$line" | |
fi | |
done <"$config_file" >"/tmp/autoxrandr-$file_id" | |
mv "/tmp/autoxrandr-$file_id" "$config_file" | |
echo "Removed $found_name..." | |
exit 1 | |
fi | |
# Couldn't find given config name | |
echo "Couldn't find configuration name" | |
# Didn't receive a config name | |
else | |
echo "Supply a config name as an argument" | |
echo "For example \`./auto_xrandr.sh --remove example\`" | |
fi | |
# List avalible configs | |
elif [[ "$1" == "--list" ]]; then | |
while read -r line; do | |
# Print names | |
echo "$line" | awk -F $sep '{printf "%s\n", $1}' | |
done <"$config_file" | |
# Show `help` page | |
else | |
echo "Help page | |
This xrandr helper program can save, load, list, remove and auto-load xrandr configerations monitors | |
--save \`example\` Save current xrandr command to the config file | |
--load \`example\` Load given config to xrandr | |
--remove \`example\` Remove given config from config file | |
--auto Automatically detect current display configuration and load this config | |
--list List all saved configs" | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment