]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blobdiff - autorandr.py
Consider screen rotation in `get_fb_dimensions`
[deb_pkgs/autorandr.git] / autorandr.py
index 119500bd1cbc73b18c167635ddaeb3523fd9b2dc..9524f4274d8275ca32bb9b53c8d55673cd1a5684 100755 (executable)
@@ -26,6 +26,7 @@ from __future__ import print_function
 
 import binascii
 import copy
 
 import binascii
 import copy
+import fnmatch
 import getopt
 import hashlib
 import os
 import getopt
 import hashlib
 import os
@@ -42,6 +43,13 @@ from distutils.version import LooseVersion as Version
 from functools import reduce
 from itertools import chain
 
 from functools import reduce
 from itertools import chain
 
+if sys.version_info.major == 2:
+    import ConfigParser as configparser
+else:
+    import configparser
+
+__version__ = "1.6"
+
 try:
     input = raw_input
 except NameError:
 try:
     input = raw_input
 except NameError:
@@ -49,6 +57,7 @@ except NameError:
 
 virtual_profiles = [
     # (name, description, callback)
 
 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),
     ("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),
@@ -59,33 +68,29 @@ help_text = """
 Usage: autorandr [options]
 
 -h, --help              get this small help
 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>
 -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
 --config                dump your current xrandr setup
---dry-run               don't change anything, only print the xrandr commands
 --debug                 enable verbose output
 --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>.
 
 
  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()
 
  The following virtual configurations are available:
 """.strip()
@@ -136,8 +141,8 @@ class XrandrOutput(object):
     # This regular expression is used to parse an output in `xrandr --verbose'
     XRANDR_OUTPUT_REGEXP = """(?x)
         ^(?P<output>[^ ]+)\s+                                                           # Line starts with output name
     # 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)
         )
             unknown\ connection |
             (?P<connected>connected)
         )
@@ -149,7 +154,7 @@ class XrandrOutput(object):
             (?:\(0x[0-9a-fA-F]+\)\s+)?                                                  # XID
             (?P<rotate>(?:normal|left|right|inverted))\s+                               # Rotation
             (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)?                                       # Reflection
             (?:\(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
         (?:[\ \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
@@ -162,8 +167,8 @@ class XrandrOutput(object):
         ))+
         \s*
         (?P<modes>(?:
         ))+
         \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
         )*)
              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
         )*)
@@ -218,12 +223,23 @@ class XrandrOutput(object):
     @property
     def option_vector(self):
         "Return the command line parameters for XRandR for this instance"
     @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"
 
     @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):
 
     @property
     def sort_key(self):
@@ -269,28 +285,37 @@ class XrandrOutput(object):
             xrandr_output = xrandr_output.replace("\r\n", "\n")
             match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
         except:
             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)
         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:
         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"]:
 
         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
             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:
         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
 
         if not match["width"]:
             options["off"] = None
@@ -326,9 +351,10 @@ class XrandrOutput(object):
                 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"]:
                 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
             if match["gamma"]:
                 gamma = match["gamma"].strip()
                 # xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
@@ -363,7 +389,8 @@ class XrandrOutput(object):
             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:
             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"]
 
         output = options["output"]
         del options["output"]
 
@@ -376,6 +403,10 @@ class XrandrOutput(object):
                 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
                 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 self.edid == other.edid
 
     def __ne__(self, other):
@@ -398,11 +429,14 @@ class XrandrOutput(object):
         else:
             for name in set(chain.from_iterable((self.options.keys(), other.options.keys()))):
                 if name not in other.options:
         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:
                 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]:
                 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
 
 
         return diffs
 
 
@@ -430,9 +464,9 @@ def debug_regexp(pattern, string):
                 break
             bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
         partial_length = 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."
     except ImportError:
         pass
     return "Debug information would be available if the `regex' module was installed."
@@ -488,7 +522,11 @@ def load_profiles(profile_path):
             if config[output_name].edid is None:
                 del config[output_name]
 
             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
 
 
     return profiles
 
@@ -593,6 +631,38 @@ def call_and_retry(*args, **kwargs):
     return retval
 
 
     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_width, o_height = map(int, output.options["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()
+                o_width = int(detail.get("w")) + int(detail.get("x", "0"))
+                o_height = int(detail.get("h")) + int(detail.get("y", "0"))
+        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)
 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)
@@ -621,6 +691,13 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     #   explicitly, so avoid it unless necessary.
     #   (See https://github.com/phillipberndt/autorandr/issues/72)
 
     #   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 = []
     auxiliary_changes_pre = []
     disable_outputs = []
     enable_outputs = []
@@ -634,9 +711,9 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
 
             option_vector = new_configuration[output].option_vector
             if xrandr_version() >= Version("1.3.0"):
 
             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:
                     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)
                     else:
                         try:
                             option_index = option_vector.index("--%s" % option)
@@ -656,7 +733,8 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     # 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:
     # 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.
             # 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.
@@ -680,10 +758,24 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
 
 
 def is_equal_configuration(source_configuration, target_configuration):
 
 
 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():
     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
 
 
     return True
 
 
@@ -697,22 +789,33 @@ def add_unused_outputs(source_configuration, target_configuration):
 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():
 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":
 
 
 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])
+        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:
         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
                     configuration[output].options["pos"] = "0x0"
                 else:
                     configuration[output].options["off"] = None
@@ -728,7 +831,13 @@ def generate_virtual_profile(configuration, modes, profile_name):
         for output in configuration:
             configuration[output].options = {}
             if output in modes and configuration[output].edid:
         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
                 configuration[output].options["mode"] = mode["name"]
                 configuration[output].options["rate"] = mode["rate"]
                 configuration[output].options["pos"] = pos_specifier % shift
@@ -736,20 +845,34 @@ def generate_virtual_profile(configuration, modes, profile_name):
             else:
                 configuration[output].options["off"] = None
     elif profile_name == "clone-largest":
             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:
         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"
                 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"]))
+                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
                 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
 
 
     return configuration
 
 
@@ -757,18 +880,18 @@ def print_profile_differences(one, another):
     "Print the differences between two profiles for debugging"
     if one == another:
         return
     "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:
     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:
         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]):
         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():
 
 
 def exit_help():
@@ -802,11 +925,10 @@ def exec_scripts(profile_path, script_name, meta_information=None):
     Returns True unless any of the scripts exited with non-zero exit status.
     """
     all_ok = True
     Returns True unless any of the scripts exited with non-zero exit status.
     """
     all_ok = True
+    env = os.environ.copy()
     if meta_information:
     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 there are multiple candidates, the XDG spec tells to only use the first one.
     ran_scripts = set()
@@ -815,12 +937,13 @@ def exec_scripts(profile_path, script_name, meta_information=None):
     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")
 
     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:
     if profile_path:
-        candidate_directories = chain((profile_path,), candidate_directories)
+        candidate_directories.append(profile_path)
 
     for folder in candidate_directories:
 
     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):
         if script_name not in ran_scripts:
             script = os.path.join(folder, script_name)
             if os.access(script, os.X_OK | os.F_OK):
@@ -907,8 +1030,8 @@ def dispatch_call_to_sessions(argv):
 
         process_environ = {}
         for environ_entry in open(environ_file).read().split("\0"):
 
         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
                 if name == "DISPLAY" and "." in value:
                     value = value[:value.find(".")]
                 process_environ[name] = value
@@ -953,9 +1076,30 @@ def dispatch_call_to_sessions(argv):
             X11_displays_done.add(display)
 
 
             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:
 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"])
+        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)),
     except getopt.GetoptError as e:
         print("Failed to parse options: {0}.\n"
               "Use --help to get usage information.".format(str(e)),
@@ -967,6 +1111,14 @@ def main(argv):
     if "-h" in options or "--help" in options:
         exit_help()
 
     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:
     # Batch mode
     if "--batch" in options:
         if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
@@ -989,6 +1141,7 @@ def main(argv):
             if os.path.isdir(system_profile_path):
                 profiles.update(load_profiles(system_profile_path))
                 profile_symlinks.update(get_symlinks(system_profile_path))
             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")
         # 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")
@@ -998,6 +1151,7 @@ def main(argv):
         if os.path.isdir(profile_path):
             profiles.update(load_profiles(profile_path))
             profile_symlinks.update(get_symlinks(profile_path))
         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:
         # Sort by descending mtime
         profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
     except Exception as e:
@@ -1028,11 +1182,16 @@ def main(argv):
         options["--save"] = options["-s"]
     if "--save" in options:
         if options["--save"] in (x[0] for x in virtual_profiles):
         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"])
+            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)
         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"])
         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"])
@@ -1042,9 +1201,11 @@ def main(argv):
         options["--remove"] = options["-r"]
     if "--remove" in options:
         if options["--remove"] in (x[0] for x in virtual_profiles):
         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"])
+            raise AutorandrException("Cannot remove profile '%s':\n"
+                                     "This configuration name is a reserved virtual configuration." % options["--remove"])
         if options["--remove"] not in profiles.keys():
         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"])
         try:
             remove = True
             profile_folder = os.path.join(profile_path, options["--remove"])
@@ -1052,7 +1213,8 @@ def main(argv):
             profile_dirlist.remove("config")
             profile_dirlist.remove("setup")
             if profile_dirlist:
             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
                 response = input("Do you really want to remove profile '%s'? If so, type 'yes': " % options["--remove"]).strip()
                 if response != "yes":
                     remove = False
@@ -1072,6 +1234,8 @@ def main(argv):
         options["--load"] = options["-l"]
     if "--load" in options:
         load_profile = options["--load"]
         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 = []
     else:
         # Find the active profile(s) first, for the block script (See #42)
         current_profiles = []
@@ -1086,22 +1250,30 @@ def main(argv):
 
         for profile_name in profiles.keys():
             if profile_blocked(os.path.join(profile_path, profile_name), block_script_metadata):
 
         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
                 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)")
             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 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:
         load_profile = options["--default"]
 
     if load_profile:
@@ -1139,6 +1311,7 @@ def main(argv):
                 script_metadata = {
                     "CURRENT_PROFILE": load_profile,
                     "PROFILE_FOLDER": scripts_path,
                 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:
                 }
                 exec_scripts(scripts_path, "preswitch", script_metadata)
                 if "--debug" in options:
@@ -1171,7 +1344,9 @@ def exception_handled_main(argv=sys.argv):
             print("Exception: {0}".format(e.__class__.__name__))
             sys.exit(2)
 
             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
 
 
         raise