Aus Linux-Magazin 06/2015

Externe Monitore per Bash-Skript spiegeln

© fifoprod, 123RF

Auf Konferenzen ist Schlomo Schapiro ein häufig anzutreffender Redner. Es nervt ihn aber bis heute, wenn externe Monitore oder Beamer die Auflösung seines Laptops verhunzen. Ein selbst geschriebenes Bash-Skript, das sich auf Xrandr stützt, hilft ihm aus der Bredouille.

Wie viele meiner Kollegen spiele ich auf Konferenzen Präsentationen von meinem eigenen Laptop ab. Mein Dell Latitude E6430 funktioniert unter Ubuntu tadellos. Doch es bleibt ein kritischer Moment: Schließe ich das Gerät an einen Beamer oder anderen Monitor an, taucht das Bild nicht immer gleich in der gewünschten Form auf. Die Bildschirmspiegelung liefert meist nur eine magere Auflösung von 1024 mal 768 Pixeln zurück, mit dicken schwarzen Trauerrändern links und rechts. Dabei bringt der Laptop einen Bildschirm im 16:9-Format mit, der mit 1600 mal 900 Pixeln auflöst.

Der größte gemeinsame Nenner

Ein kurze Recherche zum Thema Bildschirmauflösungen enthüllt die Ursache: 1024 mal 768 Pixel ist die maximale Auflösung, die mein Laptopdisplay und der extern angeschlossene Beamer oder Monitor gemeinsam haben. Alle höheren Auflösungen sind nur auf einem der beiden Geräte verfügbar. Beim Spiegeln wählt mein Ubuntu daher automatisch die 1024-mal-768-Auflösung als größten gemeinsamen Nenner zwischen den beiden Anzeigegeräten [1].

Da der externe Monitor als typischer Büromonitor ein 16:10-Format verwendet, bietet er über die DDC-Schnittstelle [2] auch nur 16:10-Auflösungen an. Das Laptopdisplay bringt hingegen neben seiner nativen 16:9-Auflösung auch noch einige niedrigere Auflösungen mit, die aber kaum ein Bildschirm oder Beamer zu beherrschen scheint.

In vielen Situationen will ich als Vortragender schlicht, dass der externe Monitor oder Beamer den Bildschirminhalt des Laptops spiegelt. Egal ob es sich um einen Vortrag oder um Pairworking handelt: In der Regel geht es darum, dass ich mit dem Laptop arbeiten kann, während andere dabei zusehen.

Auftritt Xrandr

Mit einer 1024-mal-768-Auflösung macht die Arbeit am Laptop wenig Spaß, unter Linux gibt es mit Xrandr glücklicherweise ein Konfigurationstool für Randr [3], das Abhilfe schafft. Der etwas längere »xrandr« -Aufruf

xrandr --fb 1600x900 --output LVDS1  --mode 1600x900 --scale 1x1 --output HDMI3 --same-as LVDS1 --mode 1920x1200 --scale-from 1600x900

spiegelt meinen Laptopbildschirm auf den externen Monitor und skaliert dabei das Bild von der nativen 1600-mal-900-Auflösung auf dem Laptop zu einer nativen 1900-mal-1200-Auflösung auf dem externen Monitor.

Dieser streckt das Bild aufgrund der 16:9-auf-16:10-Umwandlung leicht in der Vertikalen, was aber in den meisten Fällen niemandem auffällt. Bei den mittlerweile weit verbreiteten 16:9-Beamern funktioniert der Trick sogar noch besser. Egal ob der Beamer mit 720, 768 oder 1080 Zeilen arbeitet, dank der Xrandr-Skalierung passt das Ergebnis immer.

Automatisch glücklich

Nach einigen Monaten und einer steigenden Zahl an »xrandr« -Befehlszeilen stellte sich die Frage nach einer Automatisierung des ganzen Vorgangs. Leider unterstützt die Monitoreinstellung von Ubuntu die neuen Skalierungsoptionen von Xrandr 1.3 noch nicht. Zwar lässt sich eine ganze Reihe weiterer Tools für die Bildschirmeinstellungen nachinstallieren, diese verwalten jedoch im besten Fall nur unterschiedliche Profile. Keines der Werkzeuge stellte automatisch eine optimale Spiegelkonfiguration ein (Stand Juni 2014).

Mit etwas Bash- und Sed-Wissen lässt sich das Problem allerdings lösen. Das Ergebnis heißt »automirror.sh« [4], Listing 1 zeigt das komplette Skript. Es steht unter der GPL und benötigt als Abhängigkeiten unter Ubuntu das »xrandr« -Tool und »notify-send« . Beide sind Teil der Standardinstallation von Ubuntu, aber auch auf anderen Linux-Distributionen verfügbar. Das Skript hat ansonsten keine weiteren Abhängigkeiten und lässt sich nach »~/bin« oder »/usr/bin« kopieren.

Listing 1

automirror.sh

01 #!/bin/bash
02 set -e -E -u
03
04 XRANDR_STATUS_PROGRAM=${XRANDR_STATUS_PROGRAM:-xrandr}
05 XRANDR_SET_PROGRAM=${XRANDR_SET_PROGRAM: -xrandr}
06
07 PRIMARY_DISPLAY=${AUTOMIRROR_PRIMARY_DISPLAY:-LVDS1}
08 NOTIFY_SEND=( ${AUTOMIRROR_NOTIFY_COMMAND: -notify-send -a automirror -i automirror "Automatic Mirror Configuration"} )
09
10 # force called programs to english output
11 LANG=C LC_ALL=C
12
13 function die {
14     echo 1>&2 "$*"
15     exit 10
16 }
17
18 function get_display_resolution {
19     local find_display="$1" ; shift
20     local display_list="$1"
21     while read display width_mm height_mm width height ; do
22         if [[ "$display" == "$find_display" ]] ; then
23             echo ${width}x${height}
24             return 0
25         fi
26     done <<<"$display_list"
27     die "Could not determine resolution for '$find_display'. Display Data:
28 $display_list"
29 }
30
31 function get_highest_display {
32     local display_list="$1" ; shift
33     local data=( $(sort -r -n -k 5 <<<"$display_list") )
34     echo $data
35 }
36
37 xrandr_current="$($XRANDR_STATUS_PROGRAM)"
38
39 # find connected displays by filtering those that are connected and have a size set in millimeters (mm)
40 connected_displays=( $(sed -n -e 's/^\(.*\) connected.*mm$/\1/p' <<<"$xrandr_current") )
41
42 # See http://stackoverflow.com/a/1252191/2042547 for how to use sed to replace newlines
43 # display_list is a list of displays with their maximum/optimum pixel and physical dimensions
44 # thanks to the first sed I know that here is only a SINGLE space
45 display_list="$(sed ':a;N;$!ba;s/\n   / /g'<<<"$xrandr_current" | sed -n -e 's/^\([a-zA-Z0-9_-]\+\) connected.* \([0-9]\+\)mm.* \([0-9]\+\)mm \([0-9]\+\)x\([0-9]\+\).*$/\1 \2 \3 \4 \5/p' )"
46 : connected_displays: ${connected_displays[@]}
47 : display_list: "$display_list"
48
49 if [[ -z "$display_list" ]] ; then
50     die "Could not find any displays connected. XRANDR output:
51 $xrandr_current"
52 fi
53
54 # if the primary display is NOT connected then use the highest display as primary
55 if [[ "${connected_displays[*]}" != *$PRIMARY_DISPLAY* ]] ; then
56     PRIMARY_DISPLAY=$(get_highest_display "$display_list")
57 fi
58 frame_buffer_resolution=$(get_display_resolution $PRIMARY_DISPLAY "$display_list")
59
60 : $frame_buffer_resolution
61
62 xrandr_set_args=( --fb $frame_buffer_resolution )
63 notify_string=""
64 if (( ${#connected_displays[@]} == 1 )) ; then
65     xrandr_set_args+=( --output $connected_displays --mode $frame_buffer_resolution --scale 1x1 )
66     notify_string="$connected_displays reset to $frame_buffer_resolution"
67 else
68     other_display_list="$(grep -v ^$PRIMARY_DISPLAY <<<"$display_list")"
69     $XRANDR_SET_PROGRAM $(while read display junk ; do echo " --output $display --scale 1x1 --off" ; done <<<"$other_display_list")
70     xrandr_set_args+=( --output $PRIMARY_DISPLAY --mode $frame_buffer_resolution --scale 1x1 )
71     notify_string="$PRIMARY_DISPLAY is primary at $frame_buffer_resolution"
72     while read display junk ; do
73         mode="$(get_display_resolution $display "$other_display_list")"
74         xrandr_set_args+=( --output $display --same-as $PRIMARY_DISPLAY --mode "$mode" --scale-from $frame_buffer_resolution  )
75         notify_string="$notify_string\n$display is scaled mirror at $mode"
76     done <<<"$other_display_list"
77 fi
78
79 #logger -s -t "$0" -- Running $XRANDR_SET_PROGRAM "${xrandr_set_args[@]}"
80
81 $XRANDR_SET_PROGRAM "${xrandr_set_args[@]}"
82 ret=$?
83 "${NOTIFY_SEND[@]}" "$notify_string"
84 exit $ret

Erkennungsdienst

Der Ablauf im Skript ist einfach und folgt dem menschlichen Entscheidungsweg. Die Zeilen 4 bis 8 setzen zunächst globale Umgebungsvariablen. Die darin aufgerufenen externen Programme lassen sich dank der Variablen einfach ersetzen, wenn es darum geht, Tests zu konstruieren. In der Variablen »connected_displays« (Zeile 40) speichert das Skript die aktiven Bildschirmausgänge und ihre Auflösung in Millimetern, »display_list« (Zeile 45) bestimmt für die aktiven Bildschirmausgänge die optimale Auflösung und die Höhe in Zeilen. Xrandr sortiert die Liste der Auflösungen derart, dass die erste Auflösung meist auch die empfohlene ist. Der hässliche »sed« -Ausdruck spart viele Zeilen Bash und sei mir verziehen.

Die Zeilen 55 bis 57 legen ein primäres Display fest: Entweder handelt es sich um den Laptopbildschirm, wenn dieser aktiv ist, oder um den Bildschirm mit den meisten Zeilen.

Hängt der Primärbildschirm nicht am LVDS1-Anschluss, wie es Zeile 7 annimmt, kann der Benutzer über die Umgebungsvariable »PRIMARY_DISPLAY« auch einen anderen Anschluss einrichten. Für dieses Display definiert das Skript dann die virtuelle Bildschirmauflösung für den Framebuffer (Zeile 58), die restlichen Displays landen in der »other_display_list« (Zeile 68).

Die While-Schleife ab Zeile 72 erzeugt die »xrandr« -Befehlszeilen mit den nativen Display-Auflösungen und ermittelt die Modi über die Funktion »get_display_resolution()« aus Zeile 18. Zeile 74 gleicht dann die Skalierung des »PRIMARY_DISPLAY« mit jener der restlichen Displays ab. Zeile 75 generiert aus den ermittelten Auflösungen einen String für die Desktop-Benachrichtigung. In Zeile 81 konfiguriert Xrandr sämtliche Bildschirme, und »notify-send« informiert den Benutzer in Zeile 83 über die Auflösungsentscheidungen (Abbildung 1).

Abbildung 1: Über »notify-send« informiert das Skript seine Desktop-Nutzer über Änderungen an der Bildschirmauflösung.

Abbildung 1: Über »notify-send« informiert das Skript seine Desktop-Nutzer über Änderungen an der Bildschirmauflösung.

Testbetrieb

Immer mal wieder treffe ich auf neue Kombination von internen und externen Anzeigegeräten, die mich dazu zwingen, das Skript anzupassen. Um dabei nicht aus Versehen ein funktionierendes Szenario zu zerstören, enthält das Github-Projekt zu Automirror [4] Unittests für alle Szenarien.

Das »runtests.sh« -Skript aus Listing 2 iteriert in den Zeile 19 bis 32 über die Testfälle im »testdata/« -Verzeichnis und führt für jeden einzelnen Testfall die »do_test« -Funktion in den Zeilen 4 bis 15 aus. Ein Testfall bestehlt aus einer Textdatei mit der Ausgabe von »xrandr« für eine bestimmte Konfiguration sowie aus einer Antwortdatei (»*_result.txt« ) mit den zu erwartenden Xrandr-Aufrufen.

Listing 2

runtests.sh

01 #!/bin/bash
02 set -e -E -u
03
04 function do_test {
05     local xrandr_output="$1" ; shift
06     local script_output="$1" ; shift
07     res="$( XRANDR_STATUS_PROGRAM="cat $xrandr_output" XRANDR_SET_PROGRAM=echo AUTOMIRROR_NOTIFY_COMMAND=: ./automirror.sh)"
08     if [[ "$res" == "$(< $script_output)" ]] ; then
09         return 0
10     else
11         echo "Difference:"
12         diff -u --label EXPECTED_RESULT --label ACTUAL_RESULT -- $script_output - <<<"$res"
13         return 1
14     fi
15 }
16
17 failed=0
18
19 for test_case in testdata/*.txt ; do
20     [[ "$test_case" == *result.txt ]] && continue
21     script_output=${test_case%.txt}_result.txt
22     if [[ "$script_output" && -r "$script_output" ]] ; then
23         if do_test "$test_case" "$script_output" ; then
24            echo "OK $test_case"
25         else
26             let failed++ 1
27             echo "FAILED $test_case"
28         fi
29     else
30         echo "Missing test result file '$script_output'"
31     fi
32 done
33
34 if (( failed == 0 )) ; then
35     exit 0
36 else
37     echo $failed TESTS FAILED
38     exit 1
39 fi

Für den Testdurchlauf ersetzen »cat« und »echo« in Zeile 7 mit Hilfe der Umgebungsvariablen »XRANDR_STATUS_PROGRAM« und »XRANDR_SET_PROGRAM« die Programme »xrandr« und »notify-send« . Das »automirror.sh« -Skript muss sich dann gegen alle Testfälle beweisen, andernfalls zeigt »runtests.sh« den fehlgeschlagenen Test und das erwartete Ergebnis.

Im Paketformat

Besteht das Skript alle Tests, baut das Makefile noch ein passendes Debian-Paket zur Installation [5]. Das zugehörige Debian-Changelog und die Version erzeugt das Tool »git-dch« automatisch aus den Git-Commits.

Um also eine neue Konfiguration zu unterstützen, entwerfe ich zuerst einen neuen Testfall und passe anschließend das Skript so lange an, bis die alten und neuen Tests funktionieren. Ein »git commit -a« und ein »make release deb« produzieren schließlich ein neues Debian-Paket, das umgehend in Githubs Software-Repository wandert.

Der Autor

Schlomo Schapiro http://go.schapiro.org/schlomo ist Systemarchitekt und Open-Source-Evangelist bei Immobilienscout24 in Berlin. Er ist Autor diverser Open-Source-Projekte und engagiert sich für ein agiles Mindset und eine Dev-Ops-orientierte Kultur in der IT.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 3 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben