]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blobdiff - autorandr.py
Fix Typo: ModuleNotFound → ModuleNotFoundError
[deb_pkgs/autorandr.git] / autorandr.py
index 05b2a5c6c8bed96b8ca7c0e7138a1f049b93529e..c9c4f16985c9012ed9f9a92574fb389646314b7a 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # encoding: utf-8
 #
 # autorandr.py
@@ -36,17 +36,24 @@ import subprocess
 import sys
 import shutil
 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.11"
+
 try:
     input = raw_input
 except NameError:
@@ -65,19 +72,23 @@ help_text = """
 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
+--current               only list current (active) configuration(s)
 --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
+--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
 
  If no suitable profile can be identified, the current configuration is kept.
  To change this behaviour and switch to a fallback configuration, specify
@@ -90,6 +101,18 @@ Usage: autorandr [options]
 """.strip()
 
 
+def is_closed_lid(output):
+    if not re.match(r'(eDP(-?[0-9]\+)*|LVDS(-?[0-9]\+)*)', output):
+        return False
+    lids = glob.glob("/proc/acpi/button/lid/*/state")
+    if len(lids) == 1:
+        state_file = lids[0]
+        with open(state_file) as f:
+            content = f.read()
+            return "close" in content
+    return False
+
+
 class AutorandrException(Exception):
     def __init__(self, message, original_exception=None, report_bug=False):
         self.message = message
@@ -134,7 +157,7 @@ class XrandrOutput(object):
 
     # This regular expression is used to parse an output in `xrandr --verbose'
     XRANDR_OUTPUT_REGEXP = """(?x)
-        ^(?P<output>[^ ]+)\s+                                                           # Line starts with output name
+        ^\s*(?P<output>\S[^ ]*)\s+                                                      # Line starts with output name
         (?:                                                                             # Differentiate disconnected and connected
             disconnected |                                                              # in first line
             unknown\ connection |
@@ -154,7 +177,8 @@ class XrandrOutput(object):
         (?:[\ \t]*tracking\ (?P<tracking>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))?               # Tracking information
         (?:[\ \t]*border\ (?P<border>(?:[0-9]+/){3}[0-9]+))?                            # Border information
         (?:\s*(?:                                                                       # Properties of the output
-            Gamma: (?P<gamma>(?:inf|[0-9\.: e])+) |                                     # Gamma value
+            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
             EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) |                               # EDID of the output
             (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)*                                 # Other properties
@@ -311,6 +335,12 @@ class XrandrOutput(object):
         else:
             edid = "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
 
+        # An output can be disconnected but still have a mode configured. This can only happen
+        # as a residual situation after a disconnect, you cannot associate a mode with an disconnected
+        # output.
+        #
+        # This code needs to be careful not to mix the two. An output should only be configured to
+        # "off" if it doesn't have a mode associated with it, which is modelled as "not a width" here.
         if not match["width"]:
             options["off"] = None
         else:
@@ -320,10 +350,11 @@ class XrandrOutput(object):
                 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
             else:
                 if match["rotate"] not in ("left", "right"):
-                    options["mode"] = "%sx%s" % (match["width"], match["height"])
+                    options["mode"] = "%sx%s" % (match["width"] or 0, match["height"] or 0)
                 else:
-                    options["mode"] = "%sx%s" % (match["height"], match["width"])
-            options["rotate"] = match["rotate"]
+                    options["mode"] = "%sx%s" % (match["height"] or 0, match["width"] or 0)
+            if match["rotate"]:
+                options["rotate"] = match["rotate"]
             if match["primary"]:
                 options["primary"] = None
             if match["reflect"] == "X":
@@ -332,7 +363,8 @@ class XrandrOutput(object):
                 options["reflect"] = "y"
             elif match["reflect"] == "X and Y":
                 options["reflect"] = "xy"
-            options["pos"] = "%sx%s" % (match["x"], match["y"])
+            if match["x"] or match["y"]:
+                options["pos"] = "%sx%s" % (match["x"] or "0", match["y"] or "0")
             if match["panning"]:
                 panning = [match["panning"]]
                 if match["tracking"]:
@@ -356,6 +388,8 @@ class XrandrOutput(object):
                 # so we approximate by 1e-10.
                 gamma = ":".join([str(max(1e-10, round(1. / float(x), 3))) for x in gamma.split(":")])
                 options["gamma"] = gamma
+            if match["crtc"]:
+                options["crtc"] = match["crtc"]
             if match["rate"]:
                 options["rate"] = match["rate"]
 
@@ -397,6 +431,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
+            if "*" in self.edid:
+                return match_asterisk(self.edid, other.edid) > 0
+            elif "*" in other.edid:
+                return match_asterisk(other.edid, self.edid) > 0
         return self.edid == other.edid
 
     def __ne__(self, other):
@@ -484,6 +522,12 @@ def parse_xrandr_output():
         if output_modes:
             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:
+        for output_name in outputs.keys():
+            if is_closed_lid(output_name):
+                outputs[output_name].edid = None
+
     return outputs, modes
 
 
@@ -533,8 +577,29 @@ def get_symlinks(profile_path):
     return symlinks
 
 
+def match_asterisk(pattern, data):
+    """Match data against a pattern
+
+    The difference to fnmatch is that this function only accepts patterns with a single
+    asterisk and that it returns a "closeness" number, which is larger the better the match.
+    Zero indicates no match at all.
+    """
+    if "*" not in pattern:
+        return 1 if pattern == data else 0
+    parts = pattern.split("*")
+    if len(parts) > 2:
+        raise ValueError("Only patterns with a single asterisk are supported, %s is invalid" % pattern)
+    if not data.startswith(parts[0]):
+        return 0
+    if not data.endswith(parts[1]):
+        return 0
+    matched = len(pattern)
+    total = len(data) + 1
+    return matched * 1. / total
+
+
 def find_profiles(current_config, profiles):
-    "Find profiles matching the currently connected outputs"
+    "Find profiles matching the currently connected outputs, sorting asterisk matches to the back"
     detected_profiles = []
     for profile_name, profile in profiles.items():
         config = profile["config"]
@@ -548,7 +613,9 @@ def find_profiles(current_config, profiles):
         if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].edid)):
             continue
         if matches:
-            detected_profiles.append(profile_name)
+            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
 
 
@@ -561,6 +628,17 @@ def profile_blocked(profile_path, meta_information=None):
     return not exec_scripts(profile_path, "block", meta_information)
 
 
+def check_configuration_pre_save(configuration):
+    "Check that a configuration is safe for saving."
+    outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
+    for output in outputs:
+        if "off" not in configuration[output].options and not configuration[output].edid:
+            return ("`%(o)s' is not off (has a mode configured) but is disconnected (does not have an EDID).\n"
+                    "This typically means that it has been recently unplugged and then not properly disabled\n"
+                    "by the user. Please disable it (e.g. using `xrandr --output %(o)s --off`) and then rerun\n"
+                    "this command.") % {"o": output}
+
+
 def output_configuration(configuration, config):
     "Write a configuration file"
     outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
@@ -576,13 +654,20 @@ def output_setup(configuration, setup):
             print(output, configuration[output].edid, file=setup)
 
 
-def save_configuration(profile_path, configuration):
+def save_configuration(profile_path, profile_name, configuration, forced=False):
     "Save a configuration into a profile"
     if not os.path.isdir(profile_path):
         os.makedirs(profile_path)
-    with open(os.path.join(profile_path, "config"), "w") as config:
+    config_path = os.path.join(profile_path, "config")
+    setup_path = os.path.join(profile_path, "setup")
+    if os.path.isfile(config_path) and not forced:
+        raise AutorandrException('Refusing to overwrite config "{}" without passing "--force"!'.format(profile_name))
+    if os.path.isfile(setup_path) and not forced:
+        raise AutorandrException('Refusing to overwrite config "{}" without passing "--force"!'.format(profile_name))
+
+    with open(config_path, "w") as config:
         output_configuration(configuration, config)
-    with open(os.path.join(profile_path, "setup"), "w") as setup:
+    with open(setup_path, "w") as setup:
         output_setup(configuration, setup)
 
 
@@ -621,8 +706,47 @@ def call_and_retry(*args, **kwargs):
     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.
+        match = re.search("[0-9]{3,}x[0-9]{3,}", output.options["mode"])
+        if not match:
+            return None
+        o_mode = match.group(0)
+        o_width, o_height = map(int, o_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(default="0")
+                o_width = int(detail.get("w")) + int(detail.get("x"))
+                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)
+
+
 def apply_configuration(new_configuration, current_configuration, dry_run=False):
     "Apply a configuration"
+    found_top_left_monitor = 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"]
@@ -649,6 +773,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)
 
+    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 = []
@@ -657,6 +788,9 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
         if not new_configuration[output].edid or "off" in new_configuration[output].options:
             disable_outputs.append(new_configuration[output].option_vector)
         else:
+            if output not in current_configuration:
+                raise AutorandrException("New profile configures output %s which does not exist in current xrandr --verbose output. "
+                                         "Don't know how to proceed." % output)
             if "off" not in current_configuration[output].options:
                 remain_active_count += 1
 
@@ -672,8 +806,21 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
                                 option_vector = option_vector[:option_index] + option_vector[option_index + 2:]
                         except ValueError:
                             pass
-
-            enable_outputs.append(option_vector)
+            if not found_top_left_monitor:
+                position = new_configuration[output].options.get("pos", "0x0")
+                if position == "0x0":
+                    found_top_left_monitor = True
+                    enable_outputs.insert(0, option_vector)
+                elif not found_left_monitor and position.startswith("0x"):
+                    found_left_monitor = True
+                    enable_outputs.insert(0, option_vector)
+                elif not found_top_monitor and position.endswith("x0"):
+                    found_top_monitor = True
+                    enable_outputs.insert(0, option_vector)
+                else:
+                    enable_outputs.append(option_vector)
+            else:
+                enable_outputs.append(option_vector)
 
     # Perform pe-change auxiliary changes
     if auxiliary_changes_pre:
@@ -700,6 +847,13 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
         # In the context of a xrandr call that changes the display state, `--query' should do nothing
         disable_outputs.insert(0, ['--query'])
 
+    # 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 found_top_monitor and found_left_monitor:
+            enable_outputs.insert(0, enable_outputs[0])
+
     # Enable the remaining outputs in pairs of two operations
     operations = disable_outputs + enable_outputs
     for index in range(0, len(operations), 2):
@@ -709,10 +863,24 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
 
 
 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():
-        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
 
 
@@ -773,8 +941,8 @@ def generate_virtual_profile(configuration, modes, profile_name):
                     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
@@ -793,8 +961,8 @@ def generate_virtual_profile(configuration, modes, profile_name):
                     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"
@@ -817,18 +985,18 @@ def print_profile_differences(one, another):
     "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:
-                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:
-                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]):
-                print("| [Output %s] %s" % (output, line), file=sys.stderr)
-    print("\\-", file=sys.stderr)
+                print("| [Output %s] %s" % (output, line))
+    print("\\-")
 
 
 def exit_help():
@@ -874,11 +1042,12 @@ 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")
 
-    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"))
+    candidate_directories = []
     if profile_path:
         candidate_directories.append(profile_path)
+    candidate_directories.append(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"))
 
     for folder in candidate_directories:
         if script_name not in ran_scripts:
@@ -937,14 +1106,21 @@ 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([])
+            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)
             os.environ.clear()
             os.environ.update(process_environ)
-            os.execl(autorandr_binary, autorandr_binary, *argv[1:])
-            os.exit(1)
+            if sys.executable != "" and sys.executable != None:
+                os.execl(sys.executable, sys.executable, autorandr_binary, *argv[1:])
+            else:
+                os.execl(autorandr_binary, autorandr_binary, *argv[1:])
+            sys.exit(1)
         os.waitpid(child_pid, 0)
 
     for directory in os.listdir("/proc"):
@@ -966,7 +1142,11 @@ def dispatch_call_to_sessions(argv):
             continue
 
         process_environ = {}
-        for environ_entry in open(environ_file).read().split("\0"):
+        for environ_entry in open(environ_file, 'rb').read().split(b"\0"):
+            try:
+                environ_entry = environ_entry.decode("ascii")
+            except UnicodeDecodeError:
+                continue
             name, sep, value = environ_entry.partition("=")
             if name and sep:
                 if name == "DISPLAY" and "." in value:
@@ -1013,6 +1193,15 @@ def dispatch_call_to_sessions(argv):
             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"""
@@ -1026,7 +1215,8 @@ 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",
+                                    "list", "current", "detected", "version"])
     except getopt.GetoptError as e:
         print("Failed to parse options: {0}.\n"
               "Use --help to get usage information.".format(str(e)),
@@ -1038,6 +1228,14 @@ def main(argv):
     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:
@@ -1103,10 +1301,21 @@ def main(argv):
         if options["--save"] in (x[0] for x in virtual_profiles):
             raise AutorandrException("Cannot save current configuration as profile '%s':\n"
                                      "This configuration name is a reserved virtual configuration." % options["--save"])
+        error = check_configuration_pre_save(config)
+        if error:
+            print("Cannot save current configuration as profile '%s':" % options["--save"])
+            print(error)
+            sys.exit(1)
         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})
+            save_configuration(profile_folder, options['--save'], config, forced="--force" in options)
+            exec_scripts(profile_folder, "postsave", {
+                "CURRENT_PROFILE": options["--save"],
+                "PROFILE_FOLDER": profile_folder,
+                "MONITORS": ":".join(enabled_monitors(config)),
+            })
+        except AutorandrException as e:
+            raise e
         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"])
@@ -1163,24 +1372,39 @@ def main(argv):
             "CURRENT_PROFILES": ":".join(current_profiles)
         }
 
+        best_index = 9999
         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 not any(opt in options for opt in ("--current", "--detected", "--list")):
+                    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:
+                if len(detected_profiles) == 1:
+                    index = 1
+                    props.append("(detected)")
+                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
+            elif "--detected" in options:
+                continue
             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 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)))
             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:
@@ -1218,6 +1442,7 @@ def main(argv):
                 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: