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