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