To change this behaviour and switch to a fallback configuration, specify
--default <profile>.
- Another script called "postswitch "can be placed in the directory
- ~/.autorandr as well as in any profile directories: The scripts are executed
- after a mode switch has taken place and can notify window managers.
+ 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.
The following virtual configurations are available:
""".strip()
)? # .. but everything of the above only if the screen is in use.
).*
(?:\s*(?: # Properties of the output
- Gamma: (?P<gamma>[0-9\.:\s]+) | # Gamma value
- Transform: (?P<transform>[0-9\.\s]+) | # Transformation matrix
- EDID: (?P<edid>[0-9a-f\s]+) | # EDID of the output
+ Gamma: (?P<gamma>[0-9\.: ]+) | # Gamma value
+ Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) | # Transformation matrix
+ EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) | # EDID of the output
(?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)* # Other properties
))+
\s*
"gamma": "1.0:1.0:1.0",
}
- XRANDR_DEFAULTS = dict(XRANDR_13_DEFAULTS.items() + XRANDR_12_DEFAULTS.items())
+ XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
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))
This method also returns a list of modes supported by the output.
"""
try:
+ xrandr_output = xrandr_output.replace("\r\n", "\n")
match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
except:
raise RuntimeError("Parsing XRandR output failed, there is an error in the regular expression.")
raise RuntimeError("Parsing XRandR output failed, the regular expression did not match: %s" % debug)
remainder = xrandr_output[len(match_object.group(0)):]
if remainder:
- raise RuntimeError(("Parsing XRandR output failed, %d bytes left unmatched after regular expression,"
- "starting with ..'%s'.") % (len(remainder), remainder[:10]))
-
+ raise RuntimeError(("Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
+ "starting at byte %d with ..'%s'.") % (len(remainder), len(len(match_object.group(0))), remainder[:10]))
match = match_object.groupdict()
if config[output_name].edid is None:
del config[output_name]
- profiles[profile] = config
+ profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
return profiles
-def find_profile(current_config, profiles):
- "Find a profile matching the currently connected outputs"
+def find_profiles(current_config, profiles):
+ "Find profiles matching the currently connected outputs"
+ detected_profiles = []
for profile_name, profile in profiles.items():
+ config = profile["config"]
matches = True
- for name, output in profile.items():
+ for name, output in config.items():
if not output.edid:
continue
if name not in current_config or not output.edid_equals(current_config[name]):
matches = False
break
- if not matches or any(( name not in profile.keys() for name in current_config.keys() if current_config[name].edid )):
+ if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
continue
if matches:
- return profile_name
+ detected_profiles.append(profile_name)
+ return detected_profiles
def profile_blocked(profile_path):
"Check if a profile is blocked"
with open(os.path.join(profile_path, "setup"), "w") as setup:
output_setup(configuration, setup)
+def update_mtime(filename):
+ "Update a file's mtime"
+ try:
+ os.utime(filename, None)
+ return True
+ except:
+ return False
+
def apply_configuration(configuration, dry_run=False):
"Apply a configuration"
outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
# Disable all unused outputs
argv = base_argv[:]
+ disable_argv = []
for output in outputs:
if not configuration[output].edid or "off" in configuration[output].options:
- argv += configuration[output].option_vector
- if argv != base_argv:
- if subprocess.call(argv) != 0:
- return False
+ disable_argv += configuration[output].option_vector
+ if disable_argv:
+ if subprocess.call(base_argv + disable_argv) != 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.
+ pass
+ else:
+ disable_argv = []
# Enable remaining outputs in pairs of two
remaining_outputs = [ x for x in outputs if configuration[x].edid ]
for index in range(0, len(remaining_outputs), 2):
- if subprocess.call((base_argv[:] + configuration[remaining_outputs[index]].option_vector + (configuration[remaining_outputs[index + 1]].option_vector if index < len(remaining_outputs) - 1 else []))) != 0:
- return False
+ argv = base_argv[:]
+ if disable_argv:
+ argv += disable_argv
+ disable_argv = []
+ argv += configuration[remaining_outputs[index]].option_vector + (configuration[remaining_outputs[index + 1]].option_vector if index < len(remaining_outputs) - 1 else [])
+ if subprocess.call(argv) != 0:
+ raise RuntimeError("Command failed: %s" % " ".join(argv))
def add_unused_outputs(source_configuration, target_configuration):
"Add outputs that are missing in target to target, in 'off' state"
print(str(e))
options = { "--help": True }
- profile_path = os.path.expanduser("~/.autorandr")
-
+ profiles = {}
try:
- profiles = load_profiles(profile_path)
- except OSError as e:
- if e.errno == 2: # No such file or directory
- profiles = {}
- else:
- raise e
+ # Load profiles from each XDG config directory
+ for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
+ system_profile_path = os.path.join(directory, "autorandr")
+ if os.path.isdir(system_profile_path):
+ profiles.update(load_profiles(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 not os.path.isdir(profile_path):
+ # Elsewise, follow the XDG specification
+ 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))
+ # Sort by descending mtime
+ profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
except Exception as e:
print("Failed to load profiles:\n%s" % str(e), file=sys.stderr)
sys.exit(1)
if "-h" in options or "--help" in options:
exit_help()
- detected_profile = find_profile(config, profiles)
+ detected_profiles = find_profiles(config, profiles)
load_profile = False
if "-l" in options:
if profile_blocked(os.path.join(profile_path, profile_name)):
print("%s (blocked)" % profile_name, file=sys.stderr)
continue
- if detected_profile == profile_name:
+ if profile_name in detected_profiles:
print("%s (detected)" % profile_name, file=sys.stderr)
- if "-c" in options or "--change" in options:
- load_profile = detected_profile
+ if ("-c" in options or "--change" in options) and not load_profile:
+ load_profile = profile_name
else:
print(profile_name, file=sys.stderr)
if load_profile:
if load_profile in ( x[0] for x in virtual_profiles ):
- profile = generate_virtual_profile(config, modes, load_profile)
+ load_config = generate_virtual_profile(config, modes, load_profile)
+ scripts_path = os.path.join(profile_path, load_profile)
else:
try:
profile = profiles[load_profile]
+ load_config = profile["config"]
+ scripts_path = profile["path"]
except KeyError:
print("Failed to load profile '%s':\nProfile not found" % load_profile, file=sys.stderr)
sys.exit(1)
- add_unused_outputs(config, profile)
- if profile == dict(config) and not "-f" in options and not "--force" in options:
+ if load_profile in detected_profiles and detected_profiles[0] != load_profile:
+ update_mtime(os.path.join(scripts_path, "config"))
+ add_unused_outputs(config, load_config)
+ 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)
try:
if "--dry-run" in options:
- apply_configuration(profile, True)
+ apply_configuration(load_config, True)
else:
- exec_scripts(os.path.join(profile_path, load_profile), "preswitch")
- apply_configuration(profile, False)
- exec_scripts(os.path.join(profile_path, load_profile), "postswitch")
+ exec_scripts(scripts_path, "preswitch")
+ apply_configuration(load_config, False)
+ exec_scripts(scripts_path, "postswitch")
except Exception as e:
print("Failed to apply profile '%s':\n%s" % (load_profile, str(e)), file=sys.stderr)
sys.exit(1)