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