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
41 from collections import OrderedDict
42 from distutils.version import LooseVersion as Version
43 from functools import reduce
44 from itertools import chain
46 if sys.version_info.major == 2:
47 import ConfigParser as configparser
59 # (name, description, callback)
60 ("off", "Disable all outputs", None),
61 ("common", "Clone all connected outputs at the largest common resolution", None),
62 ("clone-largest", "Clone all connected outputs with the largest resolution (scaled down if necessary)", None),
63 ("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
64 ("vertical", "Stack all connected outputs vertically at their largest resolution", None),
77 Usage: autorandr [options]
79 -h, --help get this small help
80 -c, --change automatically load the first detected profile
81 -d, --default <profile> make profile <profile> the default profile
82 -l, --load <profile> load profile <profile>
83 -s, --save <profile> save your current setup to profile <profile>
84 -r, --remove <profile> remove profile <profile>
85 --batch run autorandr for all users with active X11 sessions
86 --current only list current (active) configuration(s)
87 --config dump your current xrandr setup
88 --debug enable verbose output
89 --detected only list detected (available) configuration(s)
90 --dry-run don't change anything, only print the xrandr commands
91 --fingerprint fingerprint your current hardware setup
92 --force force (re)loading of a profile / overwrite exiting files
93 --skip-options <option> comma separated list of xrandr arguments (e.g. "gamma")
94 to skip both in detecting changes and applying a profile
95 --version show version information and exit
97 If no suitable profile can be identified, the current configuration is kept.
98 To change this behaviour and switch to a fallback configuration, specify
101 autorandr supports a set of per-profile and global hooks. See the documentation
104 The following virtual configurations are available:
108 def is_closed_lid(output):
109 if not re.match(r'(eDP(-?[0-9]\+)*|LVDS(-?[0-9]\+)*)', output):
111 lids = glob.glob("/proc/acpi/button/lid/*/state")
114 with open(state_file) as f:
116 return "close" in content
120 class AutorandrException(Exception):
121 def __init__(self, message, original_exception=None, report_bug=False):
122 self.message = message
123 self.report_bug = report_bug
124 if original_exception:
125 self.original_exception = original_exception
126 trace = sys.exc_info()[2]
128 trace = trace.tb_next
129 self.line = trace.tb_lineno
130 self.file_name = trace.tb_frame.f_code.co_filename
134 frame = inspect.currentframe().f_back
135 self.line = frame.f_lineno
136 self.file_name = frame.f_code.co_filename
139 self.file_name = None
140 self.original_exception = None
142 if os.path.abspath(self.file_name) == os.path.abspath(sys.argv[0]):
143 self.file_name = None
146 retval = [self.message]
148 retval.append(" (line %d%s)" % (self.line, ("; %s" % self.file_name) if self.file_name else ""))
149 if self.original_exception:
150 retval.append(":\n ")
151 retval.append(str(self.original_exception).replace("\n", "\n "))
153 retval.append("\nThis appears to be a bug. Please help improving autorandr by reporting it upstream:"
154 "\nhttps://github.com/phillipberndt/autorandr/issues"
155 "\nPlease attach the output of `xrandr --verbose` to your bug report if appropriate.")
156 return "".join(retval)
159 class XrandrOutput(object):
160 "Represents an XRandR output"
162 # This regular expression is used to parse an output in `xrandr --verbose'
163 XRANDR_OUTPUT_REGEXP = """(?x)
164 ^\s*(?P<output>\S[^ ]*)\s+ # Line starts with output name
165 (?: # Differentiate disconnected and connected
166 disconnected | # in first line
167 unknown\ connection |
168 (?P<connected>connected)
171 (?P<primary>primary\ )? # Might be primary screen
173 (?P<width>[0-9]+)x(?P<height>[0-9]+) # Resolution (might be overridden below!)
174 \+(?P<x>-?[0-9]+)\+(?P<y>-?[0-9]+)\s+ # Position
175 (?:\(0x[0-9a-fA-F]+\)\s+)? # XID
176 (?P<rotate>(?:normal|left|right|inverted))\s+ # Rotation
177 (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)? # Reflection
178 )? # .. but only if the screen is in use.
179 (?:[\ \t]*\([^\)]+\))(?:\s*[0-9]+mm\sx\s[0-9]+mm)?
180 (?:[\ \t]*panning\ (?P<panning>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Panning information
181 (?:[\ \t]*tracking\ (?P<tracking>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Tracking information
182 (?:[\ \t]*border\ (?P<border>(?:[0-9]+/){3}[0-9]+))? # Border information
183 (?:\s*(?: # Properties of the output
184 Gamma: (?P<gamma>(?:inf|-?[0-9\.\-: e])+) | # Gamma value
185 CRTC:\s*(?P<crtc>[0-9]) | # CRTC value
186 Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) | # Transformation matrix
187 EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) | # EDID of the output
188 %s | # Properties to include in the profile
189 (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)* # Other properties
193 (?P<mode_name>\S+).+?\*current.*\s+ # Interesting (current) resolution:
194 h:\s+width\s+(?P<mode_width>[0-9]+).+\s+ # Extract rate
195 v:\s+height\s+(?P<mode_height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
196 \S+(?:(?!\*current).)+\s+h:.+\s+v:.+\s* # Other resolutions
198 """ % ("|".join([r"{}:\s*(?P<{}>[\S ]*\S+)"
199 .format(re.sub(r"(\s)", r"\\\1", p),
200 re.sub(r"\W+", "_", p.lower())) for p in properties]))
202 XRANDR_OUTPUT_MODES_REGEXP = """(?x)
203 (?P<name>\S+).+?(?P<preferred>\+preferred)?\s+
204 h:\s+width\s+(?P<width>[0-9]+).+\s+
205 v:\s+height\s+(?P<height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
208 XRANDR_13_DEFAULTS = {
209 "transform": "1,0,0,0,1,0,0,0,1",
213 XRANDR_12_DEFAULTS = {
216 "gamma": "1.0:1.0:1.0",
219 XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
221 EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
224 return "<%s%s %s>" % (self.output, self.short_edid, " ".join(self.option_vector))
227 def short_edid(self):
228 return ("%s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else ""
231 def options_with_defaults(self):
232 "Return the options dictionary, augmented with the default values that weren't set"
233 if "off" in self.options:
236 if xrandr_version() >= Version("1.3"):
237 options.update(self.XRANDR_13_DEFAULTS)
238 if xrandr_version() >= Version("1.2"):
239 options.update(self.XRANDR_12_DEFAULTS)
240 options.update(self.options)
241 return {a: b for a, b in options.items() if a not in self.ignored_options}
244 def filtered_options(self):
245 "Return a dictionary of options without ignored options"
246 return {a: b for a, b in self.options.items() if a not in self.ignored_options}
249 def option_vector(self):
250 "Return the command line parameters for XRandR for this instance"
251 args = ["--output", self.output]
252 for option, arg in sorted(self.options_with_defaults.items()):
253 if option[:5] == "prop-":
255 for prop, xrandr_prop in [(re.sub(r"\W+", "_", p.lower()), p) for p in properties]:
256 if prop == option[5:]:
258 args.append(xrandr_prop)
262 print("Warning: Unknown property `%s' in config file. Skipping." % option[5:], file=sys.stderr)
265 args.append("--%s" % option)
271 def option_string(self):
272 "Return the command line parameters in the configuration file format"
273 options = ["output %s" % self.output]
274 for option, arg in sorted(self.filtered_options.items()):
276 options.append("%s %s" % (option, arg))
278 options.append(option)
279 return "\n".join(options)
283 "Return a key to sort the outputs for xrandr invocation"
286 if "off" in self.options:
288 if "pos" in self.options:
289 x, y = map(float, self.options["pos"].split("x"))
294 def __init__(self, output, edid, options):
295 "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
298 self.options = options
299 self.ignored_options = []
300 self.remove_default_option_values()
302 def set_ignored_options(self, options):
303 "Set a list of xrandr options that are never used (neither when comparing configurations nor when applying them)"
304 self.ignored_options = list(options)
306 def remove_default_option_values(self):
307 "Remove values from the options dictionary that are superflous"
308 if "off" in self.options and len(self.options.keys()) > 1:
309 self.options = {"off": None}
311 for option, default_value in self.XRANDR_DEFAULTS.items():
312 if option in self.options and self.options[option] == default_value:
313 del self.options[option]
316 def from_xrandr_output(cls, xrandr_output):
317 """Instanciate an XrandrOutput from the output of `xrandr --verbose'
319 This method also returns a list of modes supported by the output.
322 xrandr_output = xrandr_output.replace("\r\n", "\n")
323 match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
325 raise AutorandrException("Parsing XRandR output failed, there is an error in the regular expression.",
328 debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
329 raise AutorandrException("Parsing XRandR output failed, the regular expression did not match: %s" % debug,
331 remainder = xrandr_output[len(match_object.group(0)):]
333 raise AutorandrException("Parsing XRandR output failed, %d bytes left unmatched after "
334 "regular expression, starting at byte %d with ..'%s'." %
335 (len(remainder), len(match_object.group(0)), remainder[:10]),
338 match = match_object.groupdict()
343 for mode_match in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]):
344 if mode_match.group("name"):
345 modes.append(mode_match.groupdict())
347 raise AutorandrException("Parsing XRandR output failed, couldn't find any display modes", report_bug=True)
350 if not match["connected"]:
353 edid = "".join(match["edid"].strip().split())
355 edid = "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
357 # An output can be disconnected but still have a mode configured. This can only happen
358 # as a residual situation after a disconnect, you cannot associate a mode with an disconnected
361 # This code needs to be careful not to mix the two. An output should only be configured to
362 # "off" if it doesn't have a mode associated with it, which is modelled as "not a width" here.
363 if not match["width"]:
364 options["off"] = None
366 if match["mode_name"]:
367 options["mode"] = match["mode_name"]
368 elif match["mode_width"]:
369 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
371 if match["rotate"] not in ("left", "right"):
372 options["mode"] = "%sx%s" % (match["width"] or 0, match["height"] or 0)
374 options["mode"] = "%sx%s" % (match["height"] or 0, match["width"] or 0)
376 options["rotate"] = match["rotate"]
378 options["primary"] = None
379 if match["reflect"] == "X":
380 options["reflect"] = "x"
381 elif match["reflect"] == "Y":
382 options["reflect"] = "y"
383 elif match["reflect"] == "X and Y":
384 options["reflect"] = "xy"
385 if match["x"] or match["y"]:
386 options["pos"] = "%sx%s" % (match["x"] or "0", match["y"] or "0")
388 panning = [match["panning"]]
389 if match["tracking"]:
390 panning += ["/", match["tracking"]]
392 panning += ["/", match["border"]]
393 options["panning"] = "".join(panning)
394 if match["transform"]:
395 transformation = ",".join(match["transform"].strip().split())
396 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
397 options["transform"] = transformation
398 if not match["mode_name"]:
399 # TODO We'd need to apply the reverse transformation here. Let's see if someone complains,
400 # I doubt that this special case is actually required.
401 print("Warning: Output %s has a transformation applied. Could not determine correct mode! "
402 "Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
404 gamma = match["gamma"].strip()
405 # xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
406 # Also, it is not able to work with non-standard gamma ramps. Finally, it auto-corrects 0 to 1,
407 # so we approximate by 1e-10.
408 gamma = ":".join([str(max(1e-10, round(1. / float(x), 3))) for x in gamma.split(":")])
409 options["gamma"] = gamma
411 options["crtc"] = match["crtc"]
413 options["rate"] = match["rate"]
414 for prop in [re.sub(r"\W+", "_", p.lower()) for p in properties]:
416 options["prop-" + prop] = match[prop]
418 return XrandrOutput(match["output"], edid, options), modes
421 def from_config_file(cls, edid_map, configuration):
422 "Instanciate an XrandrOutput from the contents of a configuration file"
424 for line in configuration.split("\n"):
426 line = line.split(None, 1)
427 if line and line[0].startswith("#"):
429 options[line[0]] = line[1] if len(line) > 1 else None
433 if options["output"] in edid_map:
434 edid = edid_map[options["output"]]
436 # This fuzzy matching is for legacy autorandr that used sysfs output names
437 fuzzy_edid_map = [re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys()]
438 fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
439 if fuzzy_output in fuzzy_edid_map:
440 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
441 elif "off" not in options:
442 raise AutorandrException("Failed to find an EDID for output `%s' in setup file, required as `%s' "
443 "is not off in config file." % (options["output"], options["output"]))
444 output = options["output"]
445 del options["output"]
447 return XrandrOutput(output, edid, options)
449 def edid_equals(self, other):
450 "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
451 if self.edid and other.edid:
452 if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
453 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
454 if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
455 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
457 return match_asterisk(self.edid, other.edid) > 0
458 elif "*" in other.edid:
459 return match_asterisk(other.edid, self.edid) > 0
460 return self.edid == other.edid
462 def __ne__(self, other):
463 return not (self == other)
465 def __eq__(self, other):
466 return self.edid_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
468 def verbose_diff(self, other):
469 "Compare to another XrandrOutput and return a list of human readable differences"
471 if not self.edid_equals(other):
472 diffs.append("EDID `%s' differs from `%s'" % (self.short_edid, other.short_edid))
473 if self.output != other.output:
474 diffs.append("Output name `%s' differs from `%s'" % (self.output, other.output))
475 if "off" in self.options and "off" not in other.options:
476 diffs.append("The output is disabled currently, but active in the new configuration")
477 elif "off" in other.options and "off" not in self.options:
478 diffs.append("The output is currently enabled, but inactive in the new configuration")
480 for name in set(chain.from_iterable((self.options.keys(), other.options.keys()))):
481 if name not in other.options:
482 diffs.append("Option --%s %sis not present in the new configuration" %
483 (name, "(= `%s') " % self.options[name] if self.options[name] else ""))
484 elif name not in self.options:
485 diffs.append("Option --%s (`%s' in the new configuration) is not present currently" %
486 (name, other.options[name]))
487 elif self.options[name] != other.options[name]:
488 diffs.append("Option --%s %sis `%s' in the new configuration" %
489 (name, "(= `%s') " % self.options[name] if self.options[name] else "", other.options[name]))
493 def xrandr_version():
494 "Return the version of XRandR that this system uses"
495 if getattr(xrandr_version, "version", False) is False:
496 version_string = os.popen("xrandr -v").read()
498 version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
499 xrandr_version.version = Version(version)
500 except AttributeError:
501 xrandr_version.version = Version("1.3.0")
503 return xrandr_version.version
506 def debug_regexp(pattern, string):
507 "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
510 bounds = (0, len(string))
511 while bounds[0] != bounds[1]:
512 half = int((bounds[0] + bounds[1]) / 2)
513 if half == bounds[0]:
515 bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
516 partial_length = bounds[0]
517 return ("Regular expression matched until position %d, ..'%s', and did not match from '%s'.." %
518 (partial_length, string[max(0, partial_length - 20):partial_length],
519 string[partial_length:partial_length + 10]))
522 return "Debug information would be available if the `regex' module was installed."
525 def parse_xrandr_output():
526 "Parse the output of `xrandr --verbose' into a list of outputs"
527 xrandr_output = os.popen("xrandr -q --verbose").read()
528 if not xrandr_output:
529 raise AutorandrException("Failed to run xrandr")
531 # We are not interested in screens
532 xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
534 # Split at output boundaries and instanciate an XrandrOutput per output
535 split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
536 if len(split_xrandr_output) < 2:
537 raise AutorandrException("No output boundaries found", report_bug=True)
538 outputs = OrderedDict()
539 modes = OrderedDict()
540 for i in range(1, len(split_xrandr_output), 2):
541 output_name = split_xrandr_output[i].split()[0]
542 output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i + 2]))
543 outputs[output_name] = output
545 modes[output_name] = output_modes
547 # consider a closed lid as disconnected if other outputs are connected
548 if sum(o.edid != None for o in outputs.values()) > 1:
549 for output_name in outputs.keys():
550 if is_closed_lid(output_name):
551 outputs[output_name].edid = None
553 return outputs, modes
556 def load_profiles(profile_path):
557 "Load the stored profiles"
560 for profile in os.listdir(profile_path):
561 config_name = os.path.join(profile_path, profile, "config")
562 setup_name = os.path.join(profile_path, profile, "setup")
563 if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
566 edids = dict([x.split() for x in (y.strip() for y in open(setup_name).readlines()) if x and x[0] != "#"])
570 for line in chain(open(config_name).readlines(), ["output"]):
571 if line[:6] == "output" and buffer:
572 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
577 for output_name in list(config.keys()):
578 if config[output_name].edid is None:
579 del config[output_name]
581 profiles[profile] = {
583 "path": os.path.join(profile_path, profile),
584 "config-mtime": os.stat(config_name).st_mtime,
590 def get_symlinks(profile_path):
591 "Load all symlinks from a directory"
594 for link in os.listdir(profile_path):
595 file_name = os.path.join(profile_path, link)
596 if os.path.islink(file_name):
597 symlinks[link] = os.readlink(file_name)
602 def match_asterisk(pattern, data):
603 """Match data against a pattern
605 The difference to fnmatch is that this function only accepts patterns with a single
606 asterisk and that it returns a "closeness" number, which is larger the better the match.
607 Zero indicates no match at all.
609 if "*" not in pattern:
610 return 1 if pattern == data else 0
611 parts = pattern.split("*")
613 raise ValueError("Only patterns with a single asterisk are supported, %s is invalid" % pattern)
614 if not data.startswith(parts[0]):
616 if not data.endswith(parts[1]):
618 matched = len(pattern)
619 total = len(data) + 1
620 return matched * 1. / total
623 def find_profiles(current_config, profiles):
624 "Find profiles matching the currently connected outputs, sorting asterisk matches to the back"
625 detected_profiles = []
626 for profile_name, profile in profiles.items():
627 config = profile["config"]
629 for name, output in config.items():
632 if name not in current_config or not output.edid_equals(current_config[name]):
635 if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].edid)):
638 closeness = max(match_asterisk(output.edid, current_config[name].edid), match_asterisk(
639 current_config[name].edid, output.edid))
640 detected_profiles.append((closeness, profile_name))
641 detected_profiles = [o[1] for o in sorted(detected_profiles, key=lambda x: -x[0])]
642 return detected_profiles
645 def profile_blocked(profile_path, meta_information=None):
646 """Check if a profile is blocked.
648 meta_information is expected to be an dictionary. It will be passed to the block scripts
649 in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
651 return not exec_scripts(profile_path, "block", meta_information)
654 def check_configuration_pre_save(configuration):
655 "Check that a configuration is safe for saving."
656 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
657 for output in outputs:
658 if "off" not in configuration[output].options and not configuration[output].edid:
659 return ("`%(o)s' is not off (has a mode configured) but is disconnected (does not have an EDID).\n"
660 "This typically means that it has been recently unplugged and then not properly disabled\n"
661 "by the user. Please disable it (e.g. using `xrandr --output %(o)s --off`) and then rerun\n"
662 "this command.") % {"o": output}
665 def output_configuration(configuration, config):
666 "Write a configuration file"
667 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
668 for output in outputs:
669 print(configuration[output].option_string, file=config)
672 def output_setup(configuration, setup):
673 "Write a setup (fingerprint) file"
674 outputs = sorted(configuration.keys())
675 for output in outputs:
676 if configuration[output].edid:
677 print(output, configuration[output].edid, file=setup)
680 def save_configuration(profile_path, profile_name, configuration, forced=False):
681 "Save a configuration into a profile"
682 if not os.path.isdir(profile_path):
683 os.makedirs(profile_path)
684 config_path = os.path.join(profile_path, "config")
685 setup_path = os.path.join(profile_path, "setup")
686 if os.path.isfile(config_path) and not forced:
687 raise AutorandrException('Refusing to overwrite config "{}" without passing "--force"!'.format(profile_name))
688 if os.path.isfile(setup_path) and not forced:
689 raise AutorandrException('Refusing to overwrite config "{}" without passing "--force"!'.format(profile_name))
691 with open(config_path, "w") as config:
692 output_configuration(configuration, config)
693 with open(setup_path, "w") as setup:
694 output_setup(configuration, setup)
697 def update_mtime(filename):
698 "Update a file's mtime"
700 os.utime(filename, None)
706 def call_and_retry(*args, **kwargs):
707 """Wrapper around subprocess.call that retries failed calls.
709 This function calls subprocess.call and on non-zero exit states,
710 waits a second and then retries once. This mitigates #47,
711 a timing issue with some drivers.
713 if "dry_run" in kwargs:
714 dry_run = kwargs["dry_run"]
715 del kwargs["dry_run"]
718 kwargs_redirected = dict(kwargs)
720 if hasattr(subprocess, "DEVNULL"):
721 kwargs_redirected["stdout"] = getattr(subprocess, "DEVNULL")
723 kwargs_redirected["stdout"] = open(os.devnull, "w")
724 kwargs_redirected["stderr"] = kwargs_redirected["stdout"]
725 retval = subprocess.call(*args, **kwargs_redirected)
728 retval = subprocess.call(*args, **kwargs)
732 def get_fb_dimensions(configuration):
735 for output in configuration.values():
736 if "off" in output.options or not output.edid:
738 # This won't work with all modes -- but it's a best effort.
739 match = re.search("[0-9]{3,}x[0-9]{3,}", output.options["mode"])
742 o_mode = match.group(0)
743 o_width, o_height = map(int, o_mode.split("x"))
744 if "transform" in output.options:
745 a, b, c, d, e, f, g, h, i = map(float, output.options["transform"].split(","))
746 w = (g * o_width + h * o_height + i)
747 x = (a * o_width + b * o_height + c) / w
748 y = (d * o_width + e * o_height + f) / w
749 o_width, o_height = x, y
750 if "rotate" in output.options:
751 if output.options["rotate"] in ("left", "right"):
752 o_width, o_height = o_height, o_width
753 if "pos" in output.options:
754 o_left, o_top = map(int, output.options["pos"].split("x"))
757 if "panning" in output.options:
758 match = re.match("(?P<w>[0-9]+)x(?P<h>[0-9]+)(?:\+(?P<x>[0-9]+))?(?:\+(?P<y>[0-9]+))?.*", output.options["panning"])
760 detail = match.groupdict(default="0")
761 o_width = int(detail.get("w")) + int(detail.get("x"))
762 o_height = int(detail.get("h")) + int(detail.get("y"))
763 width = max(width, o_width)
764 height = max(height, o_height)
765 return int(width), int(height)
768 def apply_configuration(new_configuration, current_configuration, dry_run=False):
769 "Apply a configuration"
770 found_top_left_monitor = False
771 found_left_monitor = False
772 found_top_monitor = False
773 outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
775 base_argv = ["echo", "xrandr"]
777 base_argv = ["xrandr"]
779 # There are several xrandr / driver bugs we need to take care of here:
780 # - We cannot enable more than two screens at the same time
781 # See https://github.com/phillipberndt/autorandr/pull/6
782 # and commits f4cce4d and 8429886.
783 # - We cannot disable all screens
784 # See https://github.com/phillipberndt/autorandr/pull/20
785 # - We should disable screens before enabling others, because there's
786 # a limit on the number of enabled screens
787 # - We must make sure that the screen at 0x0 is activated first,
788 # or the other (first) screen to be activated would be moved there.
789 # - If an active screen already has a transformation and remains active,
790 # the xrandr call fails with an invalid RRSetScreenSize parameter error.
791 # Update the configuration in 3 passes in that case. (On Haswell graphics,
793 # - Some implementations can not handle --transform at all, so avoid it unless
794 # necessary. (See https://github.com/phillipberndt/autorandr/issues/37)
795 # - Some implementations can not handle --panning without specifying --fb
796 # explicitly, so avoid it unless necessary.
797 # (See https://github.com/phillipberndt/autorandr/issues/72)
799 fb_dimensions = get_fb_dimensions(new_configuration)
801 base_argv += ["--fb", "%dx%d" % fb_dimensions]
803 # Failed to obtain frame-buffer size. Doesn't matter, xrandr will choose for the user.
806 auxiliary_changes_pre = []
809 remain_active_count = 0
810 for output in outputs:
811 if not new_configuration[output].edid or "off" in new_configuration[output].options:
812 disable_outputs.append(new_configuration[output].option_vector)
814 if output not in current_configuration:
815 raise AutorandrException("New profile configures output %s which does not exist in current xrandr --verbose output. "
816 "Don't know how to proceed." % output)
817 if "off" not in current_configuration[output].options:
818 remain_active_count += 1
820 option_vector = new_configuration[output].option_vector
821 if xrandr_version() >= Version("1.3.0"):
822 for option, off_value in (("transform", "none"), ("panning", "0x0")):
823 if option in current_configuration[output].options:
824 auxiliary_changes_pre.append(["--output", output, "--%s" % option, off_value])
827 option_index = option_vector.index("--%s" % option)
828 if option_vector[option_index + 1] == XrandrOutput.XRANDR_DEFAULTS[option]:
829 option_vector = option_vector[:option_index] + option_vector[option_index + 2:]
832 if not found_top_left_monitor:
833 position = new_configuration[output].options.get("pos", "0x0")
834 if position == "0x0":
835 found_top_left_monitor = True
836 enable_outputs.insert(0, option_vector)
837 elif not found_left_monitor and position.startswith("0x"):
838 found_left_monitor = True
839 enable_outputs.insert(0, option_vector)
840 elif not found_top_monitor and position.endswith("x0"):
841 found_top_monitor = True
842 enable_outputs.insert(0, option_vector)
844 enable_outputs.append(option_vector)
846 enable_outputs.append(option_vector)
848 # Perform pe-change auxiliary changes
849 if auxiliary_changes_pre:
850 argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
851 if call_and_retry(argv, dry_run=dry_run) != 0:
852 raise AutorandrException("Command failed: %s" % " ".join(argv))
854 # Disable unused outputs, but make sure that there always is at least one active screen
855 disable_keep = 0 if remain_active_count else 1
856 if len(disable_outputs) > disable_keep:
857 argv = base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))
858 if call_and_retry(argv, dry_run=dry_run) != 0:
859 # Disabling the outputs failed. Retry with the next command:
860 # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
861 # This does not occur if simultaneously the primary screen is reset.
864 disable_outputs = disable_outputs[-1:] if disable_keep else []
866 # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
867 # disable the last two screens. This is a problem, so if this would happen, instead disable only
868 # one screen in the first call below.
869 if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
870 # In the context of a xrandr call that changes the display state, `--query' should do nothing
871 disable_outputs.insert(0, ['--query'])
873 # If we did not find a candidate, we might need to inject a call
874 # If there is no output to disable, we will enable 0x and x0 at the same time
875 if not found_top_left_monitor and len(disable_outputs) > 0:
876 # If the call to 0x and x0 is splitted, inject one of them
877 if found_top_monitor and found_left_monitor:
878 enable_outputs.insert(0, enable_outputs[0])
880 # Enable the remaining outputs in pairs of two operations
881 operations = disable_outputs + enable_outputs
882 for index in range(0, len(operations), 2):
883 argv = base_argv + list(chain.from_iterable(operations[index:index + 2]))
884 if call_and_retry(argv, dry_run=dry_run) != 0:
885 raise AutorandrException("Command failed: %s" % " ".join(argv))
888 def is_equal_configuration(source_configuration, target_configuration):
890 Check if all outputs from target are already configured correctly in source and
891 that no other outputs are active.
893 for output in target_configuration.keys():
894 if "off" in target_configuration[output].options:
895 if (output in source_configuration and "off" not in source_configuration[output].options):
898 if (output not in source_configuration) or (source_configuration[output] != target_configuration[output]):
900 for output in source_configuration.keys():
901 if "off" in source_configuration[output].options:
902 if output in target_configuration and "off" not in target_configuration[output].options:
905 if output not in target_configuration:
910 def add_unused_outputs(source_configuration, target_configuration):
911 "Add outputs that are missing in target to target, in 'off' state"
912 for output_name, output in source_configuration.items():
913 if output_name not in target_configuration:
914 target_configuration[output_name] = XrandrOutput(output_name, output.edid, {"off": None})
917 def remove_irrelevant_outputs(source_configuration, target_configuration):
918 "Remove outputs from target that ought to be 'off' and already are"
919 for output_name, output in source_configuration.items():
920 if "off" in output.options:
921 if output_name in target_configuration:
922 if "off" in target_configuration[output_name].options:
923 del target_configuration[output_name]
926 def generate_virtual_profile(configuration, modes, profile_name):
927 "Generate one of the virtual profiles"
928 configuration = copy.deepcopy(configuration)
929 if profile_name == "common":
931 for output, output_modes in modes.items():
933 if configuration[output].edid:
934 for mode in output_modes:
935 mode_set.add((mode["width"], mode["height"]))
936 mode_sets.append(mode_set)
937 common_resolution = reduce(lambda a, b: a & b, mode_sets[1:], mode_sets[0])
938 common_resolution = sorted(common_resolution, key=lambda a: int(a[0]) * int(a[1]))
939 if common_resolution:
940 for output in configuration:
941 configuration[output].options = {}
942 if output in modes and configuration[output].edid:
943 modes_sorted = sorted(modes[output], key=lambda x: 0 if x["preferred"] else 1)
944 modes_filtered = [x for x in modes_sorted if (x["width"], x["height"]) == common_resolution[-1]]
945 mode = modes_filtered[0]
946 configuration[output].options["mode"] = mode['name']
947 configuration[output].options["pos"] = "0x0"
949 configuration[output].options["off"] = None
950 elif profile_name in ("horizontal", "vertical"):
952 if profile_name == "horizontal":
953 shift_index = "width"
954 pos_specifier = "%sx0"
956 shift_index = "height"
957 pos_specifier = "0x%s"
959 for output in configuration:
960 configuration[output].options = {}
961 if output in modes and configuration[output].edid:
963 score = int(a["width"]) * int(a["height"])
967 output_modes = sorted(modes[output], key=key)
968 mode = output_modes[-1]
969 configuration[output].options["mode"] = mode["name"]
970 configuration[output].options["rate"] = mode["rate"]
971 configuration[output].options["pos"] = pos_specifier % shift
972 shift += int(mode[shift_index])
974 configuration[output].options["off"] = None
975 elif profile_name == "clone-largest":
976 modes_unsorted = [output_modes[0] for output, output_modes in modes.items()]
977 modes_sorted = sorted(modes_unsorted, key=lambda x: int(x["width"]) * int(x["height"]), reverse=True)
978 biggest_resolution = modes_sorted[0]
979 for output in configuration:
980 configuration[output].options = {}
981 if output in modes and configuration[output].edid:
983 score = int(a["width"]) * int(a["height"])
987 output_modes = sorted(modes[output], key=key)
988 mode = output_modes[-1]
989 configuration[output].options["mode"] = mode["name"]
990 configuration[output].options["rate"] = mode["rate"]
991 configuration[output].options["pos"] = "0x0"
992 scale = max(float(biggest_resolution["width"]) / float(mode["width"]),
993 float(biggest_resolution["height"]) / float(mode["height"]))
994 mov_x = (float(mode["width"]) * scale - float(biggest_resolution["width"])) / -2
995 mov_y = (float(mode["height"]) * scale - float(biggest_resolution["height"])) / -2
996 configuration[output].options["transform"] = "{},0,{},0,{},{},0,0,1".format(scale, mov_x, scale, mov_y)
998 configuration[output].options["off"] = None
999 elif profile_name == "off":
1000 for output in configuration:
1001 for key in list(configuration[output].options.keys()):
1002 del configuration[output].options[key]
1003 configuration[output].options["off"] = None
1004 return configuration
1007 def print_profile_differences(one, another):
1008 "Print the differences between two profiles for debugging"
1011 print("| Differences between the two profiles:")
1012 for output in set(chain.from_iterable((one.keys(), another.keys()))):
1013 if output not in one:
1014 if "off" not in another[output].options:
1015 print("| Output `%s' is missing from the active configuration" % output)
1016 elif output not in another:
1017 if "off" not in one[output].options:
1018 print("| Output `%s' is missing from the new configuration" % output)
1020 for line in one[output].verbose_diff(another[output]):
1021 print("| [Output %s] %s" % (output, line))
1026 "Print help and exit"
1028 for profile in virtual_profiles:
1029 name, description = profile[:2]
1030 description = [description]
1032 while len(description[0]) > max_width + 1:
1033 left_over = description[0][max_width:]
1034 description[0] = description[0][:max_width] + "-"
1035 description.insert(1, " %-15s %s" % ("", left_over))
1036 description = "\n".join(description)
1037 print(" %-15s %s" % (name, description))
1041 def exec_scripts(profile_path, script_name, meta_information=None):
1044 This will run all executables from the profile folder, and global per-user
1045 and system-wide configuration folders, named script_name or residing in
1046 subdirectories named script_name.d.
1048 If profile_path is None, only global scripts will be invoked.
1050 meta_information is expected to be an dictionary. It will be passed to the block scripts
1051 in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
1053 Returns True unless any of the scripts exited with non-zero exit status.
1056 env = os.environ.copy()
1057 if meta_information:
1058 for key, value in meta_information.items():
1059 env["AUTORANDR_{}".format(key.upper())] = str(value)
1061 # If there are multiple candidates, the XDG spec tells to only use the first one.
1064 user_profile_path = os.path.expanduser("~/.autorandr")
1065 if not os.path.isdir(user_profile_path):
1066 user_profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
1068 candidate_directories = []
1070 candidate_directories.append(profile_path)
1071 candidate_directories.append(user_profile_path)
1072 for config_dir in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":"):
1073 candidate_directories.append(os.path.join(config_dir, "autorandr"))
1075 for folder in candidate_directories:
1076 if script_name not in ran_scripts:
1077 script = os.path.join(folder, script_name)
1078 if os.access(script, os.X_OK | os.F_OK):
1080 all_ok &= subprocess.call(script, env=env) != 0
1082 raise AutorandrException("Failed to execute user command: %s" % (script,))
1083 ran_scripts.add(script_name)
1085 script_folder = os.path.join(folder, "%s.d" % script_name)
1086 if os.access(script_folder, os.R_OK | os.X_OK) and os.path.isdir(script_folder):
1087 for file_name in os.listdir(script_folder):
1088 check_name = "d/%s" % (file_name,)
1089 if check_name not in ran_scripts:
1090 script = os.path.join(script_folder, file_name)
1091 if os.access(script, os.X_OK | os.F_OK):
1093 all_ok &= subprocess.call(script, env=env) != 0
1095 raise AutorandrException("Failed to execute user command: %s" % (script,))
1096 ran_scripts.add(check_name)
1101 def dispatch_call_to_sessions(argv):
1102 """Invoke autorandr for each open local X11 session with the given options.
1104 The function iterates over all processes not owned by root and checks
1105 whether they have DISPLAY and XAUTHORITY variables set. It strips the
1106 screen from any variable it finds (i.e. :0.0 becomes :0) and checks whether
1107 this display has been handled already. If it has not, it forks, changes
1108 uid/gid to the user owning the process, reuses the process's environment
1109 and runs autorandr with the parameters from argv.
1111 This function requires root permissions. It only works for X11 servers that
1112 have at least one non-root process running. It is susceptible for attacks
1113 where one user runs a process with another user's DISPLAY variable - in
1114 this case, it might happen that autorandr is invoked for the other user,
1115 which won't work. Since no other harm than prevention of automated
1116 execution of autorandr can be done this way, the assumption is that in this
1117 situation, the local administrator will handle the situation."""
1119 X11_displays_done = set()
1121 autorandr_binary = os.path.abspath(argv[0])
1122 backup_candidates = {}
1124 def fork_child_autorandr(pwent, process_environ):
1125 print("Running autorandr as %s for display %s" % (pwent.pw_name, process_environ["DISPLAY"]))
1126 child_pid = os.fork()
1128 # This will throw an exception if any of the privilege changes fails,
1129 # so it should be safe. Also, note that since the environment
1130 # is taken from a process owned by the user, reusing it should
1131 # not leak any information.
1133 os.setgroups(os.getgrouplist(pwent.pw_name, pwent.pw_gid))
1134 except AttributeError:
1135 # Python 2 doesn't have getgrouplist
1137 os.setresgid(pwent.pw_gid, pwent.pw_gid, pwent.pw_gid)
1138 os.setresuid(pwent.pw_uid, pwent.pw_uid, pwent.pw_uid)
1139 os.chdir(pwent.pw_dir)
1141 os.environ.update(process_environ)
1142 if sys.executable != "" and sys.executable != None:
1143 os.execl(sys.executable, sys.executable, autorandr_binary, *argv[1:])
1145 os.execl(autorandr_binary, autorandr_binary, *argv[1:])
1147 os.waitpid(child_pid, 0)
1149 for directory in os.listdir("/proc"):
1150 directory = os.path.join("/proc/", directory)
1151 if not os.path.isdir(directory):
1153 environ_file = os.path.join(directory, "environ")
1154 if not os.path.isfile(environ_file):
1156 uid = os.stat(environ_file).st_uid
1158 # The following line assumes that user accounts start at 1000 and that
1159 # no one works using the root or another system account. This is rather
1160 # restrictive, but de facto default. Alternatives would be to use the
1161 # UID_MIN from /etc/login.defs or FIRST_UID from /etc/adduser.conf;
1162 # but effectively, both values aren't binding in any way.
1163 # If this breaks your use case, please file a bug on Github.
1167 process_environ = {}
1168 for environ_entry in open(environ_file, 'rb').read().split(b"\0"):
1170 environ_entry = environ_entry.decode("ascii")
1171 except UnicodeDecodeError:
1173 name, sep, value = environ_entry.partition("=")
1175 if name == "DISPLAY" and "." in value:
1176 value = value[:value.find(".")]
1177 process_environ[name] = value
1179 if "DISPLAY" not in process_environ:
1180 # Cannot work with this environment, skip.
1183 # To allow scripts to detect batch invocation (especially useful for predetect)
1184 process_environ["AUTORANDR_BATCH_PID"] = str(os.getpid())
1185 process_environ["UID"] = str(uid)
1187 display = process_environ["DISPLAY"]
1189 if "XAUTHORITY" not in process_environ:
1190 # It's very likely that we cannot work with this environment either,
1191 # but keep it as a backup just in case we don't find anything else.
1192 backup_candidates[display] = process_environ
1195 if display not in X11_displays_done:
1197 pwent = pwd.getpwuid(uid)
1199 # User has no pwd entry
1202 fork_child_autorandr(pwent, process_environ)
1203 X11_displays_done.add(display)
1205 # Run autorandr for any users/displays which didn't have a process with
1207 for display, process_environ in backup_candidates.items():
1208 if display not in X11_displays_done:
1210 pwent = pwd.getpwuid(int(process_environ["UID"]))
1212 # User has no pwd entry
1215 fork_child_autorandr(pwent, process_environ)
1216 X11_displays_done.add(display)
1219 def enabled_monitors(config):
1221 for monitor in config:
1222 if "--off" in config[monitor].option_vector:
1224 monitors.append(monitor)
1228 def read_config(options, directory):
1229 """Parse a configuration config.ini from directory and merge it into
1230 the options dictionary"""
1231 config = configparser.ConfigParser()
1232 config.read(os.path.join(directory, "settings.ini"))
1233 if config.has_section("config"):
1234 for key, value in config.items("config"):
1235 options.setdefault("--%s" % key, value)
1240 opts, args = getopt.getopt(argv[1:], "s:r:l:d:cfh",
1241 ["batch", "dry-run", "change", "default=", "save=", "remove=", "load=",
1242 "force", "fingerprint", "config", "debug", "skip-options=", "help",
1243 "current", "detected", "version"])
1244 except getopt.GetoptError as e:
1245 print("Failed to parse options: {0}.\n"
1246 "Use --help to get usage information.".format(str(e)),
1248 sys.exit(posix.EX_USAGE)
1250 options = dict(opts)
1252 if "-h" in options or "--help" in options:
1255 if "--version" in options:
1256 print("autorandr " + __version__)
1259 if "--current" in options and "--detected" in options:
1260 print("--current and --detected are mutually exclusive.", file=sys.stderr)
1261 sys.exit(posix.EX_USAGE)
1264 if "--batch" in options:
1265 if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
1266 dispatch_call_to_sessions([x for x in argv if x != "--batch"])
1268 print("--batch mode can only be used by root and if $DISPLAY is unset")
1270 if "AUTORANDR_BATCH_PID" in os.environ:
1271 user = pwd.getpwuid(os.getuid())
1272 user = user.pw_name if user else "#%d" % os.getuid()
1273 print("autorandr running as user %s (started from batch instance)" % user)
1276 profile_symlinks = {}
1278 # Load profiles from each XDG config directory
1279 # The XDG spec says that earlier entries should take precedence, so reverse the order
1280 for directory in reversed(os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":")):
1281 system_profile_path = os.path.join(directory, "autorandr")
1282 if os.path.isdir(system_profile_path):
1283 profiles.update(load_profiles(system_profile_path))
1284 profile_symlinks.update(get_symlinks(system_profile_path))
1285 read_config(options, system_profile_path)
1286 # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
1287 # profile_path is also used later on to store configurations
1288 profile_path = os.path.expanduser("~/.autorandr")
1289 if not os.path.isdir(profile_path):
1290 # Elsewise, follow the XDG specification
1291 profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
1292 if os.path.isdir(profile_path):
1293 profiles.update(load_profiles(profile_path))
1294 profile_symlinks.update(get_symlinks(profile_path))
1295 read_config(options, profile_path)
1296 # Sort by descending mtime
1297 profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
1298 except Exception as e:
1299 raise AutorandrException("Failed to load profiles", e)
1301 profile_symlinks = {k: v for k, v in profile_symlinks.items() if v in (x[0] for x in virtual_profiles) or v in profiles}
1303 exec_scripts(None, "predetect")
1304 config, modes = parse_xrandr_output()
1306 if "--fingerprint" in options:
1307 output_setup(config, sys.stdout)
1310 if "--config" in options:
1311 output_configuration(config, sys.stdout)
1314 if "--skip-options" in options:
1315 skip_options = [y[2:] if y[:2] == "--" else y for y in (x.strip() for x in options["--skip-options"].split(","))]
1316 for profile in profiles.values():
1317 for output in profile["config"].values():
1318 output.set_ignored_options(skip_options)
1319 for output in config.values():
1320 output.set_ignored_options(skip_options)
1323 options["--save"] = options["-s"]
1324 if "--save" in options:
1325 if options["--save"] in (x[0] for x in virtual_profiles):
1326 raise AutorandrException("Cannot save current configuration as profile '%s':\n"
1327 "This configuration name is a reserved virtual configuration." % options["--save"])
1328 error = check_configuration_pre_save(config)
1330 print("Cannot save current configuration as profile '%s':" % options["--save"])
1334 profile_folder = os.path.join(profile_path, options["--save"])
1335 save_configuration(profile_folder, options['--save'], config, forced="--force" in options)
1336 exec_scripts(profile_folder, "postsave", {
1337 "CURRENT_PROFILE": options["--save"],
1338 "PROFILE_FOLDER": profile_folder,
1339 "MONITORS": ":".join(enabled_monitors(config)),
1341 except AutorandrException as e:
1343 except Exception as e:
1344 raise AutorandrException("Failed to save current configuration as profile '%s'" % (options["--save"],), e)
1345 print("Saved current configuration as profile '%s'" % options["--save"])
1349 options["--remove"] = options["-r"]
1350 if "--remove" in options:
1351 if options["--remove"] in (x[0] for x in virtual_profiles):
1352 raise AutorandrException("Cannot remove profile '%s':\n"
1353 "This configuration name is a reserved virtual configuration." % options["--remove"])
1354 if options["--remove"] not in profiles.keys():
1355 raise AutorandrException("Cannot remove profile '%s':\n"
1356 "This profile does not exist." % options["--remove"])
1359 profile_folder = os.path.join(profile_path, options["--remove"])
1360 profile_dirlist = os.listdir(profile_folder)
1361 profile_dirlist.remove("config")
1362 profile_dirlist.remove("setup")
1364 print("Profile folder '%s' contains the following additional files:\n"
1365 "---\n%s\n---" % (options["--remove"], "\n".join(profile_dirlist)))
1366 response = input("Do you really want to remove profile '%s'? If so, type 'yes': " % options["--remove"]).strip()
1367 if response != "yes":
1370 shutil.rmtree(profile_folder)
1371 print("Removed profile '%s'" % options["--remove"])
1373 print("Profile '%s' was not removed" % options["--remove"])
1374 except Exception as e:
1375 raise AutorandrException("Failed to remove profile '%s'" % (options["--remove"],), e)
1378 detected_profiles = find_profiles(config, profiles)
1379 load_profile = False
1382 options["--load"] = options["-l"]
1383 if "--load" in options:
1384 load_profile = options["--load"]
1385 elif len(args) == 1:
1386 load_profile = args[0]
1388 # Find the active profile(s) first, for the block script (See #42)
1389 current_profiles = []
1390 for profile_name in profiles.keys():
1391 configs_are_equal = is_equal_configuration(config, profiles[profile_name]["config"])
1392 if configs_are_equal:
1393 current_profiles.append(profile_name)
1394 block_script_metadata = {
1395 "CURRENT_PROFILE": "".join(current_profiles[:1]),
1396 "CURRENT_PROFILES": ":".join(current_profiles)
1400 for profile_name in profiles.keys():
1401 if profile_blocked(os.path.join(profile_path, profile_name), block_script_metadata):
1402 if "--current" not in options and "--detected" not in options:
1403 print("%s (blocked)" % profile_name)
1406 if profile_name in detected_profiles:
1407 if len(detected_profiles) == 1:
1409 props.append("(detected)")
1411 index = detected_profiles.index(profile_name) + 1
1412 props.append("(detected) (%d%s match)" % (index, ["st", "nd", "rd"][index - 1] if index < 4 else "th"))
1413 if ("-c" in options or "--change" in options) and index < best_index:
1414 load_profile = profile_name
1416 elif "--detected" in options:
1418 if profile_name in current_profiles:
1419 props.append("(current)")
1420 elif "--current" in options:
1422 if "--current" in options or "--detected" in options:
1423 print("%s" % (profile_name, ))
1425 print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)))
1426 if not configs_are_equal and "--debug" in options and profile_name in detected_profiles:
1427 print_profile_differences(config, profiles[profile_name]["config"])
1430 options["--default"] = options["-d"]
1431 if not load_profile and "--default" in options and ("-c" in options or "--change" in options):
1432 load_profile = options["--default"]
1435 if load_profile in profile_symlinks:
1436 if "--debug" in options:
1437 print("'%s' symlinked to '%s'" % (load_profile, profile_symlinks[load_profile]))
1438 load_profile = profile_symlinks[load_profile]
1440 if load_profile in (x[0] for x in virtual_profiles):
1441 load_config = generate_virtual_profile(config, modes, load_profile)
1442 scripts_path = os.path.join(profile_path, load_profile)
1445 profile = profiles[load_profile]
1446 load_config = profile["config"]
1447 scripts_path = profile["path"]
1449 raise AutorandrException("Failed to load profile '%s': Profile not found" % load_profile)
1450 if load_profile in detected_profiles and detected_profiles[0] != load_profile:
1451 update_mtime(os.path.join(scripts_path, "config"))
1452 add_unused_outputs(config, load_config)
1453 if load_config == dict(config) and "-f" not in options and "--force" not in options:
1454 print("Config already loaded", file=sys.stderr)
1456 if "--debug" in options and load_config != dict(config):
1457 print("Loading profile '%s'" % load_profile)
1458 print_profile_differences(config, load_config)
1460 remove_irrelevant_outputs(config, load_config)
1463 if "--dry-run" in options:
1464 apply_configuration(load_config, config, True)
1467 "CURRENT_PROFILE": load_profile,
1468 "PROFILE_FOLDER": scripts_path,
1469 "MONITORS": ":".join(enabled_monitors(load_config)),
1471 exec_scripts(scripts_path, "preswitch", script_metadata)
1472 if "--debug" in options:
1473 print("Going to run:")
1474 apply_configuration(load_config, config, True)
1475 apply_configuration(load_config, config, False)
1476 exec_scripts(scripts_path, "postswitch", script_metadata)
1477 except AutorandrException as e:
1478 raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, e.report_bug)
1479 except Exception as e:
1480 raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
1482 if "--dry-run" not in options and "--debug" in options:
1483 new_config, _ = parse_xrandr_output()
1484 if not is_equal_configuration(new_config, load_config):
1485 print("The configuration change did not go as expected:")
1486 print_profile_differences(new_config, load_config)
1491 def exception_handled_main(argv=sys.argv):
1494 except AutorandrException as e:
1495 print(e, file=sys.stderr)
1497 except Exception as e:
1498 if not len(str(e)): # BdbQuit
1499 print("Exception: {0}".format(e.__class__.__name__))
1502 print("Unhandled exception ({0}). Please report this as a bug at "
1503 "https://github.com/phillipberndt/autorandr/issues.".format(e),
1508 if __name__ == '__main__':
1509 exception_handled_main()