]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blob - autorandr
License information (1/2)
[deb_pkgs/autorandr.git] / autorandr
1 #!/bin/sh
2 #
3 # Automatically select a display configuration based on connected devices
4 #
5 # Copyright (c) 2013 Stefan Tomanek <stefan.tomanek@wertarbyte.de>
6 #
7 # THIS IS _NOT_ OSS CODE!
8 #
9 #
10 #
11 # How to use:
12 #
13 # Save your current display configuration and setup with:
14 #  $ autorandr --save mobile
15 #
16 # Connect an additional display, configure your setup and save it:
17 #  $ autorandr --save docked
18 #
19 # Now autorandr can detect which hardware setup is active:
20 #  $ autorandr
21 #    mobile
22 #    docked (detected)
23 #
24 # To automatically reload your setup, just append --change to the command line
25 #
26 # To manually load a profile, you can use the --load <profile> option.
27 #
28 # autorandr tries to avoid reloading an identical configuration. To force the
29 # (re)configuration, apply --force.
30 #
31 # To prevent a profile from being loaded, place a script call "block" in its
32 # directory. The script is evaluated before the screen setup is inspected, and
33 # in case of it returning a value of 0 the profile is skipped. This can be used
34 # to query the status of a docking station you are about to leave.
35 #
36 # If no suitable profile can be identified, the current configuration is kept.
37 # To change this behaviour and switch to a fallback configuration, specify
38 # --default <profile>
39 #
40 # Another script called "postswitch "can be placed in the directory
41 # ~/.autorandr as well as in all profile directories: The scripts are executed
42 # after a mode switch has taken place and can notify window managers or other
43 # applications about it.
44 #
45 #
46 # While the script uses xrandr by default, calling it by the name "autodisper"
47 # or "auto-disper" forces it to use the "disper" utility, which is useful for
48 # controlling nvidia chipsets. The formats for fingerprinting the current setup
49 # and saving/loading the current configuration are adjusted accordingly.
50
51 XRANDR=/usr/bin/xrandr
52 DISPER=/usr/bin/disper
53 XDPYINFO=/usr/bin/xdpyinfo
54 PROFILES=~/.autorandr
55 CONFIG=~/.autorandr.conf
56 RESERVED_PROFILE_NAMES=`cat <<EOF
57  common     Clone all connected outputs at the largest common resolution
58  horizontal Stack all connected outputs horizontally at their largest resolution
59  vertical   Stack all connected outputs vertically at their largest resolution
60 EOF`
61
62 CHANGE_PROFILE=0
63 FORCE_LOAD=0
64 DEFAULT_PROFILE=""
65 SAVE_PROFILE=""
66
67 FP_METHODS="setup_fp_sysfs_edid setup_fp_xrandr_edid"
68 CURRENT_CFG_METHOD="current_cfg_xrandr"
69 LOAD_METHOD="load_cfg_xrandr"
70
71 SCRIPTNAME="$(basename $0)"
72 # when called as autodisper/auto-disper, we assume different defaults
73 if [ "$SCRIPTNAME" = "auto-disper" ] || [ "$SCRIPTNAME" = "autodisper" ]; then
74         echo "Assuming disper defaults..." >&2
75         FP_METHODS="setup_fp_disper"
76         CURRENT_CFG_METHOD="current_cfg_disper"
77         LOAD_METHOD="load_cfg_disper"
78 fi
79
80 if [ -f $CONFIG ]; then
81         echo "Loading configuration from '$CONFIG'" >&2
82         . $CONFIG
83 fi
84
85 setup_fp_xrandr_edid() {
86         $XRANDR -q --verbose | awk '
87         ORS="";
88         / (dis)?connected/ { DEVICE=gensub("-([A-Z]-)?", "", "g", $1) " "; }
89         /^[[:blank:]]+EDID:/ {
90                 print DEVICE
91                 DEVICE=""
92                 for(getline; /^[[:blank:]]+[0-9a-f]+$/; getline) {
93                         print $1;
94                 }
95                 print "\n";
96         }
97         END {
98                 print "\n";
99         }
100         '
101 }
102
103 setup_fp_sysfs_edid() {
104         which xxd >/dev/null 2>&1 || return
105         $XRANDR -q > /dev/null
106         for DEVICE in /sys/class/drm/card*-*; do
107                 [ -e "${DEVICE}/status" ] && grep -q "^connected$" "${DEVICE}/status" || continue
108                 echo -n "$(echo "${DEVICE}/edid" | sed -re 's#^.+card[0-9]+-([^/]+).+#\1#; s#-([A-Z]-)?##') "
109                         xxd -c 256 -ps "${DEVICE}/edid" | awk 'ORS=""; /.+/ { print; }'
110                 echo
111         done
112 }
113
114 setup_fp_disper() {
115         $DISPER -l | grep '^display '
116 }
117
118 setup_fp() {
119         local FP="";
120         for M in $FP_METHODS; do
121                 FP="$($M)"
122                 if [ -n "$FP" ]; then
123                         break
124                 fi
125         done
126         if [ -z "$FP" ]; then
127                 echo "Unable to fingerprint display configuration" >&2
128                 return
129         fi
130         echo "$FINGERPRINT" | sort
131 }
132
133 current_cfg_xrandr() {
134         local PRIMARY_SETUP="";
135         if [ -x "$XDPYINFO" ]; then
136                 PRIMARY_SETUP="$($XDPYINFO -ext XINERAMA | awk '/^  head #0:/ {printf $3 $5}')"
137         fi
138         $XRANDR -q | awk -v primary_setup="${PRIMARY_SETUP}" '
139         # display is connected and has a mode
140         /^[^ ]+ connected [^(]/ {
141                 print "output "$1;
142                 if ($3 == "primary") {
143                         print $3
144                         split($4, A, "+")
145                         $4=$5
146                 }
147                 else {
148                         split($3, A, "+");
149                         if (A[1] A[2] "," A[3] == primary_setup)
150                                 print "primary";
151                 }
152                 if (($4 == "left") || ($4 == "right")) {
153                         split(A[1], B, "x");
154                         A[1] = B[2]"x"B[1];
155                 }
156                 print "mode "A[1];
157                 print "pos "A[2]"x"A[3];
158                 if ($4 !~ /^\(/) {
159                         print "rotate "$4;
160                 }
161                 else {
162                         print "rotate normal";
163                 }
164                 next;
165         }
166         # disconnected or disabled displays
167         /^[^ ]+ (dis)?connected / ||
168         /^[^ ]+ unknown connection / {
169                 print "output "$1;
170                 print "off";
171                 next;
172         }'
173 }
174
175 current_cfg_disper() {
176         $DISPER -p
177 }
178
179 common_cfg_xrandr() {
180         $XRANDR -q | awk '
181         # variables:
182         #   output: current output
183         #   outputlist: space sep list of all outputs
184         #   outputarr: array of all connected outputs
185         #   outputarrsize: number of connected outputs
186         #   modelist[800x600]: space sep list of outputs supporting mode
187         # display is connected
188         /^[^ ]+ connected / {
189             output=$1;
190             outputlist=outputlist " " output
191             outputarr[outputarrsize++]=output
192         }
193         # disconnected or disabled displays
194         /^[^ ]+ disconnected / ||
195         /^[^ ]+ unknown connection / {
196             print "output " $1;
197             print "off";
198         }
199         # modes available on a screen
200         /^   [0-9]+x[0-9]+/ {
201             modelist[$1]=modelist[$1] " " output
202         }
203         END {
204             # find common mode with largest screen area
205             for (m in modelist) {
206                 if (modelist[m] == outputlist) {
207                     # calculate area of resolution
208                     split(m, wh, "x");
209                     if (wh[1]*wh[2] >= maxdim) {
210                         maxdim=wh[1]*wh[2]
211                         maxmode=m
212                     }
213                 }
214             }
215             if (maxmode) {
216                 for (i in outputarr) {
217                     print "output " outputarr[i];
218                     print "mode " maxmode;
219                     print "pos 0x0";
220                     if (i > 0) {
221                         print "same-as " outputarr[0]
222                     }
223                 }
224             }
225         }' \
226                 | load_cfg_xrandr -
227 }
228
229 stack_cfg_xrandr() {
230         $XRANDR -q | awk -v stack="${STACK}" '
231         # variables:
232         #   stack: "horizontal" (anything except vertical) or "vertical"
233         #   output: current output
234         #   firstmode: pick first mode after output
235         #   posX,posY: position of the next output
236         BEGIN {
237             posX=posY=0
238         }
239         # display is connected
240         /^[^ ]+ connected / {
241             output=$1;
242             print "output " $1;
243             firstmode=1
244         }
245         # disconnected or disabled displays
246         /^[^ ]+ disconnected / ||
247         /^[^ ]+ unknown connection / {
248             print "output " $1;
249             print "off";
250         }
251         # modes available on a screen, but pick only the first
252         /^   [0-9]+x[0-9]+/ {
253             if (!firstmode) next;
254             firstmode=0
255             # output mode at current virtual desktop pos
256             print "mode " $1;
257             print "pos " posX "x" posY;
258             # calculate position of next output
259             split($1, wh, "x");
260             if (stack == "vertical")
261                 posY += wh[2];
262             else
263                 posX += wh[1];
264         }' \
265                 | load_cfg_xrandr -
266 }
267
268 current_cfg() {
269         $CURRENT_CFG_METHOD;
270 }
271
272 blocked() {
273         local PROFILE="$1"
274         [ ! -x "$PROFILES/$PROFILE/block" ] && return 1
275
276         "$PROFILES/$PROFILE/block" "$PROFILE"
277 }
278
279 config_equal() {
280         local PROFILE="$1"
281         if [ "$(cat "$PROFILES/$PROFILE/config")" = "$(current_cfg)" ]; then
282                 echo "Config already loaded"
283                 return 0
284         else
285                 return 1
286         fi
287 }
288
289 load_cfg_xrandr() {
290         # sed 1: Prefix arguments with "--"
291         # sed 2: Merge arguments into one line per output
292         # sed 3:  * Merge all --off outputs into the first line
293         #         * Place the output with --pos 0x0 on the second line
294         #         * Remaining outputs are appended as they appear
295         #         * Keep everything in hold buffer until the last line
296         # sed 4: Remove empty lines caused by G and H on empty hold buffer
297         # sed 5: Join lines enabling screens in pairs of two (See https://github.com/phillipberndt/autorandr/pull/6)
298         sed 's/^/--/' "$1" | sed -e '
299                 :START
300                 /\n--output/{P;D}
301                 s/\n/ /
302                 N;bSTART' | sed -e '
303                         / --off/{
304                                 G
305                                 # Merge with next line if it contains --off
306                                 s/\n\([^\n]* --off\)/ \1/
307                                 h
308                                 $!d;b
309                         }
310                         / --pos 0x0/{
311                                 G
312                                 # Swap lines 1 and 2 if --off is found
313                                 / --off/ s/^\([^\n]*\)\n\([^\n]*\)/\2\n\1/
314                                 h
315                                 $!d;b
316                         }
317                         H
318                         $!d
319                         x' | sed -e '
320                                 /./ !d' | sed -e '
321                                         /--mode/{ N; s/\n/ /; }
322                                 ' | xargs -L 1 $XRANDR
323 }
324
325 load_cfg_disper() {
326         $DISPER -i < "$1"
327 }
328
329 load() {
330         local PROFILE="$1"
331         local CONF="$PROFILES/$PROFILE/config"
332         local IS_VIRTUAL_PROFILE=`echo "$RESERVED_PROFILE_NAMES" | grep -c "^ $PROFILE "`
333
334         if [ ! -f "$CONF" -a $IS_VIRTUAL_PROFILE -eq 0 ]; then
335                 echo " -> Error: Profile '$PROFILE' does not exist." >&2
336                 return
337         fi
338
339         if [ -x "$PROFILES/preswitch" ]; then
340                 "$PROFILES/preswitch" "$PROFILE"
341         fi
342         if [ -x "$PROFILES/$PROFILE/preswitch" ]; then
343                 "$PROFILES/$PROFILE/preswitch" "$PROFILE"
344         fi
345
346         if [ -f "$CONF" ]; then
347                 echo " -> loading profile $PROFILE"
348                 if [ $IS_VIRTUAL_PROFILE -ne 0 ]; then
349                         echo " -> Warning: Existing profile overrides virtual profile with same name" >&2
350                 fi
351                 $LOAD_METHOD "$CONF"
352         else
353                 # Virtual profiles
354                 if [ $PROFILE = "common" ]; then
355                         echo " -> setting largest common mode in cloned mode"
356                         common_cfg_xrandr
357                 elif [ $PROFILE = "horizontal" ]; then
358                         echo " -> stacking all outputs horizontally at their largest modes"
359                         STACK="horizontal" stack_cfg_xrandr
360                 elif [ $PROFILE = "vertical" ]; then
361                         echo " -> stacking all outputs vertically at their largest modes"
362                         STACK="vertical" stack_cfg_xrandr
363                 fi
364         fi
365
366         if [ -x "$PROFILES/$PROFILE/postswitch" ]; then
367                 "$PROFILES/$PROFILE/postswitch" "$PROFILE"
368         fi
369         if [ -x "$PROFILES/postswitch" ]; then
370                 "$PROFILES/postswitch" "$PROFILE"
371         fi
372 }
373
374 help() {
375         cat <<EOH
376 Usage: $SCRIPTNAME [options]
377
378 -h, --help              get this small help
379 -c, --change            reload current setup
380 -s, --save <profile>    save your current setup to profile <profile>
381 -l, --load <profile>    load profile <profile>
382 -d, --default <profile> make profile <profile> the default profile
383 --force                 force (re)loading of a profile
384 --fingerprint           fingerprint your current hardware setup
385 --config                dump your current xrandr setup
386
387  To prevent a profile from being loaded, place a script call "block" in its
388  directory. The script is evaluated before the screen setup is inspected, and
389  in case of it returning a value of 0 the profile is skipped. This can be used
390  to query the status of a docking station you are about to leave.
391
392  If no suitable profile can be identified, the current configuration is kept.
393  To change this behaviour and switch to a fallback configuration, specify
394  --default <profile>.
395
396  Another script called "postswitch "can be placed in the directory
397  ~/.autorandr as well as in any profile directories: The scripts are executed
398  after a mode switch has taken place and can notify window managers.
399
400  When called by the name "autodisper" or "auto-disper", the script uses "disper"
401  instead of "xrandr" to detect, configure and save the display configuration.
402
403  If xrandr is used, the following virtual configurations are available:
404 ${RESERVED_PROFILE_NAMES}
405
406 EOH
407         exit
408 }
409 # process parameters
410 OPTS=$(getopt -n autorandr -o s:l:d:cfh --long change,default:,save:,load:,force,fingerprint,config,help -- "$@")
411 if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
412 eval set -- "$OPTS"
413
414 while true; do
415         case "$1" in
416                 -c|--change) CHANGE_PROFILE=1; shift ;;
417                 -d|--default) DEFAULT_PROFILE="$2"; shift 2 ;;
418                 -s|--save) SAVE_PROFILE="$2"; shift 2 ;;
419                 -l|--load) LOAD_PROFILE="$2"; shift 2 ;;
420                 -h|--help) help ;;
421                 --force) FORCE_LOAD=1; shift ;;
422                 --fingerprint) setup_fp; exit 0;;
423                 --config) current_cfg; exit 0;;
424                 --) shift; break ;;
425                 *) echo "Error: $1"; exit 1;;
426         esac
427 done
428
429 CURRENT_SETUP="$(setup_fp)"
430
431 if [ -n "$SAVE_PROFILE" ]; then
432         if echo "$RESERVED_PROFILE_NAMES" | grep -q "^ $SAVE_PROFILE "; then
433                 echo "Cannot save current configuration as profile '${SAVE_PROFILE}': This configuration name is a reserved virtual configuration."
434                 exit 1
435         fi
436         echo "Saving current configuration as profile '${SAVE_PROFILE}'"
437         mkdir -p "$PROFILES/$SAVE_PROFILE"
438         echo "$CURRENT_SETUP" > "$PROFILES/$SAVE_PROFILE/setup"
439         $CURRENT_CFG_METHOD > "$PROFILES/$SAVE_PROFILE/config"
440         exit 0
441 fi
442
443 if [ -n "$LOAD_PROFILE" ]; then
444         CHANGE_PROFILE=1 FORCE_LOAD=1 load "$LOAD_PROFILE"
445         exit $?
446 fi
447
448 for SETUP_FILE in $PROFILES/*/setup; do
449         if ! [ -e $SETUP_FILE ]; then
450                 break
451         fi
452         PROFILE="$(basename $(dirname "$SETUP_FILE"))"
453         echo -n "$PROFILE"
454
455         if blocked "$PROFILE"; then
456                 echo " (blocked)"
457                 continue
458         fi
459
460         # This sed command is for compatibility with old versions that did not try
461         # to normalize device names
462         FILE_SETUP="$(sed -re 's#-([A-Z]-)?##g; s#card[0-9]##;' "$PROFILES/$PROFILE/setup")"
463         # Detect the md5sum in fingerprint files created using the old sysfs fingerprinting
464         # If it is detected, output a warning and calculate the legacy variant of the current
465         # setup.
466         if echo "$FILE_SETUP" | grep -Eq "^[^ ]+ [0-9a-f]{32}$"; then
467                 echo -n " (Obsolete fingerprint format. Please update using --save.) "
468
469                 if [ -z "$LEGACY_CURRENT_SETUP" ]; then
470                         LEGACY_CURRENT_SETUP="$(echo "$CURRENT_SETUP" | while read DEVICE EDID; do
471                                 echo -n "${DEVICE} "
472                                 echo -n "${EDID}" | xxd -r -ps | md5sum - | awk '{print $1}'
473                         done)"
474                 fi
475                 FILE_SETUP="$(echo "$FILE_SETUP" | sort)"
476                 if [ "$LEGACY_CURRENT_SETUP" = "$FILE_SETUP" ]; then
477                         CURRENT_SETUP="$LEGACY_CURRENT_SETUP"
478                 fi
479         fi
480
481         if [ "$CURRENT_SETUP" = "$FILE_SETUP" ]; then
482                 echo " (detected)"
483                 if [ "$CHANGE_PROFILE" -eq 1 ]; then
484                         if [ "$FORCE_LOAD" -eq 1 ] || ! config_equal "$PROFILE"; then
485                                 load "$PROFILE"
486                         fi
487                 fi
488                 # found the profile, exit with success
489                 exit 0
490         else
491                 echo ""
492         fi
493 done
494
495 # we did not find the profile, load default
496 if [ -n "$DEFAULT_PROFILE" ]; then
497         echo "No suitable profile detected, falling back to $DEFAULT_PROFILE"
498         load "$DEFAULT_PROFILE"
499 fi
500 exit 1
501
502 # Local Variables:
503 # tab-width: 8
504 # sh-basic-offset: 8
505 # sh-indentation: 8
506 # indent-tabs-mode: t
507 # End: