]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blob - autorandr
allow legacy autorandr to be off while connected
[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                 output=$1
166                 print "output "$1;
167                 if ($3 == "primary") {
168                         print $3
169                         split($4, A, "+")
170                         $4=$5
171                 }
172                 else {
173                         split($3, A, "+");
174                         if (A[1] A[2] "," A[3] == primary_setup)
175                                 print "primary";
176                 }
177                 if (($4 == "left") || ($4 == "right")) {
178                         split(A[1], B, "x");
179                         A[1] = B[2]"x"B[1];
180                 }
181                 print "mode "A[1];
182                 print "pos "A[2]"x"A[3];
183                 if ($4 !~ /^\(/) {
184                         print "rotate "$4;
185                 }
186                 else {
187                         print "rotate normal";
188                 }
189                 next;
190         }
191         /  [0-9]+x[0-9]+ .+/ {
192                 if (output) {
193                         for (n=1; n<10; n++) {
194                                 if($n ~ /[0-9]+\.[0-9]+\*/) {
195                                         print "rate " gensub(/(+|\*)/, "", "g", $n);
196                                 }
197                         }
198                 }
199         }
200         # disconnected or disabled displays
201         /^[^ ]+ (dis)?connected / ||
202         /^[^ ]+ unknown connection / {
203                 output=""
204                 print "output "$1;
205                 print "off";
206                 next;
207         }'
208 }
209
210 current_cfg_disper() {
211         $DISPER -p
212 }
213
214 common_cfg_xrandr() {
215         $XRANDR -q | awk '
216         # variables:
217         #   output: current output
218         #   outputlist: space sep list of all outputs
219         #   outputarr: array of all connected outputs
220         #   outputarrsize: number of connected outputs
221         #   modelist[800x600]: space sep list of outputs supporting mode
222         # display is connected
223         /^[^ ]+ connected / {
224             output=$1;
225             outputlist=outputlist " " output
226             outputarr[outputarrsize++]=output
227         }
228         # disconnected or disabled displays
229         /^[^ ]+ disconnected / ||
230         /^[^ ]+ unknown connection / {
231             print "output " $1;
232             print "off";
233         }
234         # modes available on a screen
235         /^   [0-9]+x[0-9]+/ {
236             modelist[$1]=modelist[$1] " " output
237         }
238         END {
239             # find common mode with largest screen area
240             for (m in modelist) {
241                 if (modelist[m] == outputlist) {
242                     # calculate area of resolution
243                     split(m, wh, "x");
244                     if (wh[1]*wh[2] >= maxdim) {
245                         maxdim=wh[1]*wh[2]
246                         maxmode=m
247                     }
248                 }
249             }
250             if (maxmode) {
251                 for (i in outputarr) {
252                     print "output " outputarr[i];
253                     print "mode " maxmode;
254                     print "pos 0x0";
255                     if (i > 0) {
256                         print "same-as " outputarr[0]
257                     }
258                 }
259             }
260         }' \
261                 | load_cfg_xrandr -
262 }
263
264 stack_cfg_xrandr() {
265         $XRANDR -q | awk -v stack="${STACK}" '
266         # variables:
267         #   stack: "horizontal" (anything except vertical) or "vertical"
268         #   output: current output
269         #   firstmode: pick first mode after output
270         #   posX,posY: position of the next output
271         BEGIN {
272             posX=posY=0
273         }
274         # display is connected
275         /^[^ ]+ connected / {
276             output=$1;
277             print "output " $1;
278             firstmode=1
279         }
280         # disconnected or disabled displays
281         /^[^ ]+ disconnected / ||
282         /^[^ ]+ unknown connection / {
283             print "output " $1;
284             print "off";
285         }
286         # modes available on a screen, but pick only the first
287         /^   [0-9]+x[0-9]+/ {
288             if (!firstmode) next;
289             firstmode=0
290             # output mode at current virtual desktop pos
291             print "mode " $1;
292             print "pos " posX "x" posY;
293             # calculate position of next output
294             split($1, wh, "x");
295             if (stack == "vertical")
296                 posY += wh[2];
297             else
298                 posX += wh[1];
299         }' \
300                 | load_cfg_xrandr -
301 }
302
303 current_cfg() {
304         $CURRENT_CFG_METHOD;
305 }
306
307 blocked() {
308         local PROFILE="$1"
309         [ ! -x "$PROFILES/$PROFILE/block" ] && return 1
310
311         "$PROFILES/$PROFILE/block" "$PROFILE"
312 }
313
314 config_equal() {
315         local PROFILE="$1"
316         if [ "$(cat "$PROFILES/$PROFILE/config")" = "$(current_cfg)" ]; then
317                 echo "Config already loaded"
318                 return 0
319         else
320                 return 1
321         fi
322 }
323
324 load_cfg_xrandr() {
325         # sed 1: Prefix arguments with "--"
326         # sed 2: Merge arguments into one line per output
327         # sed 3:  * Merge all --off outputs into the first line
328         #         * Place the output with --pos 0x0 on the second line
329         #         * Remaining outputs are appended as they appear
330         #         * Keep everything in hold buffer until the last line
331         # sed 4: Remove empty lines caused by G and H on empty hold buffer
332         # sed 5: Join lines enabling screens in pairs of two (See https://github.com/phillipberndt/autorandr/pull/6)
333         sed 's/^/--/' "$1" | sed -e '
334                 :START
335                 /\n--output/{P;D}
336                 s/\n/ /
337                 N;bSTART' | sed -e '
338                         / --off/{
339                                 G
340                                 # Merge with next line if it contains --off
341                                 s/\n\([^\n]* --off\)/ \1/
342                                 h
343                                 $!d;b
344                         }
345                         / --pos 0x0/{
346                                 G
347                                 # Swap lines 1 and 2 if --off is found
348                                 / --off/ s/^\([^\n]*\)\n\([^\n]*\)/\2\n\1/
349                                 h
350                                 $!d;b
351                         }
352                         H
353                         $!d
354                         x' | sed -e '
355                                 /./ !d' | sed -e '
356                                         /--mode/{ N; s/\n/ /; }
357                                 ' | xargs -L 1 $XRANDR
358 }
359
360 load_cfg_disper() {
361         $DISPER -i < "$1"
362 }
363
364 load() {
365         local PROFILE="$1"
366         local CONF="$PROFILES/$PROFILE/config"
367         local IS_VIRTUAL_PROFILE=`echo "$RESERVED_PROFILE_NAMES" | grep -c "^ $PROFILE "`
368
369         if [ ! -f "$CONF" -a $IS_VIRTUAL_PROFILE -eq 0 ]; then
370                 echo " -> Error: Profile '$PROFILE' does not exist." >&2
371                 return
372         fi
373
374         if [ -x "$PROFILES/preswitch" ]; then
375                 "$PROFILES/preswitch" "$PROFILE"
376         fi
377         if [ -x "$PROFILES/$PROFILE/preswitch" ]; then
378                 "$PROFILES/$PROFILE/preswitch" "$PROFILE"
379         fi
380
381         if [ -f "$CONF" ]; then
382                 echo " -> loading profile $PROFILE"
383                 if [ $IS_VIRTUAL_PROFILE -ne 0 ]; then
384                         echo " -> Warning: Existing profile overrides virtual profile with same name" >&2
385                 fi
386                 $LOAD_METHOD "$CONF"
387         else
388                 # Virtual profiles
389                 if [ $PROFILE = "common" ]; then
390                         echo " -> setting largest common mode in cloned mode"
391                         common_cfg_xrandr
392                 elif [ $PROFILE = "horizontal" ]; then
393                         echo " -> stacking all outputs horizontally at their largest modes"
394                         STACK="horizontal" stack_cfg_xrandr
395                 elif [ $PROFILE = "vertical" ]; then
396                         echo " -> stacking all outputs vertically at their largest modes"
397                         STACK="vertical" stack_cfg_xrandr
398                 fi
399         fi
400
401         if [ -x "$PROFILES/$PROFILE/postswitch" ]; then
402                 "$PROFILES/$PROFILE/postswitch" "$PROFILE"
403         fi
404         if [ -x "$PROFILES/postswitch" ]; then
405                 "$PROFILES/postswitch" "$PROFILE"
406         fi
407 }
408
409 help() {
410         cat <<EOH
411 Usage: $SCRIPTNAME [options]
412
413 -h, --help              get this small help
414 -c, --change            reload current setup
415 -s, --save <profile>    save your current setup to profile <profile>
416 -l, --load <profile>    load profile <profile>
417 -d, --default <profile> make profile <profile> the default profile
418 --force                 force (re)loading of a profile
419 --fingerprint           fingerprint your current hardware setup
420 --config                dump your current xrandr setup
421
422  To prevent a profile from being loaded, place a script call "block" in its
423  directory. The script is evaluated before the screen setup is inspected, and
424  in case of it returning a value of 0 the profile is skipped. This can be used
425  to query the status of a docking station you are about to leave.
426
427  If no suitable profile can be identified, the current configuration is kept.
428  To change this behaviour and switch to a fallback configuration, specify
429  --default <profile>.
430
431  Another script called "postswitch "can be placed in the directory
432  ~/.autorandr as well as in any profile directories: The scripts are executed
433  after a mode switch has taken place and can notify window managers.
434
435  When called by the name "autodisper" or "auto-disper", the script uses "disper"
436  instead of "xrandr" to detect, configure and save the display configuration.
437
438  If xrandr is used, the following virtual configurations are available:
439 ${RESERVED_PROFILE_NAMES}
440
441 EOH
442         exit
443 }
444 # process parameters
445 OPTS=$(getopt -n autorandr -o s:l:d:cfh --long change,default:,save:,load:,force,fingerprint,config,help -- "$@")
446 if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
447 eval set -- "$OPTS"
448
449 while true; do
450         case "$1" in
451                 -c|--change) CHANGE_PROFILE=1; shift ;;
452                 -d|--default) DEFAULT_PROFILE="$2"; shift 2 ;;
453                 -s|--save) SAVE_PROFILE="$2"; shift 2 ;;
454                 -l|--load) LOAD_PROFILE="$2"; shift 2 ;;
455                 -h|--help) help ;;
456                 --force) FORCE_LOAD=1; shift ;;
457                 --fingerprint) setup_fp; exit 0;;
458                 --config) current_cfg; exit 0;;
459                 --) shift; break ;;
460                 *) echo "Error: $1"; exit 1;;
461         esac
462 done
463
464 CURRENT_SETUP="$(setup_fp)"
465
466 if [ -n "$SAVE_PROFILE" ]; then
467         if echo "$RESERVED_PROFILE_NAMES" | grep -q "^ $SAVE_PROFILE "; then
468                 echo "Cannot save current configuration as profile '${SAVE_PROFILE}': This configuration name is a reserved virtual configuration."
469                 exit 1
470         fi
471         echo "Saving current configuration as profile '${SAVE_PROFILE}'"
472         mkdir -p "$PROFILES/$SAVE_PROFILE"
473         echo "$CURRENT_SETUP" > "$PROFILES/$SAVE_PROFILE/setup"
474         $CURRENT_CFG_METHOD > "$PROFILES/$SAVE_PROFILE/config"
475         exit 0
476 fi
477
478 if [ -n "$LOAD_PROFILE" ]; then
479         CHANGE_PROFILE=1 FORCE_LOAD=1 load "$LOAD_PROFILE"
480         exit $?
481 fi
482
483 for SETUP_FILE in $PROFILES/*/setup; do
484         if ! [ -e $SETUP_FILE ]; then
485                 break
486         fi
487         PROFILE="$(basename $(dirname "$SETUP_FILE"))"
488         echo -n "$PROFILE"
489
490         if blocked "$PROFILE"; then
491                 echo " (blocked)"
492                 continue
493         fi
494
495         # This sed command is for compatibility with old versions that did not try
496         # to normalize device names
497         FILE_SETUP="$(sed -re 's#-([A-Z]-)?##g; s#card[0-9]##;' "$PROFILES/$PROFILE/setup")"
498         # Detect the md5sum in fingerprint files created using the old sysfs fingerprinting
499         # If it is detected, output a warning and calculate the legacy variant of the current
500         # setup.
501         if echo "$FILE_SETUP" | grep -Eq "^[^ ]+ [0-9a-f]{32}$"; then
502                 echo -n " (Obsolete fingerprint format. Please update using --save.) "
503
504                 if [ -z "$LEGACY_CURRENT_SETUP" ]; then
505                         LEGACY_CURRENT_SETUP="$(echo "$CURRENT_SETUP" | while read DEVICE EDID; do
506                                 echo -n "${DEVICE} "
507                                 echo -n "${EDID}" | xxd -r -ps | md5sum - | awk '{print $1}'
508                         done)"
509                 fi
510                 FILE_SETUP="$(echo "$FILE_SETUP" | sort)"
511                 if [ "$LEGACY_CURRENT_SETUP" = "$FILE_SETUP" ]; then
512                         CURRENT_SETUP="$LEGACY_CURRENT_SETUP"
513                 fi
514         fi
515
516         if [ "$CURRENT_SETUP" = "$FILE_SETUP" ]; then
517                 echo " (detected)"
518                 if [ "$CHANGE_PROFILE" -eq 1 ]; then
519                         if [ "$FORCE_LOAD" -eq 1 ] || ! config_equal "$PROFILE"; then
520                                 load "$PROFILE"
521                         fi
522                 fi
523                 # found the profile, exit with success
524                 exit 0
525         else
526                 echo ""
527         fi
528 done
529
530 # we did not find the profile, load default
531 if [ -n "$DEFAULT_PROFILE" ]; then
532         echo "No suitable profile detected, falling back to $DEFAULT_PROFILE"
533         load "$DEFAULT_PROFILE"
534 fi
535 exit 1
536
537 # Local Variables:
538 # tab-width: 8
539 # sh-basic-offset: 8
540 # sh-indentation: 8
541 # indent-tabs-mode: t
542 # End: