]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blob - autorandr.py
A screen can be disabled yet primary
[deb_pkgs/autorandr.git] / autorandr.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 #
4 # autorandr.py
5 # Copyright (c) 2015, Phillip Berndt
6 #
7 # Autorandr rewrite in Python
8 #
9 # This script aims to be fully compatible with the original autorandr.
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 #
24
25 from __future__ import print_function
26 import copy
27 import getopt
28
29 import binascii
30 import hashlib
31 import os
32 import re
33 import subprocess
34 import sys
35 from distutils.version import LooseVersion as Version
36
37 from itertools import chain
38 from collections import OrderedDict
39
40 virtual_profiles = [
41     # (name, description, callback)
42     ("common", "Clone all connected outputs at the largest common resolution", None),
43     ("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
44     ("vertical", "Stack all connected outputs vertically at their largest resolution", None),
45 ]
46
47 help_text = """
48 Usage: autorandr [options]
49
50 -h, --help              get this small help
51 -c, --change            reload current setup
52 -s, --save <profile>    save your current setup to profile <profile>
53 -l, --load <profile>    load profile <profile>
54 -d, --default <profile> make profile <profile> the default profile
55 --force                 force (re)loading of a profile
56 --fingerprint           fingerprint your current hardware setup
57 --config                dump your current xrandr setup
58 --dry-run               don't change anything, only print the xrandr commands
59
60  To prevent a profile from being loaded, place a script call "block" in its
61  directory. The script is evaluated before the screen setup is inspected, and
62  in case of it returning a value of 0 the profile is skipped. This can be used
63  to query the status of a docking station you are about to leave.
64
65  If no suitable profile can be identified, the current configuration is kept.
66  To change this behaviour and switch to a fallback configuration, specify
67  --default <profile>.
68
69  Another script called "postswitch" can be placed in the directory
70  ~/.config/autorandr (or ~/.autorandr if you have an old installation) as well
71  as in any profile directories: The scripts are executed after a mode switch
72  has taken place and can notify window managers.
73
74  The following virtual configurations are available:
75 """.strip()
76
77 class XrandrOutput(object):
78     "Represents an XRandR output"
79
80     # This regular expression is used to parse an output in `xrandr --verbose'
81     XRANDR_OUTPUT_REGEXP = """(?x)
82         ^(?P<output>[^ ]+)\s+                                                           # Line starts with output name
83         (?:                                                                             # Differentiate disconnected and connected in first line
84             disconnected |
85             unknown\ connection |
86             (?P<connected>connected)
87         )
88         \s*
89         (?P<primary>primary\ )?                                                         # Might be primary screen
90         (?:\s*                                                                          # The remainder of the first line only appears as one or not at all:
91             (?P<width>[0-9]+)x(?P<height>[0-9]+)                                        # Resolution (might be overridden below!)
92             \+(?P<x>[0-9]+)\+(?P<y>[0-9]+)\s+                                           # Position
93             (?:\(0x[0-9a-fA-F]+\)\s+)?                                                  # XID
94             (?P<rotate>(?:normal|left|right|inverted))\s+                               # Rotation
95             (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)?                                       # Reflection
96         )?                                                                              # .. but everything of the above only if the screen is in use.
97         (?:[\ \t]*\(.+)?
98         (?:\s*(?:                                                                       # Properties of the output
99             Gamma: (?P<gamma>[0-9\.: ]+) |                                              # Gamma value
100             Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) |                           # Transformation matrix
101             EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) |                               # EDID of the output
102             (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)*                                 # Other properties
103         ))+
104         \s*
105         (?P<modes>(?:
106             (?P<mode_width>[0-9]+)x(?P<mode_height>[0-9]+).+?\*current.+\s+
107                 h:.+\s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz\s* |                            # Interesting (current) resolution: Extract rate
108             [0-9]+x[0-9]+.+\s+h:.+\s+v:.+\s*                                            # Other resolutions
109         )*)
110     """
111
112     XRANDR_OUTPUT_MODES_REGEXP = """(?x)
113         (?P<width>[0-9]+)x(?P<height>[0-9]+)
114         .*?(?P<preferred>\+preferred)?
115         \s+h:.+
116         \s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz
117     """
118
119     XRANDR_13_DEFAULTS = {
120         "transform": "1,0,0,0,1,0,0,0,1",
121     }
122
123     XRANDR_12_DEFAULTS = {
124         "reflect": "normal",
125         "rotate": "normal",
126         "gamma": "1.0:1.0:1.0",
127     }
128
129     XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
130
131     EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
132
133     def __repr__(self):
134         return "<%s%s %s>" % (self.output, (" %s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else "", " ".join(self.option_vector))
135
136     @property
137     def options_with_defaults(self):
138         "Return the options dictionary, augmented with the default values that weren't set"
139         if "off" in self.options:
140             return self.options
141         options = {}
142         if xrandr_version() >= Version("1.3"):
143             options.update(self.XRANDR_13_DEFAULTS)
144         if xrandr_version() >= Version("1.2"):
145             options.update(self.XRANDR_12_DEFAULTS)
146         options.update(self.options)
147         return options
148
149     @property
150     def option_vector(self):
151         "Return the command line parameters for XRandR for this instance"
152         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()))], [])
153
154     @property
155     def option_string(self):
156         "Return the command line parameters in the configuration file format"
157         return "\n".join([ " ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.options.items()))])
158
159     @property
160     def sort_key(self):
161         "Return a key to sort the outputs for xrandr invocation"
162         if not self.edid:
163             return -2
164         if "off" in self.options:
165             return -1
166         if "pos" in self.options:
167             x, y = map(float, self.options["pos"].split("x"))
168         else:
169             x, y = 0, 0
170         return x + 10000 * y
171
172     def __init__(self, output, edid, options):
173         "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
174         self.output = output
175         self.edid = edid
176         self.options = options
177         self.remove_default_option_values()
178
179     def remove_default_option_values(self):
180         "Remove values from the options dictionary that are superflous"
181         if "off" in self.options and len(self.options.keys()) > 1:
182             self.options = { "off": None }
183             return
184         for option, default_value in self.XRANDR_DEFAULTS.items():
185             if option in self.options and self.options[option] == default_value:
186                 del self.options[option]
187
188     @classmethod
189     def from_xrandr_output(cls, xrandr_output):
190         """Instanciate an XrandrOutput from the output of `xrandr --verbose'
191
192         This method also returns a list of modes supported by the output.
193         """
194         try:
195             xrandr_output = xrandr_output.replace("\r\n", "\n")
196             match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
197         except:
198             raise RuntimeError("Parsing XRandR output failed, there is an error in the regular expression.")
199         if not match_object:
200             debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
201             raise RuntimeError("Parsing XRandR output failed, the regular expression did not match: %s" % debug)
202         remainder = xrandr_output[len(match_object.group(0)):]
203         if remainder:
204             raise RuntimeError(("Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
205                                 "starting at byte %d with ..'%s'.") % (len(remainder), len(len(match_object.group(0))), remainder[:10]))
206
207         match = match_object.groupdict()
208
209         modes = []
210         if match["modes"]:
211             modes = [ x.groupdict() for x in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]) ]
212             if not modes:
213                 raise RuntimeError("Parsing XRandR output failed, couldn't find any display modes")
214
215         options = {}
216         if not match["connected"]:
217             edid = None
218         else:
219             edid = "".join(match["edid"].strip().split()) if match["edid"] else "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
220
221         if not match["width"]:
222             options["off"] = None
223         else:
224             if match["mode_width"]:
225                 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
226             else:
227                 if match["rotate"] not in ("left", "right"):
228                     options["mode"] = "%sx%s" % (match["width"], match["height"])
229                 else:
230                     options["mode"] = "%sx%s" % (match["height"], match["width"])
231             options["rotate"] = match["rotate"]
232             if match["primary"]:
233                 options["primary"] = None
234             if match["reflect"] == "X":
235                 options["reflect"] = "x"
236             elif match["reflect"] == "Y":
237                 options["reflect"] = "y"
238             elif match["reflect"] == "X and Y":
239                 options["reflect"] = "xy"
240             options["pos"] = "%sx%s" % (match["x"], match["y"])
241             if match["transform"]:
242                 transformation = ",".join(match["transform"].strip().split())
243                 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
244                     options["transform"] = transformation
245                     if not match["mode_width"]:
246                         # TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
247                         # special case is actually required.
248                         print("Warning: Output %s has a transformation applied. Could not determine correct mode!", file=sys.stderr)
249             if match["gamma"]:
250                 gamma = match["gamma"].strip()
251                 options["gamma"] = gamma
252             if match["rate"]:
253                 options["rate"] = match["rate"]
254
255         return XrandrOutput(match["output"], edid, options), modes
256
257     @classmethod
258     def from_config_file(cls, edid_map, configuration):
259         "Instanciate an XrandrOutput from the contents of a configuration file"
260         options = {}
261         for line in configuration.split("\n"):
262             if line:
263                 line = line.split(None, 1)
264                 options[line[0]] = line[1] if len(line) > 1 else None
265
266         edid = None
267
268         if options["output"] in edid_map:
269             edid = edid_map[options["output"]]
270         else:
271             # This fuzzy matching is for legacy autorandr that used sysfs output names
272             fuzzy_edid_map = [ re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys() ]
273             fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
274             if fuzzy_output in fuzzy_edid_map:
275                 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
276             elif "off" not in options:
277                 raise RuntimeError("Failed to find an EDID for output `%s' in setup file, required as `%s' is not off in config file."
278                                    % (options["output"], options["output"]))
279         output = options["output"]
280         del options["output"]
281
282         return XrandrOutput(output, edid, options)
283
284     def edid_equals(self, other):
285         "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
286         if self.edid and other.edid:
287             if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
288                 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
289             if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
290                 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
291         return self.edid == other.edid
292
293     def __eq__(self, other):
294         return self.edid_equals(other) and self.output == other.output and self.options == other.options
295
296 def xrandr_version():
297     "Return the version of XRandR that this system uses"
298     if getattr(xrandr_version, "version", False) is False:
299         version_string = os.popen("xrandr -v").read()
300         try:
301             version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
302             xrandr_version.version = Version(version)
303         except AttributeError:
304             xrandr_version.version = Version("1.3.0")
305
306     return xrandr_version.version
307
308 def debug_regexp(pattern, string):
309     "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
310     try:
311         import regex
312         bounds = ( 0, len(string) )
313         while bounds[0] != bounds[1]:
314             half = int((bounds[0] + bounds[1]) / 2)
315             if half == bounds[0]:
316                 break
317             bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
318         partial_length = bounds[0]
319         return ("Regular expression matched until position "
320               "%d, ..'%s', and did not match from '%s'.." % (partial_length, string[max(0, partial_length-20):partial_length],
321                                                              string[partial_length:partial_length+10]))
322     except ImportError:
323         pass
324     return "Debug information available if `regex' module is installed."
325
326 def parse_xrandr_output():
327     "Parse the output of `xrandr --verbose' into a list of outputs"
328     xrandr_output = os.popen("xrandr -q --verbose").read()
329     if not xrandr_output:
330         raise RuntimeError("Failed to run xrandr")
331
332     # We are not interested in screens
333     xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
334
335     # Split at output boundaries and instanciate an XrandrOutput per output
336     split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
337     if len(split_xrandr_output) < 2:
338         raise RuntimeError("No output boundaries found")
339     outputs = OrderedDict()
340     modes = OrderedDict()
341     for i in range(1, len(split_xrandr_output), 2):
342         output_name = split_xrandr_output[i].split()[0]
343         output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i+2]))
344         outputs[output_name] = output
345         if output_modes:
346             modes[output_name] = output_modes
347
348     return outputs, modes
349
350 def load_profiles(profile_path):
351     "Load the stored profiles"
352
353     profiles = {}
354     for profile in os.listdir(profile_path):
355         config_name = os.path.join(profile_path, profile, "config")
356         setup_name  = os.path.join(profile_path, profile, "setup")
357         if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
358             continue
359
360         edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
361
362         config = {}
363         buffer = []
364         for line in chain(open(config_name).readlines(), ["output"]):
365             if line[:6] == "output" and buffer:
366                 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
367                 buffer = [ line ]
368             else:
369                 buffer.append(line)
370
371         for output_name in list(config.keys()):
372             if config[output_name].edid is None:
373                 del config[output_name]
374
375         profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
376
377     return profiles
378
379 def find_profiles(current_config, profiles):
380     "Find profiles matching the currently connected outputs"
381     detected_profiles = []
382     for profile_name, profile in profiles.items():
383         config = profile["config"]
384         matches = True
385         for name, output in config.items():
386             if not output.edid:
387                 continue
388             if name not in current_config or not output.edid_equals(current_config[name]):
389                 matches = False
390                 break
391         if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
392             continue
393         if matches:
394             detected_profiles.append(profile_name)
395     return detected_profiles
396
397 def profile_blocked(profile_path):
398     "Check if a profile is blocked"
399     script = os.path.join(profile_path, "block")
400     if not os.access(script, os.X_OK | os.F_OK):
401         return False
402     return subprocess.call(script) == 0
403
404 def output_configuration(configuration, config):
405     "Write a configuration file"
406     outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
407     for output in outputs:
408         print(configuration[output].option_string, file=config)
409
410 def output_setup(configuration, setup):
411     "Write a setup (fingerprint) file"
412     outputs = sorted(configuration.keys())
413     for output in outputs:
414         if configuration[output].edid:
415             print(output, configuration[output].edid, file=setup)
416
417 def save_configuration(profile_path, configuration):
418     "Save a configuration into a profile"
419     if not os.path.isdir(profile_path):
420         os.makedirs(profile_path)
421     with open(os.path.join(profile_path, "config"), "w") as config:
422         output_configuration(configuration, config)
423     with open(os.path.join(profile_path, "setup"), "w") as setup:
424         output_setup(configuration, setup)
425
426 def update_mtime(filename):
427     "Update a file's mtime"
428     try:
429         os.utime(filename, None)
430         return True
431     except:
432         return False
433
434 def apply_configuration(new_configuration, current_configuration, dry_run=False):
435     "Apply a configuration"
436     outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
437     if dry_run:
438         base_argv = [ "echo", "xrandr" ]
439     else:
440         base_argv = [ "xrandr" ]
441
442     # There are several xrandr / driver bugs we need to take care of here:
443     # - We cannot enable more than two screens at the same time
444     #   See https://github.com/phillipberndt/autorandr/pull/6
445     #   and commits f4cce4d and 8429886.
446     # - We cannot disable all screens
447     #   See https://github.com/phillipberndt/autorandr/pull/20
448     # - We should disable screens before enabling others, because there's
449     #   a limit on the number of enabled screens
450     # - We must make sure that the screen at 0x0 is activated first,
451     #   or the other (first) screen to be activated would be moved there.
452     # - If an active screen already has a transformation and remains active,
453     #   the xrandr call fails with an invalid RRSetScreenSize parameter error.
454     #   Update the configuration in 3 passes in that case.  (On Haswell graphics,
455     #   at least.)
456
457     auxiliary_changes_pre = []
458     disable_outputs = []
459     enable_outputs = []
460     remain_active_count = 0
461     for output in outputs:
462         if not new_configuration[output].edid or "off" in new_configuration[output].options:
463             disable_outputs.append(new_configuration[output].option_vector)
464         else:
465             if "off" not in current_configuration[output].options:
466                 remain_active_count += 1
467             enable_outputs.append(new_configuration[output].option_vector)
468             if xrandr_version() >= Version("1.3.0") and "transform" in current_configuration[output].options:
469                 auxiliary_changes_pre.append(["--output", output, "--transform", "none"])
470
471     # Perform pe-change auxiliary changes
472     if auxiliary_changes_pre:
473         argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
474         if subprocess.call(argv) != 0:
475             raise RuntimeError("Command failed: %s" % " ".join(argv))
476
477     # Disable unused outputs, but make sure that there always is at least one active screen
478     disable_keep = 0 if remain_active_count else 1
479     if len(disable_outputs) > disable_keep:
480         if subprocess.call(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
481             # Disabling the outputs failed. Retry with the next command:
482             # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
483             # This does not occur if simultaneously the primary screen is reset.
484             pass
485         else:
486             disable_outputs = disable_outputs[-1:] if disable_keep else []
487
488     # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
489     # disable the last two screens. This is a problem, so if this would happen, instead disable only
490     # one screen in the first call below.
491     if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
492         # In the context of a xrandr call that changes the display state, `--query' should do nothing
493         disable_outputs.insert(0, ['--query'])
494
495     # Enable the remaining outputs in pairs of two operations
496     operations = disable_outputs + enable_outputs
497     for index in range(0, len(operations), 2):
498         argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
499         if subprocess.call(argv) != 0:
500             raise RuntimeError("Command failed: %s" % " ".join(argv))
501
502 def add_unused_outputs(source_configuration, target_configuration):
503     "Add outputs that are missing in target to target, in 'off' state"
504     for output_name, output in source_configuration.items():
505         if output_name not in target_configuration:
506             target_configuration[output_name] = XrandrOutput(output_name, output.edid, { "off": None })
507
508 def remove_irrelevant_outputs(source_configuration, target_configuration):
509     "Remove outputs from target that ought to be 'off' and already are"
510     for output_name, output in source_configuration.items():
511         if "off" in output.options and output_name in target_configuration and "off" in target_configuration[output_name].options:
512             del target_configuration[output_name]
513
514 def generate_virtual_profile(configuration, modes, profile_name):
515     "Generate one of the virtual profiles"
516     configuration = copy.deepcopy(configuration)
517     if profile_name == "common":
518         common_resolution = [ set(( ( mode["width"], mode["height"] ) for mode in output )) for output in modes.values() ]
519         common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
520         common_resolution = sorted(common_resolution, key=lambda a: int(a[0])*int(a[1]))
521         if common_resolution:
522             for output in configuration:
523                 configuration[output].options = {}
524                 if output in modes:
525                     configuration[output].options["mode"] = "%sx%s" % common_resolution[-1]
526                     configuration[output].options["pos"] = "0x0"
527                 else:
528                     configuration[output].options["off"] = None
529     elif profile_name in ("horizontal", "vertical"):
530         shift = 0
531         if profile_name == "horizontal":
532             shift_index = "width"
533             pos_specifier = "%sx0"
534         else:
535             shift_index = "height"
536             pos_specifier = "0x%s"
537
538         for output in configuration:
539             configuration[output].options = {}
540             if output in modes:
541                 mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
542                 configuration[output].options["mode"] = "%sx%s" % (mode["width"], mode["height"])
543                 configuration[output].options["rate"] = mode["rate"]
544                 configuration[output].options["pos"] = pos_specifier % shift
545                 shift += int(mode[shift_index])
546             else:
547                 configuration[output].options["off"] = None
548     return configuration
549
550 def exit_help():
551     "Print help and exit"
552     print(help_text)
553     for profile in virtual_profiles:
554         print("  %-10s %s" % profile[:2])
555     sys.exit(0)
556
557 def exec_scripts(profile_path, script_name):
558     "Run userscripts"
559     for script in (os.path.join(profile_path, script_name), os.path.join(os.path.dirname(profile_path), script_name)):
560         if os.access(script, os.X_OK | os.F_OK):
561             subprocess.call(script)
562
563 def main(argv):
564     try:
565        options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "help" ])[0])
566     except getopt.GetoptError as e:
567         print(str(e))
568         options = { "--help": True }
569
570     profiles = {}
571     try:
572         # Load profiles from each XDG config directory
573         for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
574             system_profile_path = os.path.join(directory, "autorandr")
575             if os.path.isdir(system_profile_path):
576                 profiles.update(load_profiles(system_profile_path))
577         # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
578         # profile_path is also used later on to store configurations
579         profile_path = os.path.expanduser("~/.autorandr")
580         if not os.path.isdir(profile_path):
581             # Elsewise, follow the XDG specification
582             profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
583         if os.path.isdir(profile_path):
584             profiles.update(load_profiles(profile_path))
585         # Sort by descending mtime
586         profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
587     except Exception as e:
588         print("Failed to load profiles:\n%s" % str(e), file=sys.stderr)
589         sys.exit(1)
590
591     try:
592         config, modes = parse_xrandr_output()
593     except Exception as e:
594         print("Failed to parse current configuration from XRandR:\n%s" % str(e), file=sys.stderr)
595         sys.exit(1)
596
597     if "--fingerprint" in options:
598         output_setup(config, sys.stdout)
599         sys.exit(0)
600
601     if "--config" in options:
602         output_configuration(config, sys.stdout)
603         sys.exit(0)
604
605     if "-s" in options:
606         options["--save"] = options["-s"]
607     if "--save" in options:
608         if options["--save"] in ( x[0] for x in virtual_profiles ):
609             print("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
610             sys.exit(1)
611         try:
612             save_configuration(os.path.join(profile_path, options["--save"]), config)
613         except Exception as e:
614             print("Failed to save current configuration as profile '%s':\n%s" % (options["--save"], str(e)), file=sys.stderr)
615             sys.exit(1)
616         print("Saved current configuration as profile '%s'" % options["--save"])
617         sys.exit(0)
618
619     if "-h" in options or "--help" in options:
620         exit_help()
621
622     detected_profiles = find_profiles(config, profiles)
623     load_profile = False
624
625     if "-l" in options:
626         options["--load"] = options["-l"]
627     if "--load" in options:
628         load_profile = options["--load"]
629     else:
630         for profile_name in profiles.keys():
631             if profile_blocked(os.path.join(profile_path, profile_name)):
632                 print("%s (blocked)" % profile_name, file=sys.stderr)
633                 continue
634             if profile_name in detected_profiles:
635                 print("%s (detected)" % profile_name, file=sys.stderr)
636                 if ("-c" in options or "--change" in options) and not load_profile:
637                     load_profile = profile_name
638             else:
639                 print(profile_name, file=sys.stderr)
640
641     if "-d" in options:
642         options["--default"] = options["-d"]
643     if not load_profile and "--default" in options:
644         load_profile = options["--default"]
645
646     if load_profile:
647         if load_profile in ( x[0] for x in virtual_profiles ):
648             load_config = generate_virtual_profile(config, modes, load_profile)
649             scripts_path = os.path.join(profile_path, load_profile)
650         else:
651             try:
652                 profile = profiles[load_profile]
653                 load_config = profile["config"]
654                 scripts_path = profile["path"]
655             except KeyError:
656                 print("Failed to load profile '%s':\nProfile not found" % load_profile, file=sys.stderr)
657                 sys.exit(1)
658             if load_profile in detected_profiles and detected_profiles[0] != load_profile:
659                 update_mtime(os.path.join(scripts_path, "config"))
660         add_unused_outputs(config, load_config)
661         if load_config == dict(config) and not "-f" in options and not "--force" in options:
662             print("Config already loaded", file=sys.stderr)
663             sys.exit(0)
664         remove_irrelevant_outputs(config, load_config)
665
666         try:
667             if "--dry-run" in options:
668                 apply_configuration(load_config, config, True)
669             else:
670                 exec_scripts(scripts_path, "preswitch")
671                 apply_configuration(load_config, config, False)
672                 exec_scripts(scripts_path, "postswitch")
673         except Exception as e:
674             print("Failed to apply profile '%s':\n%s" % (load_profile, str(e)), file=sys.stderr)
675             sys.exit(1)
676
677     sys.exit(0)
678
679 if __name__ == '__main__':
680     try:
681         main(sys.argv)
682     except Exception as e:
683         print("General failure. Please report this as a bug:\n%s" % (str(e),), file=sys.stderr)
684         sys.exit(1)