#!/bin/sh
#
-# Automatically select a display configuration based on connected devives
+# Automatically select a display configuration based on connected devices
#
-# Stefan Tomanek <stefan.tomanek@wertarbyte.de>
+# autorandr was originally written by Stefan Tomanek <stefan.tomanek@wertarbyte.de>
+# For licensing reasons, this version does not contain non-trivial code from the
+# original version and from authors that did not consent with OSS licensing this
+# program.
#
-# How to use:
#
-# Save your current display configuration and setup with
-# autorandr --save mobile
#
-# Connect an additional display, configure your setup and save it
-# autorand --save docked
+# THE FOLLOWING LICENCE AGREEMENT IS PRELIMINARY AND INVALID UNTIL ISSUE #7 AT
+# https://github.com/phillipberndt/autorandr/issues/7
+# HAS BEEN RESOLVED!
#
-# Now autorandr can detect which hardware setup is active:
-# # autorandr
-# mobile
-# docked (detected)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
#
-# To automatically reload your setup, just append --change to the command line
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
#
-# To manually load a profile, you can use the --load <profile> option.
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-# To prevent a profile from being loaded, place a script call "block" in its
-# directory. The script is evaluated before the screen setup is inspected, and
-# in case of it returning a value of 0 the profile is skipped. This can be used
-# to query the status of a docking station you are about to leave.
-#
-# If no suitable profile can be identified, the current configuration is kept.
-# To change this behaviour and switch to a fallback configuration, specify
-# --default <profile>
-#
-# Another script called "postswitch "can be placed in the directory
-# ~/.auto-disper as well as in all profile directories: The scripts are
-# executed after a mode switch has taken place and can notify window managers
-# or other applications about it.
XRANDR=/usr/bin/xrandr
-PROFILES=~/.autorandr/
+DISPER=/usr/bin/disper
+PROFILES=~/.autorandr
+CONFIG=~/.autorandr.conf
+RESERVED_PROFILE_NAMES=`cat <<EOF
+ common Clone all connected outputs at the largest common resolution
+ horizontal Stack all connected outputs horizontally at their largest resolution
+ vertical Stack all connected outputs vertically at their largest resolution
+EOF`
CHANGE_PROFILE=0
+FORCE_LOAD=0
DEFAULT_PROFILE=""
SAVE_PROFILE=""
+FP_METHODS="setup_fp_sysfs_edid setup_fp_xrandr_edid"
+CURRENT_CFG_METHOD="current_cfg_xrandr"
+LOAD_METHOD="load_cfg_xrandr"
+
+SCRIPTNAME="$(basename $0)"
+# when called as autodisper/auto-disper, we assume different defaults
+if [ "$SCRIPTNAME" = "auto-disper" ] || [ "$SCRIPTNAME" = "autodisper" ]; then
+ echo "Assuming disper defaults..." >&2
+ CURRENT_CFG_METHOD="current_cfg_disper"
+ LOAD_METHOD="load_cfg_disper"
+fi
+
+if [ -f $CONFIG ]; then
+ echo "Loading configuration from '$CONFIG'" >&2
+ . $CONFIG
+fi
+
setup_fp_xrandr_edid() {
$XRANDR -q --verbose | awk '
- /^[^ ]+ (dis)?connected / { DEV=$1; }
- $1 ~ /^[a-f0-9]+$/ { ID[DEV] = ID[DEV] $1 }
- END { for (X in ID) { print X " " ID[X]; } }'
+ ORS="";
+ / (dis)?connected/ { DEVICE=gensub("-([A-Z]-)?", "", "g", $1) " "; }
+ /^[[:blank:]]+EDID:/ {
+ print DEVICE
+ DEVICE=""
+ for(getline; /^[[:blank:]]+[0-9a-f]+$/; getline) {
+ print $1;
+ }
+ print "\n";
+ }
+ END {
+ print "\n";
+ }
+ '
}
setup_fp_sysfs_edid() {
- # hash the EDIDs of all _connected_ devices
- for P in /sys/class/drm/card*-*/; do
- if grep -q "^connected$" < "${P}status"; then
- echo -n "$(basename "$P") "
- md5sum ${P}edid | awk '{print $1}'
- fi
+ which xxd >/dev/null 2>&1 || return
+ $XRANDR -q > /dev/null
+ for DEVICE in /sys/class/drm/card*-*; do
+ [ -e "${DEVICE}/status" ] && grep -q "^connected$" "${DEVICE}/status" || continue
+ echo -n "$(echo "${DEVICE}/edid" | sed -re 's#^.+card[0-9]+-([^/]+).+#\1#; s#-([A-Z]-)?##') "
+ xxd -c 256 -ps "${DEVICE}/edid" | awk 'ORS=""; /.+/ { print; }'
+ echo
done
}
setup_fp() {
- local METHODS="setup_fp_xrandr_edid setup_fp_sysfs_edid"
- local FP="";
- for M in $METHODS; do
- FP="$($M)"
- [ -n "$FP" ] && break;
+ FINGERPRINT=""
+ for METHOD in $FP_METHODS; do
+ FINGERPRINT="$($METHOD)"
+ [ -n "$FINGERPRINT" ] && break
done
- if [ -z "$FP" ]; then
- echo "Unable to fingerprint display configuration" >&2
- return
+ if [ -z "$FINGERPRINT" ]; then
+ echo "Unable to fingerprint display configuration." >&2
+ return 0
fi
- echo "$FP"
+ echo "$FINGERPRINT" | sort
}
-
-current_cfg() {
+current_cfg_xrandr() {
$XRANDR -q | awk '
- /^[^ ]+ disconnected / {
- print "output "$1;
- print "off";
- }
- /^[^ ]+ connected / {
- split($3, A, "+");
+ # display is connected and has a mode
+ /^[^ ]+ connected [^(]/ {
print "output "$1;
+ if ($3 == "primary") {
+ print $3
+ split($4, A, "+")
+ $4=$5
+ }
+ else {
+ split($3, A, "+");
+ }
+ if (($4 == "left") || ($4 == "right")) {
+ split(A[1], B, "x");
+ A[1] = B[2]"x"B[1];
+ }
print "mode "A[1];
print "pos "A[2]"x"A[3];
+ if ($4 !~ /^\(/) {
+ print "rotate "$4;
+ }
+ else {
+ print "rotate normal";
+ }
+ next;
+ }
+ # disconnected or disabled displays
+ /^[^ ]+ (dis)?connected / ||
+ /^[^ ]+ unknown connection / {
+ print "output "$1;
+ print "off";
+ next;
}'
}
+current_cfg_disper() {
+ $DISPER -p
+}
+
+common_cfg_xrandr() {
+ $XRANDR -q | awk '
+ # variables:
+ # output: current output
+ # outputlist: space sep list of all outputs
+ # outputarr: array of all connected outputs
+ # outputarrsize: number of connected outputs
+ # modelist[800x600]: space sep list of outputs supporting mode
+ # display is connected
+ /^[^ ]+ connected / {
+ output=$1;
+ outputlist=outputlist " " output
+ outputarr[outputarrsize++]=output
+ }
+ # disconnected or disabled displays
+ /^[^ ]+ disconnected / ||
+ /^[^ ]+ unknown connection / {
+ print "output " $1;
+ print "off";
+ }
+ # modes available on a screen
+ /^ [0-9]+x[0-9]+/ {
+ modelist[$1]=modelist[$1] " " output
+ }
+ END {
+ # find common mode with largest screen area
+ for (m in modelist) {
+ if (modelist[m] == outputlist) {
+ # calculate area of resolution
+ split(m, wh, "x");
+ if (wh[1]*wh[2] >= maxdim) {
+ maxdim=wh[1]*wh[2]
+ maxmode=m
+ }
+ }
+ }
+ if (maxmode) {
+ for (i in outputarr) {
+ print "output " outputarr[i];
+ print "mode " maxmode;
+ print "pos 0x0";
+ if (i > 0) {
+ print "same-as " outputarr[0]
+ }
+ }
+ }
+ }' \
+ | load_cfg_xrandr -
+}
+
+stack_cfg_xrandr() {
+ $XRANDR -q | awk -v stack="${STACK}" '
+ # variables:
+ # stack: "horizontal" (anything except vertical) or "vertical"
+ # output: current output
+ # firstmode: pick first mode after output
+ # posX,posY: position of the next output
+ BEGIN {
+ posX=posY=0
+ }
+ # display is connected
+ /^[^ ]+ connected / {
+ output=$1;
+ print "output " $1;
+ firstmode=1
+ }
+ # disconnected or disabled displays
+ /^[^ ]+ disconnected / ||
+ /^[^ ]+ unknown connection / {
+ print "output " $1;
+ print "off";
+ }
+ # modes available on a screen, but pick only the first
+ /^ [0-9]+x[0-9]+/ {
+ if (!firstmode) next;
+ firstmode=0
+ # output mode at current virtual desktop pos
+ print "mode " $1;
+ print "pos " posX "x" posY;
+ # calculate position of next output
+ split($1, wh, "x");
+ if (stack == "vertical")
+ posY += wh[2];
+ else
+ posX += wh[1];
+ }' \
+ | load_cfg_xrandr -
+}
+
+current_cfg() {
+ $CURRENT_CFG_METHOD;
+}
+
blocked() {
- local PROFILE="$1"
- [ ! -x "$PROFILES/$PROFILE/block" ] && return 1
+ [ ! -x "$PROFILES/$1/block" ] && return 1
+ "$PROFILES/$1/block" "$1"
+}
+
+config_equal() {
+ if [ "$(cat "$PROFILES/$1/config")" = "$(current_cfg)" ]; then
+ echo "Config already loaded." >&2
+ return 0
+ fi
+ return 1
+}
+
+load_cfg_xrandr() {
+ # sed 1: Prefix arguments with "--"
+ # sed 2: Merge arguments into one line per output
+ # sed 3: * Merge all --off outputs into the first line
+ # * Place the output with --pos 0x0 on the second line
+ # * Remaining outputs are appended as they appear
+ # * Keep everything in hold buffer until the last line
+ # sed 4: Remove empty lines caused by G and H on empty hold buffer
+ # sed 5: Join lines enabling screens in pairs of two (See https://github.com/phillipberndt/autorandr/pull/6)
+ sed 's/^/--/' "$1" | sed -e '
+ :START
+ /\n--output/{P;D}
+ s/\n/ /
+ N;bSTART' | sed -e '
+ / --off/{
+ G
+ # Merge with next line if it contains --off
+ s/\n\([^\n]* --off\)/ \1/
+ h
+ $!d;b
+ }
+ / --pos 0x0/{
+ G
+ # Swap lines 1 and 2 if --off is found
+ / --off/ s/^\([^\n]*\)\n\([^\n]*\)/\2\n\1/
+ h
+ $!d;b
+ }
+ H
+ $!d
+ x' | sed -e '
+ /./ !d' | sed -e '
+ /--mode/{ N; s/\n/ /; }
+ ' | xargs -L 1 $XRANDR
+}
- "$PROFILES/$PROFILE/block" "$PROFILE"
+load_cfg_disper() {
+ $DISPER -i < "$1"
}
load() {
local PROFILE="$1"
- if [ "$CHANGE_PROFILE" -eq 1 ] && [ -e "$PROFILES/$PROFILE/config" ] ; then
+ local CONF="$PROFILES/$PROFILE/config"
+ local IS_VIRTUAL_PROFILE=`echo "$RESERVED_PROFILE_NAMES" | grep -c "^ $PROFILE "`
+
+ if [ ! -f "$CONF" -a $IS_VIRTUAL_PROFILE -eq 0 ]; then
+ echo " -> Error: Profile '$PROFILE' does not exist." >&2
+ return
+ fi
+
+ if [ -x "$PROFILES/preswitch" ]; then
+ "$PROFILES/preswitch" "$PROFILE"
+ fi
+ if [ -x "$PROFILES/$PROFILE/preswitch" ]; then
+ "$PROFILES/$PROFILE/preswitch" "$PROFILE"
+ fi
+
+ if [ -f "$CONF" ]; then
echo " -> loading profile $PROFILE"
- sed 's!^!--!' "$PROFILES/$PROFILE/config" | xargs xrandr
+ if [ $IS_VIRTUAL_PROFILE -ne 0 ]; then
+ echo " -> Warning: Existing profile overrides virtual profile with same name" >&2
+ fi
+ $LOAD_METHOD "$CONF"
+ else
+ # Virtual profiles
+ if [ $PROFILE = "common" ]; then
+ echo " -> setting largest common mode in cloned mode"
+ common_cfg_xrandr
+ elif [ $PROFILE = "horizontal" ]; then
+ echo " -> stacking all outputs horizontally at their largest modes"
+ STACK="horizontal" stack_cfg_xrandr
+ elif [ $PROFILE = "vertical" ]; then
+ echo " -> stacking all outputs vertically at their largest modes"
+ STACK="vertical" stack_cfg_xrandr
+ fi
+ fi
- [ -x "$PROFILES/$PROFILE/postswitch" ] && \
- "$PROFILES/$PROFILE/postswitch" "$PROFILE"
- [ -x "$PROFILES/postswitch" ] && \
- "$PROFILES/postswitch" "$PROFILE"
+ if [ -x "$PROFILES/$PROFILE/postswitch" ]; then
+ "$PROFILES/$PROFILE/postswitch" "$PROFILE"
+ fi
+ if [ -x "$PROFILES/postswitch" ]; then
+ "$PROFILES/postswitch" "$PROFILE"
fi
}
help() {
cat <<EOH
-Usage: autorandr action [profile-name]
+Usage: $SCRIPTNAME [options]
-h, --help get this small help
-c, --change reload current setup
-s, --save <profile> save your current setup to profile <profile>
-l, --load <profile> load profile <profile>
---fingerprint fingerprints your actual config
+-d, --default <profile> make profile <profile> the default profile
+--force force (re)loading of a profile
+--fingerprint fingerprint your current hardware setup
+--config dump your current xrandr setup
To prevent a profile from being loaded, place a script call "block" in its
directory. The script is evaluated before the screen setup is inspected, and
If no suitable profile can be identified, the current configuration is kept.
To change this behaviour and switch to a fallback configuration, specify
- --default <profile>
+ --default <profile>.
- Another script called "postswitch "can be placed in the directory
- ~/.auto-disper as well as in all 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
+ ~/.autorandr as well as in any profile directories: The scripts are executed
+ after a mode switch has taken place and can notify window managers. The same
+ goes for "preswitch", which will be executed before a mode switch.
+
+ When called by the name "autodisper" or "auto-disper", the script uses "disper"
+ instead of "xrandr" to configure and save the display configuration.
+
+ If xrandr is used, the following virtual configurations are available:
+${RESERVED_PROFILE_NAMES}
EOH
exit
}
# process parameters
-OPTS=$(getopt -n autorandr -o s:l:d:cfh --long change,default:,save:,load:,fingerprint,help -- "$@")
+OPTS=$(getopt -n autorandr -o s:l:d:cfh --long change,default:,save:,load:,force,fingerprint,config,help -- "$@")
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
eval set -- "$OPTS"
-d|--default) DEFAULT_PROFILE="$2"; shift 2 ;;
-s|--save) SAVE_PROFILE="$2"; shift 2 ;;
-l|--load) LOAD_PROFILE="$2"; shift 2 ;;
- -h|--help) help ;;
+ -h|--help) help ;;
+ --force) FORCE_LOAD=1; shift ;;
--fingerprint) setup_fp; exit 0;;
+ --config) current_cfg; exit 0;;
--) shift; break ;;
*) echo "Error: $1"; exit 1;;
esac
CURRENT_SETUP="$(setup_fp)"
if [ -n "$SAVE_PROFILE" ]; then
+ if echo "$RESERVED_PROFILE_NAMES" | grep -q "^ $SAVE_PROFILE "; then
+ echo "Cannot save current configuration as profile '${SAVE_PROFILE}': This configuration name is a reserved virtual configuration."
+ exit 1
+ fi
echo "Saving current configuration as profile '${SAVE_PROFILE}'"
mkdir -p "$PROFILES/$SAVE_PROFILE"
echo "$CURRENT_SETUP" > "$PROFILES/$SAVE_PROFILE/setup"
- current_cfg > "$PROFILES/$SAVE_PROFILE/config"
+ $CURRENT_CFG_METHOD > "$PROFILES/$SAVE_PROFILE/config"
exit 0
fi
if [ -n "$LOAD_PROFILE" ]; then
- CHANGE_PROFILE=1 load "$LOAD_PROFILE"
+ CHANGE_PROFILE=1 FORCE_LOAD=1 load "$LOAD_PROFILE"
exit $?
fi
-for SETUP_FILE in $PROFILES/*/setup; do
- if ! [ -e $SETUP_FILE ]; then
- break
- fi
- PROFILE="$(basename $(dirname "$SETUP_FILE"))"
+for PROFILE_PATH in $PROFILES/*; do
+ PROFILE="$(basename "$PROFILE_PATH")"
+ SETUP_FILE="${PROFILE_PATH}/setup"
+
+ [ -e $SETUP_FILE ] || continue
echo -n "$PROFILE"
if blocked "$PROFILE"; then
continue
fi
- FILE_SETUP="$(cat "$PROFILES/$PROFILE/setup")"
+ # This sed command is for compatibility with old versions that did not try
+ # to normalize device names
+ FILE_SETUP="$(sed -re 's#-([A-Z]-)?##g; s#card[0-9]##;' "$PROFILES/$PROFILE/setup")"
+ # Detect the md5sum in fingerprint files created using the old sysfs fingerprinting
+ # If it is detected, output a warning and calculate the legacy variant of the current
+ # setup.
+ if echo "$FILE_SETUP" | grep -Eq "^[^ ]+ [0-9a-f]{32}$"; then
+ echo -n " (Obsolete fingerprint format. Please update using --save.) "
+
+ if [ -z "$LEGACY_CURRENT_SETUP" ]; then
+ LEGACY_CURRENT_SETUP="$(echo "$CURRENT_SETUP" | while read DEVICE EDID; do
+ echo -n "${DEVICE} "
+ echo -n "${EDID}" | xxd -r -ps | md5sum - | awk '{print $1}'
+ done)"
+ fi
+ FILE_SETUP="$(echo "$FILE_SETUP" | sort)"
+ if [ "$LEGACY_CURRENT_SETUP" = "$FILE_SETUP" ]; then
+ CURRENT_SETUP="$LEGACY_CURRENT_SETUP"
+ fi
+ fi
+
if [ "$CURRENT_SETUP" = "$FILE_SETUP" ]; then
echo " (detected)"
- load "$PROFILE"
+ if [ "$CHANGE_PROFILE" -eq 1 ]; then
+ if [ "$FORCE_LOAD" -eq 1 ] || ! config_equal "$PROFILE"; then
+ load "$PROFILE"
+ fi
+ fi
# found the profile, exit with success
exit 0
else
load "$DEFAULT_PROFILE"
fi
exit 1
+
+# Local Variables:
+# tab-width: 8
+# sh-basic-offset: 8
+# sh-indentation: 8
+# indent-tabs-mode: t
+# End: