]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blobdiff - autorandr.py
Add a configuration file for default option values
[deb_pkgs/autorandr.git] / autorandr.py
index a78b222123bfbae2c41d207d841a5b89042a9f8d..9c272a79f05b8b199ae43580fe212bd3a569b7fd 100755 (executable)
@@ -42,6 +42,11 @@ 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
+
 try:
     input = raw_input
 except NameError:
 try:
     input = raw_input
 except NameError:
@@ -49,6 +54,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),
@@ -60,32 +66,25 @@ Usage: autorandr [options]
 
 -h, --help              get this small help
 -c, --change            reload current setup
 
 -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>
 -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
 --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.
+--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
 
  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()
@@ -218,12 +217,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):
@@ -286,15 +296,20 @@ class XrandrOutput(object):
 
         modes = []
         if match["modes"]:
 
         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
@@ -497,7 +512,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
 
@@ -643,9 +662,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)
@@ -717,14 +736,23 @@ def generate_virtual_profile(configuration, modes, profile_name):
     "Generate one of the virtual profiles"
     configuration = copy.deepcopy(configuration)
     if profile_name == "common":
     "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
@@ -740,7 +768,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, b):
+                    score = int(a["width"]) * int(a["height"])
+                    if a["preferred"]:
+                        score += 10**6
+                    return score
+                modes = sorted(modes[output], key=key)
+                mode = 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
@@ -748,11 +782,19 @@ 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, b):
+                    score = int(a["width"]) * int(a["height"])
+                    if a["preferred"]:
+                        score += 10**6
+                    return score
+                modes = sorted(modes[output], key=key)
+                mode = 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"
@@ -763,6 +805,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
 
 
@@ -817,7 +864,8 @@ def exec_scripts(profile_path, script_name, meta_information=None):
     all_ok = True
     env = os.environ.copy()
     if meta_information:
     all_ok = True
     env = os.environ.copy()
     if meta_information:
-        env.update({"AUTORANDR_%s" % str(key).upper(): str(value) for (key, value) in meta_information.items()})
+        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()
@@ -826,12 +874,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):
@@ -918,8 +967,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
@@ -964,6 +1013,14 @@ def dispatch_call_to_sessions(argv):
             X11_displays_done.add(display)
 
 
             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"))
+    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",
 def main(argv):
     try:
         opts, args = getopt.getopt(argv[1:], "s:r:l:d:cfh",
@@ -1002,6 +1059,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")
@@ -1011,6 +1069,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: