5 # Copyright (c) 2015, Phillip Berndt
7 # Autorandr rewrite in Python
9 # This script aims to be fully compatible with the original autorandr.
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.
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.
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/>.
25 from __future__ import print_function
35 from distutils.version import LooseVersion as Version
37 from itertools import chain
38 from collections import OrderedDict
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),
48 Usage: autorandr [options]
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
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.
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
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.
74 The following virtual configurations are available:
77 class XrandrOutput(object):
78 "Represents an XRandR output"
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
86 (?P<connected>connected)
89 (?P<primary>primary\ )? # Might be primary screen
90 (?:\s* # The remainder of the first line only appears as one or not at all:
91 (?P<width>[0-9]+)x(?P<height>[0-9]+) # Resolution (might be overridden below!)
92 \+(?P<x>[0-9]+)\+(?P<y>[0-9]+)\s+ # Position
93 (?:\(0x[0-9a-fA-F]+\)\s+)? # XID
94 (?P<rotate>(?:normal|left|right|inverted))\s+ # Rotation
95 (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)? # Reflection
96 )? # .. but everything of the above only if the screen is in use.
98 (?:\s*(?: # Properties of the output
99 Gamma: (?P<gamma>[0-9\.: ]+) | # Gamma value
100 Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) | # Transformation matrix
101 EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) | # EDID of the output
102 (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)* # Other properties
106 (?P<mode_width>[0-9]+)x(?P<mode_height>[0-9]+).+?\*current.+\s+
107 h:.+\s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz\s* | # Interesting (current) resolution: Extract rate
108 [0-9]+x[0-9]+.+\s+h:.+\s+v:.+\s* # Other resolutions
112 XRANDR_OUTPUT_MODES_REGEXP = """(?x)
113 (?P<width>[0-9]+)x(?P<height>[0-9]+)
114 .*?(?P<preferred>\+preferred)?
116 \s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz
119 XRANDR_13_DEFAULTS = {
120 "transform": "1,0,0,0,1,0,0,0,1",
123 XRANDR_12_DEFAULTS = {
126 "gamma": "1.0:1.0:1.0",
129 XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
131 EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
134 return "<%s%s %s>" % (self.output, (" %s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else "", " ".join(self.option_vector))
137 def options_with_defaults(self):
138 "Return the options dictionary, augmented with the default values that weren't set"
139 if "off" in self.options:
142 if xrandr_version() >= Version("1.3"):
143 options.update(self.XRANDR_13_DEFAULTS)
144 if xrandr_version() >= Version("1.2"):
145 options.update(self.XRANDR_12_DEFAULTS)
146 options.update(self.options)
150 def option_vector(self):
151 "Return the command line parameters for XRandR for this instance"
152 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()))], [])
155 def option_string(self):
156 "Return the command line parameters in the configuration file format"
157 return "\n".join([ " ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.options.items()))])
161 "Return a key to sort the outputs for xrandr invocation"
164 if "off" in self.options:
166 if "pos" in self.options:
167 x, y = map(float, self.options["pos"].split("x"))
172 def __init__(self, output, edid, options):
173 "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
176 self.options = options
177 self.remove_default_option_values()
179 def remove_default_option_values(self):
180 "Remove values from the options dictionary that are superflous"
181 if "off" in self.options and len(self.options.keys()) > 1:
182 self.options = { "off": None }
184 for option, default_value in self.XRANDR_DEFAULTS.items():
185 if option in self.options and self.options[option] == default_value:
186 del self.options[option]
189 def from_xrandr_output(cls, xrandr_output):
190 """Instanciate an XrandrOutput from the output of `xrandr --verbose'
192 This method also returns a list of modes supported by the output.
195 xrandr_output = xrandr_output.replace("\r\n", "\n")
196 match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
198 raise RuntimeError("Parsing XRandR output failed, there is an error in the regular expression.")
200 debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
201 raise RuntimeError("Parsing XRandR output failed, the regular expression did not match: %s" % debug)
202 remainder = xrandr_output[len(match_object.group(0)):]
204 raise RuntimeError(("Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
205 "starting at byte %d with ..'%s'.") % (len(remainder), len(len(match_object.group(0))), remainder[:10]))
207 match = match_object.groupdict()
211 modes = [ x.groupdict() for x in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]) ]
213 raise RuntimeError("Parsing XRandR output failed, couldn't find any display modes")
216 if not match["connected"]:
219 edid = "".join(match["edid"].strip().split()) if match["edid"] else "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
221 if not match["width"]:
222 options["off"] = None
224 if match["mode_width"]:
225 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
227 if match["rotate"] not in ("left", "right"):
228 options["mode"] = "%sx%s" % (match["width"], match["height"])
230 options["mode"] = "%sx%s" % (match["height"], match["width"])
231 options["rotate"] = match["rotate"]
233 options["primary"] = None
234 if match["reflect"] == "X":
235 options["reflect"] = "x"
236 elif match["reflect"] == "Y":
237 options["reflect"] = "y"
238 elif match["reflect"] == "X and Y":
239 options["reflect"] = "xy"
240 options["pos"] = "%sx%s" % (match["x"], match["y"])
241 if match["transform"]:
242 transformation = ",".join(match["transform"].strip().split())
243 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
244 options["transform"] = transformation
245 if not match["mode_width"]:
246 # TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
247 # special case is actually required.
248 print("Warning: Output %s has a transformation applied. Could not determine correct mode!", file=sys.stderr)
250 gamma = match["gamma"].strip()
251 options["gamma"] = gamma
253 options["rate"] = match["rate"]
255 return XrandrOutput(match["output"], edid, options), modes
258 def from_config_file(cls, edid_map, configuration):
259 "Instanciate an XrandrOutput from the contents of a configuration file"
261 for line in configuration.split("\n"):
263 line = line.split(None, 1)
264 options[line[0]] = line[1] if len(line) > 1 else None
268 if options["output"] in edid_map:
269 edid = edid_map[options["output"]]
271 # This fuzzy matching is for legacy autorandr that used sysfs output names
272 fuzzy_edid_map = [ re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys() ]
273 fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
274 if fuzzy_output in fuzzy_edid_map:
275 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
276 elif "off" not in options:
277 raise RuntimeError("Failed to find an EDID for output `%s' in setup file, required as `%s' is not off in config file."
278 % (options["output"], options["output"]))
279 output = options["output"]
280 del options["output"]
282 return XrandrOutput(output, edid, options)
284 def edid_equals(self, other):
285 "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
286 if self.edid and other.edid:
287 if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
288 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
289 if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
290 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
291 return self.edid == other.edid
293 def __eq__(self, other):
294 return self.edid_equals(other) and self.output == other.output and self.options == other.options
296 def xrandr_version():
297 "Return the version of XRandR that this system uses"
298 if getattr(xrandr_version, "version", False) is False:
299 version_string = os.popen("xrandr -v").read()
301 version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
302 xrandr_version.version = Version(version)
303 except AttributeError:
304 xrandr_version.version = Version("1.3.0")
306 return xrandr_version.version
308 def debug_regexp(pattern, string):
309 "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
312 bounds = ( 0, len(string) )
313 while bounds[0] != bounds[1]:
314 half = int((bounds[0] + bounds[1]) / 2)
315 if half == bounds[0]:
317 bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
318 partial_length = bounds[0]
319 return ("Regular expression matched until position "
320 "%d, ..'%s', and did not match from '%s'.." % (partial_length, string[max(0, partial_length-20):partial_length],
321 string[partial_length:partial_length+10]))
324 return "Debug information available if `regex' module is installed."
326 def parse_xrandr_output():
327 "Parse the output of `xrandr --verbose' into a list of outputs"
328 xrandr_output = os.popen("xrandr -q --verbose").read()
329 if not xrandr_output:
330 raise RuntimeError("Failed to run xrandr")
332 # We are not interested in screens
333 xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
335 # Split at output boundaries and instanciate an XrandrOutput per output
336 split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
337 if len(split_xrandr_output) < 2:
338 raise RuntimeError("No output boundaries found")
339 outputs = OrderedDict()
340 modes = OrderedDict()
341 for i in range(1, len(split_xrandr_output), 2):
342 output_name = split_xrandr_output[i].split()[0]
343 output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i+2]))
344 outputs[output_name] = output
346 modes[output_name] = output_modes
348 return outputs, modes
350 def load_profiles(profile_path):
351 "Load the stored profiles"
354 for profile in os.listdir(profile_path):
355 config_name = os.path.join(profile_path, profile, "config")
356 setup_name = os.path.join(profile_path, profile, "setup")
357 if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
360 edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
364 for line in chain(open(config_name).readlines(), ["output"]):
365 if line[:6] == "output" and buffer:
366 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
371 for output_name in list(config.keys()):
372 if config[output_name].edid is None:
373 del config[output_name]
375 profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
379 def find_profiles(current_config, profiles):
380 "Find profiles matching the currently connected outputs"
381 detected_profiles = []
382 for profile_name, profile in profiles.items():
383 config = profile["config"]
385 for name, output in config.items():
388 if name not in current_config or not output.edid_equals(current_config[name]):
391 if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
394 detected_profiles.append(profile_name)
395 return detected_profiles
397 def profile_blocked(profile_path):
398 "Check if a profile is blocked"
399 script = os.path.join(profile_path, "block")
400 if not os.access(script, os.X_OK | os.F_OK):
402 return subprocess.call(script) == 0
404 def output_configuration(configuration, config):
405 "Write a configuration file"
406 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
407 for output in outputs:
408 print(configuration[output].option_string, file=config)
410 def output_setup(configuration, setup):
411 "Write a setup (fingerprint) file"
412 outputs = sorted(configuration.keys())
413 for output in outputs:
414 if configuration[output].edid:
415 print(output, configuration[output].edid, file=setup)
417 def save_configuration(profile_path, configuration):
418 "Save a configuration into a profile"
419 if not os.path.isdir(profile_path):
420 os.makedirs(profile_path)
421 with open(os.path.join(profile_path, "config"), "w") as config:
422 output_configuration(configuration, config)
423 with open(os.path.join(profile_path, "setup"), "w") as setup:
424 output_setup(configuration, setup)
426 def update_mtime(filename):
427 "Update a file's mtime"
429 os.utime(filename, None)
434 def apply_configuration(new_configuration, current_configuration, dry_run=False):
435 "Apply a configuration"
436 outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
438 base_argv = [ "echo", "xrandr" ]
440 base_argv = [ "xrandr" ]
442 # There are several xrandr / driver bugs we need to take care of here:
443 # - We cannot enable more than two screens at the same time
444 # See https://github.com/phillipberndt/autorandr/pull/6
445 # and commits f4cce4d and 8429886.
446 # - We cannot disable all screens
447 # See https://github.com/phillipberndt/autorandr/pull/20
448 # - We should disable screens before enabling others, because there's
449 # a limit on the number of enabled screens
450 # - We must make sure that the screen at 0x0 is activated first,
451 # or the other (first) screen to be activated would be moved there.
452 # - If an active screen already has a transformation and remains active,
453 # the xrandr call fails with an invalid RRSetScreenSize parameter error.
454 # Update the configuration in 3 passes in that case. (On Haswell graphics,
457 auxiliary_changes_pre = []
460 remain_active_count = 0
461 for output in outputs:
462 if not new_configuration[output].edid or "off" in new_configuration[output].options:
463 disable_outputs.append(new_configuration[output].option_vector)
465 if "off" not in current_configuration[output].options:
466 remain_active_count += 1
467 enable_outputs.append(new_configuration[output].option_vector)
468 if xrandr_version() >= Version("1.3.0") and "transform" in current_configuration[output].options:
469 auxiliary_changes_pre.append(["--output", output, "--transform", "none"])
471 # Perform pe-change auxiliary changes
472 if auxiliary_changes_pre:
473 argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
474 if subprocess.call(argv) != 0:
475 raise RuntimeError("Command failed: %s" % " ".join(argv))
477 # Disable unused outputs, but make sure that there always is at least one active screen
478 disable_keep = 0 if remain_active_count else 1
479 if len(disable_outputs) > disable_keep:
480 if subprocess.call(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
481 # Disabling the outputs failed. Retry with the next command:
482 # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
483 # This does not occur if simultaneously the primary screen is reset.
486 disable_outputs = disable_outputs[-1:] if disable_keep else []
488 # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
489 # disable the last two screens. This is a problem, so if this would happen, instead disable only
490 # one screen in the first call below.
491 if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
492 # In the context of a xrandr call that changes the display state, `--query' should do nothing
493 disable_outputs.insert(0, ['--query'])
495 # Enable the remaining outputs in pairs of two operations
496 operations = disable_outputs + enable_outputs
497 for index in range(0, len(operations), 2):
498 argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
499 if subprocess.call(argv) != 0:
500 raise RuntimeError("Command failed: %s" % " ".join(argv))
502 def add_unused_outputs(source_configuration, target_configuration):
503 "Add outputs that are missing in target to target, in 'off' state"
504 for output_name, output in source_configuration.items():
505 if output_name not in target_configuration:
506 target_configuration[output_name] = XrandrOutput(output_name, output.edid, { "off": None })
508 def remove_irrelevant_outputs(source_configuration, target_configuration):
509 "Remove outputs from target that ought to be 'off' and already are"
510 for output_name, output in source_configuration.items():
511 if "off" in output.options and output_name in target_configuration and "off" in target_configuration[output_name].options:
512 del target_configuration[output_name]
514 def generate_virtual_profile(configuration, modes, profile_name):
515 "Generate one of the virtual profiles"
516 configuration = copy.deepcopy(configuration)
517 if profile_name == "common":
518 common_resolution = [ set(( ( mode["width"], mode["height"] ) for mode in output )) for output in modes.values() ]
519 common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
520 common_resolution = sorted(common_resolution, key=lambda a: int(a[0])*int(a[1]))
521 if common_resolution:
522 for output in configuration:
523 configuration[output].options = {}
525 configuration[output].options["mode"] = "%sx%s" % common_resolution[-1]
526 configuration[output].options["pos"] = "0x0"
528 configuration[output].options["off"] = None
529 elif profile_name in ("horizontal", "vertical"):
531 if profile_name == "horizontal":
532 shift_index = "width"
533 pos_specifier = "%sx0"
535 shift_index = "height"
536 pos_specifier = "0x%s"
538 for output in configuration:
539 configuration[output].options = {}
541 mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
542 configuration[output].options["mode"] = "%sx%s" % (mode["width"], mode["height"])
543 configuration[output].options["rate"] = mode["rate"]
544 configuration[output].options["pos"] = pos_specifier % shift
545 shift += int(mode[shift_index])
547 configuration[output].options["off"] = None
551 "Print help and exit"
553 for profile in virtual_profiles:
554 print(" %-10s %s" % profile[:2])
557 def exec_scripts(profile_path, script_name):
559 for script in (os.path.join(profile_path, script_name), os.path.join(os.path.dirname(profile_path), script_name)):
560 if os.access(script, os.X_OK | os.F_OK):
561 subprocess.call(script)
565 options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "help" ])[0])
566 except getopt.GetoptError as e:
568 options = { "--help": True }
572 # Load profiles from each XDG config directory
573 for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
574 system_profile_path = os.path.join(directory, "autorandr")
575 if os.path.isdir(system_profile_path):
576 profiles.update(load_profiles(system_profile_path))
577 # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
578 # profile_path is also used later on to store configurations
579 profile_path = os.path.expanduser("~/.autorandr")
580 if not os.path.isdir(profile_path):
581 # Elsewise, follow the XDG specification
582 profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
583 if os.path.isdir(profile_path):
584 profiles.update(load_profiles(profile_path))
585 # Sort by descending mtime
586 profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
587 except Exception as e:
588 print("Failed to load profiles:\n%s" % str(e), file=sys.stderr)
592 config, modes = parse_xrandr_output()
593 except Exception as e:
594 print("Failed to parse current configuration from XRandR:\n%s" % str(e), file=sys.stderr)
597 if "--fingerprint" in options:
598 output_setup(config, sys.stdout)
601 if "--config" in options:
602 output_configuration(config, sys.stdout)
606 options["--save"] = options["-s"]
607 if "--save" in options:
608 if options["--save"] in ( x[0] for x in virtual_profiles ):
609 print("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
612 save_configuration(os.path.join(profile_path, options["--save"]), config)
613 except Exception as e:
614 print("Failed to save current configuration as profile '%s':\n%s" % (options["--save"], str(e)), file=sys.stderr)
616 print("Saved current configuration as profile '%s'" % options["--save"])
619 if "-h" in options or "--help" in options:
622 detected_profiles = find_profiles(config, profiles)
626 options["--load"] = options["-l"]
627 if "--load" in options:
628 load_profile = options["--load"]
630 for profile_name in profiles.keys():
631 if profile_blocked(os.path.join(profile_path, profile_name)):
632 print("%s (blocked)" % profile_name, file=sys.stderr)
634 if profile_name in detected_profiles:
635 print("%s (detected)" % profile_name, file=sys.stderr)
636 if ("-c" in options or "--change" in options) and not load_profile:
637 load_profile = profile_name
639 print(profile_name, file=sys.stderr)
642 options["--default"] = options["-d"]
643 if not load_profile and "--default" in options:
644 load_profile = options["--default"]
647 if load_profile in ( x[0] for x in virtual_profiles ):
648 load_config = generate_virtual_profile(config, modes, load_profile)
649 scripts_path = os.path.join(profile_path, load_profile)
652 profile = profiles[load_profile]
653 load_config = profile["config"]
654 scripts_path = profile["path"]
656 print("Failed to load profile '%s':\nProfile not found" % load_profile, file=sys.stderr)
658 if load_profile in detected_profiles and detected_profiles[0] != load_profile:
659 update_mtime(os.path.join(scripts_path, "config"))
660 add_unused_outputs(config, load_config)
661 if load_config == dict(config) and not "-f" in options and not "--force" in options:
662 print("Config already loaded", file=sys.stderr)
664 remove_irrelevant_outputs(config, load_config)
667 if "--dry-run" in options:
668 apply_configuration(load_config, config, True)
670 exec_scripts(scripts_path, "preswitch")
671 apply_configuration(load_config, config, False)
672 exec_scripts(scripts_path, "postswitch")
673 except Exception as e:
674 print("Failed to apply profile '%s':\n%s" % (load_profile, str(e)), file=sys.stderr)
679 if __name__ == '__main__':
682 except Exception as e:
683 print("General failure. Please report this as a bug:\n%s" % (str(e),), file=sys.stderr)