]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blob - autorandr
Enable outputs in pairs of two (Follow-up to commit f4cce4d)
[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 5: Join lines enabling screens in pairs of two (See https://github.com/phillipberndt/autorandr/pull/6)
286         sed 's/^/--/' "$1" | sed -e '
287                 :START
288                 /\n--output/{P;D}
289                 s/\n/ /
290                 N;bSTART' | sed -e '
291                         / --off/{
292                                 G
293                                 # Merge with next line if it contains --off
294                                 s/\n\([^\n]* --off\)/ \1/
295                                 h
296                                 $!d;b
297                         }
298                         / --pos 0x0/{
299                                 G
300                                 # Swap lines 1 and 2 if --off is found
301                                 / --off/ s/^\([^\n]*\)\n\([^\n]*\)/\2\n\1/
302                                 h
303                                 $!d;b
304                         }
305                         H
306                         $!d
307                         x' | sed -e '
308                                 /./ !d' | sed -e '
309                                         /--mode/{ N; s/\n/ /; }
310                                 ' | xargs -L 1 $XRANDR
311 }
312
313 load_cfg_disper() {
314         $DISPER -i < "$1"
315 }
316
317 load() {
318         local PROFILE="$1"
319         local CONF="$PROFILES/$PROFILE/config"
320         local IS_VIRTUAL_PROFILE=`echo "$RESERVED_PROFILE_NAMES" | grep -c "^ $PROFILE "`
321
322         if [ ! -f "$CONF" -a $IS_VIRTUAL_PROFILE -eq 0 ]; then
323                 echo " -> Error: Profile '$PROFILE' does not exist." >&2
324                 return
325         fi
326
327         if [ -x "$PROFILES/preswitch" ]; then
328                 "$PROFILES/preswitch" "$PROFILE"
329         fi
330         if [ -x "$PROFILES/$PROFILE/preswitch" ]; then
331                 "$PROFILES/$PROFILE/preswitch" "$PROFILE"
332         fi
333
334         if [ -f "$CONF" ]; then
335                 echo " -> loading profile $PROFILE"
336                 if [ $IS_VIRTUAL_PROFILE -ne 0 ]; then
337                         echo " -> Warning: Existing profile overrides virtual profile with same name" >&2
338                 fi
339                 $LOAD_METHOD "$CONF"
340         else
341                 # Virtual profiles
342                 if [ $PROFILE = "common" ]; then
343                         echo " -> setting largest common mode in cloned mode"
344                         common_cfg_xrandr
345                 elif [ $PROFILE = "horizontal" ]; then
346                         echo " -> stacking all outputs horizontally at their largest modes"
347                         STACK="horizontal" stack_cfg_xrandr
348                 elif [ $PROFILE = "vertical" ]; then
349                         echo " -> stacking all outputs vertically at their largest modes"
350                         STACK="vertical" stack_cfg_xrandr
351                 fi
352         fi
353
354         if [ -x "$PROFILES/$PROFILE/postswitch" ]; then
355                 "$PROFILES/$PROFILE/postswitch" "$PROFILE"
356         fi
357         if [ -x "$PROFILES/postswitch" ]; then
358                 "$PROFILES/postswitch" "$PROFILE"
359         fi
360 }
361
362 help() {
363         cat <<EOH
364 Usage: $SCRIPTNAME [options]
365
366 -h, --help              get this small help
367 -c, --change            reload current setup
368 -s, --save <profile>    save your current setup to profile <profile>
369 -l, --load <profile>    load profile <profile>
370 -d, --default <profile> make profile <profile> the default profile
371 --force                 force (re)loading of a profile
372 --fingerprint           fingerprint your current hardware setup
373 --config                dump your current xrandr setup
374
375  To prevent a profile from being loaded, place a script call "block" in its
376  directory. The script is evaluated before the screen setup is inspected, and
377  in case of it returning a value of 0 the profile is skipped. This can be used
378  to query the status of a docking station you are about to leave.
379
380  If no suitable profile can be identified, the current configuration is kept.
381  To change this behaviour and switch to a fallback configuration, specify
382  --default <profile>.
383
384  Another script called "postswitch "can be placed in the directory
385  ~/.autorandr as well as in any profile directories: The scripts are executed
386  after a mode switch has taken place and can notify window managers.
387
388  When called by the name "autodisper" or "auto-disper", the script uses "disper"
389  instead of "xrandr" to detect, configure and save the display configuration.
390
391  If xrandr is used, the following virtual configurations are available:
392 ${RESERVED_PROFILE_NAMES}
393
394 EOH
395         exit
396 }
397 # process parameters
398 OPTS=$(getopt -n autorandr -o s:l:d:cfh --long change,default:,save:,load:,force,fingerprint,config,help -- "$@")
399 if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
400 eval set -- "$OPTS"
401
402 while true; do
403         case "$1" in
404                 -c|--change) CHANGE_PROFILE=1; shift ;;
405                 -d|--default) DEFAULT_PROFILE="$2"; shift 2 ;;
406                 -s|--save) SAVE_PROFILE="$2"; shift 2 ;;
407                 -l|--load) LOAD_PROFILE="$2"; shift 2 ;;
408                 -h|--help) help ;;
409                 --force) FORCE_LOAD=1; shift ;;
410                 --fingerprint) setup_fp; exit 0;;
411                 --config) current_cfg; exit 0;;
412                 --) shift; break ;;
413                 *) echo "Error: $1"; exit 1;;
414         esac
415 done
416
417 CURRENT_SETUP="$(setup_fp)"
418
419 if [ -n "$SAVE_PROFILE" ]; then
420         if echo "$RESERVED_PROFILE_NAMES" | grep -q "^ $SAVE_PROFILE "; then
421                 echo "Cannot save current configuration as profile '${SAVE_PROFILE}': This configuration name is a reserved virtual configuration."
422                 exit 1
423         fi
424         echo "Saving current configuration as profile '${SAVE_PROFILE}'"
425         mkdir -p "$PROFILES/$SAVE_PROFILE"
426         echo "$CURRENT_SETUP" > "$PROFILES/$SAVE_PROFILE/setup"
427         $CURRENT_CFG_METHOD > "$PROFILES/$SAVE_PROFILE/config"
428         exit 0
429 fi
430
431 if [ -n "$LOAD_PROFILE" ]; then
432         CHANGE_PROFILE=1 FORCE_LOAD=1 load "$LOAD_PROFILE"
433         exit $?
434 fi
435
436 for SETUP_FILE in $PROFILES/*/setup; do
437         if ! [ -e $SETUP_FILE ]; then
438                 break
439         fi
440         PROFILE="$(basename $(dirname "$SETUP_FILE"))"
441         echo -n "$PROFILE"
442
443         if blocked "$PROFILE"; then
444                 echo " (blocked)"
445                 continue
446         fi
447
448         FILE_SETUP="$(cat "$PROFILES/$PROFILE/setup")"
449         if [ "$CURRENT_SETUP" = "$FILE_SETUP" ]; then
450                 echo " (detected)"
451                 if [ "$CHANGE_PROFILE" -eq 1 ]; then
452                         if [ "$FORCE_LOAD" -eq 1 ] || ! config_equal "$PROFILE"; then
453                                 load "$PROFILE"
454                         fi
455                 fi
456                 # found the profile, exit with success
457                 exit 0
458         else
459                 echo ""
460         fi
461 done
462
463 # we did not find the profile, load default
464 if [ -n "$DEFAULT_PROFILE" ]; then
465         echo "No suitable profile detected, falling back to $DEFAULT_PROFILE"
466         load "$DEFAULT_PROFILE"
467 fi
468 exit 1
469
470 # Local Variables:
471 # tab-width: 8
472 # sh-basic-offset: 8
473 # sh-indentation: 8
474 # indent-tabs-mode: t
475 # End: