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