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 functools import reduce
43 from itertools import chain
46 from packaging.version import Version
47 except ModuleNotFound:
48 from distutils.version import LooseVersion as Version
50 if sys.version_info.major == 2:
51 import ConfigParser as configparser
63 # (name, description, callback)
64 ("off", "Disable all outputs", None),
65 ("common", "Clone all connected outputs at the largest common resolution", None),
66 ("clone-largest", "Clone all connected outputs with the largest resolution (scaled down if necessary)", None),
67 ("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
68 ("vertical", "Stack all connected outputs vertically at their largest resolution", None),
72 Usage: autorandr [options]
74 -h, --help get this small help
75 -c, --change automatically load the first detected profile
76 -d, --default <profile> make profile <profile> the default profile
77 -l, --load <profile> load profile <profile>
78 -s, --save <profile> save your current setup to profile <profile>
79 -r, --remove <profile> remove profile <profile>
80 --batch run autorandr for all users with active X11 sessions
81 --current only list current (active) configuration(s)
82 --config dump your current xrandr setup
83 --debug enable verbose output
84 --detected only list detected (available) configuration(s)
85 --dry-run don't change anything, only print the xrandr commands
86 --fingerprint fingerprint your current hardware setup
87 --force force (re)loading of a profile / overwrite exiting files
88 --skip-options <option> comma separated list of xrandr arguments (e.g. "gamma")
89 to skip both in detecting changes and applying a profile
90 --version show version information and exit
92 If no suitable profile can be identified, the current configuration is kept.
93 To change this behaviour and switch to a fallback configuration, specify
96 autorandr supports a set of per-profile and global hooks. See the documentation
99 The following virtual configurations are available:
103 def is_closed_lid(output):
104 if not re.match(r'(eDP(-?[0-9]\+)*|LVDS(-?[0-9]\+)*)', output):
106 lids = glob.glob("/proc/acpi/button/lid/*/state")
109 with open(state_file) as f:
111 return "close" in content
115 class AutorandrException(Exception):
116 def __init__(self, message, original_exception=None, report_bug=False):
117 self.message = message
118 self.report_bug = report_bug
119 if original_exception:
120 self.original_exception = original_exception
121 trace = sys.exc_info()[2]
123 trace = trace.tb_next
124 self.line = trace.tb_lineno
125 self.file_name = trace.tb_frame.f_code.co_filename
129 frame = inspect.currentframe().f_back
130 self.line = frame.f_lineno
131 self.file_name = frame.f_code.co_filename
134 self.file_name = None
135 self.original_exception = None
137 if os.path.abspath(self.file_name) == os.path.abspath(sys.argv[0]):
138 self.file_name = None
141 retval = [self.message]
143 retval.append(" (line %d%s)" % (self.line, ("; %s" % self.file_name) if self.file_name else ""))
144 if self.original_exception:
145 retval.append(":\n ")
146 retval.append(str(self.original_exception).replace("\n", "\n "))
148 retval.append("\nThis appears to be a bug. Please help improving autorandr by reporting it upstream:"
149 "\nhttps://github.com/phillipberndt/autorandr/issues"
150 "\nPlease attach the output of `xrandr --verbose` to your bug report if appropriate.")
151 return "".join(retval)
154 class XrandrOutput(object):
155 "Represents an XRandR output"
157 # This regular expression is used to parse an output in `xrandr --verbose'
158 XRANDR_OUTPUT_REGEXP = """(?x)
159 ^\s*(?P<output>\S[^ ]*)\s+ # Line starts with output name
160 (?: # Differentiate disconnected and connected
161 disconnected | # in first line
162 unknown\ connection |
163 (?P<connected>connected)
166 (?P<primary>primary\ )? # Might be primary screen
168 (?P<width>[0-9]+)x(?P<height>[0-9]+) # Resolution (might be overridden below!)
169 \+(?P<x>-?[0-9]+)\+(?P<y>-?[0-9]+)\s+ # Position
170 (?:\(0x[0-9a-fA-F]+\)\s+)? # XID
171 (?P<rotate>(?:normal|left|right|inverted))\s+ # Rotation
172 (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)? # Reflection
173 )? # .. but only if the screen is in use.
174 (?:[\ \t]*\([^\)]+\))(?:\s*[0-9]+mm\sx\s[0-9]+mm)?
175 (?:[\ \t]*panning\ (?P<panning>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Panning information
176 (?:[\ \t]*tracking\ (?P<tracking>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))? # Tracking information
177 (?:[\ \t]*border\ (?P<border>(?:[0-9]+/){3}[0-9]+))? # Border information
178 (?:\s*(?: # Properties of the output
179 Gamma: (?P<gamma>(?:inf|-?[0-9\.\-: e])+) | # Gamma value
180 CRTC:\s*(?P<crtc>[0-9]) | # CRTC value
181 Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) | # Transformation matrix
182 EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) | # EDID of the output
183 (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)* # Other properties
187 (?P<mode_name>\S+).+?\*current.*\s+ # Interesting (current) resolution:
188 h:\s+width\s+(?P<mode_width>[0-9]+).+\s+ # Extract rate
189 v:\s+height\s+(?P<mode_height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
190 \S+(?:(?!\*current).)+\s+h:.+\s+v:.+\s* # Other resolutions
194 XRANDR_OUTPUT_MODES_REGEXP = """(?x)
195 (?P<name>\S+).+?(?P<preferred>\+preferred)?\s+
196 h:\s+width\s+(?P<width>[0-9]+).+\s+
197 v:\s+height\s+(?P<height>[0-9]+).+clock\s+(?P<rate>[0-9\.]+)Hz\s* |
200 XRANDR_13_DEFAULTS = {
201 "transform": "1,0,0,0,1,0,0,0,1",
205 XRANDR_12_DEFAULTS = {
208 "gamma": "1.0:1.0:1.0",
211 XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
213 EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
216 return "<%s%s %s>" % (self.output, self.short_edid, " ".join(self.option_vector))
219 def short_edid(self):
220 return ("%s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else ""
223 def options_with_defaults(self):
224 "Return the options dictionary, augmented with the default values that weren't set"
225 if "off" in self.options:
228 if xrandr_version() >= Version("1.3"):
229 options.update(self.XRANDR_13_DEFAULTS)
230 if xrandr_version() >= Version("1.2"):
231 options.update(self.XRANDR_12_DEFAULTS)
232 options.update(self.options)
233 return {a: b for a, b in options.items() if a not in self.ignored_options}
236 def filtered_options(self):
237 "Return a dictionary of options without ignored options"
238 return {a: b for a, b in self.options.items() if a not in self.ignored_options}
241 def option_vector(self):
242 "Return the command line parameters for XRandR for this instance"
243 args = ["--output", self.output]
244 for option, arg in sorted(self.options_with_defaults.items()):
245 args.append("--%s" % option)
251 def option_string(self):
252 "Return the command line parameters in the configuration file format"
253 options = ["output %s" % self.output]
254 for option, arg in sorted(self.filtered_options.items()):
256 options.append("%s %s" % (option, arg))
258 options.append(option)
259 return "\n".join(options)
263 "Return a key to sort the outputs for xrandr invocation"
266 if "off" in self.options:
268 if "pos" in self.options:
269 x, y = map(float, self.options["pos"].split("x"))
274 def __init__(self, output, edid, options):
275 "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
278 self.options = options
279 self.ignored_options = []
280 self.remove_default_option_values()
282 def set_ignored_options(self, options):
283 "Set a list of xrandr options that are never used (neither when comparing configurations nor when applying them)"
284 self.ignored_options = list(options)
286 def remove_default_option_values(self):
287 "Remove values from the options dictionary that are superflous"
288 if "off" in self.options and len(self.options.keys()) > 1:
289 self.options = {"off": None}
291 for option, default_value in self.XRANDR_DEFAULTS.items():
292 if option in self.options and self.options[option] == default_value:
293 del self.options[option]
296 def from_xrandr_output(cls, xrandr_output):
297 """Instanciate an XrandrOutput from the output of `xrandr --verbose'
299 This method also returns a list of modes supported by the output.
302 xrandr_output = xrandr_output.replace("\r\n", "\n")
303 match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
305 raise AutorandrException("Parsing XRandR output failed, there is an error in the regular expression.",
308 debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
309 raise AutorandrException("Parsing XRandR output failed, the regular expression did not match: %s" % debug,
311 remainder = xrandr_output[len(match_object.group(0)):]
313 raise AutorandrException("Parsing XRandR output failed, %d bytes left unmatched after "
314 "regular expression, starting at byte %d with ..'%s'." %
315 (len(remainder), len(match_object.group(0)), remainder[:10]),
318 match = match_object.groupdict()
323 for mode_match in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]):
324 if mode_match.group("name"):
325 modes.append(mode_match.groupdict())
327 raise AutorandrException("Parsing XRandR output failed, couldn't find any display modes", report_bug=True)
330 if not match["connected"]:
333 edid = "".join(match["edid"].strip().split())
335 edid = "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
337 # An output can be disconnected but still have a mode configured. This can only happen
338 # as a residual situation after a disconnect, you cannot associate a mode with an disconnected
341 # This code needs to be careful not to mix the two. An output should only be configured to
342 # "off" if it doesn't have a mode associated with it, which is modelled as "not a width" here.
343 if not match["width"]:
344 options["off"] = None
346 if match["mode_name"]:
347 options["mode"] = match["mode_name"]
348 elif match["mode_width"]:
349 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
351 if match["rotate"] not in ("left", "right"):
352 options["mode"] = "%sx%s" % (match["width"] or 0, match["height"] or 0)
354 options["mode"] = "%sx%s" % (match["height"] or 0, match["width"] or 0)
356 options["rotate"] = match["rotate"]
358 options["primary"] = None
359 if match["reflect"] == "X":
360 options["reflect"] = "x"
361 elif match["reflect"] == "Y":
362 options["reflect"] = "y"
363 elif match["reflect"] == "X and Y":
364 options["reflect"] = "xy"
365 if match["x"] or match["y"]:
366 options["pos"] = "%sx%s" % (match["x"] or "0", match["y"] or "0")
368 panning = [match["panning"]]
369 if match["tracking"]:
370 panning += ["/", match["tracking"]]
372 panning += ["/", match["border"]]
373 options["panning"] = "".join(panning)
374 if match["transform"]:
375 transformation = ",".join(match["transform"].strip().split())
376 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
377 options["transform"] = transformation
378 if not match["mode_name"]:
379 # TODO We'd need to apply the reverse transformation here. Let's see if someone complains,
380 # I doubt that this special case is actually required.
381 print("Warning: Output %s has a transformation applied. Could not determine correct mode! "
382 "Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
384 gamma = match["gamma"].strip()
385 # xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
386 # Also, it is not able to work with non-standard gamma ramps. Finally, it auto-corrects 0 to 1,
387 # so we approximate by 1e-10.
388 gamma = ":".join([str(max(1e-10, round(1. / float(x), 3))) for x in gamma.split(":")])
389 options["gamma"] = gamma
391 options["crtc"] = match["crtc"]
393 options["rate"] = match["rate"]
395 return XrandrOutput(match["output"], edid, options), modes
398 def from_config_file(cls, edid_map, configuration):
399 "Instanciate an XrandrOutput from the contents of a configuration file"
401 for line in configuration.split("\n"):
403 line = line.split(None, 1)
404 if line and line[0].startswith("#"):
406 options[line[0]] = line[1] if len(line) > 1 else None
410 if options["output"] in edid_map:
411 edid = edid_map[options["output"]]
413 # This fuzzy matching is for legacy autorandr that used sysfs output names
414 fuzzy_edid_map = [re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys()]
415 fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
416 if fuzzy_output in fuzzy_edid_map:
417 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
418 elif "off" not in options:
419 raise AutorandrException("Failed to find an EDID for output `%s' in setup file, required as `%s' "
420 "is not off in config file." % (options["output"], options["output"]))
421 output = options["output"]
422 del options["output"]
424 return XrandrOutput(output, edid, options)
426 def edid_equals(self, other):
427 "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
428 if self.edid and other.edid:
429 if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
430 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
431 if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
432 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
434 return match_asterisk(self.edid, other.edid) > 0
435 elif "*" in other.edid:
436 return match_asterisk(other.edid, self.edid) > 0
437 return self.edid == other.edid
439 def __ne__(self, other):
440 return not (self == other)
442 def __eq__(self, other):
443 return self.edid_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
445 def verbose_diff(self, other):
446 "Compare to another XrandrOutput and return a list of human readable differences"
448 if not self.edid_equals(other):
449 diffs.append("EDID `%s' differs from `%s'" % (self.short_edid, other.short_edid))
450 if self.output != other.output:
451 diffs.append("Output name `%s' differs from `%s'" % (self.output, other.output))
452 if "off" in self.options and "off" not in other.options:
453 diffs.append("The output is disabled currently, but active in the new configuration")
454 elif "off" in other.options and "off" not in self.options:
455 diffs.append("The output is currently enabled, but inactive in the new configuration")
457 for name in set(chain.from_iterable((self.options.keys(), other.options.keys()))):
458 if name not in other.options:
459 diffs.append("Option --%s %sis not present in the new configuration" %
460 (name, "(= `%s') " % self.options[name] if self.options[name] else ""))
461 elif name not in self.options:
462 diffs.append("Option --%s (`%s' in the new configuration) is not present currently" %
463 (name, other.options[name]))
464 elif self.options[name] != other.options[name]:
465 diffs.append("Option --%s %sis `%s' in the new configuration" %
466 (name, "(= `%s') " % self.options[name] if self.options[name] else "", other.options[name]))
470 def xrandr_version():
471 "Return the version of XRandR that this system uses"
472 if getattr(xrandr_version, "version", False) is False:
473 version_string = os.popen("xrandr -v").read()
475 version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
476 xrandr_version.version = Version(version)
477 except AttributeError:
478 xrandr_version.version = Version("1.3.0")
480 return xrandr_version.version
483 def debug_regexp(pattern, string):
484 "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
487 bounds = (0, len(string))
488 while bounds[0] != bounds[1]:
489 half = int((bounds[0] + bounds[1]) / 2)
490 if half == bounds[0]:
492 bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
493 partial_length = bounds[0]
494 return ("Regular expression matched until position %d, ..'%s', and did not match from '%s'.." %
495 (partial_length, string[max(0, partial_length - 20):partial_length],
496 string[partial_length:partial_length + 10]))
499 return "Debug information would be available if the `regex' module was installed."
502 def parse_xrandr_output():
503 "Parse the output of `xrandr --verbose' into a list of outputs"
504 xrandr_output = os.popen("xrandr -q --verbose").read()
505 if not xrandr_output:
506 raise AutorandrException("Failed to run xrandr")
508 # We are not interested in screens
509 xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
511 # Split at output boundaries and instanciate an XrandrOutput per output
512 split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
513 if len(split_xrandr_output) < 2:
514 raise AutorandrException("No output boundaries found", report_bug=True)
515 outputs = OrderedDict()
516 modes = OrderedDict()
517 for i in range(1, len(split_xrandr_output), 2):
518 output_name = split_xrandr_output[i].split()[0]
519 output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i + 2]))
520 outputs[output_name] = output
522 modes[output_name] = output_modes
524 # consider a closed lid as disconnected if other outputs are connected
525 if sum(o.edid != None for o in outputs.values()) > 1:
526 for output_name in outputs.keys():
527 if is_closed_lid(output_name):
528 outputs[output_name].edid = None
530 return outputs, modes
533 def load_profiles(profile_path):
534 "Load the stored profiles"
537 for profile in os.listdir(profile_path):
538 config_name = os.path.join(profile_path, profile, "config")
539 setup_name = os.path.join(profile_path, profile, "setup")
540 if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
543 edids = dict([x.split() for x in (y.strip() for y in open(setup_name).readlines()) if x and x[0] != "#"])
547 for line in chain(open(config_name).readlines(), ["output"]):
548 if line[:6] == "output" and buffer:
549 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
554 for output_name in list(config.keys()):
555 if config[output_name].edid is None:
556 del config[output_name]
558 profiles[profile] = {
560 "path": os.path.join(profile_path, profile),
561 "config-mtime": os.stat(config_name).st_mtime,
567 def get_symlinks(profile_path):
568 "Load all symlinks from a directory"
571 for link in os.listdir(profile_path):
572 file_name = os.path.join(profile_path, link)
573 if os.path.islink(file_name):
574 symlinks[link] = os.readlink(file_name)
579 def match_asterisk(pattern, data):
580 """Match data against a pattern
582 The difference to fnmatch is that this function only accepts patterns with a single
583 asterisk and that it returns a "closeness" number, which is larger the better the match.
584 Zero indicates no match at all.
586 if "*" not in pattern:
587 return 1 if pattern == data else 0
588 parts = pattern.split("*")
590 raise ValueError("Only patterns with a single asterisk are supported, %s is invalid" % pattern)
591 if not data.startswith(parts[0]):
593 if not data.endswith(parts[1]):
595 matched = len(pattern)
596 total = len(data) + 1
597 return matched * 1. / total
600 def find_profiles(current_config, profiles):
601 "Find profiles matching the currently connected outputs, sorting asterisk matches to the back"
602 detected_profiles = []
603 for profile_name, profile in profiles.items():
604 config = profile["config"]
606 for name, output in config.items():
609 if name not in current_config or not output.edid_equals(current_config[name]):
612 if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].edid)):
615 closeness = max(match_asterisk(output.edid, current_config[name].edid), match_asterisk(current_config[name].edid, output.edid))
616 detected_profiles.append((closeness, profile_name))
617 detected_profiles = [o[1] for o in sorted(detected_profiles, key=lambda x: -x[0])]
618 return detected_profiles
621 def profile_blocked(profile_path, meta_information=None):
622 """Check if a profile is blocked.
624 meta_information is expected to be an dictionary. It will be passed to the block scripts
625 in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
627 return not exec_scripts(profile_path, "block", meta_information)
630 def check_configuration_pre_save(configuration):
631 "Check that a configuration is safe for saving."
632 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
633 for output in outputs:
634 if "off" not in configuration[output].options and not configuration[output].edid:
635 return ("`%(o)s' is not off (has a mode configured) but is disconnected (does not have an EDID).\n"
636 "This typically means that it has been recently unplugged and then not properly disabled\n"
637 "by the user. Please disable it (e.g. using `xrandr --output %(o)s --off`) and then rerun\n"
638 "this command.") % {"o": output}
641 def output_configuration(configuration, config):
642 "Write a configuration file"
643 outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
644 for output in outputs:
645 print(configuration[output].option_string, file=config)
648 def output_setup(configuration, setup):
649 "Write a setup (fingerprint) file"
650 outputs = sorted(configuration.keys())
651 for output in outputs:
652 if configuration[output].edid:
653 print(output, configuration[output].edid, file=setup)
656 def save_configuration(profile_path, profile_name, configuration, forced=False):
657 "Save a configuration into a profile"
658 if not os.path.isdir(profile_path):
659 os.makedirs(profile_path)
660 config_path = os.path.join(profile_path, "config")
661 setup_path = os.path.join(profile_path, "setup")
662 if os.path.isfile(config_path) and not forced:
663 raise AutorandrException('Refusing to overwrite config "{}" without passing "--force"!'.format(profile_name))
664 if os.path.isfile(setup_path) and not forced:
665 raise AutorandrException('Refusing to overwrite config "{}" without passing "--force"!'.format(profile_name))
667 with open(config_path, "w") as config:
668 output_configuration(configuration, config)
669 with open(setup_path, "w") as setup:
670 output_setup(configuration, setup)
673 def update_mtime(filename):
674 "Update a file's mtime"
676 os.utime(filename, None)
682 def call_and_retry(*args, **kwargs):
683 """Wrapper around subprocess.call that retries failed calls.
685 This function calls subprocess.call and on non-zero exit states,
686 waits a second and then retries once. This mitigates #47,
687 a timing issue with some drivers.
689 if "dry_run" in kwargs:
690 dry_run = kwargs["dry_run"]
691 del kwargs["dry_run"]
694 kwargs_redirected = dict(kwargs)
696 if hasattr(subprocess, "DEVNULL"):
697 kwargs_redirected["stdout"] = getattr(subprocess, "DEVNULL")
699 kwargs_redirected["stdout"] = open(os.devnull, "w")
700 kwargs_redirected["stderr"] = kwargs_redirected["stdout"]
701 retval = subprocess.call(*args, **kwargs_redirected)
704 retval = subprocess.call(*args, **kwargs)
708 def get_fb_dimensions(configuration):
711 for output in configuration.values():
712 if "off" in output.options or not output.edid:
714 # This won't work with all modes -- but it's a best effort.
715 match = re.search("[0-9]{3,}x[0-9]{3,}", output.options["mode"])
718 o_mode = match.group(0)
719 o_width, o_height = map(int, o_mode.split("x"))
720 if "transform" in output.options:
721 a, b, c, d, e, f, g, h, i = map(float, output.options["transform"].split(","))
722 w = (g * o_width + h * o_height + i)
723 x = (a * o_width + b * o_height + c) / w
724 y = (d * o_width + e * o_height + f) / w
725 o_width, o_height = x, y
726 if "rotate" in output.options:
727 if output.options["rotate"] in ("left", "right"):
728 o_width, o_height = o_height, o_width
729 if "pos" in output.options:
730 o_left, o_top = map(int, output.options["pos"].split("x"))
733 if "panning" in output.options:
734 match = re.match("(?P<w>[0-9]+)x(?P<h>[0-9]+)(?:\+(?P<x>[0-9]+))?(?:\+(?P<y>[0-9]+))?.*", output.options["panning"])
736 detail = match.groupdict(default="0")
737 o_width = int(detail.get("w")) + int(detail.get("x"))
738 o_height = int(detail.get("h")) + int(detail.get("y"))
739 width = max(width, o_width)
740 height = max(height, o_height)
741 return int(width), int(height)
744 def apply_configuration(new_configuration, current_configuration, dry_run=False):
745 "Apply a configuration"
746 found_top_left_monitor = False
747 found_left_monitor = False
748 found_top_monitor = False
749 outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
751 base_argv = ["echo", "xrandr"]
753 base_argv = ["xrandr"]
755 # There are several xrandr / driver bugs we need to take care of here:
756 # - We cannot enable more than two screens at the same time
757 # See https://github.com/phillipberndt/autorandr/pull/6
758 # and commits f4cce4d and 8429886.
759 # - We cannot disable all screens
760 # See https://github.com/phillipberndt/autorandr/pull/20
761 # - We should disable screens before enabling others, because there's
762 # a limit on the number of enabled screens
763 # - We must make sure that the screen at 0x0 is activated first,
764 # or the other (first) screen to be activated would be moved there.
765 # - If an active screen already has a transformation and remains active,
766 # the xrandr call fails with an invalid RRSetScreenSize parameter error.
767 # Update the configuration in 3 passes in that case. (On Haswell graphics,
769 # - Some implementations can not handle --transform at all, so avoid it unless
770 # necessary. (See https://github.com/phillipberndt/autorandr/issues/37)
771 # - Some implementations can not handle --panning without specifying --fb
772 # explicitly, so avoid it unless necessary.
773 # (See https://github.com/phillipberndt/autorandr/issues/72)
775 fb_dimensions = get_fb_dimensions(new_configuration)
777 base_argv += ["--fb", "%dx%d" % fb_dimensions]
779 # Failed to obtain frame-buffer size. Doesn't matter, xrandr will choose for the user.
782 auxiliary_changes_pre = []
785 remain_active_count = 0
786 for output in outputs:
787 if not new_configuration[output].edid or "off" in new_configuration[output].options:
788 disable_outputs.append(new_configuration[output].option_vector)
790 if output not in current_configuration:
791 raise AutorandrException("New profile configures output %s which does not exist in current xrandr --verbose output. "
792 "Don't know how to proceed." % output)
793 if "off" not in current_configuration[output].options:
794 remain_active_count += 1
796 option_vector = new_configuration[output].option_vector
797 if xrandr_version() >= Version("1.3.0"):
798 for option, off_value in (("transform", "none"), ("panning", "0x0")):
799 if option in current_configuration[output].options:
800 auxiliary_changes_pre.append(["--output", output, "--%s" % option, off_value])
803 option_index = option_vector.index("--%s" % option)
804 if option_vector[option_index + 1] == XrandrOutput.XRANDR_DEFAULTS[option]:
805 option_vector = option_vector[:option_index] + option_vector[option_index + 2:]
808 if not found_top_left_monitor:
809 position = new_configuration[output].options.get("pos", "0x0")
810 if position == "0x0":
811 found_top_left_monitor = True
812 enable_outputs.insert(0, option_vector)
813 elif not found_left_monitor and position.startswith("0x"):
814 found_left_monitor = True
815 enable_outputs.insert(0, option_vector)
816 elif not found_top_monitor and position.endswith("x0"):
817 found_top_monitor = True
818 enable_outputs.insert(0, option_vector)
820 enable_outputs.append(option_vector)
822 enable_outputs.append(option_vector)
824 # Perform pe-change auxiliary changes
825 if auxiliary_changes_pre:
826 argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
827 if call_and_retry(argv, dry_run=dry_run) != 0:
828 raise AutorandrException("Command failed: %s" % " ".join(argv))
830 # Disable unused outputs, but make sure that there always is at least one active screen
831 disable_keep = 0 if remain_active_count else 1
832 if len(disable_outputs) > disable_keep:
833 argv = base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))
834 if call_and_retry(argv, dry_run=dry_run) != 0:
835 # Disabling the outputs failed. Retry with the next command:
836 # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
837 # This does not occur if simultaneously the primary screen is reset.
840 disable_outputs = disable_outputs[-1:] if disable_keep else []
842 # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
843 # disable the last two screens. This is a problem, so if this would happen, instead disable only
844 # one screen in the first call below.
845 if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
846 # In the context of a xrandr call that changes the display state, `--query' should do nothing
847 disable_outputs.insert(0, ['--query'])
849 # If we did not find a candidate, we might need to inject a call
850 # If there is no output to disable, we will enable 0x and x0 at the same time
851 if not found_top_left_monitor and len(disable_outputs) > 0:
852 # If the call to 0x and x0 is splitted, inject one of them
853 if found_top_monitor and found_left_monitor:
854 enable_outputs.insert(0, enable_outputs[0])
856 # Enable the remaining outputs in pairs of two operations
857 operations = disable_outputs + enable_outputs
858 for index in range(0, len(operations), 2):
859 argv = base_argv + list(chain.from_iterable(operations[index:index + 2]))
860 if call_and_retry(argv, dry_run=dry_run) != 0:
861 raise AutorandrException("Command failed: %s" % " ".join(argv))
864 def is_equal_configuration(source_configuration, target_configuration):
866 Check if all outputs from target are already configured correctly in source and
867 that no other outputs are active.
869 for output in target_configuration.keys():
870 if "off" in target_configuration[output].options:
871 if (output in source_configuration and "off" not in source_configuration[output].options):
874 if (output not in source_configuration) or (source_configuration[output] != target_configuration[output]):
876 for output in source_configuration.keys():
877 if "off" in source_configuration[output].options:
878 if output in target_configuration and "off" not in target_configuration[output].options:
881 if output not in target_configuration:
886 def add_unused_outputs(source_configuration, target_configuration):
887 "Add outputs that are missing in target to target, in 'off' state"
888 for output_name, output in source_configuration.items():
889 if output_name not in target_configuration:
890 target_configuration[output_name] = XrandrOutput(output_name, output.edid, {"off": None})
893 def remove_irrelevant_outputs(source_configuration, target_configuration):
894 "Remove outputs from target that ought to be 'off' and already are"
895 for output_name, output in source_configuration.items():
896 if "off" in output.options:
897 if output_name in target_configuration:
898 if "off" in target_configuration[output_name].options:
899 del target_configuration[output_name]
902 def generate_virtual_profile(configuration, modes, profile_name):
903 "Generate one of the virtual profiles"
904 configuration = copy.deepcopy(configuration)
905 if profile_name == "common":
907 for output, output_modes in modes.items():
909 if configuration[output].edid:
910 for mode in output_modes:
911 mode_set.add((mode["width"], mode["height"]))
912 mode_sets.append(mode_set)
913 common_resolution = reduce(lambda a, b: a & b, mode_sets[1:], mode_sets[0])
914 common_resolution = sorted(common_resolution, key=lambda a: int(a[0]) * int(a[1]))
915 if common_resolution:
916 for output in configuration:
917 configuration[output].options = {}
918 if output in modes and configuration[output].edid:
919 modes_sorted = sorted(modes[output], key=lambda x: 0 if x["preferred"] else 1)
920 modes_filtered = [x for x in modes_sorted if (x["width"], x["height"]) == common_resolution[-1]]
921 mode = modes_filtered[0]
922 configuration[output].options["mode"] = mode['name']
923 configuration[output].options["pos"] = "0x0"
925 configuration[output].options["off"] = None
926 elif profile_name in ("horizontal", "vertical"):
928 if profile_name == "horizontal":
929 shift_index = "width"
930 pos_specifier = "%sx0"
932 shift_index = "height"
933 pos_specifier = "0x%s"
935 for output in configuration:
936 configuration[output].options = {}
937 if output in modes and configuration[output].edid:
939 score = int(a["width"]) * int(a["height"])
943 output_modes = sorted(modes[output], key=key)
944 mode = output_modes[-1]
945 configuration[output].options["mode"] = mode["name"]
946 configuration[output].options["rate"] = mode["rate"]
947 configuration[output].options["pos"] = pos_specifier % shift
948 shift += int(mode[shift_index])
950 configuration[output].options["off"] = None
951 elif profile_name == "clone-largest":
952 modes_unsorted = [output_modes[0] for output, output_modes in modes.items()]
953 modes_sorted = sorted(modes_unsorted, key=lambda x: int(x["width"]) * int(x["height"]), reverse=True)
954 biggest_resolution = modes_sorted[0]
955 for output in configuration:
956 configuration[output].options = {}
957 if output in modes and configuration[output].edid:
959 score = int(a["width"]) * int(a["height"])
963 output_modes = sorted(modes[output], key=key)
964 mode = output_modes[-1]
965 configuration[output].options["mode"] = mode["name"]
966 configuration[output].options["rate"] = mode["rate"]
967 configuration[output].options["pos"] = "0x0"
968 scale = max(float(biggest_resolution["width"]) / float(mode["width"]),
969 float(biggest_resolution["height"]) / float(mode["height"]))
970 mov_x = (float(mode["width"]) * scale - float(biggest_resolution["width"])) / -2
971 mov_y = (float(mode["height"]) * scale - float(biggest_resolution["height"])) / -2
972 configuration[output].options["transform"] = "{},0,{},0,{},{},0,0,1".format(scale, mov_x, scale, mov_y)
974 configuration[output].options["off"] = None
975 elif profile_name == "off":
976 for output in configuration:
977 for key in list(configuration[output].options.keys()):
978 del configuration[output].options[key]
979 configuration[output].options["off"] = None
983 def print_profile_differences(one, another):
984 "Print the differences between two profiles for debugging"
987 print("| Differences between the two profiles:")
988 for output in set(chain.from_iterable((one.keys(), another.keys()))):
989 if output not in one:
990 if "off" not in another[output].options:
991 print("| Output `%s' is missing from the active configuration" % output)
992 elif output not in another:
993 if "off" not in one[output].options:
994 print("| Output `%s' is missing from the new configuration" % output)
996 for line in one[output].verbose_diff(another[output]):
997 print("| [Output %s] %s" % (output, line))
1002 "Print help and exit"
1004 for profile in virtual_profiles:
1005 name, description = profile[:2]
1006 description = [description]
1008 while len(description[0]) > max_width + 1:
1009 left_over = description[0][max_width:]
1010 description[0] = description[0][:max_width] + "-"
1011 description.insert(1, " %-15s %s" % ("", left_over))
1012 description = "\n".join(description)
1013 print(" %-15s %s" % (name, description))
1017 def exec_scripts(profile_path, script_name, meta_information=None):
1020 This will run all executables from the profile folder, and global per-user
1021 and system-wide configuration folders, named script_name or residing in
1022 subdirectories named script_name.d.
1024 If profile_path is None, only global scripts will be invoked.
1026 meta_information is expected to be an dictionary. It will be passed to the block scripts
1027 in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
1029 Returns True unless any of the scripts exited with non-zero exit status.
1032 env = os.environ.copy()
1033 if meta_information:
1034 for key, value in meta_information.items():
1035 env["AUTORANDR_{}".format(key.upper())] = str(value)
1037 # If there are multiple candidates, the XDG spec tells to only use the first one.
1040 user_profile_path = os.path.expanduser("~/.autorandr")
1041 if not os.path.isdir(user_profile_path):
1042 user_profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
1044 candidate_directories = []
1046 candidate_directories.append(profile_path)
1047 candidate_directories.append(user_profile_path)
1048 for config_dir in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":"):
1049 candidate_directories.append(os.path.join(config_dir, "autorandr"))
1051 for folder in candidate_directories:
1052 if script_name not in ran_scripts:
1053 script = os.path.join(folder, script_name)
1054 if os.access(script, os.X_OK | os.F_OK):
1056 all_ok &= subprocess.call(script, env=env) != 0
1058 raise AutorandrException("Failed to execute user command: %s" % (script,))
1059 ran_scripts.add(script_name)
1061 script_folder = os.path.join(folder, "%s.d" % script_name)
1062 if os.access(script_folder, os.R_OK | os.X_OK) and os.path.isdir(script_folder):
1063 for file_name in os.listdir(script_folder):
1064 check_name = "d/%s" % (file_name,)
1065 if check_name not in ran_scripts:
1066 script = os.path.join(script_folder, file_name)
1067 if os.access(script, os.X_OK | os.F_OK):
1069 all_ok &= subprocess.call(script, env=env) != 0
1071 raise AutorandrException("Failed to execute user command: %s" % (script,))
1072 ran_scripts.add(check_name)
1077 def dispatch_call_to_sessions(argv):
1078 """Invoke autorandr for each open local X11 session with the given options.
1080 The function iterates over all processes not owned by root and checks
1081 whether they have DISPLAY and XAUTHORITY variables set. It strips the
1082 screen from any variable it finds (i.e. :0.0 becomes :0) and checks whether
1083 this display has been handled already. If it has not, it forks, changes
1084 uid/gid to the user owning the process, reuses the process's environment
1085 and runs autorandr with the parameters from argv.
1087 This function requires root permissions. It only works for X11 servers that
1088 have at least one non-root process running. It is susceptible for attacks
1089 where one user runs a process with another user's DISPLAY variable - in
1090 this case, it might happen that autorandr is invoked for the other user,
1091 which won't work. Since no other harm than prevention of automated
1092 execution of autorandr can be done this way, the assumption is that in this
1093 situation, the local administrator will handle the situation."""
1095 X11_displays_done = set()
1097 autorandr_binary = os.path.abspath(argv[0])
1098 backup_candidates = {}
1100 def fork_child_autorandr(pwent, process_environ):
1101 print("Running autorandr as %s for display %s" % (pwent.pw_name, process_environ["DISPLAY"]))
1102 child_pid = os.fork()
1104 # This will throw an exception if any of the privilege changes fails,
1105 # so it should be safe. Also, note that since the environment
1106 # is taken from a process owned by the user, reusing it should
1107 # not leak any information.
1109 os.setgroups(os.getgrouplist(pwent.pw_name, pwent.pw_gid))
1110 except AttributeError:
1111 # Python 2 doesn't have getgrouplist
1113 os.setresgid(pwent.pw_gid, pwent.pw_gid, pwent.pw_gid)
1114 os.setresuid(pwent.pw_uid, pwent.pw_uid, pwent.pw_uid)
1115 os.chdir(pwent.pw_dir)
1117 os.environ.update(process_environ)
1118 if sys.executable != "" and sys.executable != None:
1119 os.execl(sys.executable, sys.executable, autorandr_binary, *argv[1:])
1121 os.execl(autorandr_binary, autorandr_binary, *argv[1:])
1123 os.waitpid(child_pid, 0)
1125 for directory in os.listdir("/proc"):
1126 directory = os.path.join("/proc/", directory)
1127 if not os.path.isdir(directory):
1129 environ_file = os.path.join(directory, "environ")
1130 if not os.path.isfile(environ_file):
1132 uid = os.stat(environ_file).st_uid
1134 # The following line assumes that user accounts start at 1000 and that
1135 # no one works using the root or another system account. This is rather
1136 # restrictive, but de facto default. Alternatives would be to use the
1137 # UID_MIN from /etc/login.defs or FIRST_UID from /etc/adduser.conf;
1138 # but effectively, both values aren't binding in any way.
1139 # If this breaks your use case, please file a bug on Github.
1143 process_environ = {}
1144 for environ_entry in open(environ_file, 'rb').read().split(b"\0"):
1146 environ_entry = environ_entry.decode("ascii")
1147 except UnicodeDecodeError:
1149 name, sep, value = environ_entry.partition("=")
1151 if name == "DISPLAY" and "." in value:
1152 value = value[:value.find(".")]
1153 process_environ[name] = value
1155 if "DISPLAY" not in process_environ:
1156 # Cannot work with this environment, skip.
1159 # To allow scripts to detect batch invocation (especially useful for predetect)
1160 process_environ["AUTORANDR_BATCH_PID"] = str(os.getpid())
1161 process_environ["UID"] = str(uid)
1163 display = process_environ["DISPLAY"]
1165 if "XAUTHORITY" not in process_environ:
1166 # It's very likely that we cannot work with this environment either,
1167 # but keep it as a backup just in case we don't find anything else.
1168 backup_candidates[display] = process_environ
1171 if display not in X11_displays_done:
1173 pwent = pwd.getpwuid(uid)
1175 # User has no pwd entry
1178 fork_child_autorandr(pwent, process_environ)
1179 X11_displays_done.add(display)
1181 # Run autorandr for any users/displays which didn't have a process with
1183 for display, process_environ in backup_candidates.items():
1184 if display not in X11_displays_done:
1186 pwent = pwd.getpwuid(int(process_environ["UID"]))
1188 # User has no pwd entry
1191 fork_child_autorandr(pwent, process_environ)
1192 X11_displays_done.add(display)
1195 def enabled_monitors(config):
1197 for monitor in config:
1198 if "--off" in config[monitor].option_vector:
1200 monitors.append(monitor)
1204 def read_config(options, directory):
1205 """Parse a configuration config.ini from directory and merge it into
1206 the options dictionary"""
1207 config = configparser.ConfigParser()
1208 config.read(os.path.join(directory, "settings.ini"))
1209 if config.has_section("config"):
1210 for key, value in config.items("config"):
1211 options.setdefault("--%s" % key, value)
1215 opts, args = getopt.getopt(argv[1:], "s:r:l:d:cfh",
1216 ["batch", "dry-run", "change", "default=", "save=", "remove=", "load=",
1217 "force", "fingerprint", "config", "debug", "skip-options=", "help",
1218 "current", "detected", "version"])
1219 except getopt.GetoptError as e:
1220 print("Failed to parse options: {0}.\n"
1221 "Use --help to get usage information.".format(str(e)),
1223 sys.exit(posix.EX_USAGE)
1225 options = dict(opts)
1227 if "-h" in options or "--help" in options:
1230 if "--version" in options:
1231 print("autorandr " + __version__)
1234 if "--current" in options and "--detected" in options:
1235 print("--current and --detected are mutually exclusive.", file=sys.stderr)
1236 sys.exit(posix.EX_USAGE)
1239 if "--batch" in options:
1240 if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
1241 dispatch_call_to_sessions([x for x in argv if x != "--batch"])
1243 print("--batch mode can only be used by root and if $DISPLAY is unset")
1245 if "AUTORANDR_BATCH_PID" in os.environ:
1246 user = pwd.getpwuid(os.getuid())
1247 user = user.pw_name if user else "#%d" % os.getuid()
1248 print("autorandr running as user %s (started from batch instance)" % user)
1251 profile_symlinks = {}
1253 # Load profiles from each XDG config directory
1254 # The XDG spec says that earlier entries should take precedence, so reverse the order
1255 for directory in reversed(os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":")):
1256 system_profile_path = os.path.join(directory, "autorandr")
1257 if os.path.isdir(system_profile_path):
1258 profiles.update(load_profiles(system_profile_path))
1259 profile_symlinks.update(get_symlinks(system_profile_path))
1260 read_config(options, system_profile_path)
1261 # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
1262 # profile_path is also used later on to store configurations
1263 profile_path = os.path.expanduser("~/.autorandr")
1264 if not os.path.isdir(profile_path):
1265 # Elsewise, follow the XDG specification
1266 profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
1267 if os.path.isdir(profile_path):
1268 profiles.update(load_profiles(profile_path))
1269 profile_symlinks.update(get_symlinks(profile_path))
1270 read_config(options, profile_path)
1271 # Sort by descending mtime
1272 profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
1273 except Exception as e:
1274 raise AutorandrException("Failed to load profiles", e)
1276 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}
1278 exec_scripts(None, "predetect")
1279 config, modes = parse_xrandr_output()
1281 if "--fingerprint" in options:
1282 output_setup(config, sys.stdout)
1285 if "--config" in options:
1286 output_configuration(config, sys.stdout)
1289 if "--skip-options" in options:
1290 skip_options = [y[2:] if y[:2] == "--" else y for y in (x.strip() for x in options["--skip-options"].split(","))]
1291 for profile in profiles.values():
1292 for output in profile["config"].values():
1293 output.set_ignored_options(skip_options)
1294 for output in config.values():
1295 output.set_ignored_options(skip_options)
1298 options["--save"] = options["-s"]
1299 if "--save" in options:
1300 if options["--save"] in (x[0] for x in virtual_profiles):
1301 raise AutorandrException("Cannot save current configuration as profile '%s':\n"
1302 "This configuration name is a reserved virtual configuration." % options["--save"])
1303 error = check_configuration_pre_save(config)
1305 print("Cannot save current configuration as profile '%s':" % options["--save"])
1309 profile_folder = os.path.join(profile_path, options["--save"])
1310 save_configuration(profile_folder, options['--save'], config, forced="--force" in options)
1311 exec_scripts(profile_folder, "postsave", {
1312 "CURRENT_PROFILE": options["--save"],
1313 "PROFILE_FOLDER": profile_folder,
1314 "MONITORS": ":".join(enabled_monitors(config)),
1316 except AutorandrException as e:
1318 except Exception as e:
1319 raise AutorandrException("Failed to save current configuration as profile '%s'" % (options["--save"],), e)
1320 print("Saved current configuration as profile '%s'" % options["--save"])
1324 options["--remove"] = options["-r"]
1325 if "--remove" in options:
1326 if options["--remove"] in (x[0] for x in virtual_profiles):
1327 raise AutorandrException("Cannot remove profile '%s':\n"
1328 "This configuration name is a reserved virtual configuration." % options["--remove"])
1329 if options["--remove"] not in profiles.keys():
1330 raise AutorandrException("Cannot remove profile '%s':\n"
1331 "This profile does not exist." % options["--remove"])
1334 profile_folder = os.path.join(profile_path, options["--remove"])
1335 profile_dirlist = os.listdir(profile_folder)
1336 profile_dirlist.remove("config")
1337 profile_dirlist.remove("setup")
1339 print("Profile folder '%s' contains the following additional files:\n"
1340 "---\n%s\n---" % (options["--remove"], "\n".join(profile_dirlist)))
1341 response = input("Do you really want to remove profile '%s'? If so, type 'yes': " % options["--remove"]).strip()
1342 if response != "yes":
1345 shutil.rmtree(profile_folder)
1346 print("Removed profile '%s'" % options["--remove"])
1348 print("Profile '%s' was not removed" % options["--remove"])
1349 except Exception as e:
1350 raise AutorandrException("Failed to remove profile '%s'" % (options["--remove"],), e)
1353 detected_profiles = find_profiles(config, profiles)
1354 load_profile = False
1357 options["--load"] = options["-l"]
1358 if "--load" in options:
1359 load_profile = options["--load"]
1360 elif len(args) == 1:
1361 load_profile = args[0]
1363 # Find the active profile(s) first, for the block script (See #42)
1364 current_profiles = []
1365 for profile_name in profiles.keys():
1366 configs_are_equal = is_equal_configuration(config, profiles[profile_name]["config"])
1367 if configs_are_equal:
1368 current_profiles.append(profile_name)
1369 block_script_metadata = {
1370 "CURRENT_PROFILE": "".join(current_profiles[:1]),
1371 "CURRENT_PROFILES": ":".join(current_profiles)
1375 for profile_name in profiles.keys():
1376 if profile_blocked(os.path.join(profile_path, profile_name), block_script_metadata):
1377 if "--current" not in options and "--detected" not in options:
1378 print("%s (blocked)" % profile_name)
1381 if profile_name in detected_profiles:
1382 if len(detected_profiles) == 1:
1384 props.append("(detected)")
1386 index = detected_profiles.index(profile_name) + 1
1387 props.append("(detected) (%d%s match)" % (index, ["st", "nd", "rd"][index - 1] if index < 4 else "th"))
1388 if ("-c" in options or "--change" in options) and index < best_index:
1389 load_profile = profile_name
1391 elif "--detected" in options:
1393 if profile_name in current_profiles:
1394 props.append("(current)")
1395 elif "--current" in options:
1397 if "--current" in options or "--detected" in options:
1398 print("%s" % (profile_name, ))
1400 print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)))
1401 if not configs_are_equal and "--debug" in options and profile_name in detected_profiles:
1402 print_profile_differences(config, profiles[profile_name]["config"])
1405 options["--default"] = options["-d"]
1406 if not load_profile and "--default" in options and ("-c" in options or "--change" in options):
1407 load_profile = options["--default"]
1410 if load_profile in profile_symlinks:
1411 if "--debug" in options:
1412 print("'%s' symlinked to '%s'" % (load_profile, profile_symlinks[load_profile]))
1413 load_profile = profile_symlinks[load_profile]
1415 if load_profile in (x[0] for x in virtual_profiles):
1416 load_config = generate_virtual_profile(config, modes, load_profile)
1417 scripts_path = os.path.join(profile_path, load_profile)
1420 profile = profiles[load_profile]
1421 load_config = profile["config"]
1422 scripts_path = profile["path"]
1424 raise AutorandrException("Failed to load profile '%s': Profile not found" % load_profile)
1425 if load_profile in detected_profiles and detected_profiles[0] != load_profile:
1426 update_mtime(os.path.join(scripts_path, "config"))
1427 add_unused_outputs(config, load_config)
1428 if load_config == dict(config) and "-f" not in options and "--force" not in options:
1429 print("Config already loaded", file=sys.stderr)
1431 if "--debug" in options and load_config != dict(config):
1432 print("Loading profile '%s'" % load_profile)
1433 print_profile_differences(config, load_config)
1435 remove_irrelevant_outputs(config, load_config)
1438 if "--dry-run" in options:
1439 apply_configuration(load_config, config, True)
1442 "CURRENT_PROFILE": load_profile,
1443 "PROFILE_FOLDER": scripts_path,
1444 "MONITORS": ":".join(enabled_monitors(load_config)),
1446 exec_scripts(scripts_path, "preswitch", script_metadata)
1447 if "--debug" in options:
1448 print("Going to run:")
1449 apply_configuration(load_config, config, True)
1450 apply_configuration(load_config, config, False)
1451 exec_scripts(scripts_path, "postswitch", script_metadata)
1452 except AutorandrException as e:
1453 raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, e.report_bug)
1454 except Exception as e:
1455 raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
1457 if "--dry-run" not in options and "--debug" in options:
1458 new_config, _ = parse_xrandr_output()
1459 if not is_equal_configuration(new_config, load_config):
1460 print("The configuration change did not go as expected:")
1461 print_profile_differences(new_config, load_config)
1466 def exception_handled_main(argv=sys.argv):
1469 except AutorandrException as e:
1470 print(e, file=sys.stderr)
1472 except Exception as e:
1473 if not len(str(e)): # BdbQuit
1474 print("Exception: {0}".format(e.__class__.__name__))
1477 print("Unhandled exception ({0}). Please report this as a bug at "
1478 "https://github.com/phillipberndt/autorandr/issues.".format(e),
1483 if __name__ == '__main__':
1484 exception_handled_main()