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