]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/commitdiff
New upstream version 1.13.3 upstream/1.13.3
authorDon Armstrong <don@donarmstrong.com>
Fri, 28 Apr 2023 23:25:30 +0000 (16:25 -0700)
committerDon Armstrong <don@donarmstrong.com>
Fri, 28 Apr 2023 23:25:30 +0000 (16:25 -0700)
Makefile
README.md
autorandr.1
autorandr.py
caca [deleted file]
contrib/packaging/debian/debian/control
contrib/packaging/debian/make_deb.sh
contrib/packaging/rpm/autorandr.spec
setup.py

index c53510b8075e0f68673b80f867e03ff46b2dbc6d..e184faaf8ffa288ffe1726b3a2f65a0cb9735cad 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -40,7 +40,8 @@ all:
 DEFAULT_TARGETS=autorandr
 
 install_autorandr:
-       install -D -m 755 autorandr.py ${DESTDIR}${PREFIX}/bin/autorandr
+       mkdir -p ${DESTDIR}${PREFIX}/bin
+       install -m 755 autorandr.py ${DESTDIR}${PREFIX}/bin/autorandr
 
 uninstall_autorandr:
        rm -f ${DESTDIR}${PREFIX}/bin/autorandr
@@ -52,7 +53,8 @@ DEFAULT_TARGETS+=bash_completion
 endif
 
 install_bash_completion:
-       install -D -m 644 contrib/bash_completion/autorandr ${DESTDIR}/${BASH_COMPLETIONS_DIR}/autorandr
+       mkdir -p ${DESTDIR}/${BASH_COMPLETIONS_DIR}
+       install -m 644 contrib/bash_completion/autorandr ${DESTDIR}/${BASH_COMPLETIONS_DIR}/autorandr
 
 uninstall_bash_completion:
        rm -f ${DESTDIR}/${BASH_COMPLETIONS_DIR}/autorandr
@@ -62,13 +64,19 @@ XDG_AUTOSTART_DIR=/etc/xdg/autostart
 DEFAULT_TARGETS+=autostart_config
 
 install_autostart_config:
-       install -D -m 644 contrib/etc/xdg/autostart/autorandr.desktop ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr.desktop
+       mkdir -p ${DESTDIR}/${XDG_AUTOSTART_DIR}
+       install -m 644 contrib/etc/xdg/autostart/autorandr.desktop ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr.desktop
+       # KDE-specific autostart (workaround for https://github.com/systemd/systemd/issues/18791)
+       install -m 644 contrib/etc/xdg/autostart/autorandr.desktop ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr-kde.desktop
+       desktop-file-edit --remove-key=X-GNOME-Autostart-Phase --add-only-show-in=KDE ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr-kde.desktop
+
 ifneq ($(PREFIX),/usr/)
        sed -i -re 's#/usr/bin/autorandr#$(subst #,\#,${PREFIX})/bin/autorandr#g' ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr.desktop
 endif
 
 uninstall_autostart_config:
        rm -f ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr.desktop
+       rm -f ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr-kde.desktop
 
 # Rules for systemd
 SYSTEMD_UNIT_DIR:=$(shell pkg-config --variable=systemdsystemunitdir systemd 2>/dev/null)
@@ -78,19 +86,23 @@ endif
 
 install_systemd:
        $(if $(SYSTEMD_UNIT_DIR),,$(error SYSTEMD_UNIT_DIR is not defined))
-       install -D -m 644 contrib/systemd/autorandr.service ${DESTDIR}/${SYSTEMD_UNIT_DIR}/autorandr.service
+       mkdir -p ${DESTDIR}/${SYSTEMD_UNIT_DIR}
+       install -m 644 contrib/systemd/autorandr.service ${DESTDIR}/${SYSTEMD_UNIT_DIR}/autorandr.service
+       install -m 644 contrib/systemd/autorandr-lid-listener.service ${DESTDIR}/${SYSTEMD_UNIT_DIR}/autorandr-lid-listener.service
 ifneq ($(PREFIX),/usr/)
        sed -i -re 's#/usr/bin/autorandr#$(subst #,\#,${PREFIX})/bin/autorandr#g' ${DESTDIR}/${SYSTEMD_UNIT_DIR}/autorandr.service
 endif
        @echo
-       @echo "To activate the systemd unit, run this command as root:"
+       @echo "To activate the systemd units, run this command as root:"
        @echo "    systemctl daemon-reload"
        @echo "    systemctl enable autorandr.service"
+       @echo "    systemctl enable autorandr-lid-listener.service"
        @echo
 
 uninstall_systemd:
        $(if $(SYSTEMD_UNIT_DIR),,$(error SYSTEMD_UNIT_DIR is not defined))
        rm -f ${DESTDIR}/${SYSTEMD_UNIT_DIR}/autorandr.service
+       rm -f ${DESTDIR}/${SYSTEMD_UNIT_DIR}/autorandr-lid-listener.service
 
 # Rules for pmutils
 PM_SLEEPHOOKS_DIR:=$(shell pkg-config --variable=pm_sleephooks pm-utils 2>/dev/null)
@@ -102,7 +114,8 @@ endif
 
 install_pmutils:
        $(if $(PM_SLEEPHOOKS_DIR),,$(error PM_SLEEPHOOKS_DIR is not defined))
-       install -D -m 755 contrib/pm-utils/40autorandr ${DESTDIR}/${PM_SLEEPHOOKS_DIR}/40autorandr
+       mkdir -p ${DESTDIR}/${PM_SLEEPHOOKS_DIR}
+       install -m 755 contrib/pm-utils/40autorandr ${DESTDIR}/${PM_SLEEPHOOKS_DIR}/40autorandr
 ifneq ($(PREFIX),/usr/)
        sed -i -re 's#/usr/bin/autorandr#$(subst #,\#,${PREFIX})/bin/autorandr#g' ${DESTDIR}/${PM_SLEEPHOOKS_DIR}/40autorandr
 endif
@@ -121,7 +134,7 @@ endif
 install_udev:
        $(if $(UDEV_RULES_DIR),,$(error UDEV_RULES_DIR is not defined))
        mkdir -p ${DESTDIR}/${UDEV_RULES_DIR}/
-       echo 'ACTION=="change", SUBSYSTEM=="drm", RUN+="$(if $(findstring systemd, $(TARGETS)),/bin/systemctl start --no-block autorandr.service,${PREFIX}/bin/autorandr --batch --change --default default)"' > ${DESTDIR}/${UDEV_RULES_DIR}/40-monitor-hotplug.rules
+       echo 'ACTION=="change", SUBSYSTEM=="drm", RUN+="$(if $(findstring systemd, $(MAKECMDGOALS)),/bin/systemctl start --no-block autorandr.service,${PREFIX}/bin/autorandr --batch --change --default default)"' > ${DESTDIR}/${UDEV_RULES_DIR}/40-monitor-hotplug.rules
        @echo
        @echo "To activate the udev rules, run this command as root:"
        @echo "    udevadm control --reload-rules"
@@ -151,8 +164,10 @@ contrib/autorandr_launcher/autorandr-launcher: contrib/autorandr_launcher/autora
        $(CC) $(CFLAGS) $(LAUNCHER_CFLAGS) -o $@ $+ $(LDFLAGS) $(LAUNCHER_LDLIBS) $(LDLIBS)
 
 install_launcher: contrib/autorandr_launcher/autorandr-launcher
-       install -D -m 755 contrib/autorandr_launcher/autorandr-launcher ${DESTDIR}${PREFIX}/bin/autorandr-launcher
-       install -D -m 644 contrib/etc/xdg/autostart/autorandr-launcher.desktop ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr-launcher.desktop
+       mkdir -p ${DESTDIR}${PREFIX}/bin
+       install -m 755 contrib/autorandr_launcher/autorandr-launcher ${DESTDIR}${PREFIX}/bin/autorandr-launcher
+       mkdir -p ${DESTDIR}/${XDG_AUTOSTART_DIR}
+       install -m 644 contrib/etc/xdg/autostart/autorandr-launcher.desktop ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr-launcher.desktop
 ifneq ($(PREFIX),/usr/)
        sed -i -re 's#/usr/bin/autorandr-launcher#$(subst #,\#,${PREFIX})/bin/autorandr-launcher#g' ${DESTDIR}/${XDG_AUTOSTART_DIR}/autorandr-launcher.desktop
 endif
index 7d320a174605e3c93340fbf958f09771487f4976..865091d738f9cd5241845f5b84ec06dd805117d9 100644 (file)
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ Contributors to this version of autorandr are:
 
 * Adrián López
 * andersonjacob
+* Alexander Lochmann
 * Alexander Wirt
 * Brice Waegeneire
 * Chris Dunder
@@ -58,6 +59,7 @@ Contributors to this version of autorandr are:
 * Tomasz Bogdal
 * Victor Häggqvist
 * Jan-Oliver Kaiser
+* Alexandre Viau
 
 ## Installation/removal
 
@@ -73,10 +75,14 @@ you can
 
 * Use the [official Arch package](https://www.archlinux.org/packages/community/any/autorandr/).
 * Use the [official Debian package](https://packages.debian.org/sid/x11/autorandr) on sid
-* Use the [ebuild from zugaina](https://gpo.zugaina.org/x11-misc/autorandr) on Gentoo.
+* Use the [FreeBSD Ports Collection](https://www.freshports.org/x11/autorandr/) on FreeBSD.
+* Use the [official Gentoo package](https://packages.gentoo.org/packages/x11-misc/autorandr).
 * Use the
   [nix package](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/misc/autorandr.nix)
   on NixOS.
+* Use the
+  [guix package](https://git.savannah.gnu.org/cgit/guix.git/log/gnu/packages/xdisorg.scm?qt=grep&q=autorandr)
+  on Guix.
 * Use the [SlackBuild](https://slackbuilds.org/repository/14.2/desktop/autorandr/) on Slackware.
 * Use the automated nightlies generated by the
   [openSUSE build service](https://build.opensuse.org/package/show/home:phillipberndt/autorandr)
@@ -183,6 +189,8 @@ where they will only be executed on changes regarding that specific profile.
 
 Instead (or in addition) to these scripts, you can also place as many executable
 files as you like in subdirectories called `script_name.d` (e.g. `postswitch.d`).
+The order of execution of scripts in these directories is by file name, you can
+force a certain ordering by naming them `10-wallpaper`, `20-restart-wm`, etc.
 
 If a script with the same name occurs multiple times, user configuration
 takes precedence over system configuration (as specified by the
@@ -249,6 +257,24 @@ options nvidia_drm modeset=1
 
 ## Changelog
 
+**autorandr 1.13.3**
+* *2023-01-24* Revert udev rule to rely on "change" event (see #324)
+
+**autorandr 1.13.2**
+* *2023-01-23* Fix autostart in KDE (see #320)
+* *2023-01-23* Match add/remove rather than change in udev rule (see #321)
+* *2023-01-23* Fix wildcard use in EDIDs (see #322)
+* *2023-01-23* Do a final xrandr call to set the frame buffer size (see #319)
+
+**autorandr 1.13.1**
+* *2023-01-16* Fix bug with Version comparison
+
+**autorandr 1.13**
+* *2023-01-15* Add reversed horizontal/vertical profiles
+* *2023-01-15* Fix distutils deprecation warning
+* *2023-01-15* Print error when user script fails
+* *2022-12-01* Support `--skip-options set` to skip setting properties
+
 **autorandr 1.12.1**
 * *2021-12-22* Fix `--match-edid` (see #273)
 
index d20264b920e3963db2fb3d21a95d9e558f297608..776b3ef94d79d5b8092a0f78a762bcc4f5dc14d8 100644 (file)
@@ -56,6 +56,9 @@ Fingerprint the current hardware setup
 .BR \-\-match-edid
 Match displays based on edid instead of name
 .TP
+.BR \-\-ignore-lid
+By default, closed lids are considered as disconnected if other outputs are detected. This flag disables this behaviour.
+.TP
 .BR \-\-force
 Force loading or reloading of a profile
 .TP
@@ -78,6 +81,53 @@ Configuration files are searched for in the \fIautorandr
 In each of those directories it looks for directories with \fIconfig\fR and
 \fIsetup\fR in them.  It is best to manage these files with the
 \fBautorandr\fR utility.
+
+.SH DEFAULT OPTIONS
+
+You can store default values for any option in an INI-file located at
+\fI~/.config/autorandr/settings.ini\fR. In a config section, you may
+place any default values in the form \fIoption-name=option-argument\fR.
+
+.SH HOOK SCRIPTS
+
+Three more scripts can be placed in the configuration directory:
+.TP
+\fIpostswitch\fR
+Executed after a mode switch has taken place. This can be used to notify
+window managers or other applications about the switch.
+.TP
+\fIpreswitch\fR
+Executed before a mode switch takes place.
+.TP
+\fIpostsave\fR
+Executed after a profile was stored or altered.
+.TP
+\fIpredetect\fR
+Executed before autorandr attempts to run xrandr.
+
+.PP
+These scripts must be executable and can be placed directly in the
+configuration directory, where they will always be executed, or in
+the profile subdirectories, where they will only be executed on changes
+regarding that specific profile.
+
+Instead (or in addition) to these scripts, you can also place as many
+executable files as you like in subdirectories called script_name.d
+(e.g. postswitch.d).
+.PP
+
+Some of autorandr's state is exposed as environment variables prefixed with
+\fIAUTORANDR_\fR, such as:
+\fIAUTORANDR_CURRENT_PROFILE\fR,
+\fIAUTORANDR_CURRENT_PROFILES\fR,
+\fIAUTORANDR_PROFILE_FOLDER\fR,
+and \fIAUTORANDR_MONITORS\fR
+with the intention that they can be used within the hook scripts.
+
+The one kink is that during \fIpreswitch\fR, \fIAUTORANDR_CURRENT_PROFILE\fR
+is reporting the upcoming profile rather
+than the current one.
+
 .SH AUTHOR
 \fRPhillip Berndt <phillip.berndt@googlemail.com>
 .br
index 6d0163c0a4dc9fd1f207865101206dae00915ec1..a2d8b56916e13be80d208ef00c68d32e12ce8cd6 100755 (executable)
@@ -28,6 +28,7 @@ import binascii
 import copy
 import getopt
 import hashlib
+import math
 import os
 import posix
 import pwd
@@ -43,17 +44,13 @@ from collections import OrderedDict
 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
@@ -67,6 +64,8 @@ virtual_profiles = [
     ("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 = [
@@ -100,6 +99,7 @@ Usage: autorandr [options]
 --detected              only list detected (available) configuration(s)
 --dry-run               don't change anything, only print the xrandr commands
 --fingerprint           fingerprint your current hardware setup
+--ignore-lid            treat outputs as connected even if their lids are closed
 --match-edid            match diplays based on edid instead of name
 --force                 force (re)loading of a profile / overwrite exiting files
 --list                  list configurations
@@ -118,6 +118,35 @@ Usage: autorandr [options]
 """.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
@@ -202,6 +231,7 @@ class XrandrOutput(object):
             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
@@ -237,7 +267,7 @@ class XrandrOutput(object):
     EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
 
     def __repr__(self):
-        return "<%s%s %s>" % (self.output, self.short_edid, " ".join(self.option_vector))
+        return "<%s%s %s>" % (self.output, self.fingerprint, " ".join(self.option_vector))
 
     @property
     def short_edid(self):
@@ -254,12 +284,17 @@ class XrandrOutput(object):
         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):
@@ -316,8 +351,36 @@ class XrandrOutput(object):
         self.edid = edid
         self.options = options
         self.ignored_options = []
+        self.parse_serial_from_edid()
         self.remove_default_option_values()
 
+    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)
+            # Check EDID header, and checksum
+            if raw[:8] != b'\x00\xff\xff\xff\xff\xff\xff\x00' or sum(raw) % 256 != 0:
+                return
+            serial_no = int.from_bytes(raw[15:11:-1], byteorder='little')
+
+            serial_text = None
+            # Offsets of standard timing information descriptors 1-4
+            # (see https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format)
+            for timing_bytes in (raw[54:72], raw[72:90], raw[90:108], raw[108:126]):
+                if timing_bytes[0:2] == b'\x00\x00':
+                    timing_type = timing_bytes[3]
+                    if timing_type == 0xFF:
+                        buffer = timing_bytes[5:]
+                        buffer = buffer.partition(b'\x0a')[0]
+                        serial_text = buffer.decode('cp437')
+            self.serial = serial_text if serial_text else "0x{:x}".format(serial_no) if serial_no != 0 else None
+
     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)
@@ -419,6 +482,8 @@ class XrandrOutput(object):
                         # 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
@@ -437,7 +502,7 @@ class XrandrOutput(object):
         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"):
@@ -458,13 +523,23 @@ class XrandrOutput(object):
             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"]
 
         return XrandrOutput(output, edid, options)
 
+    @property
+    def fingerprint(self):
+        return str(self.serial) if self.serial else self.short_edid
+
+    def fingerprint_equals(self, other):
+        if self.serial and other.serial:
+           return self.serial == other.serial
+        else:
+           return self.edid_equals(other)
+
     def edid_equals(self, other):
         "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
         if self.edid and other.edid:
@@ -482,13 +557,13 @@ class XrandrOutput(object):
         return not (self == other)
 
     def __eq__(self, other):
-        return self.edid_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
+        return self.fingerprint_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 not self.fingerprint_equals(other):
+            diffs.append("EDID `%s' differs from `%s'" % (self.fingerprint, other.fingerprint))
         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:
@@ -541,7 +616,10 @@ def debug_regexp(pattern, string):
     return "Debug information would be available if the `regex' module was installed."
 
 
-def parse_xrandr_output():
+def parse_xrandr_output(
+    *,
+    ignore_lid,
+):
     "Parse the output of `xrandr --verbose' into a list of outputs"
     xrandr_output = os.popen("xrandr -q --verbose").read()
     if not xrandr_output:
@@ -564,7 +642,11 @@ def parse_xrandr_output():
             modes[output_name] = output_modes
 
     # consider a closed lid as disconnected if other outputs are connected
-    if sum(o.edid != None for o in outputs.values()) > 1:
+    if not ignore_lid and sum(
+        o.edid != None
+        for o
+        in outputs.values()
+    ) > 1:
         for output_name in outputs.keys():
             if is_closed_lid(output_name):
                 outputs[output_name].edid = None
@@ -588,7 +670,7 @@ def load_profiles(profile_path):
         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)
@@ -640,33 +722,33 @@ def match_asterisk(pattern, data):
 
 
 def update_profiles_edid(profiles, config):
-    edid_map = {}
+    fp_map = {}
     for c in config:
-        if config[c].edid is not None:
-            edid_map[config[c].edid] = c
+        if config[c].fingerprint is not None:
+            fp_map[config[c].fingerprint] = c
 
     for p in profiles:
         profile_config = profiles[p]["config"]
 
-        for edid in edid_map:
+        for fingerprint in fp_map:
             for c in list(profile_config.keys()):
-                if profile_config[c].edid != edid or c == edid_map[edid]:
+                if profile_config[c].fingerprint != fingerprint or c == fp_map[fingerprint]:
                     continue
 
-                print("%s: renaming display %s to %s" % (p, c, edid_map[edid]))
+                print("%s: renaming display %s to %s" % (p, c, fp_map[fingerprint]))
 
                 tmp_disp = profile_config[c]
 
-                if edid_map[edid] in profile_config:
+                if fp_map[fingerprint] in profile_config:
                     # Swap the two entries
-                    profile_config[c] = profile_config[edid_map[edid]]
+                    profile_config[c] = profile_config[fp_map[fingerprint]]
                     profile_config[c].output = c
                 else:
                     # Object is reassigned to another key, drop this one
                     del profile_config[c]
 
-                profile_config[edid_map[edid]] = tmp_disp
-                profile_config[edid_map[edid]].output = edid_map[edid]
+                profile_config[fp_map[fingerprint]] = tmp_disp
+                profile_config[fp_map[fingerprint]].output = fp_map[fingerprint]
 
 
 def find_profiles(current_config, profiles):
@@ -676,12 +758,12 @@ def find_profiles(current_config, profiles):
         config = profile["config"]
         matches = True
         for name, output in config.items():
-            if not output.edid:
+            if not output.fingerprint:
                 continue
-            if name not in current_config or not output.edid_equals(current_config[name]):
+            if name not in current_config or not output.fingerprint_equals(current_config[name]):
                 matches = False
                 break
-        if not matches or any((name not in config.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].fingerprint)):
             continue
         if matches:
             closeness = max(match_asterisk(output.edid, current_config[name].edid), match_asterisk(
@@ -810,7 +892,7 @@ def get_fb_dimensions(configuration):
                 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):
@@ -843,10 +925,10 @@ 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 = []
@@ -894,7 +976,12 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     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
@@ -927,7 +1014,14 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     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):
@@ -992,16 +1086,18 @@ def generate_virtual_profile(configuration, modes, profile_name):
                     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):
@@ -1123,21 +1219,21 @@ def exec_scripts(profile_path, script_name, meta_information=None):
             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
@@ -1191,6 +1287,16 @@ def dispatch_call_to_sessions(argv):
             sys.exit(1)
         os.waitpid(child_pid, 0)
 
+    # 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. If this breaks your use case, set the
+    # env var AUTORANDR_UID_MIN as appropriate. (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.)
+    uid_min = 1000
+    if 'AUTORANDR_UID_MIN' in os.environ:
+      uid_min = int(os.environ['AUTORANDR_UID_MIN'])
+
     for directory in os.listdir("/proc"):
         directory = os.path.join("/proc/", directory)
         if not os.path.isdir(directory):
@@ -1200,13 +1306,7 @@ def dispatch_call_to_sessions(argv):
             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:
+        if uid < uid_min:
             continue
 
         process_environ = {}
@@ -1281,10 +1381,32 @@ def read_config(options, directory):
 
 def main(argv):
     try:
-        opts, args = getopt.getopt(argv[1:], "s:r:l:d:cfh",
-                                   ["batch", "dry-run", "change", "cycle", "default=", "save=", "remove=", "load=",
-                                    "force", "fingerprint", "config", "debug", "skip-options=", "help",
-                                    "list", "current", "detected", "version", "match-edid"])
+        opts, args = getopt.getopt(
+            argv[1:],
+            "s:r:l:d:cfh",
+            [
+                "batch",
+                "dry-run",
+                "change",
+                "cycle",
+                "default=",
+                "save=",
+                "remove=",
+                "load=",
+                "force",
+                "fingerprint",
+                "config",
+                "debug",
+                "skip-options=",
+                "help",
+                "list",
+                "current",
+                "detected",
+                "version",
+                "match-edid",
+                "ignore-lid"
+            ]
+        )
     except getopt.GetoptError as e:
         print("Failed to parse options: {0}.\n"
               "Use --help to get usage information.".format(str(e)),
@@ -1341,7 +1463,12 @@ def main(argv):
         raise AutorandrException("Failed to load profiles", e)
 
     exec_scripts(None, "predetect")
-    config, modes = parse_xrandr_output()
+
+    ignore_lid = "--ignore-lid" in options
+
+    config, modes = parse_xrandr_output(
+        ignore_lid=ignore_lid,
+    )
 
     if "--match-edid" in options:
         update_profiles_edid(profiles, config)
@@ -1533,7 +1660,12 @@ def main(argv):
             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()
+            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)
diff --git a/caca b/caca
deleted file mode 100644 (file)
index e69de29..0000000
index 7fb2be8666eb60bed46434ee31170c4c5fcceeff..f5d6b516aef5a290e04dcdb7b215af3220aec498 100644 (file)
@@ -7,7 +7,7 @@ Build-Depends: debhelper (>=9)
 Standards-Version: 3.9.6
 Homepage: https://github.com/phillipberndt/autorandr
 Architecture: all
-Depends: x11-xserver-utils, python
+Depends: x11-xserver-utils, python3
 Description: Automatically select a display configuration for connected devices
  Autorandr is a script for managing xrandr configurations based on the
  connected devices. It can be set up to automatically switch to a stored
index fe8fe375f18b2ca8ee206cb5a527a45bf9636992..32d1b7b1f94a5056f0064520fe0afe7b80774443 100755 (executable)
@@ -45,7 +45,7 @@ SIZE=$(du -s $D | awk '{print $1}')
 
 cp -r "$P/debian" "$D/DEBIAN"
 chmod 0755 "$D/DEBIAN"
-[ -d "$D/etc" ] && (cd $D; find etc -type f) > "$D/DEBIAN/conffiles"
+[ -d "$D/etc" ] && (cd $D; find etc -type f -printf '/%p\n') > "$D/DEBIAN/conffiles"
 sed -i -re "s#Version:.+#Version: $V#" "$D/DEBIAN/control"
 echo "Installed-Size: $SIZE" >> "$D/DEBIAN/control"
 fakeroot dpkg-deb -b "$D" "$O"
index 26533df59ff8173ea829b40532b1c579043e06c3..a43b16ca76b5c191f7e6f0aefa10925c1fa15500 100644 (file)
@@ -15,6 +15,8 @@ BuildRequires: systemd
 BuildRequires: udev
 BuildRequires: desktop-file-utils
 
+Recommends:    (%{name}-bash-completion = %{version}-%{release} if bash)
+Recommends:    (%{name}-zsh-completion = %{version}-%{release} if zsh)
 
 %description
 %{summary}.
index ed729ba60edd2cb83791cbbde7eed3510fea4ac2..0b29943775e50610260e8fca2739ef26bccb79f7 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@ except:
 setup(
     name='autorandr',
 
-    version='1.12.1.post1',
+    version='1.13.3.post1',
 
     description='Automatically select a display configuration based on connected devices',
     long_description=long_description,