]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blobdiff - autorandr.py
New upstream version 1.14
[deb_pkgs/autorandr.git] / autorandr.py
index ec159bf6a0b5fc4a85b855f58cfba04b4a371f38..93b4e79ca09a13ac621bffa92f6302d508d60bdf 100755 (executable)
@@ -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
@@ -42,17 +44,13 @@ from collections import OrderedDict
 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.11"
+__version__ = "1.14"
 
 try:
     input = raw_input
@@ -66,6 +64,8 @@ virtual_profiles = [
     ("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),
     ("vertical", "Stack all connected outputs vertically at their largest resolution", None),
+    ("horizontal-reverse", "Stack all connected outputs horizontally at their largest resolution in reverse order", None),
+    ("vertical-reverse", "Stack all connected outputs vertically at their largest resolution in reverse order", None),
 ]
 
 properties = [
@@ -99,7 +99,8 @@ Usage: autorandr [options]
 --detected              only list detected (available) configuration(s)
 --dry-run               don't change anything, only print the xrandr commands
 --fingerprint           fingerprint your current hardware setup
---match-edid            match diplays based on edid instead of name
+--ignore-lid            treat outputs as connected even if their lids are closed
+--match-edid            match displays 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")
@@ -117,6 +118,35 @@ Usage: autorandr [options]
 """.strip()
 
 
+class Version(object):
+    def __init__(self, version):
+        self._version = version
+        self._version_parts = re.split("([0-9]+)", version)
+
+    def __eq__(self, other):
+        return self._version_parts == other._version_parts
+
+    def __lt__(self, other):
+        for my, theirs in zip(self._version_parts, other._version_parts):
+            if my.isnumeric() and theirs.isnumeric():
+                my = int(my)
+                theirs = int(theirs)
+            if my < theirs:
+                return True
+        return len(theirs) > len(my)
+
+    def __ge__(self, other):
+        return not (self < other)
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __le__(self, other):
+        return (self < other) or (self == other)
+
+    def __gt__(self, other):
+        return self >= other and not (self == other)
+
 def is_closed_lid(output):
     if not re.match(r'(eDP(-?[0-9]\+)*|LVDS(-?[0-9]\+)*)', output):
         return False
@@ -201,6 +231,7 @@ 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
@@ -236,7 +267,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):
@@ -253,29 +284,37 @@ 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()):
-            if option[:5] == "prop-":
+            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[5:]:
+                    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[5:], file=sys.stderr)
+                    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:
@@ -307,19 +346,47 @@ class XrandrOutput(object):
         return x + 10000 * y
 
     def __init__(self, output, edid, options):
-        "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
+        "Instantiate using output name, edid and a dictionary of XRandR command line parameters"
         self.output = output
         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
+            if "*" 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)
 
     def remove_default_option_values(self):
-        "Remove values from the options dictionary that are superflous"
+        "Remove values from the options dictionary that are superfluous"
         if "off" in self.options and len(self.options.keys()) > 1:
             self.options = {"off": None}
             return
@@ -329,7 +396,7 @@ class XrandrOutput(object):
 
     @classmethod
     def from_xrandr_output(cls, xrandr_output):
-        """Instanciate an XrandrOutput from the output of `xrandr --verbose'
+        """Instantiate an XrandrOutput from the output of `xrandr --verbose'
 
         This method also returns a list of modes supported by the output.
         """
@@ -415,6 +482,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
@@ -428,13 +497,13 @@ class XrandrOutput(object):
                 options["rate"] = match["rate"]
             for prop in [re.sub(r"\W+", "_", p.lower()) for p in properties]:
                 if match[prop]:
-                    options["prop-" + prop] = match[prop]
+                    options["x-prop-" + prop] = match[prop]
 
         return XrandrOutput(match["output"], edid, options), modes
 
     @classmethod
-    def from_config_file(cls, edid_map, configuration):
-        "Instanciate an XrandrOutput from the contents of a configuration file"
+    def from_config_file(cls, profile, edid_map, configuration):
+        "Instantiate an XrandrOutput from the contents of a configuration file"
         options = {}
         for line in configuration.split("\n"):
             if line:
@@ -454,13 +523,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:
@@ -478,13 +557,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:
@@ -537,7 +616,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:
@@ -546,7 +628,7 @@ def parse_xrandr_output():
     # We are not interested in screens
     xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
 
-    # Split at output boundaries and instanciate an XrandrOutput per output
+    # Split at output boundaries and instantiate an XrandrOutput per output
     split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
     if len(split_xrandr_output) < 2:
         raise AutorandrException("No output boundaries found", report_bug=True)
@@ -560,7 +642,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
@@ -584,7 +670,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)
@@ -636,33 +722,33 @@ def match_asterisk(pattern, data):
 
 
 def update_profiles_edid(profiles, config):
-    edid_map = {}
+    fp_map = {}
     for c in config:
-        if config[c].edid is not None:
-            edid_map[config[c].edid] = c
+        if config[c].fingerprint is not None:
+            fp_map[config[c].fingerprint] = c
 
     for p in profiles:
         profile_config = profiles[p]["config"]
 
-        for edid in edid_map:
-            for c in profile_config.keys():
-                if profile_config[c].edid != edid or c == edid_map[edid]:
+        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, edid_map[edid]))
+                print("%s: renaming display %s to %s" % (p, c, fp_map[fingerprint]), file=sys.stderr)
 
                 tmp_disp = profile_config[c]
 
-                if edid_map[edid] in profile_config:
+                if fp_map[fingerprint] in profile_config:
                     # Swap the two entries
-                    profile_config[c] = profile_config[edid_map[edid]]
+                    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[edid_map[edid]] = tmp_disp
-                profile_config[edid_map[edid]].output = edid_map[edid]
+                profile_config[fp_map[fingerprint]] = tmp_disp
+                profile_config[fp_map[fingerprint]].output = fp_map[fingerprint]
 
 
 def find_profiles(current_config, profiles):
@@ -672,12 +758,12 @@ 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(
@@ -755,23 +841,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):
@@ -807,7 +892,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):
@@ -816,10 +901,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
@@ -843,10 +925,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 = []
@@ -894,7 +976,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
@@ -918,7 +1005,7 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     # If we did not find a candidate, we might need to inject a call
     # If there is no output to disable, we will enable 0x and x0 at the same time
     if not found_top_left_monitor and len(disable_outputs) > 0:
-        # If the call to 0x and x0 is splitted, inject one of them
+        # If the call to 0x and x0 is split, inject one of them
         if found_top_monitor and found_left_monitor:
             enable_outputs.insert(0, enable_outputs[0])
 
@@ -927,7 +1014,14 @@ 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)))
+
+    # Adjust the frame buffer to match (see #319)
+    if fb_args:
+        argv = base_argv
+        if call_and_retry(argv, dry_run=dry_run) != 0:
+            raise AutorandrException("Command failed: %s" % " ".join(map(shlex.quote, argv)))
+
 
 
 def is_equal_configuration(source_configuration, target_configuration):
@@ -992,16 +1086,18 @@ def generate_virtual_profile(configuration, modes, profile_name):
                     configuration[output].options["pos"] = "0x0"
                 else:
                     configuration[output].options["off"] = None
-    elif profile_name in ("horizontal", "vertical"):
+    elif profile_name in ("horizontal", "vertical", "horizontal-reverse", "vertical-reverse"):
         shift = 0
-        if profile_name == "horizontal":
+        if profile_name.startswith("horizontal"):
             shift_index = "width"
             pos_specifier = "%sx0"
         else:
             shift_index = "height"
             pos_specifier = "0x%s"
-
-        for output in configuration:
+            
+        config_iter = reversed(configuration) if "reverse" in profile_name else iter(configuration)
+            
+        for output in config_iter:
             configuration[output].options = {}
             if output in modes and configuration[output].edid:
                 def key(a):
@@ -1123,21 +1219,21 @@ def exec_scripts(profile_path, script_name, meta_information=None):
             if os.access(script, os.X_OK | os.F_OK):
                 try:
                     all_ok &= subprocess.call(script, env=env) != 0
-                except:
-                    raise AutorandrException("Failed to execute user command: %s" % (script,))
+                except Exception as e:
+                    raise AutorandrException("Failed to execute user command: %s. Error: %s" % (script, str(e)))
                 ran_scripts.add(script_name)
 
         script_folder = os.path.join(folder, "%s.d" % script_name)
         if os.access(script_folder, os.R_OK | os.X_OK) and os.path.isdir(script_folder):
-            for file_name in os.listdir(script_folder):
+            for file_name in sorted(os.listdir(script_folder)):
                 check_name = "d/%s" % (file_name,)
                 if check_name not in ran_scripts:
                     script = os.path.join(script_folder, file_name)
                     if os.access(script, os.X_OK | os.F_OK):
                         try:
                             all_ok &= subprocess.call(script, env=env) != 0
-                        except:
-                            raise AutorandrException("Failed to execute user command: %s" % (script,))
+                        except Exception as e:
+                            raise AutorandrException("Failed to execute user command: %s. Error: %s" % (script, str(e)))
                         ran_scripts.add(check_name)
 
     return all_ok
@@ -1191,6 +1287,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):
@@ -1200,13 +1306,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 = {}
@@ -1225,6 +1325,11 @@ def dispatch_call_to_sessions(argv):
             # Cannot work with this environment, skip.
             continue
 
+        if "WAYLAND_DISPLAY" in process_environ and process_environ["WAYLAND_DISPLAY"]:
+            if "--debug" in argv:
+                print("Detected Wayland session '{0}'. Skipping.".format(process_environ["WAYLAND_DISPLAY"]))
+            continue
+
         # To allow scripts to detect batch invocation (especially useful for predetect)
         process_environ["AUTORANDR_BATCH_PID"] = str(os.getpid())
         process_environ["UID"] = str(uid)
@@ -1281,10 +1386,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", "cycle", "default=", "save=", "remove=", "load=",
-                                    "force", "fingerprint", "config", "debug", "skip-options=", "help",
-                                    "list", "current", "detected", "version", "match-edid"])
+        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)),
@@ -1315,6 +1442,9 @@ def main(argv):
         user = pwd.getpwuid(os.getuid())
         user = user.pw_name if user else "#%d" % os.getuid()
         print("autorandr running as user %s (started from batch instance)" % user)
+    if ("WAYLAND_DISPLAY" in os.environ and os.environ["WAYLAND_DISPLAY"]):
+        print("Detected Wayland session '{0}'. Exiting.".format(os.environ["WAYLAND_DISPLAY"]), file=sys.stderr)
+        sys.exit(1)
 
     profiles = {}
     profile_symlinks = {}
@@ -1341,7 +1471,12 @@ def main(argv):
         raise AutorandrException("Failed to load profiles", e)
 
     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)
@@ -1533,7 +1668,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)