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