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 functools import reduce
38 from itertools import chain
39 from collections import OrderedDict
45 # (name, description, callback)
46 ("common", "Clone all connected outputs at the largest common resolution", None),
47 ("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
48 ("vertical", "Stack all connected outputs vertically at their largest resolution", None),
52 Usage: autorandr [options]
54 -h, --help get this small help
55 -c, --change reload current setup
56 -s, --save <profile> save your current setup to profile <profile>
57 -l, --load <profile> load profile <profile>
58 -d, --default <profile> make profile <profile> the default profile
59 --force force (re)loading of a profile
60 --fingerprint fingerprint your current hardware setup
61 --config dump your current xrandr setup
62 --dry-run don't change anything, only print the xrandr commands
64 To prevent a profile from being loaded, place a script call "block" in its
65 directory. The script is evaluated before the screen setup is inspected, and
66 in case of it returning a value of 0 the profile is skipped. This can be used
67 to query the status of a docking station you are about to leave.
69 If no suitable profile can be identified, the current configuration is kept.
70 To change this behaviour and switch to a fallback configuration, specify
73 Another script called "postswitch" can be placed in the directory
74 ~/.config/autorandr (or ~/.autorandr if you have an old installation) as well
75 as in any profile directories: The scripts are executed after a mode switch
76 has taken place and can notify window managers.
78 The following virtual configurations are available:
81 class AutorandrException(Exception):
82 def __init__(self, message, original_exception=None, report_bug=False):
83 self.message = message
84 self.report_bug = report_bug
85 if original_exception:
86 self.original_exception = original_exception
87 trace = sys.exc_info()[2]
90 self.line = trace.tb_lineno
94 self.line = inspect.currentframe().f_back.f_lineno
97 self.original_exception = None
100 retval = [ self.message ]
102 retval.append(" (line %d)" % self.line)
103 if self.original_exception:
104 retval.append(":\n ")
105 retval.append(str(self.original_exception).replace("\n", "\n "))
107 retval.append("\nThis appears to be a bug. Please help improving autorandr by reporting it upstream."
108 "\nPlease attach the output of `xrandr --verbose` to your bug report if appropriate.")
109 return "".join(retval)
111 class XrandrOutput(object):
112 "Represents an XRandR output"
114 # This regular expression is used to parse an output in `xrandr --verbose'
115 XRANDR_OUTPUT_REGEXP = """(?x)
116 ^(?P<output>[^ ]+)\s+ # Line starts with output name
117 (?: # Differentiate disconnected and connected in first line
119 unknown\ connection |
120 (?P<connected>connected)
123 (?P<primary>primary\ )? # Might be primary screen
125 (?P<width>[0-9]+)x(?P<height>[0-9]+) # Resolution (might be overridden below!)
126 \+(?P<x>-?[0-9]+)\+(?P<y>-?[0-9]+)\s+ # Position
127 (?:\(0x[0-9a-fA-F]+\)\s+)? # XID
128 (?P<rotate>(?:normal|left|right|inverted))\s+ # Rotation
129 (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)? # Reflection
130 )? # .. but everything of the above only if the screen is in use.
131 (?:[\ \t]*\([^\)]+\))(?:\s*[0-9]+mm\sx\s[0-9]+mm)?
132 (?:[\ \t]*panning\ (?P<panning>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Panning information
133 (?:[\ \t]*tracking\ (?P<tracking>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Tracking information
134 (?:[\ \t]*border\ (?P<border>(?:[0-9]+/){3}[0-9]+))? # Border information
135 (?:\s*(?: # Properties of the output
136 Gamma: (?P<gamma>(?:inf|[0-9\.: e])+) | # Gamma value
137 Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) | # Transformation matrix
138 EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) | # EDID of the output
139 (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)* # Other properties
143 (?P<mode_name>\S+).+?\*current.*\s+ # Interesting (current) resolution: Extract rate
144 h:\s+width\s+(?P<mode_width>[0-9]+).+\s+
145 v:\s+height\s+(?P<mode_height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
146 \S+(?:(?!\*current).)+\s+h:.+\s+v:.+\s* # Other resolutions
150 XRANDR_OUTPUT_MODES_REGEXP = """(?x)
151 (?P<name>\S+).+?(?P<preferred>\+preferred)?\s+
152 h:\s+width\s+(?P<width>[0-9]+).+\s+
153 v:\s+height\s+(?P<height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
156 XRANDR_13_DEFAULTS = {
157 "transform": "1,0,0,0,1,0,0,0,1",
161 XRANDR_12_DEFAULTS = {
164 "gamma": "1.0:1.0:1.0",
167 XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
169 EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
172 return "<%s%s %s>" % (self.output, (" %s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else "", " ".join(self.option_vector))
175 def options_with_defaults(self):
176 "Return the options dictionary, augmented with the default values that weren't set"
177 if "off" in self.options:
180 if xrandr_version() >= Version("1.3"):
181 options.update(self.XRANDR_13_DEFAULTS)
182 if xrandr_version() >= Version("1.2"):
183 options.update(self.XRANDR_12_DEFAULTS)
184 options.update(self.options)
188 def option_vector(self):
189 "Return the command line parameters for XRandR for this instance"
190 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()))], [])
193 def option_string(self):
194 "Return the command line parameters in the configuration file format"
195 return "\n".join([ " ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.options.items()))])
199 "Return a key to sort the outputs for xrandr invocation"
202 if "off" in self.options:
204 if "pos" in self.options:
205 x, y = map(float, self.options["pos"].split("x"))
210 def __init__(self, output, edid, options):
211 "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
214 self.options = options
215 self.remove_default_option_values()
217 def remove_default_option_values(self):
218 "Remove values from the options dictionary that are superflous"
219 if "off" in self.options and len(self.options.keys()) > 1:
220 self.options = { "off": None }
222 for option, default_value in self.XRANDR_DEFAULTS.items():
223 if option in self.options and self.options[option] == default_value:
224 del self.options[option]
227 def from_xrandr_output(cls, xrandr_output):
228 """Instanciate an XrandrOutput from the output of `xrandr --verbose'
230 This method also returns a list of modes supported by the output.
233 xrandr_output = xrandr_output.replace("\r\n", "\n")
234 match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
236 raise AutorandrException("Parsing XRandR output failed, there is an error in the regular expression.", report_bug = True)
238 debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
239 raise AutorandrException("Parsing XRandR output failed, the regular expression did not match: %s" % debug, report_bug = True)
240 remainder = xrandr_output[len(match_object.group(0)):]
242 raise AutorandrException(("Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
243 "starting at byte %d with ..'%s'.") % (len(remainder), len(match_object.group(0)), remainder[:10]), report_bug=True)
245 match = match_object.groupdict()
249 modes = [ x.groupdict() for x in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]) if x.group("name") ]
251 raise AutorandrException("Parsing XRandR output failed, couldn't find any display modes", report_bug=True)
254 if not match["connected"]:
257 edid = "".join(match["edid"].strip().split()) if match["edid"] else "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
259 if not match["width"]:
260 options["off"] = None
262 if match["mode_name"]:
263 options["mode"] = match["mode_name"]
264 elif match["mode_width"]:
265 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
267 if match["rotate"] not in ("left", "right"):
268 options["mode"] = "%sx%s" % (match["width"], match["height"])
270 options["mode"] = "%sx%s" % (match["height"], match["width"])
271 options["rotate"] = match["rotate"]
273 options["primary"] = None
274 if match["reflect"] == "X":
275 options["reflect"] = "x"
276 elif match["reflect"] == "Y":
277 options["reflect"] = "y"
278 elif match["reflect"] == "X and Y":
279 options["reflect"] = "xy"
280 options["pos"] = "%sx%s" % (match["x"], match["y"])
282 panning = [ match["panning"] ]
283 if match["tracking"]:
284 panning += [ "/", match["tracking"] ]
286 panning += [ "/", match["border"] ]
287 options["panning"] = "".join(panning)
288 if match["transform"]:
289 transformation = ",".join(match["transform"].strip().split())
290 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
291 options["transform"] = transformation
292 if not match["mode_name"]:
293 # TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
294 # special case is actually required.
295 print("Warning: Output %s has a transformation applied. Could not determine correct mode! Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
297 gamma = match["gamma"].strip()
298 # xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
299 # Also, it is not able to work with non-standard gamma ramps. Finally, it auto-corrects 0 to 1,
300 # so we approximate by 1e-10.
301 gamma = ":".join([ str(max(1e-10, round(1./float(x), 3))) for x in gamma.split(":") ])
302 options["gamma"] = gamma
304 options["rate"] = match["rate"]
306 return XrandrOutput(match["output"], edid, options), modes
309 def from_config_file(cls, edid_map, configuration):
310 "Instanciate an XrandrOutput from the contents of a configuration file"
312 for line in configuration.split("\n"):
314 line = line.split(None, 1)
315 options[line[0]] = line[1] if len(line) > 1 else None
319 if options["output"] in edid_map:
320 edid = edid_map[options["output"]]
322 # This fuzzy matching is for legacy autorandr that used sysfs output names
323 fuzzy_edid_map = [ re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys() ]
324 fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
325 if fuzzy_output in fuzzy_edid_map:
326 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
327 elif "off" not in options:
328 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"]))
329 output = options["output"]
330 del options["output"]
332 return XrandrOutput(output, edid, options)
334 def edid_equals(self, other):
335 "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
336 if self.edid and other.edid:
337 if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
338 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
339 if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
340 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
341 return self.edid == other.edid
343 def __eq__(self, other):
344 return self.edid_equals(other) and self.output == other.output and self.options == other.options
346 def xrandr_version():
347 "Return the version of XRandR that this system uses"
348 if getattr(xrandr_version, "version", False) is False:
349 version_string = os.popen("xrandr -v").read()
351 version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
352 xrandr_version.version = Version(version)
353 except AttributeError:
354 xrandr_version.version = Version("1.3.0")
356 return xrandr_version.version
358 def debug_regexp(pattern, string):
359 "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
362 bounds = ( 0, len(string) )
363 while bounds[0] != bounds[1]:
364 half = int((bounds[0] + bounds[1]) / 2)
365 if half == bounds[0]:
367 bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
368 partial_length = bounds[0]
369 return ("Regular expression matched until position "
370 "%d, ..'%s', and did not match from '%s'.." % (partial_length, string[max(0, partial_length-20):partial_length],
371 string[partial_length:partial_length+10]))
374 return "Debug information would be available if the `regex' module was installed."
376 def parse_xrandr_output():
377 "Parse the output of `xrandr --verbose' into a list of outputs"
378 xrandr_output = os.popen("xrandr -q --verbose").read()
379 if not xrandr_output:
380 raise AutorandrException("Failed to run xrandr")
382 # We are not interested in screens
383 xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
385 # Split at output boundaries and instanciate an XrandrOutput per output
386 split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
387 if len(split_xrandr_output) < 2:
388 raise AutorandrException("No output boundaries found", report_bug=True)
389 outputs = OrderedDict()
390 modes = OrderedDict()
391 for i in range(1, len(split_xrandr_output), 2):
392 output_name = split_xrandr_output[i].split()[0]
393 output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i+2]))
394 outputs[output_name] = output
396 modes[output_name] = output_modes
398 return outputs, modes
400 def load_profiles(profile_path):
401 "Load the stored profiles"
404 for profile in os.listdir(profile_path):
405 config_name = os.path.join(profile_path, profile, "config")
406 setup_name = os.path.join(profile_path, profile, "setup")
407 if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
410 edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
414 for line in chain(open(config_name).readlines(), ["output"]):
415 if line[:6] == "output" and buffer:
416 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
421 for output_name in list(config.keys()):
422 if config[output_name].edid is None:
423 del config[output_name]
425 profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
429 def find_profiles(current_config, profiles):
430 "Find profiles matching the currently connected outputs"
431 detected_profiles = []
432 for profile_name, profile in profiles.items():
433 config = profile["config"]
435 for name, output in config.items():
438 if name not in current_config or not output.edid_equals(current_config[name]):
441 if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
444 detected_profiles.append(profile_name)
445 return detected_profiles
447 def profile_blocked(profile_path):
448 "Check if a profile is blocked"
449 script = os.path.join(profile_path, "block")
450 if not os.access(script, os.X_OK | os.F_OK):
452 return subprocess.call(script) == 0
454 def output_configuration(configuration, config):
455 "Write a configuration file"
456 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
457 for output in outputs:
458 print(configuration[output].option_string, file=config)
460 def output_setup(configuration, setup):
461 "Write a setup (fingerprint) file"
462 outputs = sorted(configuration.keys())
463 for output in outputs:
464 if configuration[output].edid:
465 print(output, configuration[output].edid, file=setup)
467 def save_configuration(profile_path, configuration):
468 "Save a configuration into a profile"
469 if not os.path.isdir(profile_path):
470 os.makedirs(profile_path)
471 with open(os.path.join(profile_path, "config"), "w") as config:
472 output_configuration(configuration, config)
473 with open(os.path.join(profile_path, "setup"), "w") as setup:
474 output_setup(configuration, setup)
476 def update_mtime(filename):
477 "Update a file's mtime"
479 os.utime(filename, None)
484 def apply_configuration(new_configuration, current_configuration, dry_run=False):
485 "Apply a configuration"
486 outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
488 base_argv = [ "echo", "xrandr" ]
490 base_argv = [ "xrandr" ]
492 # There are several xrandr / driver bugs we need to take care of here:
493 # - We cannot enable more than two screens at the same time
494 # See https://github.com/phillipberndt/autorandr/pull/6
495 # and commits f4cce4d and 8429886.
496 # - We cannot disable all screens
497 # See https://github.com/phillipberndt/autorandr/pull/20
498 # - We should disable screens before enabling others, because there's
499 # a limit on the number of enabled screens
500 # - We must make sure that the screen at 0x0 is activated first,
501 # or the other (first) screen to be activated would be moved there.
502 # - If an active screen already has a transformation and remains active,
503 # the xrandr call fails with an invalid RRSetScreenSize parameter error.
504 # Update the configuration in 3 passes in that case. (On Haswell graphics,
507 auxiliary_changes_pre = []
510 remain_active_count = 0
511 for output in outputs:
512 if not new_configuration[output].edid or "off" in new_configuration[output].options:
513 disable_outputs.append(new_configuration[output].option_vector)
515 if "off" not in current_configuration[output].options:
516 remain_active_count += 1
517 enable_outputs.append(new_configuration[output].option_vector)
518 if xrandr_version() >= Version("1.3.0") and "transform" in current_configuration[output].options:
519 auxiliary_changes_pre.append(["--output", output, "--transform", "none"])
521 # Perform pe-change auxiliary changes
522 if auxiliary_changes_pre:
523 argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
524 if subprocess.call(argv) != 0:
525 raise AutorandrException("Command failed: %s" % " ".join(argv))
527 # Disable unused outputs, but make sure that there always is at least one active screen
528 disable_keep = 0 if remain_active_count else 1
529 if len(disable_outputs) > disable_keep:
530 if subprocess.call(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
531 # Disabling the outputs failed. Retry with the next command:
532 # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
533 # This does not occur if simultaneously the primary screen is reset.
536 disable_outputs = disable_outputs[-1:] if disable_keep else []
538 # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
539 # disable the last two screens. This is a problem, so if this would happen, instead disable only
540 # one screen in the first call below.
541 if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
542 # In the context of a xrandr call that changes the display state, `--query' should do nothing
543 disable_outputs.insert(0, ['--query'])
545 # Enable the remaining outputs in pairs of two operations
546 operations = disable_outputs + enable_outputs
547 for index in range(0, len(operations), 2):
548 argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
549 if subprocess.call(argv) != 0:
550 raise AutorandrException("Command failed: %s" % " ".join(argv))
552 def add_unused_outputs(source_configuration, target_configuration):
553 "Add outputs that are missing in target to target, in 'off' state"
554 for output_name, output in source_configuration.items():
555 if output_name not in target_configuration:
556 target_configuration[output_name] = XrandrOutput(output_name, output.edid, { "off": None })
558 def remove_irrelevant_outputs(source_configuration, target_configuration):
559 "Remove outputs from target that ought to be 'off' and already are"
560 for output_name, output in source_configuration.items():
561 if "off" in output.options and output_name in target_configuration and "off" in target_configuration[output_name].options:
562 del target_configuration[output_name]
564 def generate_virtual_profile(configuration, modes, profile_name):
565 "Generate one of the virtual profiles"
566 configuration = copy.deepcopy(configuration)
567 if profile_name == "common":
568 common_resolution = [ set(( ( mode["width"], mode["height"] ) for mode in output )) for output in modes.values() ]
569 common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
570 common_resolution = sorted(common_resolution, key=lambda a: int(a[0])*int(a[1]))
571 if common_resolution:
572 for output in configuration:
573 configuration[output].options = {}
575 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]
576 configuration[output].options["pos"] = "0x0"
578 configuration[output].options["off"] = None
579 elif profile_name in ("horizontal", "vertical"):
581 if profile_name == "horizontal":
582 shift_index = "width"
583 pos_specifier = "%sx0"
585 shift_index = "height"
586 pos_specifier = "0x%s"
588 for output in configuration:
589 configuration[output].options = {}
591 mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
592 configuration[output].options["mode"] = mode["name"]
593 configuration[output].options["rate"] = mode["rate"]
594 configuration[output].options["pos"] = pos_specifier % shift
595 shift += int(mode[shift_index])
597 configuration[output].options["off"] = None
601 "Print help and exit"
603 for profile in virtual_profiles:
604 print(" %-10s %s" % profile[:2])
607 def exec_scripts(profile_path, script_name):
609 for script in (os.path.join(profile_path, script_name), os.path.join(os.path.dirname(profile_path), script_name)):
610 if os.access(script, os.X_OK | os.F_OK):
611 subprocess.call(script)
615 options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "help" ])[0])
616 except getopt.GetoptError as e:
617 print("Failed to parse options: {0}.\n"
618 "Use --help to get usage information.".format(str(e)),
620 sys.exit(posix.EX_USAGE)
624 # Load profiles from each XDG config directory
625 for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
626 system_profile_path = os.path.join(directory, "autorandr")
627 if os.path.isdir(system_profile_path):
628 profiles.update(load_profiles(system_profile_path))
629 # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
630 # profile_path is also used later on to store configurations
631 profile_path = os.path.expanduser("~/.autorandr")
632 if not os.path.isdir(profile_path):
633 # Elsewise, follow the XDG specification
634 profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
635 if os.path.isdir(profile_path):
636 profiles.update(load_profiles(profile_path))
637 # Sort by descending mtime
638 profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
639 except Exception as e:
640 raise AutorandrException("Failed to load profiles", e)
642 config, modes = parse_xrandr_output()
644 if "--fingerprint" in options:
645 output_setup(config, sys.stdout)
648 if "--config" in options:
649 output_configuration(config, sys.stdout)
653 options["--save"] = options["-s"]
654 if "--save" in options:
655 if options["--save"] in ( x[0] for x in virtual_profiles ):
656 raise AutorandrException("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
658 save_configuration(os.path.join(profile_path, options["--save"]), config)
659 except Exception as e:
660 raise AutorandrException("Failed to save current configuration as profile '%s'" % (options["--save"],), e)
661 print("Saved current configuration as profile '%s'" % options["--save"])
664 if "-h" in options or "--help" in options:
667 detected_profiles = find_profiles(config, profiles)
671 options["--load"] = options["-l"]
672 if "--load" in options:
673 load_profile = options["--load"]
675 for profile_name in profiles.keys():
676 if profile_blocked(os.path.join(profile_path, profile_name)):
677 print("%s (blocked)" % profile_name, file=sys.stderr)
679 if profile_name in detected_profiles:
680 print("%s (detected)" % profile_name, file=sys.stderr)
681 if ("-c" in options or "--change" in options) and not load_profile:
682 load_profile = profile_name
684 print(profile_name, file=sys.stderr)
687 options["--default"] = options["-d"]
688 if not load_profile and "--default" in options:
689 load_profile = options["--default"]
692 if load_profile in ( x[0] for x in virtual_profiles ):
693 load_config = generate_virtual_profile(config, modes, load_profile)
694 scripts_path = os.path.join(profile_path, load_profile)
697 profile = profiles[load_profile]
698 load_config = profile["config"]
699 scripts_path = profile["path"]
701 raise AutorandrException("Failed to load profile '%s': Profile not found" % load_profile)
702 if load_profile in detected_profiles and detected_profiles[0] != load_profile:
703 update_mtime(os.path.join(scripts_path, "config"))
704 add_unused_outputs(config, load_config)
705 if load_config == dict(config) and not "-f" in options and not "--force" in options:
706 print("Config already loaded", file=sys.stderr)
708 remove_irrelevant_outputs(config, load_config)
711 if "--dry-run" in options:
712 apply_configuration(load_config, config, True)
714 exec_scripts(scripts_path, "preswitch")
715 apply_configuration(load_config, config, False)
716 exec_scripts(scripts_path, "postswitch")
717 except Exception as e:
718 raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
722 if __name__ == '__main__':
725 except AutorandrException as e:
726 print(e, file=sys.stderr)
728 except Exception as e:
729 if not len(str(e)): # BdbQuit
730 print("Exception: {0}".format(e.__class__.__name__))
733 print("Unhandled exception ({0}). Please report this as a bug.".format(e), file=sys.stderr)