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 AutorandrException(Exception):
78 def __init__(self, message, original_exception=None, report_bug=False):
79 self.message = message
80 self.report_bug = report_bug
81 if original_exception:
82 self.original_exception = original_exception
83 trace = sys.exc_info()[2]
86 self.line = trace.tb_lineno
90 self.line = inspect.currentframe().f_back.f_lineno
93 self.original_exception = None
96 retval = [ self.message ]
98 retval.append(" (line %d)" % self.line)
99 if self.original_exception:
100 retval.append(":\n ")
101 retval.append(str(self.original_exception).replace("\n", "\n "))
103 retval.append("\nThis appears to be a bug. Please help improving autorandr by reporting it upstream."
104 "\nPlease attach the output of `xrandr --verbose` to your bug report if appropriate.")
105 return "".join(retval)
107 class XrandrOutput(object):
108 "Represents an XRandR output"
110 # This regular expression is used to parse an output in `xrandr --verbose'
111 XRANDR_OUTPUT_REGEXP = """(?x)
112 ^(?P<output>[^ ]+)\s+ # Line starts with output name
113 (?: # Differentiate disconnected and connected in first line
115 unknown\ connection |
116 (?P<connected>connected)
119 (?P<primary>primary\ )? # Might be primary screen
121 (?P<width>[0-9]+)x(?P<height>[0-9]+) # Resolution (might be overridden below!)
122 \+(?P<x>-?[0-9]+)\+(?P<y>-?[0-9]+)\s+ # Position
123 (?:\(0x[0-9a-fA-F]+\)\s+)? # XID
124 (?P<rotate>(?:normal|left|right|inverted))\s+ # Rotation
125 (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)? # Reflection
126 )? # .. but everything of the above only if the screen is in use.
127 (?:[\ \t]*\([^\)]+\))(?:\s*[0-9]+mm\sx\s[0-9]+mm)?
128 (?:[\ \t]*panning\ (?P<panning>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Panning information
129 (?:[\ \t]*tracking\ (?P<tracking>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Tracking information
130 (?:[\ \t]*border\ (?P<border>(?:[0-9]+/){3}[0-9]+))? # Border information
131 (?:\s*(?: # Properties of the output
132 Gamma: (?P<gamma>[0-9\.: ]+) | # Gamma value
133 Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) | # Transformation matrix
134 EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) | # EDID of the output
135 (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)* # Other properties
139 (?P<mode_name>\S+).+?\*current.*\s+ # Interesting (current) resolution: Extract rate
140 h:\s+width\s+(?P<mode_width>[0-9]+).+\s+
141 v:\s+height\s+(?P<mode_height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
142 \S+(?:(?!\*current).)+\s+h:.+\s+v:.+\s* # Other resolutions
146 XRANDR_OUTPUT_MODES_REGEXP = """(?x)
147 (?P<name>\S+).+?(?P<preferred>\+preferred)?\s+
148 h:\s+width\s+(?P<width>[0-9]+).+\s+
149 v:\s+height\s+(?P<height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
152 XRANDR_13_DEFAULTS = {
153 "transform": "1,0,0,0,1,0,0,0,1",
157 XRANDR_12_DEFAULTS = {
160 "gamma": "1.0:1.0:1.0",
163 XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
165 EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
168 return "<%s%s %s>" % (self.output, (" %s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else "", " ".join(self.option_vector))
171 def options_with_defaults(self):
172 "Return the options dictionary, augmented with the default values that weren't set"
173 if "off" in self.options:
176 if xrandr_version() >= Version("1.3"):
177 options.update(self.XRANDR_13_DEFAULTS)
178 if xrandr_version() >= Version("1.2"):
179 options.update(self.XRANDR_12_DEFAULTS)
180 options.update(self.options)
184 def option_vector(self):
185 "Return the command line parameters for XRandR for this instance"
186 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()))], [])
189 def option_string(self):
190 "Return the command line parameters in the configuration file format"
191 return "\n".join([ " ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.options.items()))])
195 "Return a key to sort the outputs for xrandr invocation"
198 if "off" in self.options:
200 if "pos" in self.options:
201 x, y = map(float, self.options["pos"].split("x"))
206 def __init__(self, output, edid, options):
207 "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
210 self.options = options
211 self.remove_default_option_values()
213 def remove_default_option_values(self):
214 "Remove values from the options dictionary that are superflous"
215 if "off" in self.options and len(self.options.keys()) > 1:
216 self.options = { "off": None }
218 for option, default_value in self.XRANDR_DEFAULTS.items():
219 if option in self.options and self.options[option] == default_value:
220 del self.options[option]
223 def from_xrandr_output(cls, xrandr_output):
224 """Instanciate an XrandrOutput from the output of `xrandr --verbose'
226 This method also returns a list of modes supported by the output.
229 xrandr_output = xrandr_output.replace("\r\n", "\n")
230 match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
232 raise AutorandrException("Parsing XRandR output failed, there is an error in the regular expression.", report_bug = True)
234 debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
235 raise AutorandrException("Parsing XRandR output failed, the regular expression did not match: %s" % debug, report_bug = True)
236 remainder = xrandr_output[len(match_object.group(0)):]
238 raise AutorandrException(("Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
239 "starting at byte %d with ..'%s'.") % (len(remainder), len(match_object.group(0)), remainder[:10]), report_bug=True)
241 match = match_object.groupdict()
245 modes = [ x.groupdict() for x in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]) if x.group("name") ]
247 raise AutorandrException("Parsing XRandR output failed, couldn't find any display modes", report_bug=True)
250 if not match["connected"]:
253 edid = "".join(match["edid"].strip().split()) if match["edid"] else "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
255 if not match["width"]:
256 options["off"] = None
258 if match["mode_name"]:
259 options["mode"] = match["mode_name"]
260 elif match["mode_width"]:
261 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
263 if match["rotate"] not in ("left", "right"):
264 options["mode"] = "%sx%s" % (match["width"], match["height"])
266 options["mode"] = "%sx%s" % (match["height"], match["width"])
267 options["rotate"] = match["rotate"]
269 options["primary"] = None
270 if match["reflect"] == "X":
271 options["reflect"] = "x"
272 elif match["reflect"] == "Y":
273 options["reflect"] = "y"
274 elif match["reflect"] == "X and Y":
275 options["reflect"] = "xy"
276 options["pos"] = "%sx%s" % (match["x"], match["y"])
278 panning = [ match["panning"] ]
279 if match["tracking"]:
280 panning += [ "/", match["tracking"] ]
282 panning += [ "/", match["border"] ]
283 options["panning"] = "".join(panning)
284 if match["transform"]:
285 transformation = ",".join(match["transform"].strip().split())
286 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
287 options["transform"] = transformation
288 if not match["mode_name"]:
289 # TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
290 # special case is actually required.
291 print("Warning: Output %s has a transformation applied. Could not determine correct mode! Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
293 gamma = match["gamma"].strip()
294 options["gamma"] = gamma
296 options["rate"] = match["rate"]
298 return XrandrOutput(match["output"], edid, options), modes
301 def from_config_file(cls, edid_map, configuration):
302 "Instanciate an XrandrOutput from the contents of a configuration file"
304 for line in configuration.split("\n"):
306 line = line.split(None, 1)
307 options[line[0]] = line[1] if len(line) > 1 else None
311 if options["output"] in edid_map:
312 edid = edid_map[options["output"]]
314 # This fuzzy matching is for legacy autorandr that used sysfs output names
315 fuzzy_edid_map = [ re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys() ]
316 fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
317 if fuzzy_output in fuzzy_edid_map:
318 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
319 elif "off" not in options:
320 raise AutorandrException("Failed to find an EDID for output `%s' in setup file, required as `%s' is not off in config file." % (options["output"], options["output"]))
321 output = options["output"]
322 del options["output"]
324 return XrandrOutput(output, edid, options)
326 def edid_equals(self, other):
327 "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
328 if self.edid and other.edid:
329 if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
330 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
331 if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
332 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
333 return self.edid == other.edid
335 def __eq__(self, other):
336 return self.edid_equals(other) and self.output == other.output and self.options == other.options
338 def xrandr_version():
339 "Return the version of XRandR that this system uses"
340 if getattr(xrandr_version, "version", False) is False:
341 version_string = os.popen("xrandr -v").read()
343 version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
344 xrandr_version.version = Version(version)
345 except AttributeError:
346 xrandr_version.version = Version("1.3.0")
348 return xrandr_version.version
350 def debug_regexp(pattern, string):
351 "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
354 bounds = ( 0, len(string) )
355 while bounds[0] != bounds[1]:
356 half = int((bounds[0] + bounds[1]) / 2)
357 if half == bounds[0]:
359 bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
360 partial_length = bounds[0]
361 return ("Regular expression matched until position "
362 "%d, ..'%s', and did not match from '%s'.." % (partial_length, string[max(0, partial_length-20):partial_length],
363 string[partial_length:partial_length+10]))
366 return "Debug information would be available if the `regex' module was installed."
368 def parse_xrandr_output():
369 "Parse the output of `xrandr --verbose' into a list of outputs"
370 xrandr_output = os.popen("xrandr -q --verbose").read()
371 if not xrandr_output:
372 raise AutorandrException("Failed to run xrandr")
374 # We are not interested in screens
375 xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
377 # Split at output boundaries and instanciate an XrandrOutput per output
378 split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
379 if len(split_xrandr_output) < 2:
380 raise AutorandrException("No output boundaries found", report_bug=True)
381 outputs = OrderedDict()
382 modes = OrderedDict()
383 for i in range(1, len(split_xrandr_output), 2):
384 output_name = split_xrandr_output[i].split()[0]
385 output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i+2]))
386 outputs[output_name] = output
388 modes[output_name] = output_modes
390 return outputs, modes
392 def load_profiles(profile_path):
393 "Load the stored profiles"
396 for profile in os.listdir(profile_path):
397 config_name = os.path.join(profile_path, profile, "config")
398 setup_name = os.path.join(profile_path, profile, "setup")
399 if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
402 edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
406 for line in chain(open(config_name).readlines(), ["output"]):
407 if line[:6] == "output" and buffer:
408 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
413 for output_name in list(config.keys()):
414 if config[output_name].edid is None:
415 del config[output_name]
417 profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
421 def find_profiles(current_config, profiles):
422 "Find profiles matching the currently connected outputs"
423 detected_profiles = []
424 for profile_name, profile in profiles.items():
425 config = profile["config"]
427 for name, output in config.items():
430 if name not in current_config or not output.edid_equals(current_config[name]):
433 if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
436 detected_profiles.append(profile_name)
437 return detected_profiles
439 def profile_blocked(profile_path):
440 "Check if a profile is blocked"
441 script = os.path.join(profile_path, "block")
442 if not os.access(script, os.X_OK | os.F_OK):
444 return subprocess.call(script) == 0
446 def output_configuration(configuration, config):
447 "Write a configuration file"
448 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
449 for output in outputs:
450 print(configuration[output].option_string, file=config)
452 def output_setup(configuration, setup):
453 "Write a setup (fingerprint) file"
454 outputs = sorted(configuration.keys())
455 for output in outputs:
456 if configuration[output].edid:
457 print(output, configuration[output].edid, file=setup)
459 def save_configuration(profile_path, configuration):
460 "Save a configuration into a profile"
461 if not os.path.isdir(profile_path):
462 os.makedirs(profile_path)
463 with open(os.path.join(profile_path, "config"), "w") as config:
464 output_configuration(configuration, config)
465 with open(os.path.join(profile_path, "setup"), "w") as setup:
466 output_setup(configuration, setup)
468 def update_mtime(filename):
469 "Update a file's mtime"
471 os.utime(filename, None)
476 def apply_configuration(new_configuration, current_configuration, dry_run=False):
477 "Apply a configuration"
478 outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
480 base_argv = [ "echo", "xrandr" ]
482 base_argv = [ "xrandr" ]
484 # There are several xrandr / driver bugs we need to take care of here:
485 # - We cannot enable more than two screens at the same time
486 # See https://github.com/phillipberndt/autorandr/pull/6
487 # and commits f4cce4d and 8429886.
488 # - We cannot disable all screens
489 # See https://github.com/phillipberndt/autorandr/pull/20
490 # - We should disable screens before enabling others, because there's
491 # a limit on the number of enabled screens
492 # - We must make sure that the screen at 0x0 is activated first,
493 # or the other (first) screen to be activated would be moved there.
494 # - If an active screen already has a transformation and remains active,
495 # the xrandr call fails with an invalid RRSetScreenSize parameter error.
496 # Update the configuration in 3 passes in that case. (On Haswell graphics,
499 auxiliary_changes_pre = []
502 remain_active_count = 0
503 for output in outputs:
504 if not new_configuration[output].edid or "off" in new_configuration[output].options:
505 disable_outputs.append(new_configuration[output].option_vector)
507 if "off" not in current_configuration[output].options:
508 remain_active_count += 1
509 enable_outputs.append(new_configuration[output].option_vector)
510 if xrandr_version() >= Version("1.3.0") and "transform" in current_configuration[output].options:
511 auxiliary_changes_pre.append(["--output", output, "--transform", "none"])
513 # Perform pe-change auxiliary changes
514 if auxiliary_changes_pre:
515 argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
516 if subprocess.call(argv) != 0:
517 raise AutorandrException("Command failed: %s" % " ".join(argv))
519 # Disable unused outputs, but make sure that there always is at least one active screen
520 disable_keep = 0 if remain_active_count else 1
521 if len(disable_outputs) > disable_keep:
522 if subprocess.call(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
523 # Disabling the outputs failed. Retry with the next command:
524 # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
525 # This does not occur if simultaneously the primary screen is reset.
528 disable_outputs = disable_outputs[-1:] if disable_keep else []
530 # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
531 # disable the last two screens. This is a problem, so if this would happen, instead disable only
532 # one screen in the first call below.
533 if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
534 # In the context of a xrandr call that changes the display state, `--query' should do nothing
535 disable_outputs.insert(0, ['--query'])
537 # Enable the remaining outputs in pairs of two operations
538 operations = disable_outputs + enable_outputs
539 for index in range(0, len(operations), 2):
540 argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
541 if subprocess.call(argv) != 0:
542 raise AutorandrException("Command failed: %s" % " ".join(argv))
544 def add_unused_outputs(source_configuration, target_configuration):
545 "Add outputs that are missing in target to target, in 'off' state"
546 for output_name, output in source_configuration.items():
547 if output_name not in target_configuration:
548 target_configuration[output_name] = XrandrOutput(output_name, output.edid, { "off": None })
550 def remove_irrelevant_outputs(source_configuration, target_configuration):
551 "Remove outputs from target that ought to be 'off' and already are"
552 for output_name, output in source_configuration.items():
553 if "off" in output.options and output_name in target_configuration and "off" in target_configuration[output_name].options:
554 del target_configuration[output_name]
556 def generate_virtual_profile(configuration, modes, profile_name):
557 "Generate one of the virtual profiles"
558 configuration = copy.deepcopy(configuration)
559 if profile_name == "common":
560 common_resolution = [ set(( ( mode["width"], mode["height"] ) for mode in output )) for output in modes.values() ]
561 common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
562 common_resolution = sorted(common_resolution, key=lambda a: int(a[0])*int(a[1]))
563 if common_resolution:
564 for output in configuration:
565 configuration[output].options = {}
567 configuration[output].options["mode"] = [ x["name"] for x in sorted(modes[output], key=lambda x: 0 if x["preferred"] else 1) if x["width"] == common_resolution[-1][0] and x["height"] == common_resolution[-1][1] ][0]
568 configuration[output].options["pos"] = "0x0"
570 configuration[output].options["off"] = None
571 elif profile_name in ("horizontal", "vertical"):
573 if profile_name == "horizontal":
574 shift_index = "width"
575 pos_specifier = "%sx0"
577 shift_index = "height"
578 pos_specifier = "0x%s"
580 for output in configuration:
581 configuration[output].options = {}
583 mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
584 configuration[output].options["mode"] = mode["name"]
585 configuration[output].options["rate"] = mode["rate"]
586 configuration[output].options["pos"] = pos_specifier % shift
587 shift += int(mode[shift_index])
589 configuration[output].options["off"] = None
593 "Print help and exit"
595 for profile in virtual_profiles:
596 print(" %-10s %s" % profile[:2])
599 def exec_scripts(profile_path, script_name):
601 for script in (os.path.join(profile_path, script_name), os.path.join(os.path.dirname(profile_path), script_name)):
602 if os.access(script, os.X_OK | os.F_OK):
603 subprocess.call(script)
607 options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "help" ])[0])
608 except getopt.GetoptError as e:
610 options = { "--help": True }
614 # Load profiles from each XDG config directory
615 for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
616 system_profile_path = os.path.join(directory, "autorandr")
617 if os.path.isdir(system_profile_path):
618 profiles.update(load_profiles(system_profile_path))
619 # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
620 # profile_path is also used later on to store configurations
621 profile_path = os.path.expanduser("~/.autorandr")
622 if not os.path.isdir(profile_path):
623 # Elsewise, follow the XDG specification
624 profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
625 if os.path.isdir(profile_path):
626 profiles.update(load_profiles(profile_path))
627 # Sort by descending mtime
628 profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
629 except Exception as e:
630 raise AutorandrException("Failed to load profiles", e)
632 config, modes = parse_xrandr_output()
634 if "--fingerprint" in options:
635 output_setup(config, sys.stdout)
638 if "--config" in options:
639 output_configuration(config, sys.stdout)
643 options["--save"] = options["-s"]
644 if "--save" in options:
645 if options["--save"] in ( x[0] for x in virtual_profiles ):
646 raise AutorandrException("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
648 save_configuration(os.path.join(profile_path, options["--save"]), config)
649 except Exception as e:
650 raise AutorandrException("Failed to save current configuration as profile '%s'" % (options["--save"],), e)
651 print("Saved current configuration as profile '%s'" % options["--save"])
654 if "-h" in options or "--help" in options:
657 detected_profiles = find_profiles(config, profiles)
661 options["--load"] = options["-l"]
662 if "--load" in options:
663 load_profile = options["--load"]
665 for profile_name in profiles.keys():
666 if profile_blocked(os.path.join(profile_path, profile_name)):
667 print("%s (blocked)" % profile_name, file=sys.stderr)
669 if profile_name in detected_profiles:
670 print("%s (detected)" % profile_name, file=sys.stderr)
671 if ("-c" in options or "--change" in options) and not load_profile:
672 load_profile = profile_name
674 print(profile_name, file=sys.stderr)
677 options["--default"] = options["-d"]
678 if not load_profile and "--default" in options:
679 load_profile = options["--default"]
682 if load_profile in ( x[0] for x in virtual_profiles ):
683 load_config = generate_virtual_profile(config, modes, load_profile)
684 scripts_path = os.path.join(profile_path, load_profile)
687 profile = profiles[load_profile]
688 load_config = profile["config"]
689 scripts_path = profile["path"]
691 raise AutorandrException("Failed to load profile '%s': Profile not found" % load_profile)
692 if load_profile in detected_profiles and detected_profiles[0] != load_profile:
693 update_mtime(os.path.join(scripts_path, "config"))
694 add_unused_outputs(config, load_config)
695 if load_config == dict(config) and not "-f" in options and not "--force" in options:
696 print("Config already loaded", file=sys.stderr)
698 remove_irrelevant_outputs(config, load_config)
701 if "--dry-run" in options:
702 apply_configuration(load_config, config, True)
704 exec_scripts(scripts_path, "preswitch")
705 apply_configuration(load_config, config, False)
706 exec_scripts(scripts_path, "postswitch")
707 except Exception as e:
708 raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
712 if __name__ == '__main__':
715 except AutorandrException as e:
716 print(e, file=sys.stderr)
718 except Exception as e:
719 if not len(str(e)): # BdbQuit
720 print("Exception: {0}".format(e.__class__.__name__))
723 print("Unhandled exception ({0}). Please report this as a bug.".format(e), file=sys.stderr)