Skip to content

Instantly share code, notes, and snippets.

@Staninna
Last active July 5, 2022 23:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Staninna/85e7fee88cfeb98932b83f5fbffb3850 to your computer and use it in GitHub Desktop.
Save Staninna/85e7fee88cfeb98932b83f5fbffb3850 to your computer and use it in GitHub Desktop.
`autoxrandr` is the main script `_get_xrandr_command` is a helper script
#!/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())
#!/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