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.8.1"
+
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),
Usage: autorandr [options]
-h, --help get this small help
--c, --change reload current setup
+-c, --change automatically load the first detected profile
-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>
--batch run autorandr for all users with active X11 sessions
+--current only list current (active) configuration(s)
--config dump your current xrandr setup
--debug enable verbose output
+--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
# This regular expression is used to parse an output in `xrandr --verbose'
XRANDR_OUTPUT_REGEXP = """(?x)
- ^(?P<output>[^ ]+)\s+ # Line starts with output name
+ ^\s*(?P<output>\S[^ ]*)\s+ # Line starts with output name
(?: # Differentiate disconnected and connected
disconnected | # in first line
unknown\ connection |
else:
edid = "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
+ # An output can be disconnected but still have a mode configured. This can only happen
+ # as a residual situation after a disconnect, you cannot associate a mode with an disconnected
+ # output.
+ #
+ # This code needs to be careful not to mix the two. An output should only be configured to
+ # "off" if it doesn't have a mode associated with it, which is modelled as "not a width" here.
if not match["width"]:
options["off"] = None
else:
options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
else:
if match["rotate"] not in ("left", "right"):
- options["mode"] = "%sx%s" % (match["width"], match["height"])
+ options["mode"] = "%sx%s" % (match["width"] or 0, match["height"] or 0)
else:
- options["mode"] = "%sx%s" % (match["height"], match["width"])
- options["rotate"] = match["rotate"]
+ options["mode"] = "%sx%s" % (match["height"] or 0, match["width"] or 0)
+ if match["rotate"]:
+ options["rotate"] = match["rotate"]
if match["primary"]:
options["primary"] = None
if match["reflect"] == "X":
options["reflect"] = "y"
elif match["reflect"] == "X and Y":
options["reflect"] = "xy"
- options["pos"] = "%sx%s" % (match["x"], match["y"])
+ if match["x"] or match["y"]:
+ options["pos"] = "%sx%s" % (match["x"] or "0", match["y"] or "0")
if match["panning"]:
panning = [match["panning"]]
if match["tracking"]:
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):
return retval
+def get_fb_dimensions(configuration):
+ width = 0
+ height = 0
+ for output in configuration.values():
+ if "off" in output.options or not output.edid:
+ continue
+ # This won't work with all modes -- but it's a best effort.
+ o_mode = re.search("[0-9]{3,}x[0-9]{3,}", output.options["mode"]).group(0)
+ o_width, o_height = map(int, o_mode.split("x"))
+ if "transform" in output.options:
+ a, b, c, d, e, f, g, h, i = map(float, output.options["transform"].split(","))
+ w = (g * o_width + h * o_height + i)
+ x = (a * o_width + b * o_height + c) / w
+ y = (d * o_width + e * o_height + f) / w
+ o_width, o_height = x, y
+ if "rotate" in output.options:
+ if output.options["rotate"] in ("left", "right"):
+ o_width, o_height = o_height, o_width
+ if "pos" in output.options:
+ o_left, o_top = map(int, output.options["pos"].split("x"))
+ o_width += o_left
+ o_height += o_top
+ if "panning" in output.options:
+ match = re.match("(?P<w>[0-9]+)x(?P<h>[0-9]+)(?:\+(?P<x>[0-9]+))?(?:\+(?P<y>[0-9]+))?.*", output.options["panning"])
+ if match:
+ detail = match.groupdict(default="0")
+ o_width = int(detail.get("w")) + int(detail.get("x"))
+ o_height = int(detail.get("h")) + int(detail.get("y"))
+ width = max(width, o_width)
+ height = max(height, o_height)
+ return int(width), int(height)
+
+
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)
# explicitly, so avoid it unless necessary.
# (See https://github.com/phillipberndt/autorandr/issues/72)
+ fb_dimensions = get_fb_dimensions(new_configuration)
+ try:
+ base_argv += ["--fb", "%dx%d" % fb_dimensions]
+ except:
+ # Failed to obtain frame-buffer size. Doesn't matter, xrandr will choose for the user.
+ pass
+
auxiliary_changes_pre = []
disable_outputs = []
enable_outputs = []
if not new_configuration[output].edid or "off" in new_configuration[output].options:
disable_outputs.append(new_configuration[output].option_vector)
else:
+ if output not in current_configuration:
+ raise AutorandrException("New profile configures output %s which does not exist in current xrandr --verbose output. "
+ "Don't know how to proceed." % output)
if "off" not in current_configuration[output].options:
remain_active_count += 1
def is_equal_configuration(source_configuration, target_configuration):
- "Check if all outputs from target are already configured correctly in source"
+ """
+ Check if all outputs from target are already configured correctly in source and
+ that no other outputs are active.
+ """
for output in target_configuration.keys():
- if (output not in source_configuration) or (source_configuration[output] != target_configuration[output]):
- return False
+ if "off" in target_configuration[output].options:
+ if (output in source_configuration and "off" not in source_configuration[output].options):
+ return False
+ else:
+ if (output not in source_configuration) or (source_configuration[output] != target_configuration[output]):
+ return False
+ for output in source_configuration.keys():
+ if "off" in source_configuration[output].options:
+ if output in target_configuration and "off" not in target_configuration[output].options:
+ return False
+ else:
+ if output not in target_configuration:
+ return False
return True
for output in configuration:
configuration[output].options = {}
if output in modes and configuration[output].edid:
- def key(a, b):
+ def key(a):
score = int(a["width"]) * int(a["height"])
if a["preferred"]:
score += 10**6
return score
- modes = sorted(modes[output], key=key)
- mode = modes[-1]
+ 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
for output in configuration:
configuration[output].options = {}
if output in modes and configuration[output].edid:
- def key(a, b):
+ def key(a):
score = int(a["width"]) * int(a["height"])
if a["preferred"]:
score += 10**6
return score
- modes = sorted(modes[output], key=key)
- mode = modes[-1]
+ 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"
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
"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():
X11_displays_done.add(display)
+def enabled_monitors(config):
+ monitors = []
+ for monitor in config:
+ if "--off" in config[monitor].option_vector:
+ continue
+ monitors.append(monitor)
+ return monitors
+
+
+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:
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"])
+ "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)),
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:
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:
try:
profile_folder = os.path.join(profile_path, options["--save"])
save_configuration(profile_folder, config)
- exec_scripts(profile_folder, "postsave", {"CURRENT_PROFILE": options["--save"], "PROFILE_FOLDER": profile_folder})
+ exec_scripts(profile_folder, "postsave", {
+ "CURRENT_PROFILE": options["--save"],
+ "PROFILE_FOLDER": profile_folder,
+ "MONITORS": ":".join(enabled_monitors(config)),
+ })
except Exception as e:
raise AutorandrException("Failed to save current configuration as profile '%s'" % (options["--save"],), e)
print("Saved current configuration as profile '%s'" % options["--save"])
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"])
if "-d" in options:
options["--default"] = options["-d"]
- if not load_profile and "--default" in options:
+ if not load_profile and "--default" in options and ("-c" in options or "--change" in options):
load_profile = options["--default"]
if load_profile:
script_metadata = {
"CURRENT_PROFILE": load_profile,
"PROFILE_FOLDER": scripts_path,
+ "MONITORS": ":".join(enabled_monitors(load_config)),
}
exec_scripts(scripts_path, "preswitch", script_metadata)
if "--debug" in options: