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
91 (?P<width>[0-9]+)x(?P<height>[0-9]+) # Resolution (might be overridden below!)
92 \+(?P<x>-?[0-9]+)\+(?P<y>-?[0-9]+)\s+ # Position
93 (?:\(0x[0-9a-fA-F]+\)\s+)? # XID
94 (?P<rotate>(?:normal|left|right|inverted))\s+ # Rotation
95 (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)? # Reflection
96 )? # .. but everything of the above only if the screen is in use.
97 (?:[\ \t]*\([^\)]+\))(?:\s*[0-9]+mm\sx\s[0-9]+mm)?
98 (?:[\ \t]*panning\ (?P<panning>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Panning information
99 (?:[\ \t]*tracking\ (?P<tracking>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Tracking information
100 (?:[\ \t]*border\ (?P<border>(?:[0-9]+/){3}[0-9]+))? # Border information
101 (?:\s*(?: # Properties of the output
102 Gamma: (?P<gamma>[0-9\.: ]+) | # Gamma value
103 Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) | # Transformation matrix
104 EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) | # EDID of the output
105 (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)* # Other properties
109 (?P<mode_width>[0-9]+)x(?P<mode_height>[0-9]+).+?\*current.*\s+
110 h:.+\s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz\s* | # Interesting (current) resolution: Extract rate
111 [0-9]+x[0-9]+(?:(?!\*current).)+\s+h:.+\s+v:.+\s* # Other resolutions
115 XRANDR_OUTPUT_MODES_REGEXP = """(?x)
116 (?P<width>[0-9]+)x(?P<height>[0-9]+)
117 .*?(?P<preferred>\+preferred)?
119 \s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz
122 XRANDR_13_DEFAULTS = {
123 "transform": "1,0,0,0,1,0,0,0,1",
127 XRANDR_12_DEFAULTS = {
130 "gamma": "1.0:1.0:1.0",
133 XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
135 EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
138 return "<%s%s %s>" % (self.output, (" %s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else "", " ".join(self.option_vector))
141 def options_with_defaults(self):
142 "Return the options dictionary, augmented with the default values that weren't set"
143 if "off" in self.options:
146 if xrandr_version() >= Version("1.3"):
147 options.update(self.XRANDR_13_DEFAULTS)
148 if xrandr_version() >= Version("1.2"):
149 options.update(self.XRANDR_12_DEFAULTS)
150 options.update(self.options)
154 def option_vector(self):
155 "Return the command line parameters for XRandR for this instance"
156 return sum([["--%s" % option[0], option[1]] if option[1] else ["--%s" % option[0]] for option in chain((("output", self.output),), sorted(self.options_with_defaults.items()))], [])
159 def option_string(self):
160 "Return the command line parameters in the configuration file format"
161 return "\n".join([ " ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.options.items()))])
165 "Return a key to sort the outputs for xrandr invocation"
168 if "off" in self.options:
170 if "pos" in self.options:
171 x, y = map(float, self.options["pos"].split("x"))
176 def __init__(self, output, edid, options):
177 "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
180 self.options = options
181 self.remove_default_option_values()
183 def remove_default_option_values(self):
184 "Remove values from the options dictionary that are superflous"
185 if "off" in self.options and len(self.options.keys()) > 1:
186 self.options = { "off": None }
188 for option, default_value in self.XRANDR_DEFAULTS.items():
189 if option in self.options and self.options[option] == default_value:
190 del self.options[option]
193 def from_xrandr_output(cls, xrandr_output):
194 """Instanciate an XrandrOutput from the output of `xrandr --verbose'
196 This method also returns a list of modes supported by the output.
199 xrandr_output = xrandr_output.replace("\r\n", "\n")
200 match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
202 raise RuntimeError("Parsing XRandR output failed, there is an error in the regular expression.")
204 debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
205 raise RuntimeError("Parsing XRandR output failed, the regular expression did not match: %s" % debug)
206 remainder = xrandr_output[len(match_object.group(0)):]
208 raise RuntimeError(("Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
209 "starting at byte %d with ..'%s'.") % (len(remainder), len(len(match_object.group(0))), remainder[:10]))
211 match = match_object.groupdict()
215 modes = [ x.groupdict() for x in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]) ]
217 raise RuntimeError("Parsing XRandR output failed, couldn't find any display modes")
220 if not match["connected"]:
223 edid = "".join(match["edid"].strip().split()) if match["edid"] else "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
225 if not match["width"]:
226 options["off"] = None
228 if match["mode_width"]:
229 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
231 if match["rotate"] not in ("left", "right"):
232 options["mode"] = "%sx%s" % (match["width"], match["height"])
234 options["mode"] = "%sx%s" % (match["height"], match["width"])
235 options["rotate"] = match["rotate"]
237 options["primary"] = None
238 if match["reflect"] == "X":
239 options["reflect"] = "x"
240 elif match["reflect"] == "Y":
241 options["reflect"] = "y"
242 elif match["reflect"] == "X and Y":
243 options["reflect"] = "xy"
244 options["pos"] = "%sx%s" % (match["x"], match["y"])
246 panning = [ match["panning"] ]
247 if match["tracking"]:
248 panning += [ "/", match["tracking"] ]
250 panning += [ "/", match["border"] ]
251 options["panning"] = "".join(panning)
252 if match["transform"]:
253 transformation = ",".join(match["transform"].strip().split())
254 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
255 options["transform"] = transformation
256 if not match["mode_width"]:
257 # TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
258 # special case is actually required.
259 print("Warning: Output %s has a transformation applied. Could not determine correct mode!", file=sys.stderr)
261 gamma = match["gamma"].strip()
262 options["gamma"] = gamma
264 options["rate"] = match["rate"]
266 return XrandrOutput(match["output"], edid, options), modes
269 def from_config_file(cls, edid_map, configuration):
270 "Instanciate an XrandrOutput from the contents of a configuration file"
272 for line in configuration.split("\n"):
274 line = line.split(None, 1)
275 options[line[0]] = line[1] if len(line) > 1 else None
279 if options["output"] in edid_map:
280 edid = edid_map[options["output"]]
282 # This fuzzy matching is for legacy autorandr that used sysfs output names
283 fuzzy_edid_map = [ re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys() ]
284 fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
285 if fuzzy_output in fuzzy_edid_map:
286 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
287 elif "off" not in options:
288 raise RuntimeError("Failed to find an EDID for output `%s' in setup file, required as `%s' is not off in config file."
289 % (options["output"], options["output"]))
290 output = options["output"]
291 del options["output"]
293 return XrandrOutput(output, edid, options)
295 def edid_equals(self, other):
296 "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
297 if self.edid and other.edid:
298 if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
299 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
300 if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
301 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
302 return self.edid == other.edid
304 def __eq__(self, other):
305 return self.edid_equals(other) and self.output == other.output and self.options == other.options
307 def xrandr_version():
308 "Return the version of XRandR that this system uses"
309 if getattr(xrandr_version, "version", False) is False:
310 version_string = os.popen("xrandr -v").read()
312 version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
313 xrandr_version.version = Version(version)
314 except AttributeError:
315 xrandr_version.version = Version("1.3.0")
317 return xrandr_version.version
319 def debug_regexp(pattern, string):
320 "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
323 bounds = ( 0, len(string) )
324 while bounds[0] != bounds[1]:
325 half = int((bounds[0] + bounds[1]) / 2)
326 if half == bounds[0]:
328 bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
329 partial_length = bounds[0]
330 return ("Regular expression matched until position "
331 "%d, ..'%s', and did not match from '%s'.." % (partial_length, string[max(0, partial_length-20):partial_length],
332 string[partial_length:partial_length+10]))
335 return "Debug information available if `regex' module is installed."
337 def parse_xrandr_output():
338 "Parse the output of `xrandr --verbose' into a list of outputs"
339 xrandr_output = os.popen("xrandr -q --verbose").read()
340 if not xrandr_output:
341 raise RuntimeError("Failed to run xrandr")
343 # We are not interested in screens
344 xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
346 # Split at output boundaries and instanciate an XrandrOutput per output
347 split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
348 if len(split_xrandr_output) < 2:
349 raise RuntimeError("No output boundaries found")
350 outputs = OrderedDict()
351 modes = OrderedDict()
352 for i in range(1, len(split_xrandr_output), 2):
353 output_name = split_xrandr_output[i].split()[0]
354 output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i+2]))
355 outputs[output_name] = output
357 modes[output_name] = output_modes
359 return outputs, modes
361 def load_profiles(profile_path):
362 "Load the stored profiles"
365 for profile in os.listdir(profile_path):
366 config_name = os.path.join(profile_path, profile, "config")
367 setup_name = os.path.join(profile_path, profile, "setup")
368 if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
371 edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
375 for line in chain(open(config_name).readlines(), ["output"]):
376 if line[:6] == "output" and buffer:
377 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
382 for output_name in list(config.keys()):
383 if config[output_name].edid is None:
384 del config[output_name]
386 profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
390 def find_profiles(current_config, profiles):
391 "Find profiles matching the currently connected outputs"
392 detected_profiles = []
393 for profile_name, profile in profiles.items():
394 config = profile["config"]
396 for name, output in config.items():
399 if name not in current_config or not output.edid_equals(current_config[name]):
402 if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
405 detected_profiles.append(profile_name)
406 return detected_profiles
408 def profile_blocked(profile_path):
409 "Check if a profile is blocked"
410 script = os.path.join(profile_path, "block")
411 if not os.access(script, os.X_OK | os.F_OK):
413 return subprocess.call(script) == 0
415 def output_configuration(configuration, config):
416 "Write a configuration file"
417 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
418 for output in outputs:
419 print(configuration[output].option_string, file=config)
421 def output_setup(configuration, setup):
422 "Write a setup (fingerprint) file"
423 outputs = sorted(configuration.keys())
424 for output in outputs:
425 if configuration[output].edid:
426 print(output, configuration[output].edid, file=setup)
428 def save_configuration(profile_path, configuration):
429 "Save a configuration into a profile"
430 if not os.path.isdir(profile_path):
431 os.makedirs(profile_path)
432 with open(os.path.join(profile_path, "config"), "w") as config:
433 output_configuration(configuration, config)
434 with open(os.path.join(profile_path, "setup"), "w") as setup:
435 output_setup(configuration, setup)
437 def update_mtime(filename):
438 "Update a file's mtime"
440 os.utime(filename, None)
445 def apply_configuration(new_configuration, current_configuration, dry_run=False):
446 "Apply a configuration"
447 outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
449 base_argv = [ "echo", "xrandr" ]
451 base_argv = [ "xrandr" ]
453 # There are several xrandr / driver bugs we need to take care of here:
454 # - We cannot enable more than two screens at the same time
455 # See https://github.com/phillipberndt/autorandr/pull/6
456 # and commits f4cce4d and 8429886.
457 # - We cannot disable all screens
458 # See https://github.com/phillipberndt/autorandr/pull/20
459 # - We should disable screens before enabling others, because there's
460 # a limit on the number of enabled screens
461 # - We must make sure that the screen at 0x0 is activated first,
462 # or the other (first) screen to be activated would be moved there.
463 # - If an active screen already has a transformation and remains active,
464 # the xrandr call fails with an invalid RRSetScreenSize parameter error.
465 # Update the configuration in 3 passes in that case. (On Haswell graphics,
468 auxiliary_changes_pre = []
471 remain_active_count = 0
472 for output in outputs:
473 if not new_configuration[output].edid or "off" in new_configuration[output].options:
474 disable_outputs.append(new_configuration[output].option_vector)
476 if "off" not in current_configuration[output].options:
477 remain_active_count += 1
478 enable_outputs.append(new_configuration[output].option_vector)
479 if xrandr_version() >= Version("1.3.0") and "transform" in current_configuration[output].options:
480 auxiliary_changes_pre.append(["--output", output, "--transform", "none"])
482 # Perform pe-change auxiliary changes
483 if auxiliary_changes_pre:
484 argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
485 if subprocess.call(argv) != 0:
486 raise RuntimeError("Command failed: %s" % " ".join(argv))
488 # Disable unused outputs, but make sure that there always is at least one active screen
489 disable_keep = 0 if remain_active_count else 1
490 if len(disable_outputs) > disable_keep:
491 if subprocess.call(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
492 # Disabling the outputs failed. Retry with the next command:
493 # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
494 # This does not occur if simultaneously the primary screen is reset.
497 disable_outputs = disable_outputs[-1:] if disable_keep else []
499 # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
500 # disable the last two screens. This is a problem, so if this would happen, instead disable only
501 # one screen in the first call below.
502 if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
503 # In the context of a xrandr call that changes the display state, `--query' should do nothing
504 disable_outputs.insert(0, ['--query'])
506 # Enable the remaining outputs in pairs of two operations
507 operations = disable_outputs + enable_outputs
508 for index in range(0, len(operations), 2):
509 argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
510 if subprocess.call(argv) != 0:
511 raise RuntimeError("Command failed: %s" % " ".join(argv))
513 def add_unused_outputs(source_configuration, target_configuration):
514 "Add outputs that are missing in target to target, in 'off' state"
515 for output_name, output in source_configuration.items():
516 if output_name not in target_configuration:
517 target_configuration[output_name] = XrandrOutput(output_name, output.edid, { "off": None })
519 def remove_irrelevant_outputs(source_configuration, target_configuration):
520 "Remove outputs from target that ought to be 'off' and already are"
521 for output_name, output in source_configuration.items():
522 if "off" in output.options and output_name in target_configuration and "off" in target_configuration[output_name].options:
523 del target_configuration[output_name]
525 def generate_virtual_profile(configuration, modes, profile_name):
526 "Generate one of the virtual profiles"
527 configuration = copy.deepcopy(configuration)
528 if profile_name == "common":
529 common_resolution = [ set(( ( mode["width"], mode["height"] ) for mode in output )) for output in modes.values() ]
530 common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
531 common_resolution = sorted(common_resolution, key=lambda a: int(a[0])*int(a[1]))
532 if common_resolution:
533 for output in configuration:
534 configuration[output].options = {}
536 configuration[output].options["mode"] = "%sx%s" % common_resolution[-1]
537 configuration[output].options["pos"] = "0x0"
539 configuration[output].options["off"] = None
540 elif profile_name in ("horizontal", "vertical"):
542 if profile_name == "horizontal":
543 shift_index = "width"
544 pos_specifier = "%sx0"
546 shift_index = "height"
547 pos_specifier = "0x%s"
549 for output in configuration:
550 configuration[output].options = {}
552 mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
553 configuration[output].options["mode"] = "%sx%s" % (mode["width"], mode["height"])
554 configuration[output].options["rate"] = mode["rate"]
555 configuration[output].options["pos"] = pos_specifier % shift
556 shift += int(mode[shift_index])
558 configuration[output].options["off"] = None
562 "Print help and exit"
564 for profile in virtual_profiles:
565 print(" %-10s %s" % profile[:2])
568 def exec_scripts(profile_path, script_name):
570 for script in (os.path.join(profile_path, script_name), os.path.join(os.path.dirname(profile_path), script_name)):
571 if os.access(script, os.X_OK | os.F_OK):
572 subprocess.call(script)
576 options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "help" ])[0])
577 except getopt.GetoptError as e:
579 options = { "--help": True }
583 # Load profiles from each XDG config directory
584 for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
585 system_profile_path = os.path.join(directory, "autorandr")
586 if os.path.isdir(system_profile_path):
587 profiles.update(load_profiles(system_profile_path))
588 # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
589 # profile_path is also used later on to store configurations
590 profile_path = os.path.expanduser("~/.autorandr")
591 if not os.path.isdir(profile_path):
592 # Elsewise, follow the XDG specification
593 profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
594 if os.path.isdir(profile_path):
595 profiles.update(load_profiles(profile_path))
596 # Sort by descending mtime
597 profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
598 except Exception as e:
599 print("Failed to load profiles:\n%s" % str(e), file=sys.stderr)
603 config, modes = parse_xrandr_output()
604 except Exception as e:
605 print("Failed to parse current configuration from XRandR:\n%s" % str(e), file=sys.stderr)
608 if "--fingerprint" in options:
609 output_setup(config, sys.stdout)
612 if "--config" in options:
613 output_configuration(config, sys.stdout)
617 options["--save"] = options["-s"]
618 if "--save" in options:
619 if options["--save"] in ( x[0] for x in virtual_profiles ):
620 print("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
623 save_configuration(os.path.join(profile_path, options["--save"]), config)
624 except Exception as e:
625 print("Failed to save current configuration as profile '%s':\n%s" % (options["--save"], str(e)), file=sys.stderr)
627 print("Saved current configuration as profile '%s'" % options["--save"])
630 if "-h" in options or "--help" in options:
633 detected_profiles = find_profiles(config, profiles)
637 options["--load"] = options["-l"]
638 if "--load" in options:
639 load_profile = options["--load"]
641 for profile_name in profiles.keys():
642 if profile_blocked(os.path.join(profile_path, profile_name)):
643 print("%s (blocked)" % profile_name, file=sys.stderr)
645 if profile_name in detected_profiles:
646 print("%s (detected)" % profile_name, file=sys.stderr)
647 if ("-c" in options or "--change" in options) and not load_profile:
648 load_profile = profile_name
650 print(profile_name, file=sys.stderr)
653 options["--default"] = options["-d"]
654 if not load_profile and "--default" in options:
655 load_profile = options["--default"]
658 if load_profile in ( x[0] for x in virtual_profiles ):
659 load_config = generate_virtual_profile(config, modes, load_profile)
660 scripts_path = os.path.join(profile_path, load_profile)
663 profile = profiles[load_profile]
664 load_config = profile["config"]
665 scripts_path = profile["path"]
667 print("Failed to load profile '%s':\nProfile not found" % load_profile, file=sys.stderr)
669 if load_profile in detected_profiles and detected_profiles[0] != load_profile:
670 update_mtime(os.path.join(scripts_path, "config"))
671 add_unused_outputs(config, load_config)
672 if load_config == dict(config) and not "-f" in options and not "--force" in options:
673 print("Config already loaded", file=sys.stderr)
675 remove_irrelevant_outputs(config, load_config)
678 if "--dry-run" in options:
679 apply_configuration(load_config, config, True)
681 exec_scripts(scripts_path, "preswitch")
682 apply_configuration(load_config, config, False)
683 exec_scripts(scripts_path, "postswitch")
684 except Exception as e:
685 print("Failed to apply profile '%s':\n%s" % (load_profile, str(e)), file=sys.stderr)
690 if __name__ == '__main__':
693 except Exception as e:
694 print("General failure. Please report this as a bug:\n%s" % (str(e),), file=sys.stderr)