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