#
from __future__ import print_function
-import copy
-import getopt
import binascii
+import copy
+import getopt
import hashlib
import os
+import posix
+import pwd
import re
import subprocess
import sys
-from distutils.version import LooseVersion as Version
+import shutil
+import time
+from collections import OrderedDict
+from distutils.version import LooseVersion as Version
from functools import reduce
from itertools import chain
-from collections import OrderedDict
-
-import posix
+try:
+ input = raw_input
+except NameError:
+ pass
virtual_profiles = [
# (name, description, callback)
("common", "Clone all connected outputs at the largest common resolution", None),
+ ("clone", "Clone all connected outputs with the largest resolution and scaled down in the others", None),
("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
("vertical", "Stack all connected outputs vertically at their largest resolution", None),
]
-h, --help get this small help
-c, --change reload current setup
-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
--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 call "block" in its
+ 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.
while trace.tb_next:
trace = trace.tb_next
self.line = trace.tb_lineno
+ self.file_name = trace.tb_frame.f_code.co_filename
else:
try:
import inspect
- self.line = inspect.currentframe().f_back.f_lineno
+ frame = inspect.currentframe().f_back
+ self.line = frame.f_lineno
+ self.file_name = frame.f_code.co_filename
except:
self.line = None
+ self.file_name = None
self.original_exception = None
+ if os.path.abspath(self.file_name) == os.path.abspath(sys.argv[0]):
+ self.file_name = None
+
def __str__(self):
retval = [ self.message ]
if self.line:
- retval.append(" (line %d)" % self.line)
+ retval.append(" (line %d%s)" % (self.line, ("; %s" % self.file_name) if self.file_name else ""))
if self.original_exception:
retval.append(":\n ")
retval.append(str(self.original_exception).replace("\n", "\n "))
if self.report_bug:
- retval.append("\nThis appears to be a bug. Please help improving autorandr by reporting it upstream."
+ retval.append("\nThis appears to be a bug. Please help improving autorandr by reporting it upstream:"
+ "\nhttps://github.com/phillipberndt/autorandr/issues"
"\nPlease attach the output of `xrandr --verbose` to your bug report if appropriate.")
return "".join(retval)
EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
def __repr__(self):
- return "<%s%s %s>" % (self.output, (" %s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else "", " ".join(self.option_vector))
+ return "<%s%s %s>" % (self.output, self.short_edid, " ".join(self.option_vector))
+
+ @property
+ def short_edid(self):
+ return ("%s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else ""
@property
def options_with_defaults(self):
if xrandr_version() >= Version("1.2"):
options.update(self.XRANDR_12_DEFAULTS)
options.update(self.options)
- return options
+ return { a: b for a, b in options.items() if a not in self.ignored_options }
+
+ @property
+ def filtered_options(self):
+ "Return a dictionary of options without ignored options"
+ return { a: b for a, b in self.options.items() if a not in self.ignored_options }
@property
def option_vector(self):
@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.options.items()))])
+ return "\n".join([ " ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.filtered_options.items()))])
@property
def sort_key(self):
self.output = output
self.edid = edid
self.options = options
+ self.ignored_options = []
self.remove_default_option_values()
+ def set_ignored_options(self, options):
+ "Set a list of xrandr options that are never used (neither when comparing configurations nor when applying them)"
+ self.ignored_options = list(options)
+
def remove_default_option_values(self):
"Remove values from the options dictionary that are superflous"
if "off" in self.options and len(self.options.keys()) > 1:
for line in configuration.split("\n"):
if line:
line = line.split(None, 1)
+ if line and line[0].startswith("#"):
+ continue
options[line[0]] = line[1] if len(line) > 1 else None
edid = None
return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
return self.edid == other.edid
+ def __ne__(self, other):
+ return not (self == other)
+
def __eq__(self, other):
- return self.edid_equals(other) and self.output == other.output and self.options == other.options
+ return self.edid_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
+
+ def verbose_diff(self, other):
+ "Compare to another XrandrOutput and return a list of human readable differences"
+ diffs = []
+ if not self.edid_equals(other):
+ diffs.append("EDID `%s' differs from `%s'" % (self.short_edid, other.short_edid))
+ if self.output != other.output:
+ diffs.append("Output name `%s' differs from `%s'" % (self.output, other.output))
+ if "off" in self.options and "off" not in other.options:
+ diffs.append("The output is disabled currently, but active in the new configuration")
+ elif "off" in other.options and "off" not in self.options:
+ diffs.append("The output is currently enabled, but inactive in the new configuration")
+ else:
+ for name in set(chain.from_iterable((self.options.keys(), other.options.keys()))):
+ if name not in other.options:
+ diffs.append("Option --%s %sis not present in the new configuration" % (name, "(= `%s') " % self.options[name] if self.options[name] else ""))
+ elif name not in self.options:
+ diffs.append("Option --%s (`%s' in the new configuration) is not present currently" % (name, other.options[name]))
+ elif self.options[name] != other.options[name]:
+ diffs.append("Option --%s %sis `%s' in the new configuration" % (name, "(= `%s') " % self.options[name] if self.options[name] else "", other.options[name]))
+ return diffs
def xrandr_version():
"Return the version of XRandR that this system uses"
if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
continue
- edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
+ edids = dict([ x.split() for x in (y.strip() for y in open(setup_name).readlines()) if x and x[0] != "#" ])
config = {}
buffer = []
return profiles
+def get_symlinks(profile_path):
+ "Load all symlinks from a directory"
+
+ symlinks = {}
+ for link in os.listdir(profile_path):
+ file_name = os.path.join(profile_path, link)
+ if os.path.islink(file_name):
+ symlinks[link] = os.readlink(file_name)
+
+ return symlinks
+
def find_profiles(current_config, profiles):
"Find profiles matching the currently connected outputs"
detected_profiles = []
detected_profiles.append(profile_name)
return detected_profiles
-def profile_blocked(profile_path):
- "Check if a profile is blocked"
- script = os.path.join(profile_path, "block")
- if not os.access(script, os.X_OK | os.F_OK):
- return False
- return subprocess.call(script) == 0
+def profile_blocked(profile_path, meta_information=None):
+ """Check if a profile is blocked.
+
+ meta_information is expected to be an dictionary. It will be passed to the block scripts
+ in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
+ """
+ return not exec_scripts(profile_path, "block", meta_information)
def output_configuration(configuration, config):
"Write a configuration file"
except:
return False
+def call_and_retry(*args, **kwargs):
+ """Wrapper around subprocess.call that retries failed calls.
+
+ This function calls subprocess.call and on non-zero exit states,
+ waits a second and then retries once. This mitigates #47,
+ a timing issue with some drivers.
+ """
+ if "dry_run" in kwargs:
+ dry_run = kwargs["dry_run"]
+ del kwargs["dry_run"]
+ else:
+ dry_run = False
+ kwargs_redirected = dict(kwargs)
+ if not dry_run:
+ if hasattr(subprocess, "DEVNULL"):
+ kwargs_redirected["stdout"] = getattr(subprocess, "DEVNULL")
+ else:
+ kwargs_redirected["stdout"] = open(os.devnull, "w")
+ kwargs_redirected["stderr"] = kwargs_redirected["stdout"]
+ retval = subprocess.call(*args, **kwargs_redirected)
+ if retval != 0:
+ time.sleep(1)
+ retval = subprocess.call(*args, **kwargs)
+ return retval
+
def apply_configuration(new_configuration, current_configuration, dry_run=False):
"Apply a configuration"
outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
# the xrandr call fails with an invalid RRSetScreenSize parameter error.
# Update the configuration in 3 passes in that case. (On Haswell graphics,
# at least.)
+ # - Some implementations can not handle --transform at all, so avoid it unless
+ # necessary. (See https://github.com/phillipberndt/autorandr/issues/37)
+ # - Some implementations can not handle --panning without specifying --fb
+ # explicitly, so avoid it unless necessary.
+ # (See https://github.com/phillipberndt/autorandr/issues/72)
auxiliary_changes_pre = []
disable_outputs = []
else:
if "off" not in current_configuration[output].options:
remain_active_count += 1
- enable_outputs.append(new_configuration[output].option_vector)
- if xrandr_version() >= Version("1.3.0") and "transform" in current_configuration[output].options:
- auxiliary_changes_pre.append(["--output", output, "--transform", "none"])
+
+ option_vector = new_configuration[output].option_vector
+ if xrandr_version() >= Version("1.3.0"):
+ for option in ("transform", "panning"):
+ if option in current_configuration[output].options:
+ auxiliary_changes_pre.append(["--output", output, "--%s" % option, "none"])
+ else:
+ try:
+ option_index = option_vector.index("--%s" % option)
+ if option_vector[option_index+1] == XrandrOutput.XRANDR_DEFAULTS[option]:
+ option_vector = option_vector[:option_index] + option_vector[option_index+2:]
+ except ValueError:
+ pass
+
+ enable_outputs.append(option_vector)
# Perform pe-change auxiliary changes
if auxiliary_changes_pre:
argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
- if subprocess.call(argv) != 0:
+ if call_and_retry(argv, dry_run=dry_run) != 0:
raise AutorandrException("Command failed: %s" % " ".join(argv))
# Disable unused outputs, but make sure that there always is at least one active screen
disable_keep = 0 if remain_active_count else 1
if len(disable_outputs) > disable_keep:
- if subprocess.call(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
+ if call_and_retry(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs)), dry_run=dry_run) != 0:
# Disabling the outputs failed. Retry with the next command:
# Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
# This does not occur if simultaneously the primary screen is reset.
operations = disable_outputs + enable_outputs
for index in range(0, len(operations), 2):
argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
- if subprocess.call(argv) != 0:
+ if call_and_retry(argv, dry_run=dry_run) != 0:
raise AutorandrException("Command failed: %s" % " ".join(argv))
+def is_equal_configuration(source_configuration, target_configuration):
+ "Check if all outputs from target are already configured correctly in source"
+ for output in target_configuration.keys():
+ if (output not in source_configuration) or (source_configuration[output] != target_configuration[output]):
+ return False
+ return True
+
def add_unused_outputs(source_configuration, target_configuration):
"Add outputs that are missing in target to target, in 'off' state"
for output_name, output in source_configuration.items():
"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 )) for output in modes.values() ]
+ 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])
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:
+ 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]
configuration[output].options["pos"] = "0x0"
else:
for output in configuration:
configuration[output].options = {}
- if output in modes:
+ 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]
configuration[output].options["mode"] = mode["name"]
configuration[output].options["rate"] = mode["rate"]
shift += int(mode[shift_index])
else:
configuration[output].options["off"] = None
+ elif profile_name == "clone":
+ biggest_width_resolution = sorted([output_modes[0] for output, output_modes in modes.items()], key=lambda x: x["width"], reverse=True)[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]
+ configuration[output].options["mode"] = mode["name"]
+ configuration[output].options["rate"] = mode["rate"]
+ configuration[output].options["pos"] = "0x0"
+ x_scale = float(biggest_width_resolution["width"]) / float(mode["width"])
+ y_scale = float(biggest_width_resolution["height"]) / float(mode["height"])
+ configuration[output].options["scale"] = "{}x{}".format(x_scale, y_scale)
+ else:
+ configuration[output].options["off"] = None
return configuration
+def print_profile_differences(one, another):
+ "Print the differences between two profiles for debugging"
+ if one == another:
+ return
+ print("| Differences between the two profiles:", file=sys.stderr)
+ 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)
+ 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)
+ else:
+ for line in one[output].verbose_diff(another[output]):
+ print("| [Output %s] %s" % (output, line), file=sys.stderr)
+ print ("\\-", file=sys.stderr)
+
def exit_help():
"Print help and exit"
print(help_text)
print(" %-10s %s" % profile[:2])
sys.exit(0)
-def exec_scripts(profile_path, script_name):
- "Run userscripts"
- for script in (os.path.join(profile_path, script_name), os.path.join(os.path.dirname(profile_path), script_name)):
- if os.access(script, os.X_OK | os.F_OK):
- subprocess.call(script)
+def exec_scripts(profile_path, script_name, meta_information=None):
+ """"Run userscripts
+
+ This will run all executables from the profile folder, and global per-user
+ and system-wide configuration folders, named script_name or residing in
+ subdirectories named script_name.d.
+
+ If profile_path is None, only global scripts will be invoked.
+
+ meta_information is expected to be an dictionary. It will be passed to the block scripts
+ in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
+
+ Returns True unless any of the scripts exited with non-zero exit status.
+ """
+ all_ok = True
+ if meta_information:
+ env = os.environ.copy()
+ env.update({ "AUTORANDR_%s" % str(key).upper(): str(value) for (key, value) in meta_information.items() })
+ else:
+ env = os.environ.copy()
+
+ # If there are multiple candidates, the XDG spec tells to only use the first one.
+ ran_scripts = set()
+
+ user_profile_path = os.path.expanduser("~/.autorandr")
+ 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(":")))
+ if profile_path:
+ candidate_directories = chain((profile_path,), candidate_directories)
+
+ 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):
+ try:
+ all_ok &= subprocess.call(script, env=env) != 0
+ except:
+ raise AutorandrException("Failed to execute user command: %s" % (script,))
+ ran_scripts.add(script_name)
+
+ script_folder = os.path.join(folder, "%s.d" % script_name)
+ if os.access(script_folder, os.R_OK | os.X_OK) and os.path.isdir(script_folder):
+ for file_name in os.listdir(script_folder):
+ check_name = "d/%s" % (file_name,)
+ if check_name not in ran_scripts:
+ script = os.path.join(script_folder, file_name)
+ if os.access(script, os.X_OK | os.F_OK):
+ try:
+ all_ok &= subprocess.call(script, env=env) != 0
+ except:
+ raise AutorandrException("Failed to execute user command: %s" % (script,))
+ ran_scripts.add(check_name)
+
+ return all_ok
+
+def dispatch_call_to_sessions(argv):
+ """Invoke autorandr for each open local X11 session with the given options.
+
+ The function iterates over all processes not owned by root and checks
+ whether they have a DISPLAY variable set. It strips the screen from any
+ variable it finds (i.e. :0.0 becomes :0) and checks whether this display
+ has been handled already. If it has not, it forks, changes uid/gid to
+ the user owning the process, reuses the process's environment and runs
+ autorandr with the parameters from argv.
+
+ This function requires root permissions. It only works for X11 servers that
+ have at least one non-root process running. It is susceptible for attacks
+ where one user runs a process with another user's DISPLAY variable - in
+ this case, it might happen that autorandr is invoked for the other user,
+ which won't work. Since no other harm than prevention of automated
+ execution of autorandr can be done this way, the assumption is that in this
+ situation, the local administrator will handle the situation."""
+ X11_displays_done = set()
+
+ autorandr_binary = os.path.abspath(argv[0])
+
+ for directory in os.listdir("/proc"):
+ directory = os.path.join("/proc/", directory)
+ if not os.path.isdir(directory):
+ continue
+ environ_file = os.path.join(directory, "environ")
+ if not os.path.isfile(environ_file):
+ continue
+ uid = os.stat(environ_file).st_uid
+
+ # The following line assumes that user accounts start at 1000 and that
+ # no one works using the root or another system account. This is rather
+ # restrictive, but de facto default. Alternatives would be to use the
+ # UID_MIN from /etc/login.defs or FIRST_UID from /etc/adduser.conf;
+ # but effectively, both values aren't binding in any way.
+ # If this breaks your use case, please file a bug on Github.
+ if uid < 1000:
+ continue
+
+ process_environ = {}
+ for environ_entry in open(environ_file).read().split("\0"):
+ if "=" in environ_entry:
+ name, value = environ_entry.split("=", 1)
+ if name == "DISPLAY" and "." in value:
+ value = value[:value.find(".")]
+ process_environ[name] = value
+ display = process_environ["DISPLAY"] if "DISPLAY" in process_environ else None
+
+ # To allow scripts to detect batch invocation (especially useful for predetect)
+ process_environ["AUTORANDR_BATCH_PID"] = str(os.getpid())
+
+ if display and display not in X11_displays_done:
+ try:
+ pwent = pwd.getpwuid(uid)
+ except KeyError:
+ # User has no pwd entry
+ continue
+
+ print("Running autorandr as %s for display %s" % (pwent.pw_name, display))
+ child_pid = os.fork()
+ if child_pid == 0:
+ # This will throw an exception if any of the privilege changes fails,
+ # so it should be safe. Also, note that since the environment
+ # is taken from a process owned by the user, reusing it should
+ # not leak any information.
+ os.setgroups([])
+ os.setresgid(pwent.pw_gid, pwent.pw_gid, pwent.pw_gid)
+ os.setresuid(pwent.pw_uid, pwent.pw_uid, pwent.pw_uid)
+ os.chdir(pwent.pw_dir)
+ os.environ.clear()
+ os.environ.update(process_environ)
+ os.execl(autorandr_binary, autorandr_binary, *argv[1:])
+ os.exit(1)
+ os.waitpid(child_pid, 0)
+
+ X11_displays_done.add(display)
def main(argv):
try:
- options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "help" ])[0])
+ options = dict(getopt.getopt(argv[1:], "s:r:l:d:cfh", [ "batch", "dry-run", "change", "default=", "save=", "remove=", "load=", "force", "fingerprint", "config", "debug", "skip-options=", "help" ])[0])
except getopt.GetoptError as e:
print("Failed to parse options: {0}.\n"
"Use --help to get usage information.".format(str(e)),
file=sys.stderr)
sys.exit(posix.EX_USAGE)
+ if "-h" in options or "--help" in options:
+ exit_help()
+
+ # Batch mode
+ if "--batch" in options:
+ if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
+ dispatch_call_to_sessions([ x for x in argv if x != "--batch" ])
+ else:
+ print("--batch mode can only be used by root and if $DISPLAY is unset")
+ return
+
profiles = {}
+ profile_symlinks = {}
try:
# Load profiles from each XDG config directory
- for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
+ # The XDG spec says that earlier entries should take precedence, so reverse the order
+ for directory in reversed(os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":")):
system_profile_path = os.path.join(directory, "autorandr")
if os.path.isdir(system_profile_path):
profiles.update(load_profiles(system_profile_path))
+ profile_symlinks.update(get_symlinks(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")
profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
if os.path.isdir(profile_path):
profiles.update(load_profiles(profile_path))
+ profile_symlinks.update(get_symlinks(profile_path))
# Sort by descending mtime
profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
except Exception as e:
raise AutorandrException("Failed to load profiles", e)
+ 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 }
+
+ exec_scripts(None, "predetect")
config, modes = parse_xrandr_output()
if "--fingerprint" in options:
output_configuration(config, sys.stdout)
sys.exit(0)
+ if "--skip-options" in options:
+ skip_options = [ y[2:] if y[:2] == "--" else y for y in ( x.strip() for x in options["--skip-options"].split(",") ) ]
+ for profile in profiles.values():
+ for output in profile["config"].values():
+ output.set_ignored_options(skip_options)
+ for output in config.values():
+ output.set_ignored_options(skip_options)
+
if "-s" in options:
options["--save"] = options["-s"]
if "--save" in options:
if options["--save"] in ( x[0] for x in virtual_profiles ):
raise AutorandrException("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
try:
- save_configuration(os.path.join(profile_path, options["--save"]), config)
+ profile_folder = os.path.join(profile_path, options["--save"])
+ save_configuration(profile_folder, config)
+ exec_scripts(profile_folder, "postsave", {"CURRENT_PROFILE": options["--save"], "PROFILE_FOLDER": profile_folder})
except Exception as e:
raise AutorandrException("Failed to save current configuration as profile '%s'" % (options["--save"],), e)
print("Saved current configuration as profile '%s'" % options["--save"])
sys.exit(0)
- if "-h" in options or "--help" in options:
- exit_help()
+ if "-r" in options:
+ options["--remove"] = options["-r"]
+ if "--remove" in options:
+ if options["--remove"] in ( x[0] for x in virtual_profiles ):
+ raise AutorandrException("Cannot remove profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--remove"])
+ if options["--remove"] not in profiles.keys():
+ raise AutorandrException("Cannot remove profile '%s':\nThis profile does not exist." % options["--remove"])
+ try:
+ remove = True
+ profile_folder = os.path.join(profile_path, options["--remove"])
+ profile_dirlist = os.listdir(profile_folder)
+ profile_dirlist.remove("config")
+ profile_dirlist.remove("setup")
+ if profile_dirlist:
+ print("Profile folder '%s' contains the following additional files:\n---\n%s\n---" % (options["--remove"], "\n".join(profile_dirlist)))
+ response = input("Do you really want to remove profile '%s'? If so, type 'yes': " % options["--remove"]).strip()
+ if response != "yes":
+ remove = False
+ if remove is True:
+ shutil.rmtree(profile_folder)
+ print("Removed profile '%s'" % options["--remove"])
+ else:
+ print("Profile '%s' was not removed" % options["--remove"])
+ except Exception as e:
+ raise AutorandrException("Failed to remove profile '%s'" % (options["--remove"],), e)
+ sys.exit(0)
detected_profiles = find_profiles(config, profiles)
load_profile = False
if "--load" in options:
load_profile = options["--load"]
else:
+ # Find the active profile(s) first, for the block script (See #42)
+ current_profiles = []
for profile_name in profiles.keys():
- if profile_blocked(os.path.join(profile_path, profile_name)):
+ configs_are_equal = is_equal_configuration(config, profiles[profile_name]["config"])
+ if configs_are_equal:
+ current_profiles.append(profile_name)
+ block_script_metadata = {
+ "CURRENT_PROFILE": "".join(current_profiles[:1]),
+ "CURRENT_PROFILES": ":".join(current_profiles)
+ }
+
+ 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)
continue
+ props = []
if profile_name in detected_profiles:
- print("%s (detected)" % profile_name, file=sys.stderr)
+ props.append("(detected)")
if ("-c" in options or "--change" in options) and not load_profile:
load_profile = profile_name
- else:
- print(profile_name, file=sys.stderr)
+ if profile_name in current_profiles:
+ props.append("(current)")
+ print("%s%s%s" % (profile_name, " " if props else "", " ".join(props)), file=sys.stderr)
+ if not configs_are_equal and "--debug" in options and profile_name in detected_profiles:
+ print_profile_differences(config, profiles[profile_name]["config"])
if "-d" in options:
options["--default"] = options["-d"]
load_profile = options["--default"]
if load_profile:
+ if load_profile in profile_symlinks:
+ if "--debug" in options:
+ print("'%s' symlinked to '%s'" % (load_profile, profile_symlinks[load_profile]))
+ load_profile = profile_symlinks[load_profile]
+
if load_profile in ( x[0] for x in virtual_profiles ):
load_config = generate_virtual_profile(config, modes, load_profile)
scripts_path = os.path.join(profile_path, load_profile)
if load_config == dict(config) and not "-f" in options and not "--force" in options:
print("Config already loaded", file=sys.stderr)
sys.exit(0)
+ if "--debug" in options and load_config != dict(config):
+ print("Loading profile '%s'" % load_profile)
+ print_profile_differences(config, load_config)
+
remove_irrelevant_outputs(config, load_config)
try:
if "--dry-run" in options:
apply_configuration(load_config, config, True)
else:
- exec_scripts(scripts_path, "preswitch")
+ script_metadata = {
+ "CURRENT_PROFILE": load_profile,
+ "PROFILE_FOLDER": scripts_path,
+ }
+ exec_scripts(scripts_path, "preswitch", script_metadata)
+ if "--debug" in options:
+ print("Going to run:")
+ apply_configuration(load_config, config, True)
apply_configuration(load_config, config, False)
- exec_scripts(scripts_path, "postswitch")
+ exec_scripts(scripts_path, "postswitch", script_metadata)
+ except AutorandrException as e:
+ raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, e.report_bug)
except Exception as e:
raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
+ if "--dry-run" not in options and "--debug" in options:
+ new_config, _ = parse_xrandr_output()
+ if not is_equal_configuration(new_config, load_config):
+ print("The configuration change did not go as expected:")
+ print_profile_differences(new_config, load_config)
+
sys.exit(0)
-if __name__ == '__main__':
+def exception_handled_main(argv=sys.argv):
try:
main(sys.argv)
except AutorandrException as e:
print("Exception: {0}".format(e.__class__.__name__))
sys.exit(2)
- print("Unhandled exception ({0}). Please report this as a bug.".format(e), file=sys.stderr)
+ print("Unhandled exception ({0}). Please report this as a bug at https://github.com/phillipberndt/autorandr/issues.".format(e), file=sys.stderr)
raise
+
+if __name__ == '__main__':
+ exception_handled_main()