#!/bin/sh
#
# Automatically select a display configuration based on connected devices
#
# Stefan Tomanek <stefan.tomanek@wertarbyte.de>
#
# How to use:
#
# Save your current display configuration and setup with:
#  $ autorandr --save mobile
#
# Connect an additional display, configure your setup and save it:
#  $ autorandr --save docked
#
# Now autorandr can detect which hardware setup is active:
#  $ autorandr
#    mobile
#    docked (detected)
#
# To automatically reload your setup, just append --change to the command line
#
# To manually load a profile, you can use the --load <profile> option.
#
# autorandr tries to avoid reloading an identical configuration. To force the
# (re)configuration, apply --force.
#
# 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
# ~/.autorandr 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.
#
#
# While the script uses xrandr by default, calling it by the name "autodisper"
# or "auto-disper" forces it to use the "disper" utility, which is useful for
# controlling nvidia chipsets. The formats for fingerprinting the current setup
# and saving/loading the current configuration are adjusted accordingly.

XRANDR=/usr/bin/xrandr
DISPER=/usr/bin/disper
XDPYINFO=/usr/bin/xdpyinfo
PROFILES=~/.autorandr/
CONFIG=~/.autorandr.conf

CHANGE_PROFILE=0
FORCE_LOAD=0
DEFAULT_PROFILE=""
SAVE_PROFILE=""

FP_METHODS="setup_fp_xrandr_edid setup_fp_sysfs_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
	FP_METHODS="setup_fp_disper"
	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]; } }'
}

setup_fp_sysfs_edid() {
	# xrandr triggers the reloading of EDID data
	$XRANDR -q > /dev/null
	# hash the EDIDs of all _connected_ devices
	for P in /sys/class/drm/card*-*/; do
		# nothing found
		[ ! -d "$P" ] && continue
		if grep -q "^connected$" < "${P}status"; then
			echo -n "$(basename "$P") "
			md5sum ${P}edid | awk '{print $1}'
		fi
	done
}

setup_fp_disper() {
	$DISPER -l | grep '^display '
}

setup_fp() {
	local FP="";
	for M in $FP_METHODS; do
		FP="$($M)"
		if [ -n "$FP" ]; then
			break
		fi
	done
	if [ -z "$FP" ]; then
		echo "Unable to fingerprint display configuration" >&2
		return
	fi
	echo "$FP"
}

current_cfg_xrandr() {
	local PRIMARY_SETUP="";
	if [ -x "$XDPYINFO" ]; then
		PRIMARY_SETUP="$($XDPYINFO -ext XINERAMA | awk '/^  head #0:/ {printf $3 $5}')"
	fi
	$XRANDR -q | awk -v primary_setup="${PRIMARY_SETUP}" '
	# display is connected and has a mode
	/^[^ ]+ connected [^(]/ {
		split($3, A, "+");
		print "output "$1;
		print "mode "A[1];
		print "pos "A[2]"x"A[3];
		if ($4 !~ /^\(/) {
			print "rotate "$4;
		}
		if (A[1] A[2] "," A[3] == primary_setup)
			print "primary";
		next;
	}
	# disconnected or disabled displays
	/^[^ ]+ (dis)?connected / ||
	/^[^ ]+ unknown connection / {
		print "output "$1;
		print "off";
		next;
	}'
}

current_cfg_disper() {
	$DISPER -p
}

current_cfg() {
	$CURRENT_CFG_METHOD;
}

blocked() {
	local PROFILE="$1"
	[ ! -x "$PROFILES/$PROFILE/block" ] && return 1

	"$PROFILES/$PROFILE/block" "$PROFILE"
}

config_equal() {
	local PROFILE="$1"
	if [ "$(cat "$PROFILES/$PROFILE/config")" = "$(current_cfg)" ]; then
		echo "Config already loaded"
		return 0
	else
		return 1
	fi
}

load_cfg_xrandr() {
	sed 's!^!--!' "$1" | xargs $XRANDR
}

load_cfg_disper() {
	$DISPER -i < "$1"
}

load() {
	local PROFILE="$1"
	local CONF="$PROFILES/$PROFILE/config"
	if [ -e "$CONF" ] ; then
		echo " -> loading profile $PROFILE"
		$LOAD_METHOD "$CONF"

		[ -x "$PROFILES/$PROFILE/postswitch" ] && \
			"$PROFILES/$PROFILE/postswitch" "$PROFILE"
		[ -x "$PROFILES/postswitch" ] && \
			"$PROFILES/postswitch" "$PROFILE"
	fi
}

help() {
	cat <<EOH
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>
-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
 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
 ~/.autorandr as well as in any profile directories: The scripts are executed
 after a mode switch has taken place and can notify window managers.

 When called by the name "autodisper" or "auto-disper", the script uses "disper"
 instead of "xrandr" to detect, configure and save the display configuration.

EOH
	exit
}
# process parameters
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"

while true; do
	case "$1" in
		-c|--change) CHANGE_PROFILE=1; shift ;;
		-d|--default) DEFAULT_PROFILE="$2"; shift 2 ;;
		-s|--save) SAVE_PROFILE="$2"; shift 2 ;;
		-l|--load) LOAD_PROFILE="$2"; shift 2 ;;
	 	-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
done

CURRENT_SETUP="$(setup_fp)"

if [ -n "$SAVE_PROFILE" ]; then
	echo "Saving current configuration as profile '${SAVE_PROFILE}'"
	mkdir -p "$PROFILES/$SAVE_PROFILE"
	echo "$CURRENT_SETUP" > "$PROFILES/$SAVE_PROFILE/setup"
	$CURRENT_CFG_METHOD > "$PROFILES/$SAVE_PROFILE/config"
	exit 0
fi

if [ -n "$LOAD_PROFILE" ]; then
	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"))"
	echo -n "$PROFILE"

	if blocked "$PROFILE"; then
		echo " (blocked)"
		continue
	fi

	FILE_SETUP="$(cat "$PROFILES/$PROFILE/setup")"
	if [ "$CURRENT_SETUP" = "$FILE_SETUP" ]; then
		echo " (detected)"
		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
		echo ""
	fi
done

# we did not find the profile, load default
if [ -n "$DEFAULT_PROFILE" ]; then
	echo "No suitable profile detected, falling back to $DEFAULT_PROFILE"
	load "$DEFAULT_PROFILE"
fi
exit 1