import binascii
import copy
+import fnmatch
import getopt
import hashlib
import os
from functools import reduce
from itertools import chain
+if sys.version_info.major == 2:
+ import ConfigParser as configparser
+else:
+ import configparser
+
+__version__ = "1.5"
+
try:
input = raw_input
except NameError:
virtual_profiles = [
# (name, description, callback)
+ ("off", "Disable all outputs", None),
("common", "Clone all connected outputs at the largest common resolution", None),
("clone-largest", "Clone all connected outputs with the largest resolution (scaled down if necessary)", None),
("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
-h, --help get this small help
-c, --change reload current setup
+-d, --default <profile> make profile <profile> the default profile
+-l, --load <profile> load profile <profile>
-s, --save <profile> save your current setup to profile <profile>
-r, --remove <profile> remove profile <profile>
--l, --load <profile> load profile <profile>
--d, --default <profile> make profile <profile> the default profile
---skip-options <option> comma separated list of xrandr arguments (e.g. "gamma")
- to skip both in detecting changes and applying a profile
---force force (re)loading of a profile
---fingerprint fingerprint your current hardware setup
+--batch run autorandr for all users with active X11 sessions
+--current only list current (active) configuration(s)
--config dump your current xrandr setup
---dry-run don't change anything, only print the xrandr commands
--debug enable verbose output
---batch run autorandr for all users with active X11 sessions
-
- To prevent a profile from being loaded, place a script called "block" in its
- directory. The script is evaluated before the screen setup is inspected, and
- in case of it returning a value of 0 the profile is skipped. This can be used
- to query the status of a docking station you are about to leave.
+--detected only list detected (available) configuration(s)
+--dry-run don't change anything, only print the xrandr commands
+--fingerprint fingerprint your current hardware setup
+--force force (re)loading of a profile
+--skip-options <option> comma separated list of xrandr arguments (e.g. "gamma")
+ to skip both in detecting changes and applying a profile
+--version show version information and exit
If no suitable profile can be identified, the current configuration is kept.
To change this behaviour and switch to a fallback configuration, specify
--default <profile>.
- Another script called "postswitch" can be placed in the directory
- ~/.config/autorandr (or ~/.autorandr if you have an old installation) as well
- as in any profile directories: The scripts are executed after a mode switch
- has taken place and can notify window managers.
+ autorandr supports a set of per-profile and global hooks. See the documentation
+ for details.
The following virtual configurations are available:
""".strip()
@property
def option_vector(self):
"Return the command line parameters for XRandR for this instance"
- return sum([["--%s" % option[0], option[1]] if option[1] else ["--%s" % option[0]] for option in chain((("output", self.output),), sorted(self.options_with_defaults.items()))], [])
+ args = ["--output", self.output]
+ for option, arg in sorted(self.options_with_defaults.items()):
+ args.append("--%s" % option)
+ if arg:
+ args.append(arg)
+ return args
@property
def option_string(self):
"Return the command line parameters in the configuration file format"
- return "\n".join([" ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.filtered_options.items()))])
+ options = ["output %s" % self.output]
+ for option, arg in sorted(self.filtered_options.items()):
+ if arg:
+ options.append("%s %s" % (option, arg))
+ else:
+ options.append(option)
+ return "\n".join(options)
@property
def sort_key(self):
modes = []
if match["modes"]:
- modes = [x.groupdict() for x in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]) if x.group("name")]
+ modes = []
+ for mode_match in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]):
+ if mode_match.group("name"):
+ modes.append(mode_match.groupdict())
if not modes:
raise AutorandrException("Parsing XRandR output failed, couldn't find any display modes", report_bug=True)
options = {}
if not match["connected"]:
edid = None
+ elif match["edid"]:
+ edid = "".join(match["edid"].strip().split())
else:
- edid = "".join(match["edid"].strip().split()) if match["edid"] else "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
+ edid = "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
if not match["width"]:
options["off"] = None
return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
+ if "*" in self.edid:
+ return fnmatch.fnmatch(other.edid, self.edid)
+ elif "*" in other.edid:
+ return fnmatch.fnmatch(self.edid, other.edid)
return self.edid == other.edid
def __ne__(self, other):
if config[output_name].edid is None:
del config[output_name]
- profiles[profile] = {"config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime}
+ profiles[profile] = {
+ "config": config,
+ "path": os.path.join(profile_path, profile),
+ "config-mtime": os.stat(config_name).st_mtime,
+ }
return profiles
option_vector = new_configuration[output].option_vector
if xrandr_version() >= Version("1.3.0"):
- for option in ("transform", "panning"):
+ for option, off_value in (("transform", "none"), ("panning", "0x0")):
if option in current_configuration[output].options:
- auxiliary_changes_pre.append(["--output", output, "--%s" % option, "none"])
+ auxiliary_changes_pre.append(["--output", output, "--%s" % option, off_value])
else:
try:
option_index = option_vector.index("--%s" % option)
"Generate one of the virtual profiles"
configuration = copy.deepcopy(configuration)
if profile_name == "common":
- common_resolution = [set(((mode["width"], mode["height"]) for mode in output_modes)) for output, output_modes in modes.items() if configuration[output].edid]
- common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
+ mode_sets = []
+ for output, output_modes in modes.items():
+ mode_set = set()
+ if configuration[output].edid:
+ for mode in output_modes:
+ mode_set.add((mode["width"], mode["height"]))
+ mode_sets.append(mode_set)
+ common_resolution = reduce(lambda a, b: a & b, mode_sets[1:], mode_sets[0])
common_resolution = sorted(common_resolution, key=lambda a: int(a[0]) * int(a[1]))
if common_resolution:
for output in configuration:
configuration[output].options = {}
if output in modes and configuration[output].edid:
- configuration[output].options["mode"] = [x["name"] for x in sorted(modes[output], key=lambda x: 0 if x["preferred"] else 1) if x["width"] == common_resolution[-1][0] and x["height"] == common_resolution[-1][1]][0]
+ modes_sorted = sorted(modes[output], key=lambda x: 0 if x["preferred"] else 1)
+ modes_filtered = [x for x in modes_sorted if (x["width"], x["height"]) == common_resolution[-1]]
+ mode = modes_filtered[0]
+ configuration[output].options["mode"] = mode['name']
configuration[output].options["pos"] = "0x0"
else:
configuration[output].options["off"] = None
for output in configuration:
configuration[output].options = {}
if output in modes and configuration[output].edid:
- mode = sorted(modes[output], key=lambda a: int(a["width"]) * int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
+ def key(a):
+ score = int(a["width"]) * int(a["height"])
+ if a["preferred"]:
+ score += 10**6
+ return score
+ output_modes = sorted(modes[output], key=key)
+ mode = output_modes[-1]
configuration[output].options["mode"] = mode["name"]
configuration[output].options["rate"] = mode["rate"]
configuration[output].options["pos"] = pos_specifier % shift
else:
configuration[output].options["off"] = None
elif profile_name == "clone-largest":
- biggest_resolution = sorted([output_modes[0] for output, output_modes in modes.items()], key=lambda x: int(x["width"]) * int(x["height"]), reverse=True)[0]
+ modes_unsorted = [output_modes[0] for output, output_modes in modes.items()]
+ modes_sorted = sorted(modes_unsorted, key=lambda x: int(x["width"]) * int(x["height"]), reverse=True)
+ biggest_resolution = modes_sorted[0]
for output in configuration:
configuration[output].options = {}
if output in modes and configuration[output].edid:
- mode = sorted(modes[output], key=lambda a: int(a["width"]) * int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
+ def key(a):
+ score = int(a["width"]) * int(a["height"])
+ if a["preferred"]:
+ score += 10**6
+ return score
+ output_modes = sorted(modes[output], key=key)
+ mode = output_modes[-1]
configuration[output].options["mode"] = mode["name"]
configuration[output].options["rate"] = mode["rate"]
configuration[output].options["pos"] = "0x0"
configuration[output].options["transform"] = "{},0,{},0,{},{},0,0,1".format(scale, mov_x, scale, mov_y)
else:
configuration[output].options["off"] = None
+ elif profile_name == "off":
+ for output in configuration:
+ for key in list(configuration[output].options.keys()):
+ del configuration[output].options[key]
+ configuration[output].options["off"] = None
return configuration
"Print the differences between two profiles for debugging"
if one == another:
return
- print("| Differences between the two profiles:", file=sys.stderr)
+ print("| Differences between the two profiles:")
for output in set(chain.from_iterable((one.keys(), another.keys()))):
if output not in one:
if "off" not in another[output].options:
- print("| Output `%s' is missing from the active configuration" % output, file=sys.stderr)
+ print("| Output `%s' is missing from the active configuration" % output)
elif output not in another:
if "off" not in one[output].options:
- print("| Output `%s' is missing from the new configuration" % output, file=sys.stderr)
+ print("| Output `%s' is missing from the new configuration" % output)
else:
for line in one[output].verbose_diff(another[output]):
- print("| [Output %s] %s" % (output, line), file=sys.stderr)
- print("\\-", file=sys.stderr)
+ print("| [Output %s] %s" % (output, line))
+ print("\\-")
def exit_help():
all_ok = True
env = os.environ.copy()
if meta_information:
- env.update({"AUTORANDR_%s" % str(key).upper(): str(value) for (key, value) in meta_information.items()})
+ for key, value in meta_information.items():
+ env["AUTORANDR_{}".format(key.upper())] = str(value)
# If there are multiple candidates, the XDG spec tells to only use the first one.
ran_scripts = set()
if not os.path.isdir(user_profile_path):
user_profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
- candidate_directories = chain((user_profile_path,), (os.path.join(x, "autorandr") for x in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":")))
+ candidate_directories = [user_profile_path]
+ for config_dir in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":"):
+ candidate_directories.append(os.path.join(config_dir, "autorandr"))
if profile_path:
- candidate_directories = chain((profile_path,), candidate_directories)
+ candidate_directories.append(profile_path)
for folder in candidate_directories:
-
if script_name not in ran_scripts:
script = os.path.join(folder, script_name)
if os.access(script, os.X_OK | os.F_OK):
process_environ = {}
for environ_entry in open(environ_file).read().split("\0"):
- if "=" in environ_entry:
- name, value = environ_entry.split("=", 1)
+ name, sep, value = environ_entry.partition("=")
+ if name and sep:
if name == "DISPLAY" and "." in value:
value = value[:value.find(".")]
process_environ[name] = value
X11_displays_done.add(display)
+def read_config(options, directory):
+ """Parse a configuration config.ini from directory and merge it into
+ the options dictionary"""
+ config = configparser.ConfigParser()
+ config.read(os.path.join(directory, "settings.ini"))
+ if config.has_section("config"):
+ for key, value in config.items("config"):
+ options.setdefault("--%s" % key, value)
+
def main(argv):
try:
opts, args = getopt.getopt(argv[1:], "s:r:l:d:cfh",
["batch", "dry-run", "change", "default=", "save=", "remove=", "load=",
- "force", "fingerprint", "config", "debug", "skip-options=", "help"])
+ "force", "fingerprint", "config", "debug", "skip-options=", "help",
+ "current", "detected", "version"])
except getopt.GetoptError as e:
print("Failed to parse options: {0}.\n"
"Use --help to get usage information.".format(str(e)),
if "-h" in options or "--help" in options:
exit_help()
+ if "--version" in options:
+ print("autorandr " + __version__)
+ sys.exit(0)
+
+ if "--current" in options and "--detected" in options:
+ print("--current and --detected are mutually exclusive.", file=sys.stderr)
+ sys.exit(posix.EX_USAGE)
+
# Batch mode
if "--batch" in options:
if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
if os.path.isdir(system_profile_path):
profiles.update(load_profiles(system_profile_path))
profile_symlinks.update(get_symlinks(system_profile_path))
+ read_config(options, system_profile_path)
# For the user's profiles, prefer the legacy ~/.autorandr if it already exists
# profile_path is also used later on to store configurations
profile_path = os.path.expanduser("~/.autorandr")
if os.path.isdir(profile_path):
profiles.update(load_profiles(profile_path))
profile_symlinks.update(get_symlinks(profile_path))
+ read_config(options, profile_path)
# Sort by descending mtime
profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
except Exception as e:
for profile_name in profiles.keys():
if profile_blocked(os.path.join(profile_path, profile_name), block_script_metadata):
- print("%s (blocked)" % profile_name, file=sys.stderr)
+ if "--current" not in options and "--detected" not in options:
+ print("%s (blocked)" % profile_name)
continue
props = []
if profile_name in detected_profiles:
props.append("(detected)")
if ("-c" in options or "--change" in options) and not load_profile:
load_profile = profile_name
+ elif "--detected" in options:
+ continue
if profile_name in current_profiles:
props.append("(current)")
- print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)), file=sys.stderr)
+ elif "--current" in options:
+ continue
+ if "--current" in options or "--detected" in options:
+ print("%s" % (profile_name, ))
+ else:
+ print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)))
if not configs_are_equal and "--debug" in options and profile_name in detected_profiles:
print_profile_differences(config, profiles[profile_name]["config"])