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