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