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