import copy
import getopt
import hashlib
+import math
import os
import posix
import pwd
("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 = [
Gamma: (?P<gamma>(?:inf|-?[0-9\.\-: e])+) | # Gamma value
CRTC:\s*(?P<crtc>[0-9]) | # CRTC value
Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) | # Transformation matrix
+ filter:\s+(?P<filter>bilinear|nearest) | # Transformation filter
EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) | # EDID of the output
""" + XRANDR_PROPERTIES_REGEXP + """ | # Properties to include in the profile
(?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)* # Other properties
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
# Thx to pyedid project, the following code was
# copied (and modified) from pyedid/__init__py:21 [parse_edid()]
raw = bytes.fromhex(self.edid)
# I doubt that this special case is actually required.
print("Warning: Output %s has a transformation applied. Could not determine correct mode! "
"Using `%s'." % (match["output"], options["mode"]), file=sys.stderr)
+ if match["filter"]:
+ options["filter"] = match["filter"]
if match["gamma"]:
gamma = match["gamma"].strip()
# xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
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)
o_height = int(detail.get("h")) + int(detail.get("y"))
width = max(width, o_width)
height = max(height, o_height)
- return int(width), int(height)
+ return math.ceil(width), math.ceil(height)
def apply_configuration(new_configuration, current_configuration, dry_run=False):
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)))
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":
shift_index = "width"
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):
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)
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)