]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blobdiff - autorandr.py
Quote arguments in failed command output
[deb_pkgs/autorandr.git] / autorandr.py
index a2c6f8274e17c7782e95375419a848254b5e1010..d9b853b6fb97ef2ba9f5aa9df462571b5159cb04 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # encoding: utf-8
 #
 # autorandr.py
@@ -28,10 +28,12 @@ import binascii
 import copy
 import getopt
 import hashlib
+import math
 import os
 import posix
 import pwd
 import re
+import shlex
 import subprocess
 import sys
 import shutil
@@ -39,16 +41,20 @@ import time
 import glob
 
 from collections import OrderedDict
-from distutils.version import LooseVersion as Version
 from functools import reduce
 from itertools import chain
 
+try:
+    from packaging.version import Version
+except ModuleNotFoundError:
+    from distutils.version import LooseVersion as Version
+
 if sys.version_info.major == 2:
     import ConfigParser as configparser
 else:
     import configparser
 
-__version__ = "1.10.1"
+__version__ = "1.12.1"
 
 try:
     input = raw_input
@@ -64,6 +70,20 @@ virtual_profiles = [
     ("vertical", "Stack all connected outputs vertically at their largest resolution", None),
 ]
 
+properties = [
+    "Colorspace",
+    "max bpc",
+    "aspect ratio",
+    "Broadcast RGB",
+    "audio",
+    "non-desktop",
+    "TearFree",
+    "underscan vborder",
+    "underscan hborder",
+    "underscan",
+    "scaling mode",
+]
+
 help_text = """
 Usage: autorandr [options]
 
@@ -76,11 +96,15 @@ Usage: autorandr [options]
 --batch                 run autorandr for all users with active X11 sessions
 --current               only list current (active) configuration(s)
 --config                dump your current xrandr setup
+--cycle                 automatically load the next detected profile
 --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
+--ignore-lid            treat outputs as connected even if their lids are closed
+--match-edid            match diplays based on edid instead of name
 --force                 force (re)loading of a profile / overwrite exiting files
+--list                  list configurations
 --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
@@ -150,6 +174,11 @@ class AutorandrException(Exception):
 class XrandrOutput(object):
     "Represents an XRandR output"
 
+    XRANDR_PROPERTIES_REGEXP = "|".join(
+        [r"{}:\s*(?P<{}>[\S ]*\S+)"
+         .format(re.sub(r"\s", r"\\\g<0>", p), re.sub(r"\W+", "_", p.lower()))
+            for p in properties])
+
     # This regular expression is used to parse an output in `xrandr --verbose'
     XRANDR_OUTPUT_REGEXP = """(?x)
         ^\s*(?P<output>\S[^ ]*)\s+                                                      # Line starts with output name
@@ -175,7 +204,9 @@ class XrandrOutput(object):
             Gamma: (?P<gamma>(?:inf|-?[0-9\.\-: e])+) |                                 # Gamma value
             CRTC:\s*(?P<crtc>[0-9]) |                                                   # CRTC value
             Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) |                           # Transformation matrix
+                      filter:\s+(?P<filter>bilinear|nearest) |                          # Transformation filter
             EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) |                               # EDID of the output
+            """ + XRANDR_PROPERTIES_REGEXP + """ |                                      # Properties to include in the profile
             (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)*                                 # Other properties
         ))+
         \s*
@@ -209,7 +240,7 @@ class XrandrOutput(object):
     EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
 
     def __repr__(self):
-        return "<%s%s %s>" % (self.output, self.short_edid, " ".join(self.option_vector))
+        return "<%s%s %s>" % (self.output, self.fingerprint, " ".join(self.option_vector))
 
     @property
     def short_edid(self):
@@ -226,19 +257,39 @@ class XrandrOutput(object):
         if xrandr_version() >= Version("1.2"):
             options.update(self.XRANDR_12_DEFAULTS)
         options.update(self.options)
+        if "set" in self.ignored_options:
+            options = {a: b for a, b in options.items() if not a.startswith("x-prop")}
         return {a: b for a, b in options.items() if a not in self.ignored_options}
 
     @property
     def filtered_options(self):
         "Return a dictionary of options without ignored options"
-        return {a: b for a, b in self.options.items() if a not in self.ignored_options}
+        options = {a: b for a, b in self.options.items() if a not in self.ignored_options}
+        if "set" in self.ignored_options:
+            options = {a: b for a, b in options.items() if not a.startswith("x-prop")}
+        return options
 
     @property
     def option_vector(self):
         "Return the command line parameters for XRandR for this instance"
         args = ["--output", self.output]
         for option, arg in sorted(self.options_with_defaults.items()):
-            args.append("--%s" % option)
+            if option.startswith("x-prop-"):
+                prop_found = False
+                for prop, xrandr_prop in [(re.sub(r"\W+", "_", p.lower()), p) for p in properties]:
+                    if prop == option[7:]:
+                        args.append("--set")
+                        args.append(xrandr_prop)
+                        prop_found = True
+                        break
+                if not prop_found:
+                    print("Warning: Unknown property `%s' in config file. Skipping." % option[7:], file=sys.stderr)
+                    continue
+            elif option.startswith("x-"):
+                print("Warning: Unknown option `%s' in config file. Skipping." % option, file=sys.stderr)
+                continue
+            else:
+                args.append("--%s" % option)
             if arg:
                 args.append(arg)
         return args
@@ -273,8 +324,34 @@ class XrandrOutput(object):
         self.edid = edid
         self.options = options
         self.ignored_options = []
+        self.parse_serial_from_edid()
         self.remove_default_option_values()
 
+    def parse_serial_from_edid(self):
+        self.serial = None
+        if self.edid:
+            if self.EDID_UNAVAILABLE in self.edid:
+                return
+            # Thx to pyedid project, the following code was
+            # copied (and modified) from pyedid/__init__py:21 [parse_edid()]
+            raw = bytes.fromhex(self.edid)
+            # Check EDID header, and checksum
+            if raw[:8] != b'\x00\xff\xff\xff\xff\xff\xff\x00' or sum(raw) % 256 != 0:
+                return
+            serial_no = int.from_bytes(raw[15:11:-1], byteorder='little')
+
+            serial_text = None
+            # Offsets of standard timing information descriptors 1-4
+            # (see https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format)
+            for timing_bytes in (raw[54:72], raw[72:90], raw[90:108], raw[108:126]):
+                if timing_bytes[0:2] == b'\x00\x00':
+                    timing_type = timing_bytes[3]
+                    if timing_type == 0xFF:
+                        buffer = timing_bytes[5:]
+                        buffer = buffer.partition(b'\x0a')[0]
+                        serial_text = buffer.decode('cp437')
+            self.serial = serial_text if serial_text else "0x{:x}".format(serial_no) if serial_no != 0 else None
+
     def set_ignored_options(self, options):
         "Set a list of xrandr options that are never used (neither when comparing configurations nor when applying them)"
         self.ignored_options = list(options)
@@ -376,6 +453,8 @@ class XrandrOutput(object):
                         # 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["filter"]:
+                options["filter"] = match["filter"]
             if match["gamma"]:
                 gamma = match["gamma"].strip()
                 # xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
@@ -387,11 +466,14 @@ class XrandrOutput(object):
                 options["crtc"] = match["crtc"]
             if match["rate"]:
                 options["rate"] = match["rate"]
+            for prop in [re.sub(r"\W+", "_", p.lower()) for p in properties]:
+                if match[prop]:
+                    options["x-prop-" + prop] = match[prop]
 
         return XrandrOutput(match["output"], edid, options), modes
 
     @classmethod
-    def from_config_file(cls, edid_map, configuration):
+    def from_config_file(cls, profile, edid_map, configuration):
         "Instanciate an XrandrOutput from the contents of a configuration file"
         options = {}
         for line in configuration.split("\n"):
@@ -412,13 +494,23 @@ 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:
-                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("Profile `%s': Failed to find an EDID for output `%s' in setup file, required "
+                                         "as `%s' is not off in config file." % (profile, options["output"], options["output"]))
         output = options["output"]
         del options["output"]
 
         return XrandrOutput(output, edid, options)
 
+    @property
+    def fingerprint(self):
+        return str(self.serial) if self.serial else self.short_edid
+
+    def fingerprint_equals(self, other):
+        if self.serial and other.serial:
+           return self.serial == other.serial
+        else:
+           return self.edid_equals(other)
+
     def edid_equals(self, other):
         "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
         if self.edid and other.edid:
@@ -436,13 +528,13 @@ class XrandrOutput(object):
         return not (self == other)
 
     def __eq__(self, other):
-        return self.edid_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
+        return self.fingerprint_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
 
     def verbose_diff(self, other):
         "Compare to another XrandrOutput and return a list of human readable differences"
         diffs = []
-        if not self.edid_equals(other):
-            diffs.append("EDID `%s' differs from `%s'" % (self.short_edid, other.short_edid))
+        if not self.fingerprint_equals(other):
+            diffs.append("EDID `%s' differs from `%s'" % (self.fingerprint, other.fingerprint))
         if self.output != other.output:
             diffs.append("Output name `%s' differs from `%s'" % (self.output, other.output))
         if "off" in self.options and "off" not in other.options:
@@ -495,7 +587,10 @@ def debug_regexp(pattern, string):
     return "Debug information would be available if the `regex' module was installed."
 
 
-def parse_xrandr_output():
+def parse_xrandr_output(
+    *,
+    ignore_lid,
+):
     "Parse the output of `xrandr --verbose' into a list of outputs"
     xrandr_output = os.popen("xrandr -q --verbose").read()
     if not xrandr_output:
@@ -518,7 +613,11 @@ def parse_xrandr_output():
             modes[output_name] = output_modes
 
     # consider a closed lid as disconnected if other outputs are connected
-    if sum(o.edid != None for o in outputs.values()) > 1:
+    if not ignore_lid and sum(
+        o.edid != None
+        for o
+        in outputs.values()
+    ) > 1:
         for output_name in outputs.keys():
             if is_closed_lid(output_name):
                 outputs[output_name].edid = None
@@ -542,7 +641,7 @@ def load_profiles(profile_path):
         buffer = []
         for line in chain(open(config_name).readlines(), ["output"]):
             if line[:6] == "output" and buffer:
-                config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
+                config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(profile, edids, "".join(buffer))
                 buffer = [line]
             else:
                 buffer.append(line)
@@ -593,6 +692,36 @@ def match_asterisk(pattern, data):
     return matched * 1. / total
 
 
+def update_profiles_edid(profiles, config):
+    fp_map = {}
+    for c in config:
+        if config[c].fingerprint is not None:
+            fp_map[config[c].fingerprint] = c
+
+    for p in profiles:
+        profile_config = profiles[p]["config"]
+
+        for fingerprint in fp_map:
+            for c in list(profile_config.keys()):
+                if profile_config[c].fingerprint != fingerprint or c == fp_map[fingerprint]:
+                    continue
+
+                print("%s: renaming display %s to %s" % (p, c, fp_map[fingerprint]))
+
+                tmp_disp = profile_config[c]
+
+                if fp_map[fingerprint] in profile_config:
+                    # Swap the two entries
+                    profile_config[c] = profile_config[fp_map[fingerprint]]
+                    profile_config[c].output = c
+                else:
+                    # Object is reassigned to another key, drop this one
+                    del profile_config[c]
+
+                profile_config[fp_map[fingerprint]] = tmp_disp
+                profile_config[fp_map[fingerprint]].output = fp_map[fingerprint]
+
+
 def find_profiles(current_config, profiles):
     "Find profiles matching the currently connected outputs, sorting asterisk matches to the back"
     detected_profiles = []
@@ -600,15 +729,16 @@ def find_profiles(current_config, profiles):
         config = profile["config"]
         matches = True
         for name, output in config.items():
-            if not output.edid:
+            if not output.fingerprint:
                 continue
-            if name not in current_config or not output.edid_equals(current_config[name]):
+            if name not in current_config or not output.fingerprint_equals(current_config[name]):
                 matches = False
                 break
-        if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].edid)):
+        if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].fingerprint)):
             continue
         if matches:
-            closeness = max(match_asterisk(output.edid, current_config[name].edid), match_asterisk(current_config[name].edid, output.edid))
+            closeness = max(match_asterisk(output.edid, current_config[name].edid), match_asterisk(
+                current_config[name].edid, output.edid))
             detected_profiles.append((closeness, profile_name))
     detected_profiles = [o[1] for o in sorted(detected_profiles, key=lambda x: -x[0])]
     return detected_profiles
@@ -682,23 +812,22 @@ def call_and_retry(*args, **kwargs):
     waits a second and then retries once. This mitigates #47,
     a timing issue with some drivers.
     """
-    if "dry_run" in kwargs:
-        dry_run = kwargs["dry_run"]
-        del kwargs["dry_run"]
+    if kwargs.pop("dry_run", False):
+        for arg in args[0]:
+            print(shlex.quote(arg), end=" ")
+        print()
+        return 0
     else:
-        dry_run = False
-    kwargs_redirected = dict(kwargs)
-    if not dry_run:
         if hasattr(subprocess, "DEVNULL"):
-            kwargs_redirected["stdout"] = getattr(subprocess, "DEVNULL")
+            kwargs["stdout"] = getattr(subprocess, "DEVNULL")
         else:
-            kwargs_redirected["stdout"] = open(os.devnull, "w")
-        kwargs_redirected["stderr"] = kwargs_redirected["stdout"]
-    retval = subprocess.call(*args, **kwargs_redirected)
-    if retval != 0:
-        time.sleep(1)
+            kwargs["stdout"] = open(os.devnull, "w")
+        kwargs["stderr"] = kwargs["stdout"]
         retval = subprocess.call(*args, **kwargs)
-    return retval
+        if retval != 0:
+            time.sleep(1)
+            retval = subprocess.call(*args, **kwargs)
+        return retval
 
 
 def get_fb_dimensions(configuration):
@@ -734,7 +863,7 @@ def get_fb_dimensions(configuration):
                 o_height = int(detail.get("h")) + int(detail.get("y"))
         width = max(width, o_width)
         height = max(height, o_height)
-    return int(width), int(height)
+    return math.ceil(width), math.ceil(height)
 
 
 def apply_configuration(new_configuration, current_configuration, dry_run=False):
@@ -743,10 +872,7 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     found_left_monitor = False
     found_top_monitor = False
     outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
-    if dry_run:
-        base_argv = ["echo", "xrandr"]
-    else:
-        base_argv = ["xrandr"]
+    base_argv = ["xrandr"]
 
     # There are several xrandr / driver bugs we need to take care of here:
     # - We cannot enable more than two screens at the same time
@@ -770,10 +896,10 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
 
     fb_dimensions = get_fb_dimensions(new_configuration)
     try:
-        base_argv += ["--fb", "%dx%d" % fb_dimensions]
+        fb_args = ["--fb", "%dx%d" % fb_dimensions]
     except:
         # Failed to obtain frame-buffer size. Doesn't matter, xrandr will choose for the user.
-        pass
+        fb_args = []
 
     auxiliary_changes_pre = []
     disable_outputs = []
@@ -821,7 +947,12 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     if auxiliary_changes_pre:
         argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
         if call_and_retry(argv, dry_run=dry_run) != 0:
-            raise AutorandrException("Command failed: %s" % " ".join(argv))
+            raise AutorandrException("Command failed: %s" % " ".join(map(shlex.quote, argv)))
+
+    # Starting here, fix the frame buffer size
+    # Do not do this earlier, as disabling scaling might temporarily make the framebuffer
+    # dimensions larger than they will finally be.
+    base_argv += fb_args
 
     # Disable unused outputs, but make sure that there always is at least one active screen
     disable_keep = 0 if remain_active_count else 1
@@ -854,7 +985,7 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     for index in range(0, len(operations), 2):
         argv = base_argv + list(chain.from_iterable(operations[index:index + 2]))
         if call_and_retry(argv, dry_run=dry_run) != 0:
-            raise AutorandrException("Command failed: %s" % " ".join(argv))
+            raise AutorandrException("Command failed: %s" % " ".join(map(shlex.quote, argv)))
 
 
 def is_equal_configuration(source_configuration, target_configuration):
@@ -1101,7 +1232,11 @@ def dispatch_call_to_sessions(argv):
             # so it should be safe. Also, note that since the environment
             # is taken from a process owned by the user, reusing it should
             # not leak any information.
-            os.setgroups(os.getgrouplist(pwent.pw_name, pwent.pw_gid))
+            try:
+                os.setgroups(os.getgrouplist(pwent.pw_name, pwent.pw_gid))
+            except AttributeError:
+                # Python 2 doesn't have getgrouplist
+                os.setgroups([])
             os.setresgid(pwent.pw_gid, pwent.pw_gid, pwent.pw_gid)
             os.setresuid(pwent.pw_uid, pwent.pw_uid, pwent.pw_uid)
             os.chdir(pwent.pw_dir)
@@ -1114,6 +1249,16 @@ def dispatch_call_to_sessions(argv):
             sys.exit(1)
         os.waitpid(child_pid, 0)
 
+    # The following line assumes that user accounts start at 1000 and that no
+    # one works using the root or another system account. This is rather
+    # restrictive, but de facto default. If this breaks your use case, set the
+    # env var AUTORANDR_UID_MIN as appropriate. (Alternatives would be to use
+    # the UID_MIN from /etc/login.defs or FIRST_UID from /etc/adduser.conf; but
+    # effectively, both values aren't binding in any way.)
+    uid_min = 1000
+    if 'AUTORANDR_UID_MIN' in os.environ:
+      uid_min = int(os.environ['AUTORANDR_UID_MIN'])
+
     for directory in os.listdir("/proc"):
         directory = os.path.join("/proc/", directory)
         if not os.path.isdir(directory):
@@ -1123,13 +1268,7 @@ def dispatch_call_to_sessions(argv):
             continue
         uid = os.stat(environ_file).st_uid
 
-        # The following line assumes that user accounts start at 1000 and that
-        # no one works using the root or another system account. This is rather
-        # restrictive, but de facto default. Alternatives would be to use the
-        # UID_MIN from /etc/login.defs or FIRST_UID from /etc/adduser.conf;
-        # but effectively, both values aren't binding in any way.
-        # If this breaks your use case, please file a bug on Github.
-        if uid < 1000:
+        if uid < uid_min:
             continue
 
         process_environ = {}
@@ -1204,10 +1343,32 @@ def read_config(options, directory):
 
 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",
-                                    "current", "detected", "version"])
+        opts, args = getopt.getopt(
+            argv[1:],
+            "s:r:l:d:cfh",
+            [
+                "batch",
+                "dry-run",
+                "change",
+                "cycle",
+                "default=",
+                "save=",
+                "remove=",
+                "load=",
+                "force",
+                "fingerprint",
+                "config",
+                "debug",
+                "skip-options=",
+                "help",
+                "list",
+                "current",
+                "detected",
+                "version",
+                "match-edid",
+                "ignore-lid"
+            ]
+        )
     except getopt.GetoptError as e:
         print("Failed to parse options: {0}.\n"
               "Use --help to get usage information.".format(str(e)),
@@ -1260,15 +1421,27 @@ def main(argv):
             profiles.update(load_profiles(profile_path))
             profile_symlinks.update(get_symlinks(profile_path))
             read_config(options, profile_path)
-        # Sort by descending mtime
-        profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
     except Exception as e:
         raise AutorandrException("Failed to load profiles", e)
 
-    profile_symlinks = {k: v for k, v in profile_symlinks.items() if v in (x[0] for x in virtual_profiles) or v in profiles}
-
     exec_scripts(None, "predetect")
-    config, modes = parse_xrandr_output()
+
+    ignore_lid = "--ignore-lid" in options
+
+    config, modes = parse_xrandr_output(
+        ignore_lid=ignore_lid,
+    )
+
+    if "--match-edid" in options:
+        update_profiles_edid(profiles, config)
+
+    # Sort by mtime
+    sort_direction = -1
+    if "--cycle" in options:
+        # When cycling through profiles, put the profile least recently used to the top of the list
+        sort_direction = 1
+    profiles = OrderedDict(sorted(profiles.items(), key=lambda x: sort_direction * x[1]["config-mtime"]))
+    profile_symlinks = {k: v for k, v in profile_symlinks.items() if v in (x[0] for x in virtual_profiles) or v in profiles}
 
     if "--fingerprint" in options:
         output_setup(config, sys.stdout)
@@ -1366,10 +1539,11 @@ def main(argv):
         best_index = 9999
         for profile_name in profiles.keys():
             if profile_blocked(os.path.join(profile_path, profile_name), block_script_metadata):
-                if "--current" not in options and "--detected" not in options:
+                if not any(opt in options for opt in ("--current", "--detected", "--list")):
                     print("%s (blocked)" % profile_name)
                 continue
             props = []
+            is_current_profile = profile_name in current_profiles
             if profile_name in detected_profiles:
                 if len(detected_profiles) == 1:
                     index = 1
@@ -1377,16 +1551,17 @@ def main(argv):
                 else:
                     index = detected_profiles.index(profile_name) + 1
                     props.append("(detected) (%d%s match)" % (index, ["st", "nd", "rd"][index - 1] if index < 4 else "th"))
-                if ("-c" in options or "--change" in options) and index < best_index:
-                    load_profile = profile_name
-                    best_index = index
+                if index < best_index:
+                    if "-c" in options or "--change" in options or ("--cycle" in options and not is_current_profile):
+                        load_profile = profile_name
+                        best_index = index
             elif "--detected" in options:
                 continue
-            if profile_name in current_profiles:
+            if is_current_profile:
                 props.append("(current)")
             elif "--current" in options:
                 continue
-            if "--current" in options or "--detected" in options:
+            if any(opt in options for opt in ("--current", "--detected", "--list")):
                 print("%s" % (profile_name, ))
             else:
                 print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)))
@@ -1395,7 +1570,7 @@ def main(argv):
 
     if "-d" in options:
         options["--default"] = options["-d"]
-    if not load_profile and "--default" in options and ("-c" in options or "--change" in options):
+    if not load_profile and "--default" in options and ("-c" in options or "--change" in options or "--cycle" in options):
         load_profile = options["--default"]
 
     if load_profile:
@@ -1414,7 +1589,7 @@ def main(argv):
                 scripts_path = profile["path"]
             except KeyError:
                 raise AutorandrException("Failed to load profile '%s': Profile not found" % load_profile)
-            if load_profile in detected_profiles and detected_profiles[0] != load_profile:
+            if "--dry-run" not in options:
                 update_mtime(os.path.join(scripts_path, "config"))
         add_unused_outputs(config, load_config)
         if load_config == dict(config) and "-f" not in options and "--force" not in options:
@@ -1447,7 +1622,12 @@ def main(argv):
             raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
 
         if "--dry-run" not in options and "--debug" in options:
-            new_config, _ = parse_xrandr_output()
+            new_config, _ = parse_xrandr_output(
+                ignore_lid=ignore_lid,
+            )
+            if "--skip-options" in options:
+                for output in new_config.values():
+                    output.set_ignored_options(skip_options)
             if not is_equal_configuration(new_config, load_config):
                 print("The configuration change did not go as expected:")
                 print_profile_differences(new_config, load_config)