import binascii
import copy
+import fnmatch
import getopt
import hashlib
import os
from functools import reduce
from itertools import chain
+if sys.version_info.major == 2:
+ import ConfigParser as configparser
+else:
+ import configparser
+
+__version__ = "1.5"
+
try:
input = raw_input
except NameError:
virtual_profiles = [
# (name, description, callback)
+ ("off", "Disable all outputs", None),
("common", "Clone all connected outputs at the largest common resolution", None),
("clone-largest", "Clone all connected outputs with the largest resolution (scaled down if necessary)", None),
("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
-h, --help get this small help
-c, --change reload current setup
+-d, --default <profile> make profile <profile> the default profile
+-l, --load <profile> load profile <profile>
-s, --save <profile> save your current setup to profile <profile>
-r, --remove <profile> remove profile <profile>
--l, --load <profile> load profile <profile>
--d, --default <profile> make profile <profile> the default profile
---skip-options <option> comma separated list of xrandr arguments (e.g. "gamma")
- to skip both in detecting changes and applying a profile
---force force (re)loading of a profile
---fingerprint fingerprint your current hardware setup
+--batch run autorandr for all users with active X11 sessions
+--current only list current (active) configuration(s)
--config dump your current xrandr setup
---dry-run don't change anything, only print the xrandr commands
--debug enable verbose output
---batch run autorandr for all users with active X11 sessions
-
- To prevent a profile from being loaded, place a script called "block" in its
- directory. The script is evaluated before the screen setup is inspected, and
- in case of it returning a value of 0 the profile is skipped. This can be used
- to query the status of a docking station you are about to leave.
+--detected only list detected (available) configuration(s)
+--dry-run don't change anything, only print the xrandr commands
+--fingerprint fingerprint your current hardware setup
+--force force (re)loading of a profile
+--skip-options <option> comma separated list of xrandr arguments (e.g. "gamma")
+ to skip both in detecting changes and applying a profile
+--version show version information and exit
If no suitable profile can be identified, the current configuration is kept.
To change this behaviour and switch to a fallback configuration, specify
--default <profile>.
- Another script called "postswitch" can be placed in the directory
- ~/.config/autorandr (or ~/.autorandr if you have an old installation) as well
- as in any profile directories: The scripts are executed after a mode switch
- has taken place and can notify window managers.
+ autorandr supports a set of per-profile and global hooks. See the documentation
+ for details.
The following virtual configurations are available:
""".strip()
+
class AutorandrException(Exception):
def __init__(self, message, original_exception=None, report_bug=False):
self.message = message
self.file_name = None
def __str__(self):
- retval = [ self.message ]
+ retval = [self.message]
if self.line:
retval.append(" (line %d%s)" % (self.line, ("; %s" % self.file_name) if self.file_name else ""))
if self.original_exception:
if self.report_bug:
retval.append("\nThis appears to be a bug. Please help improving autorandr by reporting it upstream:"
"\nhttps://github.com/phillipberndt/autorandr/issues"
- "\nPlease attach the output of `xrandr --verbose` to your bug report if appropriate.")
+ "\nPlease attach the output of `xrandr --verbose` to your bug report if appropriate.")
return "".join(retval)
+
class XrandrOutput(object):
"Represents an XRandR output"
# This regular expression is used to parse an output in `xrandr --verbose'
XRANDR_OUTPUT_REGEXP = """(?x)
^(?P<output>[^ ]+)\s+ # Line starts with output name
- (?: # Differentiate disconnected and connected in first line
- disconnected |
+ (?: # Differentiate disconnected and connected
+ disconnected | # in first line
unknown\ connection |
(?P<connected>connected)
)
(?:\(0x[0-9a-fA-F]+\)\s+)? # XID
(?P<rotate>(?:normal|left|right|inverted))\s+ # Rotation
(?:(?P<reflect>X\ and\ Y|X|Y)\ axis)? # Reflection
- )? # .. but everything of the above only if the screen is in use.
+ )? # .. but only if the screen is in use.
(?:[\ \t]*\([^\)]+\))(?:\s*[0-9]+mm\sx\s[0-9]+mm)?
(?:[\ \t]*panning\ (?P<panning>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Panning information
(?:[\ \t]*tracking\ (?P<tracking>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Tracking information
))+
\s*
(?P<modes>(?:
- (?P<mode_name>\S+).+?\*current.*\s+ # Interesting (current) resolution: Extract rate
- h:\s+width\s+(?P<mode_width>[0-9]+).+\s+
+ (?P<mode_name>\S+).+?\*current.*\s+ # Interesting (current) resolution:
+ h:\s+width\s+(?P<mode_width>[0-9]+).+\s+ # Extract rate
v:\s+height\s+(?P<mode_height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
\S+(?:(?!\*current).)+\s+h:.+\s+v:.+\s* # Other resolutions
)*)
if xrandr_version() >= Version("1.2"):
options.update(self.XRANDR_12_DEFAULTS)
options.update(self.options)
- return { a: b for a, b in options.items() if a not in self.ignored_options }
+ return {a: b for a, b in options.items() if a not in self.ignored_options}
@property
def filtered_options(self):
"Return a dictionary of options without ignored options"
- return { a: b for a, b in self.options.items() if a not in self.ignored_options }
+ return {a: b for a, b in self.options.items() if a not in self.ignored_options}
@property
def option_vector(self):
"Return the command line parameters for XRandR for this instance"
- return sum([["--%s" % option[0], option[1]] if option[1] else ["--%s" % option[0]] for option in chain((("output", self.output),), sorted(self.options_with_defaults.items()))], [])
+ args = ["--output", self.output]
+ for option, arg in sorted(self.options_with_defaults.items()):
+ args.append("--%s" % option)
+ if arg:
+ args.append(arg)
+ return args
@property
def option_string(self):
"Return the command line parameters in the configuration file format"
- return "\n".join([ " ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.filtered_options.items()))])
+ options = ["output %s" % self.output]
+ for option, arg in sorted(self.filtered_options.items()):
+ if arg:
+ options.append("%s %s" % (option, arg))
+ else:
+ options.append(option)
+ return "\n".join(options)
@property
def sort_key(self):
def remove_default_option_values(self):
"Remove values from the options dictionary that are superflous"
if "off" in self.options and len(self.options.keys()) > 1:
- self.options = { "off": None }
+ self.options = {"off": None}
return
for option, default_value in self.XRANDR_DEFAULTS.items():
if option in self.options and self.options[option] == default_value:
xrandr_output = xrandr_output.replace("\r\n", "\n")
match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
except:
- raise AutorandrException("Parsing XRandR output failed, there is an error in the regular expression.", report_bug = True)
+ raise AutorandrException("Parsing XRandR output failed, there is an error in the regular expression.",
+ report_bug=True)
if not match_object:
debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
- raise AutorandrException("Parsing XRandR output failed, the regular expression did not match: %s" % debug, report_bug = True)
+ raise AutorandrException("Parsing XRandR output failed, the regular expression did not match: %s" % debug,
+ report_bug=True)
remainder = xrandr_output[len(match_object.group(0)):]
if remainder:
- raise AutorandrException(("Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
- "starting at byte %d with ..'%s'.") % (len(remainder), len(match_object.group(0)), remainder[:10]), report_bug=True)
+ raise AutorandrException("Parsing XRandR output failed, %d bytes left unmatched after "
+ "regular expression, starting at byte %d with ..'%s'." %
+ (len(remainder), len(match_object.group(0)), remainder[:10]),
+ report_bug=True)
match = match_object.groupdict()
modes = []
if match["modes"]:
- modes = [ x.groupdict() for x in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]) if x.group("name") ]
+ modes = []
+ for mode_match in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]):
+ if mode_match.group("name"):
+ modes.append(mode_match.groupdict())
if not modes:
raise AutorandrException("Parsing XRandR output failed, couldn't find any display modes", report_bug=True)
options = {}
if not match["connected"]:
edid = None
+ elif match["edid"]:
+ edid = "".join(match["edid"].strip().split())
else:
- edid = "".join(match["edid"].strip().split()) if match["edid"] else "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
+ edid = "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
if not match["width"]:
options["off"] = None
options["reflect"] = "xy"
options["pos"] = "%sx%s" % (match["x"], match["y"])
if match["panning"]:
- panning = [ match["panning"] ]
+ panning = [match["panning"]]
if match["tracking"]:
- panning += [ "/", match["tracking"] ]
+ panning += ["/", match["tracking"]]
if match["border"]:
- panning += [ "/", match["border"] ]
+ panning += ["/", match["border"]]
options["panning"] = "".join(panning)
if match["transform"]:
transformation = ",".join(match["transform"].strip().split())
if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
options["transform"] = transformation
if not match["mode_name"]:
- # TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
- # special case is actually required.
- print("Warning: Output %s has a transformation applied. Could not determine correct mode! Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
+ # TODO We'd need to apply the reverse transformation here. Let's see if someone complains,
+ # I doubt that this special case is actually required.
+ print("Warning: Output %s has a transformation applied. Could not determine correct mode! "
+ "Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
if match["gamma"]:
gamma = match["gamma"].strip()
# xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
# Also, it is not able to work with non-standard gamma ramps. Finally, it auto-corrects 0 to 1,
# so we approximate by 1e-10.
- gamma = ":".join([ str(max(1e-10, round(1./float(x), 3))) for x in gamma.split(":") ])
+ gamma = ":".join([str(max(1e-10, round(1. / float(x), 3))) for x in gamma.split(":")])
options["gamma"] = gamma
if match["rate"]:
options["rate"] = match["rate"]
edid = edid_map[options["output"]]
else:
# This fuzzy matching is for legacy autorandr that used sysfs output names
- fuzzy_edid_map = [ re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys() ]
+ fuzzy_edid_map = [re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys()]
fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
if fuzzy_output in fuzzy_edid_map:
edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
elif "off" not in options:
- raise AutorandrException("Failed to find an EDID for output `%s' in setup file, required as `%s' is not off in config file." % (options["output"], options["output"]))
+ raise AutorandrException("Failed to find an EDID for output `%s' in setup file, required as `%s' "
+ "is not off in config file." % (options["output"], options["output"]))
output = options["output"]
del options["output"]
return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
+ if "*" in self.edid:
+ return fnmatch.fnmatch(other.edid, self.edid)
+ elif "*" in other.edid:
+ return fnmatch.fnmatch(self.edid, other.edid)
return self.edid == other.edid
def __ne__(self, other):
else:
for name in set(chain.from_iterable((self.options.keys(), other.options.keys()))):
if name not in other.options:
- diffs.append("Option --%s %sis not present in the new configuration" % (name, "(= `%s') " % self.options[name] if self.options[name] else ""))
+ diffs.append("Option --%s %sis not present in the new configuration" %
+ (name, "(= `%s') " % self.options[name] if self.options[name] else ""))
elif name not in self.options:
- diffs.append("Option --%s (`%s' in the new configuration) is not present currently" % (name, other.options[name]))
+ diffs.append("Option --%s (`%s' in the new configuration) is not present currently" %
+ (name, other.options[name]))
elif self.options[name] != other.options[name]:
- diffs.append("Option --%s %sis `%s' in the new configuration" % (name, "(= `%s') " % self.options[name] if self.options[name] else "", other.options[name]))
+ diffs.append("Option --%s %sis `%s' in the new configuration" %
+ (name, "(= `%s') " % self.options[name] if self.options[name] else "", other.options[name]))
return diffs
+
def xrandr_version():
"Return the version of XRandR that this system uses"
if getattr(xrandr_version, "version", False) is False:
return xrandr_version.version
+
def debug_regexp(pattern, string):
"Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
try:
import regex
- bounds = ( 0, len(string) )
+ bounds = (0, len(string))
while bounds[0] != bounds[1]:
half = int((bounds[0] + bounds[1]) / 2)
if half == bounds[0]:
break
bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
partial_length = bounds[0]
- return ("Regular expression matched until position "
- "%d, ..'%s', and did not match from '%s'.." % (partial_length, string[max(0, partial_length-20):partial_length],
- string[partial_length:partial_length+10]))
+ return ("Regular expression matched until position %d, ..'%s', and did not match from '%s'.." %
+ (partial_length, string[max(0, partial_length - 20):partial_length],
+ string[partial_length:partial_length + 10]))
except ImportError:
pass
return "Debug information would be available if the `regex' module was installed."
+
def parse_xrandr_output():
"Parse the output of `xrandr --verbose' into a list of outputs"
xrandr_output = os.popen("xrandr -q --verbose").read()
modes = OrderedDict()
for i in range(1, len(split_xrandr_output), 2):
output_name = split_xrandr_output[i].split()[0]
- output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i+2]))
+ output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i + 2]))
outputs[output_name] = output
if output_modes:
modes[output_name] = output_modes
return outputs, modes
+
def load_profiles(profile_path):
"Load the stored profiles"
profiles = {}
for profile in os.listdir(profile_path):
config_name = os.path.join(profile_path, profile, "config")
- setup_name = os.path.join(profile_path, profile, "setup")
+ setup_name = os.path.join(profile_path, profile, "setup")
if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
continue
- edids = dict([ x.split() for x in (y.strip() for y in open(setup_name).readlines()) if x and x[0] != "#" ])
+ edids = dict([x.split() for x in (y.strip() for y in open(setup_name).readlines()) if x and x[0] != "#"])
config = {}
buffer = []
for line in chain(open(config_name).readlines(), ["output"]):
if line[:6] == "output" and buffer:
config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
- buffer = [ line ]
+ buffer = [line]
else:
buffer.append(line)
if config[output_name].edid is None:
del config[output_name]
- profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
+ profiles[profile] = {
+ "config": config,
+ "path": os.path.join(profile_path, profile),
+ "config-mtime": os.stat(config_name).st_mtime,
+ }
return profiles
+
def get_symlinks(profile_path):
"Load all symlinks from a directory"
return symlinks
+
def find_profiles(current_config, profiles):
"Find profiles matching the currently connected outputs"
detected_profiles = []
if name not in current_config or not output.edid_equals(current_config[name]):
matches = False
break
- if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
+ if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].edid)):
continue
if matches:
detected_profiles.append(profile_name)
return detected_profiles
+
def profile_blocked(profile_path, meta_information=None):
"""Check if a profile is blocked.
"""
return not exec_scripts(profile_path, "block", meta_information)
+
def output_configuration(configuration, config):
"Write a configuration file"
outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
for output in outputs:
print(configuration[output].option_string, file=config)
+
def output_setup(configuration, setup):
"Write a setup (fingerprint) file"
outputs = sorted(configuration.keys())
if configuration[output].edid:
print(output, configuration[output].edid, file=setup)
+
def save_configuration(profile_path, configuration):
"Save a configuration into a profile"
if not os.path.isdir(profile_path):
with open(os.path.join(profile_path, "setup"), "w") as setup:
output_setup(configuration, setup)
+
def update_mtime(filename):
"Update a file's mtime"
try:
except:
return False
+
def call_and_retry(*args, **kwargs):
"""Wrapper around subprocess.call that retries failed calls.
retval = subprocess.call(*args, **kwargs)
return retval
+
def apply_configuration(new_configuration, current_configuration, dry_run=False):
"Apply a configuration"
outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
if dry_run:
- base_argv = [ "echo", "xrandr" ]
+ base_argv = ["echo", "xrandr"]
else:
- base_argv = [ "xrandr" ]
+ base_argv = ["xrandr"]
# There are several xrandr / driver bugs we need to take care of here:
# - We cannot enable more than two screens at the same time
option_vector = new_configuration[output].option_vector
if xrandr_version() >= Version("1.3.0"):
- for option in ("transform", "panning"):
+ for option, off_value in (("transform", "none"), ("panning", "0x0")):
if option in current_configuration[output].options:
- auxiliary_changes_pre.append(["--output", output, "--%s" % option, "none"])
+ auxiliary_changes_pre.append(["--output", output, "--%s" % option, off_value])
else:
try:
option_index = option_vector.index("--%s" % option)
- if option_vector[option_index+1] == XrandrOutput.XRANDR_DEFAULTS[option]:
- option_vector = option_vector[:option_index] + option_vector[option_index+2:]
+ if option_vector[option_index + 1] == XrandrOutput.XRANDR_DEFAULTS[option]:
+ option_vector = option_vector[:option_index] + option_vector[option_index + 2:]
except ValueError:
pass
# Disable unused outputs, but make sure that there always is at least one active screen
disable_keep = 0 if remain_active_count else 1
if len(disable_outputs) > disable_keep:
- if call_and_retry(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs)), dry_run=dry_run) != 0:
+ argv = base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))
+ if call_and_retry(argv, dry_run=dry_run) != 0:
# Disabling the outputs failed. Retry with the next command:
# Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
# This does not occur if simultaneously the primary screen is reset.
# Enable the remaining outputs in pairs of two operations
operations = disable_outputs + enable_outputs
for index in range(0, len(operations), 2):
- argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
+ argv = base_argv + list(chain.from_iterable(operations[index:index + 2]))
if call_and_retry(argv, dry_run=dry_run) != 0:
raise AutorandrException("Command failed: %s" % " ".join(argv))
+
def is_equal_configuration(source_configuration, target_configuration):
"Check if all outputs from target are already configured correctly in source"
for output in target_configuration.keys():
return False
return True
+
def add_unused_outputs(source_configuration, target_configuration):
"Add outputs that are missing in target to target, in 'off' state"
for output_name, output in source_configuration.items():
if output_name not in target_configuration:
- target_configuration[output_name] = XrandrOutput(output_name, output.edid, { "off": None })
+ target_configuration[output_name] = XrandrOutput(output_name, output.edid, {"off": None})
+
def remove_irrelevant_outputs(source_configuration, target_configuration):
"Remove outputs from target that ought to be 'off' and already are"
for output_name, output in source_configuration.items():
- if "off" in output.options and output_name in target_configuration and "off" in target_configuration[output_name].options:
- del target_configuration[output_name]
+ if "off" in output.options:
+ if output_name in target_configuration:
+ if "off" in target_configuration[output_name].options:
+ del target_configuration[output_name]
+
def generate_virtual_profile(configuration, modes, profile_name):
"Generate one of the virtual profiles"
configuration = copy.deepcopy(configuration)
if profile_name == "common":
- common_resolution = [ set(( ( mode["width"], mode["height"] ) for mode in output_modes )) for output, output_modes in modes.items() if configuration[output].edid ]
- common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
- common_resolution = sorted(common_resolution, key=lambda a: int(a[0])*int(a[1]))
+ mode_sets = []
+ for output, output_modes in modes.items():
+ mode_set = set()
+ if configuration[output].edid:
+ for mode in output_modes:
+ mode_set.add((mode["width"], mode["height"]))
+ mode_sets.append(mode_set)
+ common_resolution = reduce(lambda a, b: a & b, mode_sets[1:], mode_sets[0])
+ common_resolution = sorted(common_resolution, key=lambda a: int(a[0]) * int(a[1]))
if common_resolution:
for output in configuration:
configuration[output].options = {}
if output in modes and configuration[output].edid:
- configuration[output].options["mode"] = [ x["name"] for x in sorted(modes[output], key=lambda x: 0 if x["preferred"] else 1) if x["width"] == common_resolution[-1][0] and x["height"] == common_resolution[-1][1] ][0]
+ modes_sorted = sorted(modes[output], key=lambda x: 0 if x["preferred"] else 1)
+ modes_filtered = [x for x in modes_sorted if (x["width"], x["height"]) == common_resolution[-1]]
+ mode = modes_filtered[0]
+ configuration[output].options["mode"] = mode['name']
configuration[output].options["pos"] = "0x0"
else:
configuration[output].options["off"] = None
for output in configuration:
configuration[output].options = {}
if output in modes and configuration[output].edid:
- mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
+ def key(a):
+ score = int(a["width"]) * int(a["height"])
+ if a["preferred"]:
+ score += 10**6
+ return score
+ output_modes = sorted(modes[output], key=key)
+ mode = output_modes[-1]
configuration[output].options["mode"] = mode["name"]
configuration[output].options["rate"] = mode["rate"]
configuration[output].options["pos"] = pos_specifier % shift
else:
configuration[output].options["off"] = None
elif profile_name == "clone-largest":
- biggest_resolution = sorted([output_modes[0] for output, output_modes in modes.items()], key=lambda x: int(x["width"])*int(x["height"]), reverse=True)[0]
+ modes_unsorted = [output_modes[0] for output, output_modes in modes.items()]
+ modes_sorted = sorted(modes_unsorted, key=lambda x: int(x["width"]) * int(x["height"]), reverse=True)
+ biggest_resolution = modes_sorted[0]
for output in configuration:
configuration[output].options = {}
if output in modes and configuration[output].edid:
- mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
+ def key(a):
+ score = int(a["width"]) * int(a["height"])
+ if a["preferred"]:
+ score += 10**6
+ return score
+ output_modes = sorted(modes[output], key=key)
+ mode = output_modes[-1]
configuration[output].options["mode"] = mode["name"]
configuration[output].options["rate"] = mode["rate"]
configuration[output].options["pos"] = "0x0"
- scale = max(float(biggest_resolution["width"]) / float(mode["width"]) ,float(biggest_resolution["height"]) / float(mode["height"]))
- mov_x = (float(mode["width"])*scale-float(biggest_resolution["width"]))/-2
- mov_y = (float(mode["height"])*scale-float(biggest_resolution["height"]))/-2
+ scale = max(float(biggest_resolution["width"]) / float(mode["width"]),
+ float(biggest_resolution["height"]) / float(mode["height"]))
+ mov_x = (float(mode["width"]) * scale - float(biggest_resolution["width"])) / -2
+ mov_y = (float(mode["height"]) * scale - float(biggest_resolution["height"])) / -2
configuration[output].options["transform"] = "{},0,{},0,{},{},0,0,1".format(scale, mov_x, scale, mov_y)
else:
configuration[output].options["off"] = None
+ elif profile_name == "off":
+ for output in configuration:
+ for key in list(configuration[output].options.keys()):
+ del configuration[output].options[key]
+ configuration[output].options["off"] = None
return configuration
+
def print_profile_differences(one, another):
"Print the differences between two profiles for debugging"
if one == another:
return
- print("| Differences between the two profiles:", file=sys.stderr)
+ print("| Differences between the two profiles:")
for output in set(chain.from_iterable((one.keys(), another.keys()))):
if output not in one:
if "off" not in another[output].options:
- print("| Output `%s' is missing from the active configuration" % output, file=sys.stderr)
+ print("| Output `%s' is missing from the active configuration" % output)
elif output not in another:
if "off" not in one[output].options:
- print("| Output `%s' is missing from the new configuration" % output, file=sys.stderr)
+ print("| Output `%s' is missing from the new configuration" % output)
else:
for line in one[output].verbose_diff(another[output]):
- print("| [Output %s] %s" % (output, line), file=sys.stderr)
- print ("\\-", file=sys.stderr)
+ print("| [Output %s] %s" % (output, line))
+ print("\\-")
+
def exit_help():
"Print help and exit"
print(help_text)
for profile in virtual_profiles:
name, description = profile[:2]
- description = [ description ]
- max_width = 78-18
+ description = [description]
+ max_width = 78 - 18
while len(description[0]) > max_width + 1:
left_over = description[0][max_width:]
description[0] = description[0][:max_width] + "-"
print(" %-15s %s" % (name, description))
sys.exit(0)
+
def exec_scripts(profile_path, script_name, meta_information=None):
""""Run userscripts
Returns True unless any of the scripts exited with non-zero exit status.
"""
all_ok = True
+ env = os.environ.copy()
if meta_information:
- env = os.environ.copy()
- env.update({ "AUTORANDR_%s" % str(key).upper(): str(value) for (key, value) in meta_information.items() })
- else:
- env = os.environ.copy()
+ for key, value in meta_information.items():
+ env["AUTORANDR_{}".format(key.upper())] = str(value)
# If there are multiple candidates, the XDG spec tells to only use the first one.
ran_scripts = set()
if not os.path.isdir(user_profile_path):
user_profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
- candidate_directories = chain((user_profile_path,), (os.path.join(x, "autorandr") for x in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":")))
+ candidate_directories = [user_profile_path]
+ for config_dir in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":"):
+ candidate_directories.append(os.path.join(config_dir, "autorandr"))
if profile_path:
- candidate_directories = chain((profile_path,), candidate_directories)
+ candidate_directories.append(profile_path)
for folder in candidate_directories:
-
if script_name not in ran_scripts:
script = os.path.join(folder, script_name)
if os.access(script, os.X_OK | os.F_OK):
return all_ok
+
def dispatch_call_to_sessions(argv):
"""Invoke autorandr for each open local X11 session with the given options.
process_environ = {}
for environ_entry in open(environ_file).read().split("\0"):
- if "=" in environ_entry:
- name, value = environ_entry.split("=", 1)
+ name, sep, value = environ_entry.partition("=")
+ if name and sep:
if name == "DISPLAY" and "." in value:
value = value[:value.find(".")]
process_environ[name] = value
fork_child_autorandr(pwent, process_environ)
X11_displays_done.add(display)
+
+def read_config(options, directory):
+ """Parse a configuration config.ini from directory and merge it into
+ the options dictionary"""
+ config = configparser.ConfigParser()
+ config.read(os.path.join(directory, "settings.ini"))
+ if config.has_section("config"):
+ for key, value in config.items("config"):
+ options.setdefault("--%s" % key, value)
+
def main(argv):
try:
- options = dict(getopt.getopt(argv[1:], "s:r:l:d:cfh", [ "batch", "dry-run", "change", "default=", "save=", "remove=", "load=", "force", "fingerprint", "config", "debug", "skip-options=", "help" ])[0])
+ opts, args = getopt.getopt(argv[1:], "s:r:l:d:cfh",
+ ["batch", "dry-run", "change", "default=", "save=", "remove=", "load=",
+ "force", "fingerprint", "config", "debug", "skip-options=", "help",
+ "current", "detected", "version"])
except getopt.GetoptError as e:
print("Failed to parse options: {0}.\n"
"Use --help to get usage information.".format(str(e)),
file=sys.stderr)
sys.exit(posix.EX_USAGE)
+ options = dict(opts)
+
if "-h" in options or "--help" in options:
exit_help()
+ if "--version" in options:
+ print("autorandr " + __version__)
+ sys.exit(0)
+
+ if "--current" in options and "--detected" in options:
+ print("--current and --detected are mutually exclusive.", file=sys.stderr)
+ sys.exit(posix.EX_USAGE)
+
# Batch mode
if "--batch" in options:
if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
- dispatch_call_to_sessions([ x for x in argv if x != "--batch" ])
+ dispatch_call_to_sessions([x for x in argv if x != "--batch"])
else:
print("--batch mode can only be used by root and if $DISPLAY is unset")
return
if os.path.isdir(system_profile_path):
profiles.update(load_profiles(system_profile_path))
profile_symlinks.update(get_symlinks(system_profile_path))
+ read_config(options, system_profile_path)
# For the user's profiles, prefer the legacy ~/.autorandr if it already exists
# profile_path is also used later on to store configurations
profile_path = os.path.expanduser("~/.autorandr")
if os.path.isdir(profile_path):
profiles.update(load_profiles(profile_path))
profile_symlinks.update(get_symlinks(profile_path))
+ read_config(options, profile_path)
# Sort by descending mtime
profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
except Exception as e:
raise AutorandrException("Failed to load profiles", e)
- profile_symlinks = { k: v for k, v in profile_symlinks.items() if v in (x[0] for x in virtual_profiles) or v in profiles }
+ profile_symlinks = {k: v for k, v in profile_symlinks.items() if v in (x[0] for x in virtual_profiles) or v in profiles}
exec_scripts(None, "predetect")
config, modes = parse_xrandr_output()
sys.exit(0)
if "--skip-options" in options:
- skip_options = [ y[2:] if y[:2] == "--" else y for y in ( x.strip() for x in options["--skip-options"].split(",") ) ]
+ skip_options = [y[2:] if y[:2] == "--" else y for y in (x.strip() for x in options["--skip-options"].split(","))]
for profile in profiles.values():
for output in profile["config"].values():
output.set_ignored_options(skip_options)
if "-s" in options:
options["--save"] = options["-s"]
if "--save" in options:
- if options["--save"] in ( x[0] for x in virtual_profiles ):
- raise AutorandrException("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
+ if options["--save"] in (x[0] for x in virtual_profiles):
+ raise AutorandrException("Cannot save current configuration as profile '%s':\n"
+ "This configuration name is a reserved virtual configuration." % options["--save"])
try:
profile_folder = os.path.join(profile_path, options["--save"])
save_configuration(profile_folder, config)
if "-r" in options:
options["--remove"] = options["-r"]
if "--remove" in options:
- if options["--remove"] in ( x[0] for x in virtual_profiles ):
- raise AutorandrException("Cannot remove profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--remove"])
+ if options["--remove"] in (x[0] for x in virtual_profiles):
+ raise AutorandrException("Cannot remove profile '%s':\n"
+ "This configuration name is a reserved virtual configuration." % options["--remove"])
if options["--remove"] not in profiles.keys():
- raise AutorandrException("Cannot remove profile '%s':\nThis profile does not exist." % options["--remove"])
+ raise AutorandrException("Cannot remove profile '%s':\n"
+ "This profile does not exist." % options["--remove"])
try:
remove = True
profile_folder = os.path.join(profile_path, options["--remove"])
profile_dirlist.remove("config")
profile_dirlist.remove("setup")
if profile_dirlist:
- print("Profile folder '%s' contains the following additional files:\n---\n%s\n---" % (options["--remove"], "\n".join(profile_dirlist)))
+ print("Profile folder '%s' contains the following additional files:\n"
+ "---\n%s\n---" % (options["--remove"], "\n".join(profile_dirlist)))
response = input("Do you really want to remove profile '%s'? If so, type 'yes': " % options["--remove"]).strip()
if response != "yes":
remove = False
options["--load"] = options["-l"]
if "--load" in options:
load_profile = options["--load"]
+ elif len(args) == 1:
+ load_profile = args[0]
else:
# Find the active profile(s) first, for the block script (See #42)
current_profiles = []
if configs_are_equal:
current_profiles.append(profile_name)
block_script_metadata = {
- "CURRENT_PROFILE": "".join(current_profiles[:1]),
+ "CURRENT_PROFILE": "".join(current_profiles[:1]),
"CURRENT_PROFILES": ":".join(current_profiles)
}
for profile_name in profiles.keys():
if profile_blocked(os.path.join(profile_path, profile_name), block_script_metadata):
- print("%s (blocked)" % profile_name, file=sys.stderr)
+ if "--current" not in options and "--detected" not in options:
+ print("%s (blocked)" % profile_name)
continue
props = []
if profile_name in detected_profiles:
props.append("(detected)")
if ("-c" in options or "--change" in options) and not load_profile:
load_profile = profile_name
+ elif "--detected" in options:
+ continue
if profile_name in current_profiles:
props.append("(current)")
- print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)), file=sys.stderr)
+ elif "--current" in options:
+ continue
+ if "--current" in options or "--detected" in options:
+ print("%s" % (profile_name, ))
+ else:
+ print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)))
if not configs_are_equal and "--debug" in options and profile_name in detected_profiles:
print_profile_differences(config, profiles[profile_name]["config"])
print("'%s' symlinked to '%s'" % (load_profile, profile_symlinks[load_profile]))
load_profile = profile_symlinks[load_profile]
- if load_profile in ( x[0] for x in virtual_profiles ):
+ if load_profile in (x[0] for x in virtual_profiles):
load_config = generate_virtual_profile(config, modes, load_profile)
scripts_path = os.path.join(profile_path, load_profile)
else:
if load_profile in detected_profiles and detected_profiles[0] != load_profile:
update_mtime(os.path.join(scripts_path, "config"))
add_unused_outputs(config, load_config)
- if load_config == dict(config) and not "-f" in options and not "--force" in options:
+ if load_config == dict(config) and "-f" not in options and "--force" not in options:
print("Config already loaded", file=sys.stderr)
sys.exit(0)
if "--debug" in options and load_config != dict(config):
sys.exit(0)
+
def exception_handled_main(argv=sys.argv):
try:
main(sys.argv)
print("Exception: {0}".format(e.__class__.__name__))
sys.exit(2)
- print("Unhandled exception ({0}). Please report this as a bug at https://github.com/phillipberndt/autorandr/issues.".format(e), file=sys.stderr)
+ print("Unhandled exception ({0}). Please report this as a bug at "
+ "https://github.com/phillipberndt/autorandr/issues.".format(e),
+ file=sys.stderr)
raise
+
if __name__ == '__main__':
exception_handled_main()