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 " % self.line)
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_width>[0-9]+)x(?P<mode_height>[0-9]+).+?\*current.*\s+
140 h:.+\s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz\s* | # Interesting (current) resolution: Extract rate
141 [0-9]+x[0-9]+(?:(?!\*current).)+\s+h:.+\s+v:.+\s* # Other resolutions
145 XRANDR_OUTPUT_MODES_REGEXP = """(?x)
146 (?P<width>[0-9]+)x(?P<height>[0-9]+)
147 .*?(?P<preferred>\+preferred)?
149 \s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz
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"]) ]
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_width"]:
259 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
261 if match["rotate"] not in ("left", "right"):
262 options["mode"] = "%sx%s" % (match["width"], match["height"])
264 options["mode"] = "%sx%s" % (match["height"], match["width"])
265 options["rotate"] = match["rotate"]
267 options["primary"] = None
268 if match["reflect"] == "X":
269 options["reflect"] = "x"
270 elif match["reflect"] == "Y":
271 options["reflect"] = "y"
272 elif match["reflect"] == "X and Y":
273 options["reflect"] = "xy"
274 options["pos"] = "%sx%s" % (match["x"], match["y"])
276 panning = [ match["panning"] ]
277 if match["tracking"]:
278 panning += [ "/", match["tracking"] ]
280 panning += [ "/", match["border"] ]
281 options["panning"] = "".join(panning)
282 if match["transform"]:
283 transformation = ",".join(match["transform"].strip().split())
284 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
285 options["transform"] = transformation
286 if not match["mode_width"]:
287 # TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
288 # special case is actually required.
289 print("Warning: Output %s has a transformation applied. Could not determine correct mode! Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
291 gamma = match["gamma"].strip()
292 options["gamma"] = gamma
294 options["rate"] = match["rate"]
296 return XrandrOutput(match["output"], edid, options), modes
299 def from_config_file(cls, edid_map, configuration):
300 "Instanciate an XrandrOutput from the contents of a configuration file"
302 for line in configuration.split("\n"):
304 line = line.split(None, 1)
305 options[line[0]] = line[1] if len(line) > 1 else None
309 if options["output"] in edid_map:
310 edid = edid_map[options["output"]]
312 # This fuzzy matching is for legacy autorandr that used sysfs output names
313 fuzzy_edid_map = [ re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys() ]
314 fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
315 if fuzzy_output in fuzzy_edid_map:
316 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
317 elif "off" not in options:
318 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"]))
319 output = options["output"]
320 del options["output"]
322 return XrandrOutput(output, edid, options)
324 def edid_equals(self, other):
325 "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
326 if self.edid and other.edid:
327 if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
328 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
329 if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
330 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
331 return self.edid == other.edid
333 def __eq__(self, other):
334 return self.edid_equals(other) and self.output == other.output and self.options == other.options
336 def xrandr_version():
337 "Return the version of XRandR that this system uses"
338 if getattr(xrandr_version, "version", False) is False:
339 version_string = os.popen("xrandr -v").read()
341 version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
342 xrandr_version.version = Version(version)
343 except AttributeError:
344 xrandr_version.version = Version("1.3.0")
346 return xrandr_version.version
348 def debug_regexp(pattern, string):
349 "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
352 bounds = ( 0, len(string) )
353 while bounds[0] != bounds[1]:
354 half = int((bounds[0] + bounds[1]) / 2)
355 if half == bounds[0]:
357 bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
358 partial_length = bounds[0]
359 return ("Regular expression matched until position "
360 "%d, ..'%s', and did not match from '%s'.." % (partial_length, string[max(0, partial_length-20):partial_length],
361 string[partial_length:partial_length+10]))
364 return "Debug information would be available if the `regex' module was installed."
366 def parse_xrandr_output():
367 "Parse the output of `xrandr --verbose' into a list of outputs"
368 xrandr_output = os.popen("xrandr -q --verbose").read()
369 if not xrandr_output:
370 raise AutorandrException("Failed to run xrandr")
372 # We are not interested in screens
373 xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
375 # Split at output boundaries and instanciate an XrandrOutput per output
376 split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
377 if len(split_xrandr_output) < 2:
378 raise AutorandrException("No output boundaries found", report_bug=True)
379 outputs = OrderedDict()
380 modes = OrderedDict()
381 for i in range(1, len(split_xrandr_output), 2):
382 output_name = split_xrandr_output[i].split()[0]
383 output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i+2]))
384 outputs[output_name] = output
386 modes[output_name] = output_modes
388 return outputs, modes
390 def load_profiles(profile_path):
391 "Load the stored profiles"
394 for profile in os.listdir(profile_path):
395 config_name = os.path.join(profile_path, profile, "config")
396 setup_name = os.path.join(profile_path, profile, "setup")
397 if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
400 edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
404 for line in chain(open(config_name).readlines(), ["output"]):
405 if line[:6] == "output" and buffer:
406 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
411 for output_name in list(config.keys()):
412 if config[output_name].edid is None:
413 del config[output_name]
415 profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
419 def find_profiles(current_config, profiles):
420 "Find profiles matching the currently connected outputs"
421 detected_profiles = []
422 for profile_name, profile in profiles.items():
423 config = profile["config"]
425 for name, output in config.items():
428 if name not in current_config or not output.edid_equals(current_config[name]):
431 if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
434 detected_profiles.append(profile_name)
435 return detected_profiles
437 def profile_blocked(profile_path):
438 "Check if a profile is blocked"
439 script = os.path.join(profile_path, "block")
440 if not os.access(script, os.X_OK | os.F_OK):
442 return subprocess.call(script) == 0
444 def output_configuration(configuration, config):
445 "Write a configuration file"
446 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
447 for output in outputs:
448 print(configuration[output].option_string, file=config)
450 def output_setup(configuration, setup):
451 "Write a setup (fingerprint) file"
452 outputs = sorted(configuration.keys())
453 for output in outputs:
454 if configuration[output].edid:
455 print(output, configuration[output].edid, file=setup)
457 def save_configuration(profile_path, configuration):
458 "Save a configuration into a profile"
459 if not os.path.isdir(profile_path):
460 os.makedirs(profile_path)
461 with open(os.path.join(profile_path, "config"), "w") as config:
462 output_configuration(configuration, config)
463 with open(os.path.join(profile_path, "setup"), "w") as setup:
464 output_setup(configuration, setup)
466 def update_mtime(filename):
467 "Update a file's mtime"
469 os.utime(filename, None)
474 def apply_configuration(new_configuration, current_configuration, dry_run=False):
475 "Apply a configuration"
476 outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
478 base_argv = [ "echo", "xrandr" ]
480 base_argv = [ "xrandr" ]
482 # There are several xrandr / driver bugs we need to take care of here:
483 # - We cannot enable more than two screens at the same time
484 # See https://github.com/phillipberndt/autorandr/pull/6
485 # and commits f4cce4d and 8429886.
486 # - We cannot disable all screens
487 # See https://github.com/phillipberndt/autorandr/pull/20
488 # - We should disable screens before enabling others, because there's
489 # a limit on the number of enabled screens
490 # - We must make sure that the screen at 0x0 is activated first,
491 # or the other (first) screen to be activated would be moved there.
492 # - If an active screen already has a transformation and remains active,
493 # the xrandr call fails with an invalid RRSetScreenSize parameter error.
494 # Update the configuration in 3 passes in that case. (On Haswell graphics,
497 auxiliary_changes_pre = []
500 remain_active_count = 0
501 for output in outputs:
502 if not new_configuration[output].edid or "off" in new_configuration[output].options:
503 disable_outputs.append(new_configuration[output].option_vector)
505 if "off" not in current_configuration[output].options:
506 remain_active_count += 1
507 enable_outputs.append(new_configuration[output].option_vector)
508 if xrandr_version() >= Version("1.3.0") and "transform" in current_configuration[output].options:
509 auxiliary_changes_pre.append(["--output", output, "--transform", "none"])
511 # Perform pe-change auxiliary changes
512 if auxiliary_changes_pre:
513 argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
514 if subprocess.call(argv) != 0:
515 raise AutorandrException("Command failed: %s" % " ".join(argv))
517 # Disable unused outputs, but make sure that there always is at least one active screen
518 disable_keep = 0 if remain_active_count else 1
519 if len(disable_outputs) > disable_keep:
520 if subprocess.call(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
521 # Disabling the outputs failed. Retry with the next command:
522 # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
523 # This does not occur if simultaneously the primary screen is reset.
526 disable_outputs = disable_outputs[-1:] if disable_keep else []
528 # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
529 # disable the last two screens. This is a problem, so if this would happen, instead disable only
530 # one screen in the first call below.
531 if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
532 # In the context of a xrandr call that changes the display state, `--query' should do nothing
533 disable_outputs.insert(0, ['--query'])
535 # Enable the remaining outputs in pairs of two operations
536 operations = disable_outputs + enable_outputs
537 for index in range(0, len(operations), 2):
538 argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
539 if subprocess.call(argv) != 0:
540 raise AutorandrException("Command failed: %s" % " ".join(argv))
542 def add_unused_outputs(source_configuration, target_configuration):
543 "Add outputs that are missing in target to target, in 'off' state"
544 for output_name, output in source_configuration.items():
545 if output_name not in target_configuration:
546 target_configuration[output_name] = XrandrOutput(output_name, output.edid, { "off": None })
548 def remove_irrelevant_outputs(source_configuration, target_configuration):
549 "Remove outputs from target that ought to be 'off' and already are"
550 for output_name, output in source_configuration.items():
551 if "off" in output.options and output_name in target_configuration and "off" in target_configuration[output_name].options:
552 del target_configuration[output_name]
554 def generate_virtual_profile(configuration, modes, profile_name):
555 "Generate one of the virtual profiles"
556 configuration = copy.deepcopy(configuration)
557 if profile_name == "common":
558 common_resolution = [ set(( ( mode["width"], mode["height"] ) for mode in output )) for output in modes.values() ]
559 common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
560 common_resolution = sorted(common_resolution, key=lambda a: int(a[0])*int(a[1]))
561 if common_resolution:
562 for output in configuration:
563 configuration[output].options = {}
565 configuration[output].options["mode"] = "%sx%s" % common_resolution[-1]
566 configuration[output].options["pos"] = "0x0"
568 configuration[output].options["off"] = None
569 elif profile_name in ("horizontal", "vertical"):
571 if profile_name == "horizontal":
572 shift_index = "width"
573 pos_specifier = "%sx0"
575 shift_index = "height"
576 pos_specifier = "0x%s"
578 for output in configuration:
579 configuration[output].options = {}
581 mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
582 configuration[output].options["mode"] = "%sx%s" % (mode["width"], mode["height"])
583 configuration[output].options["rate"] = mode["rate"]
584 configuration[output].options["pos"] = pos_specifier % shift
585 shift += int(mode[shift_index])
587 configuration[output].options["off"] = None
591 "Print help and exit"
593 for profile in virtual_profiles:
594 print(" %-10s %s" % profile[:2])
597 def exec_scripts(profile_path, script_name):
599 for script in (os.path.join(profile_path, script_name), os.path.join(os.path.dirname(profile_path), script_name)):
600 if os.access(script, os.X_OK | os.F_OK):
601 subprocess.call(script)
605 options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "help" ])[0])
606 except getopt.GetoptError as e:
608 options = { "--help": True }
612 # Load profiles from each XDG config directory
613 for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
614 system_profile_path = os.path.join(directory, "autorandr")
615 if os.path.isdir(system_profile_path):
616 profiles.update(load_profiles(system_profile_path))
617 # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
618 # profile_path is also used later on to store configurations
619 profile_path = os.path.expanduser("~/.autorandr")
620 if not os.path.isdir(profile_path):
621 # Elsewise, follow the XDG specification
622 profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
623 if os.path.isdir(profile_path):
624 profiles.update(load_profiles(profile_path))
625 # Sort by descending mtime
626 profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
627 except Exception as e:
628 raise AutorandrException("Failed to load profiles", e)
630 config, modes = parse_xrandr_output()
632 if "--fingerprint" in options:
633 output_setup(config, sys.stdout)
636 if "--config" in options:
637 output_configuration(config, sys.stdout)
641 options["--save"] = options["-s"]
642 if "--save" in options:
643 if options["--save"] in ( x[0] for x in virtual_profiles ):
644 raise AutorandrException("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
646 save_configuration(os.path.join(profile_path, options["--save"]), config)
647 except Exception as e:
648 raise AutorandrException("Failed to save current configuration as profile '%s'" % (options["--save"],), e)
649 print("Saved current configuration as profile '%s'" % options["--save"])
652 if "-h" in options or "--help" in options:
655 detected_profiles = find_profiles(config, profiles)
659 options["--load"] = options["-l"]
660 if "--load" in options:
661 load_profile = options["--load"]
663 for profile_name in profiles.keys():
664 if profile_blocked(os.path.join(profile_path, profile_name)):
665 print("%s (blocked)" % profile_name, file=sys.stderr)
667 if profile_name in detected_profiles:
668 print("%s (detected)" % profile_name, file=sys.stderr)
669 if ("-c" in options or "--change" in options) and not load_profile:
670 load_profile = profile_name
672 print(profile_name, file=sys.stderr)
675 options["--default"] = options["-d"]
676 if not load_profile and "--default" in options:
677 load_profile = options["--default"]
680 if load_profile in ( x[0] for x in virtual_profiles ):
681 load_config = generate_virtual_profile(config, modes, load_profile)
682 scripts_path = os.path.join(profile_path, load_profile)
685 profile = profiles[load_profile]
686 load_config = profile["config"]
687 scripts_path = profile["path"]
689 raise AutorandrException("Failed to load profile '%s': Profile not found" % load_profile)
690 if load_profile in detected_profiles and detected_profiles[0] != load_profile:
691 update_mtime(os.path.join(scripts_path, "config"))
692 add_unused_outputs(config, load_config)
693 if load_config == dict(config) and not "-f" in options and not "--force" in options:
694 print("Config already loaded", file=sys.stderr)
696 remove_irrelevant_outputs(config, load_config)
699 if "--dry-run" in options:
700 apply_configuration(load_config, config, True)
702 exec_scripts(scripts_path, "preswitch")
703 apply_configuration(load_config, config, False)
704 exec_scripts(scripts_path, "postswitch")
705 except Exception as e:
706 raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
710 if __name__ == '__main__':
713 except AutorandrException as e:
714 print(file=sys.stderr)
715 print(e, file=sys.stderr)
717 except Exception as e:
718 trace = sys.exc_info()[2]
720 trace = trace.tb_next
721 print("\nUnhandled exception in line %d. Please report this as a bug:\n %s" % (trace.tb_lineno, "\n ".join(str(e).split("\n")),), file=sys.stderr)