from functools import reduce
from itertools import chain
-try:
- from packaging.version import Version
-except ModuleNotFoundError:
- from distutils.version import LooseVersion as Version
if sys.version_info.major == 2:
import ConfigParser as configparser
else:
import configparser
-__version__ = "1.12.1"
+__version__ = "1.13.3"
try:
input = raw_input
("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),
("vertical", "Stack all connected outputs vertically at their largest resolution", None),
+ ("horizontal-reverse", "Stack all connected outputs horizontally at their largest resolution in reverse order", None),
+ ("vertical-reverse", "Stack all connected outputs vertically at their largest resolution in reverse order", None),
]
properties = [
""".strip()
+class Version(object):
+ def __init__(self, version):
+ self._version = version
+ self._version_parts = re.split("([0-9]+)", version)
+
+ def __eq__(self, other):
+ return self._version_parts == other._version_parts
+
+ def __lt__(self, other):
+ for my, theirs in zip(self._version_parts, other._version_parts):
+ if my.isnumeric() and theirs.isnumeric():
+ my = int(my)
+ theirs = int(theirs)
+ if my < theirs:
+ return True
+ return len(theirs) > len(my)
+
+ def __ge__(self, other):
+ return not (self < other)
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __le__(self, other):
+ return (self < other) or (self == other)
+
+ def __gt__(self, other):
+ return self >= other and not (self == other)
+
def is_closed_lid(output):
if not re.match(r'(eDP(-?[0-9]\+)*|LVDS(-?[0-9]\+)*)', output):
return False
if xrandr_version() >= Version("1.2"):
options.update(self.XRANDR_12_DEFAULTS)
options.update(self.options)
+ if "set" in self.ignored_options:
+ options = {a: b for a, b in options.items() if not a.startswith("x-prop")}
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}
+ options = {a: b for a, b in self.options.items() if a not in self.ignored_options}
+ if "set" in self.ignored_options:
+ options = {a: b for a, b in options.items() if not a.startswith("x-prop")}
+ return options
@property
def option_vector(self):
def parse_serial_from_edid(self):
self.serial = None
if self.edid:
+ if self.EDID_UNAVAILABLE in self.edid:
+ return
+ if "*" in self.edid:
+ return
# Thx to pyedid project, the following code was
# copied (and modified) from pyedid/__init__py:21 [parse_edid()]
raw = bytes.fromhex(self.edid)
return XrandrOutput(match["output"], edid, options), modes
@classmethod
- def from_config_file(cls, edid_map, configuration):
+ def from_config_file(cls, profile, edid_map, configuration):
"Instanciate an XrandrOutput from the contents of a configuration file"
options = {}
for line in configuration.split("\n"):
if fuzzy_output in fuzzy_edid_map:
edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
elif "off" not in options:
- raise AutorandrException("Failed to find an EDID for output `%s' in setup file, required as `%s' "
- "is not off in config file." % (options["output"], options["output"]))
+ raise AutorandrException("Profile `%s': Failed to find an EDID for output `%s' in setup file, required "
+ "as `%s' is not off in config file." % (profile, options["output"], options["output"]))
output = options["output"]
del options["output"]
buffer = []
for line in chain(open(config_name).readlines(), ["output"]):
if line[:6] == "output" and buffer:
- config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
+ config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(profile, edids, "".join(buffer))
buffer = [line]
else:
buffer.append(line)
fb_dimensions = get_fb_dimensions(new_configuration)
try:
- base_argv += ["--fb", "%dx%d" % fb_dimensions]
+ fb_args = ["--fb", "%dx%d" % fb_dimensions]
except:
# Failed to obtain frame-buffer size. Doesn't matter, xrandr will choose for the user.
- pass
+ fb_args = []
auxiliary_changes_pre = []
disable_outputs = []
if auxiliary_changes_pre:
argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
if call_and_retry(argv, dry_run=dry_run) != 0:
- raise AutorandrException("Command failed: %s" % " ".join(argv))
+ raise AutorandrException("Command failed: %s" % " ".join(map(shlex.quote, argv)))
+
+ # Starting here, fix the frame buffer size
+ # Do not do this earlier, as disabling scaling might temporarily make the framebuffer
+ # dimensions larger than they will finally be.
+ base_argv += fb_args
# Disable unused outputs, but make sure that there always is at least one active screen
disable_keep = 0 if remain_active_count else 1
for index in range(0, len(operations), 2):
argv = base_argv + list(chain.from_iterable(operations[index:index + 2]))
if call_and_retry(argv, dry_run=dry_run) != 0:
- raise AutorandrException("Command failed: %s" % " ".join(argv))
+ raise AutorandrException("Command failed: %s" % " ".join(map(shlex.quote, argv)))
+
+ # Adjust the frame buffer to match (see #319)
+ if fb_args:
+ argv = base_argv
+ if call_and_retry(argv, dry_run=dry_run) != 0:
+ raise AutorandrException("Command failed: %s" % " ".join(map(shlex.quote, argv)))
+
def is_equal_configuration(source_configuration, target_configuration):
configuration[output].options["pos"] = "0x0"
else:
configuration[output].options["off"] = None
- elif profile_name in ("horizontal", "vertical"):
+ elif profile_name in ("horizontal", "vertical", "horizontal-reverse", "vertical-reverse"):
shift = 0
- if profile_name == "horizontal":
+ if profile_name.startswith("horizontal"):
shift_index = "width"
pos_specifier = "%sx0"
else:
shift_index = "height"
pos_specifier = "0x%s"
-
- for output in configuration:
+
+ config_iter = reversed(configuration) if "reverse" in profile_name else iter(configuration)
+
+ for output in config_iter:
configuration[output].options = {}
if output in modes and configuration[output].edid:
def key(a):
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,))
+ except Exception as e:
+ raise AutorandrException("Failed to execute user command: %s. Error: %s" % (script, str(e)))
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):
+ for file_name in sorted(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,))
+ except Exception as e:
+ raise AutorandrException("Failed to execute user command: %s. Error: %s" % (script, str(e)))
ran_scripts.add(check_name)
return all_ok
new_config, _ = parse_xrandr_output(
ignore_lid=ignore_lid,
)
+ if "--skip-options" in options:
+ for output in new_config.values():
+ output.set_ignored_options(skip_options)
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)