Using a Bash script to mirror external monitors
Mirror, Mirror
Like many of my colleagues, I use my own laptop to play back presentations at conferences. My Dell Latitude E6430 works perfectly on Ubuntu. However, one critical problem remains: when I connect the device to a projector or a different display, I don't always get the aspect ratio I would prefer. Screen mirroring typically only gives a poor resolution of 1,024x768 pixels, with large black boxes left and right. Unfortunately, my laptop has a 16:9 display with a resolution of 1,600x900 pixels.
Common Denominator
Some research on the topic of screen resolution reveals the root cause: the maximum resolution that my laptop display and the external projector or monitor have in common is 1,024x768 pixels. All higher resolutions are only available on one of the two devices. When mirroring, my Ubuntu system thus automatically chooses the 1,024x768 resolution as the best common option between the two display devices [1].
Because the external monitor typically uses a 16:10 aspect ratio as a normal office display, it only offers 16:10 resolutions via the data display channel (DDC) interface [2]. In addition to its native 16:9 resolution, the laptop display also supports a number of lower resolutions, but many of these resolutions are rarely supported by external displays or projectors.
In many situations, as the speaker, I want the external display or projector to mirror my laptop's screen contents, so I can work on my laptop while letting others watch.
Enter xrandr
Working on my laptop with a resolution of 1,024x768 is not much fun, but luckily, I have another option. The X Server RandR extension (xrandr
) "provides automatic discovery of modes (resolutions, refresh rates, …) [and] the ability to configure output dynamically (resize, rotate, move, …)" [3]. The xrandr configuration tool lets me configure settings for RandR [4] in Linux. The call to xrandr
is pretty bulky:
xrandr --fb 1600x900 --output LVDS1 --mode 1600x900 \ --scale 1x1 --output HDMI3 --same-as LVDS1 --mode 1920x1200 \ --scale-from 1600x900
This command mirrors my laptop screen on the external display and scales the image from the native 1,600x900 resolution on my laptop to a native 1,900x1,200 resolution on the external display.
Although the external display extends the vertical dimension of the image, viewers are unlikely to notice the change. With today's crop of 16:9 projectors, this trick works even better, whether the projector works with 720, 768, or 1,080 lines, the results are usually successful thanks to xrandr scaling.
Automated Happiness
After a few months of experimenting, and an increasing number of xrandr
command lines, I started investigating how to automate the whole process. Unfortunately, the display settings in Ubuntu do not currently support the new scaling options in xrandr 1.3. Although you could retroactively install several additional tools for screen settings, all they do – in the best case – is manage different profiles. At this writing, none of these tools automatically configures the best possible mirroring configuration.
Armed with some knowledge of Bash and sed
, you can solve this problem, however. Listing 1 shows a complete script called automirror.sh
[5]; The script is GPL'd and needs the xrandr
tool and notify-send
as dependencies on Ubuntu. (Both xrandr
and notify-send
are part of the default Ubuntu installation, but they are also available on other Linux distributions.) You can copy the script to ~/bin
or /usr/bin
.
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
Buy this article as PDF
(incl. VAT)