]> 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 f0cf4ac78f13b604ab3e08a94a1f954b1fdd8c2c..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,19 +68,22 @@ 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>
 --batch                 run autorandr for all users with active X11 sessions
 -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
 --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
 --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
 
  If no suitable profile can be identified, the current configuration is kept.
  To change this behaviour and switch to a fallback configuration, specify
@@ -391,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):
@@ -615,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)
@@ -643,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 = []
@@ -703,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
 
 
@@ -762,13 +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:
-                def key(a, b):
+                def key(a):
                     score = int(a["width"]) * int(a["height"])
                     if a["preferred"]:
                         score += 10**6
                     return score
                     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
                 configuration[output].options["mode"] = mode["name"]
                 configuration[output].options["rate"] = mode["rate"]
                 configuration[output].options["pos"] = pos_specifier % shift
@@ -782,13 +851,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:
-                def key(a, b):
+                def key(a):
                     score = int(a["width"]) * int(a["height"])
                     if a["preferred"]:
                         score += 10**6
                     return score
                     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["mode"] = mode["name"]
                 configuration[output].options["rate"] = mode["rate"]
                 configuration[output].options["pos"] = "0x0"
@@ -799,6 +868,11 @@ def generate_virtual_profile(configuration, modes, profile_name):
                 configuration[output].options["transform"] = "{},0,{},0,{},{},0,0,1".format(scale, mov_x, scale, mov_y)
             else:
                 configuration[output].options["off"] = None
                 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
 
 
@@ -806,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():
@@ -956,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
@@ -1002,11 +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:
         opts, args = getopt.getopt(argv[1:], "s:r:l:d:cfh",
                                    ["batch", "dry-run", "change", "default=", "save=", "remove=", "load=",
 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)),
     except getopt.GetoptError as e:
         print("Failed to parse options: {0}.\n"
               "Use --help to get usage information.".format(str(e)),
@@ -1018,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:
@@ -1040,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")
@@ -1049,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:
@@ -1084,7 +1187,11 @@ def main(argv):
         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"])
@@ -1143,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:
@@ -1196,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: