import re
import subprocess
import sys
+import shutil
+import time
from collections import OrderedDict
from distutils.version import LooseVersion as Version
from functools import reduce
from itertools import chain
+try:
+ input = raw_input
+except NameError:
+ pass
virtual_profiles = [
# (name, description, callback)
-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")
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)
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.
+ """
+ kwargs_redirected = dict(kwargs)
+ 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)
# 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) != 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))) != 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) != 0:
raise AutorandrException("Command failed: %s" % " ".join(argv))
def is_equal_configuration(source_configuration, target_configuration):
else:
env = os.environ.copy()
- for folder in chain((profile_path, os.path.dirname(profile_path)),
- (os.path.join(x, "autorandr") for x in os.environ.get("XDG_CONFIG_DIRS", "").split(":"))):
- script = os.path.join(folder, script_name)
- if os.access(script, os.X_OK | os.F_OK):
- all_ok &= subprocess.call(script, env=env) != 0
+ # 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")
+
+ for folder in chain((profile_path, os.path.dirname(profile_path), user_profile_path),
+ (os.path.join(x, "autorandr") for x in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":"))):
+
+ if script_name not in ran_scripts:
+ script = os.path.join(folder, script_name)
+ if os.access(script, os.X_OK | os.F_OK):
+ all_ok &= subprocess.call(script, env=env) != 0
+ 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):
- script = os.path.join(script_folder, file_name)
- if os.access(script, os.X_OK | os.F_OK):
- all_ok &= subprocess.call(script, env=env) != 0
+ 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):
+ all_ok &= subprocess.call(script, env=env) != 0
+ ran_scripts.add(check_name)
return all_ok
def main(argv):
try:
- options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "debug", "skip-options=", "help" ])[0])
+ options = dict(getopt.getopt(argv[1:], "s:r:l:d:cfh", [ "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)),
profiles = {}
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))
print("Saved current configuration as profile '%s'" % options["--save"])
sys.exit(0)
+ 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)
+
if "-h" in options or "--help" in options:
exit_help()
"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", script_metadata)
except Exception 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