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