3 # Automatically select a display configuration based on connected devices
5 # autorandr was originally written by Stefan Tomanek <stefan.tomanek@wertarbyte.de>
6 # For licensing reasons, this version does not contain non-trivial code from the
7 # original version and from authors that did not consent with OSS licensing this
12 # THE FOLLOWING LICENCE AGREEMENT IS PRELIMINARY AND INVALID UNTIL ISSUE #7 AT
13 # https://github.com/phillipberndt/autorandr/issues/7
16 # This program is free software: you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation, either version 3 of the License, or
19 # (at your option) any later version.
21 # This program is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License for more details.
26 # You should have received a copy of the GNU General Public License
27 # along with this program. If not, see <http://www.gnu.org/licenses/>.
30 XRANDR=/usr/bin/xrandr
31 DISPER=/usr/bin/disper
33 CONFIG=~/.autorandr.conf
34 RESERVED_PROFILE_NAMES=`cat <<EOF
35 common Clone all connected outputs at the largest common resolution
36 horizontal Stack all connected outputs horizontally at their largest resolution
37 vertical Stack all connected outputs vertically at their largest resolution
45 FP_METHODS="setup_fp_sysfs_edid setup_fp_xrandr_edid"
46 CURRENT_CFG_METHOD="current_cfg_xrandr"
47 LOAD_METHOD="load_cfg_xrandr"
49 SCRIPTNAME="$(basename $0)"
50 # when called as autodisper/auto-disper, we assume different defaults
51 if [ "$SCRIPTNAME" = "auto-disper" ] || [ "$SCRIPTNAME" = "autodisper" ]; then
52 echo "Assuming disper defaults..." >&2
53 CURRENT_CFG_METHOD="current_cfg_disper"
54 LOAD_METHOD="load_cfg_disper"
57 if [ -f $CONFIG ]; then
58 echo "Loading configuration from '$CONFIG'" >&2
62 setup_fp_xrandr_edid() {
63 $XRANDR -q --verbose | awk '
65 / (dis)?connected/ { DEVICE=gensub("-([A-Z]-)?", "", "g", $1) " "; }
66 /^[[:blank:]]+EDID:/ {
69 for(getline; /^[[:blank:]]+[0-9a-f]+$/; getline) {
80 setup_fp_sysfs_edid() {
81 which xxd >/dev/null 2>&1 || return
82 $XRANDR -q > /dev/null
83 for DEVICE in /sys/class/drm/card*-*; do
84 [ -e "${DEVICE}/status" ] && grep -q "^connected$" "${DEVICE}/status" || continue
85 echo -n "$(echo "${DEVICE}/edid" | sed -re 's#^.+card[0-9]+-([^/]+).+#\1#; s#-([A-Z]-)?##') "
86 xxd -c 256 -ps "${DEVICE}/edid" | awk 'ORS=""; /.+/ { print; }'
93 for METHOD in $FP_METHODS; do
94 FINGERPRINT="$($METHOD)"
95 [ -n "$FINGERPRINT" ] && break
97 if [ -z "$FINGERPRINT" ]; then
98 echo "Unable to fingerprint display configuration." >&2
101 echo "$FINGERPRINT" | sort
104 current_cfg_xrandr() {
106 # display is connected and has a mode
107 /^[^ ]+ connected [^(]/ {
109 if ($3 == "primary") {
117 if (($4 == "left") || ($4 == "right")) {
122 print "pos "A[2]"x"A[3];
127 print "rotate normal";
131 # disconnected or disabled displays
132 /^[^ ]+ (dis)?connected / ||
133 /^[^ ]+ unknown connection / {
140 current_cfg_disper() {
144 common_cfg_xrandr() {
147 # output: current output
148 # outputlist: space sep list of all outputs
149 # outputarr: array of all connected outputs
150 # outputarrsize: number of connected outputs
151 # modelist[800x600]: space sep list of outputs supporting mode
152 # display is connected
153 /^[^ ]+ connected / {
155 outputlist=outputlist " " output
156 outputarr[outputarrsize++]=output
158 # disconnected or disabled displays
159 /^[^ ]+ disconnected / ||
160 /^[^ ]+ unknown connection / {
164 # modes available on a screen
166 modelist[$1]=modelist[$1] " " output
169 # find common mode with largest screen area
170 for (m in modelist) {
171 if (modelist[m] == outputlist) {
172 # calculate area of resolution
174 if (wh[1]*wh[2] >= maxdim) {
181 for (i in outputarr) {
182 print "output " outputarr[i];
183 print "mode " maxmode;
186 print "same-as " outputarr[0]
195 $XRANDR -q | awk -v stack="${STACK}" '
197 # stack: "horizontal" (anything except vertical) or "vertical"
198 # output: current output
199 # firstmode: pick first mode after output
200 # posX,posY: position of the next output
204 # display is connected
205 /^[^ ]+ connected / {
210 # disconnected or disabled displays
211 /^[^ ]+ disconnected / ||
212 /^[^ ]+ unknown connection / {
216 # modes available on a screen, but pick only the first
218 if (!firstmode) next;
220 # output mode at current virtual desktop pos
222 print "pos " posX "x" posY;
223 # calculate position of next output
225 if (stack == "vertical")
238 [ ! -x "$PROFILES/$1/block" ] && return 1
239 "$PROFILES/$1/block" "$1"
243 if [ "$(cat "$PROFILES/$1/config")" = "$(current_cfg)" ]; then
244 echo "Config already loaded." >&2
251 # sed 1: Prefix arguments with "--"
252 # sed 2: Merge arguments into one line per output
253 # sed 3: * Merge all --off outputs into the first line
254 # * Place the output with --pos 0x0 on the second line
255 # * Remaining outputs are appended as they appear
256 # * Keep everything in hold buffer until the last line
257 # sed 4: Remove empty lines caused by G and H on empty hold buffer
258 # sed 5: Join lines enabling screens in pairs of two (See https://github.com/phillipberndt/autorandr/pull/6)
259 sed 's/^/--/' "$1" | sed -e '
266 # Merge with next line if it contains --off
267 s/\n\([^\n]* --off\)/ \1/
273 # Swap lines 1 and 2 if --off is found
274 / --off/ s/^\([^\n]*\)\n\([^\n]*\)/\2\n\1/
282 /--mode/{ N; s/\n/ /; }
283 ' | xargs -L 1 $XRANDR
292 local CONF="$PROFILES/$PROFILE/config"
293 local IS_VIRTUAL_PROFILE=`echo "$RESERVED_PROFILE_NAMES" | grep -c "^ $PROFILE "`
295 if [ ! -f "$CONF" -a $IS_VIRTUAL_PROFILE -eq 0 ]; then
296 echo " -> Error: Profile '$PROFILE' does not exist." >&2
300 if [ -x "$PROFILES/preswitch" ]; then
301 "$PROFILES/preswitch" "$PROFILE"
303 if [ -x "$PROFILES/$PROFILE/preswitch" ]; then
304 "$PROFILES/$PROFILE/preswitch" "$PROFILE"
307 if [ -f "$CONF" ]; then
308 echo " -> loading profile $PROFILE"
309 if [ $IS_VIRTUAL_PROFILE -ne 0 ]; then
310 echo " -> Warning: Existing profile overrides virtual profile with same name" >&2
315 if [ $PROFILE = "common" ]; then
316 echo " -> setting largest common mode in cloned mode"
318 elif [ $PROFILE = "horizontal" ]; then
319 echo " -> stacking all outputs horizontally at their largest modes"
320 STACK="horizontal" stack_cfg_xrandr
321 elif [ $PROFILE = "vertical" ]; then
322 echo " -> stacking all outputs vertically at their largest modes"
323 STACK="vertical" stack_cfg_xrandr
327 if [ -x "$PROFILES/$PROFILE/postswitch" ]; then
328 "$PROFILES/$PROFILE/postswitch" "$PROFILE"
330 if [ -x "$PROFILES/postswitch" ]; then
331 "$PROFILES/postswitch" "$PROFILE"
337 Usage: $SCRIPTNAME [options]
339 -h, --help get this small help
340 -c, --change reload current setup
341 -s, --save <profile> save your current setup to profile <profile>
342 -l, --load <profile> load profile <profile>
343 -d, --default <profile> make profile <profile> the default profile
344 --force force (re)loading of a profile
345 --fingerprint fingerprint your current hardware setup
346 --config dump your current xrandr setup
348 To prevent a profile from being loaded, place a script call "block" in its
349 directory. The script is evaluated before the screen setup is inspected, and
350 in case of it returning a value of 0 the profile is skipped. This can be used
351 to query the status of a docking station you are about to leave.
353 If no suitable profile can be identified, the current configuration is kept.
354 To change this behaviour and switch to a fallback configuration, specify
357 Another script called "postswitch" can be placed in the directory
358 ~/.autorandr as well as in any profile directories: The scripts are executed
359 after a mode switch has taken place and can notify window managers. The same
360 goes for "preswitch", which will be executed before a mode switch.
362 When called by the name "autodisper" or "auto-disper", the script uses "disper"
363 instead of "xrandr" to configure and save the display configuration.
365 If xrandr is used, the following virtual configurations are available:
366 ${RESERVED_PROFILE_NAMES}
372 OPTS=$(getopt -n autorandr -o s:l:d:cfh --long change,default:,save:,load:,force,fingerprint,config,help -- "$@")
373 if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
378 -c|--change) CHANGE_PROFILE=1; shift ;;
379 -d|--default) DEFAULT_PROFILE="$2"; shift 2 ;;
380 -s|--save) SAVE_PROFILE="$2"; shift 2 ;;
381 -l|--load) LOAD_PROFILE="$2"; shift 2 ;;
383 --force) FORCE_LOAD=1; shift ;;
384 --fingerprint) setup_fp; exit 0;;
385 --config) current_cfg; exit 0;;
387 *) echo "Error: $1"; exit 1;;
391 CURRENT_SETUP="$(setup_fp)"
393 if [ -n "$SAVE_PROFILE" ]; then
394 if echo "$RESERVED_PROFILE_NAMES" | grep -q "^ $SAVE_PROFILE "; then
395 echo "Cannot save current configuration as profile '${SAVE_PROFILE}': This configuration name is a reserved virtual configuration."
398 echo "Saving current configuration as profile '${SAVE_PROFILE}'"
399 mkdir -p "$PROFILES/$SAVE_PROFILE"
400 echo "$CURRENT_SETUP" > "$PROFILES/$SAVE_PROFILE/setup"
401 $CURRENT_CFG_METHOD > "$PROFILES/$SAVE_PROFILE/config"
405 if [ -n "$LOAD_PROFILE" ]; then
406 CHANGE_PROFILE=1 FORCE_LOAD=1 load "$LOAD_PROFILE"
410 for PROFILE_PATH in $PROFILES/*; do
411 PROFILE="$(basename "$PROFILE_PATH")"
412 SETUP_FILE="${PROFILE_PATH}/setup"
414 [ -e $SETUP_FILE ] || continue
417 if blocked "$PROFILE"; then
422 # This sed command is for compatibility with old versions that did not try
423 # to normalize device names
424 FILE_SETUP="$(sed -re 's#-([A-Z]-)?##g; s#card[0-9]##;' "$PROFILES/$PROFILE/setup")"
425 # Detect the md5sum in fingerprint files created using the old sysfs fingerprinting
426 # If it is detected, output a warning and calculate the legacy variant of the current
428 if echo "$FILE_SETUP" | grep -Eq "^[^ ]+ [0-9a-f]{32}$"; then
429 echo -n " (Obsolete fingerprint format. Please update using --save.) "
431 if [ -z "$LEGACY_CURRENT_SETUP" ]; then
432 LEGACY_CURRENT_SETUP="$(echo "$CURRENT_SETUP" | while read DEVICE EDID; do
434 echo -n "${EDID}" | xxd -r -ps | md5sum - | awk '{print $1}'
437 FILE_SETUP="$(echo "$FILE_SETUP" | sort)"
438 if [ "$LEGACY_CURRENT_SETUP" = "$FILE_SETUP" ]; then
439 CURRENT_SETUP="$LEGACY_CURRENT_SETUP"
443 if [ "$CURRENT_SETUP" = "$FILE_SETUP" ]; then
445 if [ "$CHANGE_PROFILE" -eq 1 ]; then
446 if [ "$FORCE_LOAD" -eq 1 ] || ! config_equal "$PROFILE"; then
450 # found the profile, exit with success
457 # we did not find the profile, load default
458 if [ -n "$DEFAULT_PROFILE" ]; then
459 echo "No suitable profile detected, falling back to $DEFAULT_PROFILE"
460 load "$DEFAULT_PROFILE"
468 # indent-tabs-mode: t