-
-
Save kurahaupo/216a37ee0c6b4efaf158 to your computer and use it in GitHub Desktop.
Script using "xrandr" to configure multi-head display
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
die() { echo "$*" >&2 ; exit 1 ; } | |
declare -a R=( normal right inverted left ) # rotation descriptions | |
declare -A S | |
declare -A DX | |
declare -a EX | |
# S is all the information we know about each display | |
# keys in S[] -- concatenate the following hierarchical parts: | |
# .0 (1 2 etc) display number | |
# .n dname (device name, DX subscript) | |
# .e enabled | |
# .h height (after rotation) | |
# .m modes available on this display | |
# (also, without anything concatenated, the number of modes for this display) | |
# .0 (1 2 etc) mode number | |
# .h height of screen in this mode | |
# .w width of screen in this mode | |
# .r rotation (list R, above; multiple of 90° clockwise) | |
# .uh height *before* rotation | |
# .uw width *before* rotation | |
# .w width (after rotation) | |
# .x position offset | |
# .y position offset | |
# DX is the mapping from display-name to display-number | |
# EX is the list of display-numbers for the displays which are enabled | |
# | |
# As an example, consider the internal screen on a laptop, which is typically LVDS1 | |
# Since it's the first one output by xrandr, it is display number 0. | |
# It will almost always be enabled, and almost never rotated. | |
# So: | |
# DX[LVDS1] is 0 -- it's display #0 | |
# EX[@] includes 0 -- display #0 is enabled | |
# S[0.e] is true -- it's enabled | |
# S[0.uw] and S[0.m.0.w] are the physical width of the screen in pixels } the modeline tagged with "+" is the preferred one, and | |
# S[0.uh] and S[0.m.0.h] are the physical height of the screen in pixels } afaik it's always the first modeline #0 | |
# S[0.r] is 0 -- not rotated | |
# S[0.h] & S[0.w] are copied S[0.uh] & S[0.uw], though which is which depends on the rotation | |
# S[0.x] and S[0.y] are the offsets within the view-frame; | |
# for LVDS1 the offsets start out as 0,0 but if you assign negative offsets to the other monitors, all the offsets will all be adjusted so as to make the minimum x-offset 0 and the minimum y-offset 0. | |
# If you want mirroring, simply set both displays to the same x & y offset. | |
maxw=8192 maxh=6144 # these are only defaults in case xrandr does not output suitable values | |
dnum=-1 mnum=-1 | |
while | |
IFS= \ | |
read -r line | |
do | |
IFS=$' \t' read -r -a words <<<"$line" # array of words split on whitespace, ignoring leading & trailing whitespace | |
case $line in | |
Screen*) | |
# Grab values from "maximum WWW x HHH" if present | |
for (( i=${#words[@]}-2 ; i>=0 ; --i )) do | |
[[ ${words[i]} = maximum && ${words[i+2]} = x ]] && { | |
(( maxw = words[i+1], maxh = words[i+3] )) | |
printf 'Maximum framebuffer size %d x %d\n' $maxw $maxh | |
break # found it, don't need to keep scanning | |
} | |
done | |
;; | |
' '[vh]:*) ;; # extra info about current modeline (ignored) | |
' '*) | |
((dnum>=0)) || continue | |
[[ ${words[0]} = ?*x?* && ${words[0]} != *[!0-9x]* && ${words[0]} != *x*x* ]] || die "invalid modeline '$line' for $dnum" | |
IFS=x read w h _ <<<"${words[0]}" # NNNxNNN | |
(( S[$dnum.m.$mnum.w] = w, | |
S[$dnum.m.$mnum.h] = h, | |
S[$dnum.m] = ++mnum )) | |
# Pick the line recommended by xrandr (tagged with a "+") | |
[[ $line = *+* ]] && | |
(( S[$dnum.uw] = w, | |
S[$dnum.uh] = h )) | |
;; | |
*' connected '*) | |
dname=${words[0]} | |
(( DX[$dname] = ++dnum )) | |
EX+=($dnum) | |
S[$dnum.n]=$dname | |
S[$dnum.e]=1 | |
(( S[$dnum.x] = S[$dnum.y] = 0 )) | |
mnum=0 | |
;; | |
*' '*'connect'*) | |
dname=${words[0]} | |
(( DX[$dname] = ++dnum )) | |
S[$dnum.n]=$dname | |
mnum=0 | |
;; | |
esac | |
done < <(xrandr) | |
for dnum in ${DX[@]} | |
do | |
if (( S[$dnum.e] )) | |
then | |
printf 'Display #%u is %s using %ux%u (supporting:' "$dnum" "${S[$dnum.n]}" "${S[$dnum.uw]}" "${S[$dnum.uh]}" | |
for ((mnum=0;mnum<S[$dnum.m];mnum++)) do | |
printf ' %ux%u' "${S[$dnum.m.$mnum.w]}" "${S[$dnum.m.$mnum.h]}" | |
done | |
printf ')\n' | |
else | |
printf 'Display #%u is %s (disconnected)\n' "$dnum" "${S[$dnum.n]}" | |
fi | |
done | |
dnum0=${EX[0]} | |
[[ ${S[$dnum0.n]} = LVDS1 ]] || | |
die "First adaptor $dnum0 is ${S[$dnum0.n]} rather than LVDS1; please check source code in $0, line $LINENO" | |
# So you're reading this because the error message above was displayed. | |
# Lots of this code assumes that the first enabled device is the | |
# laptop's internal display; if it's not then lots of other stuff will | |
# go wrong... | |
dry_run=false | |
verbose=false | |
xds=false | |
make_primary=left | |
# rotate DISPLAY-NUMBER ROTATION | |
# The DISPLAY-NUMBER should be obtained from ${DX[@]} or ${EX[@]}. | |
# The ROTATION can be given as digits 0...3, or as 'upright', 'right', 'inverted' or 'left' | |
# (only the first letter or digit is used) | |
rotate() { | |
local dnum=${DX[$1]-${1:-0}} | |
[[ -n $dnum && -n ${S[$dnum.n]} ]] || die "No display number or name '$1'" | |
shift | |
# adopt new rotation, if given | |
if [[ $1 ]] | |
then | |
local u=0 r=1 i=2 l=3 # upright,right,inverted,left | |
(( S[$dnum.r] = ${1:0:1} )) # only look at the first letter of the word | |
fi | |
local z | |
# If the rotation is 0° or 180° (r is even) then the usable width & height | |
# are the corresponding device width & height; if the rotation is 90° or | |
# 270° (r is odd) then they are swapped: the usable width is the device | |
# height and the usable height is the device width. | |
(( z = S[$dnum.r] % 2, | |
S[$dnum.w] = ( z ? S[$dnum.uh] : S[$dnum.uw] ), | |
S[$dnum.h] = ( z ? S[$dnum.uw] : S[$dnum.uh] ) )) | |
} | |
for dnum in ${EX[@]} ; do rotate $dnum 0 ; done | |
# Disable all displays except the first | |
setup_single() { | |
for dnum in ${EX[@]:1} | |
do | |
(( S[$dnum.e] = 0 )) | |
done | |
EX=(${EX[0]}) # truncate list now that only one display is active | |
echo "Using single-screen layout on ${S[$EX.n]}" | |
use_defaults=0 | |
} | |
# Disable all displays except the first & second | |
setup_only2() { | |
for dnum in ${EX[@]:2} | |
do | |
(( S[$dnum.e]=0 )) | |
done | |
EX=( ${EX[0]} ${EX[1]} ) # truncate list now that only two displays are active | |
use_defaults=0 | |
} | |
# Set all displays to overlap | |
setup_mirror() { | |
for dnum in ${EX[@]:1} | |
do | |
(( S[$dnum.x]=S[$EX.x], S[$dnum.y]=S[$EX.y] )) | |
done | |
echo "Mirroring ${#EX[@]} displays" | |
use_defaults=0 | |
} | |
# Set up for home docking station, assuming it's asked for or pragmatically | |
# guessed, with the built-in display (LVDS1) to right of and | |
# down 680px from the external monitor (any, but usually DVI1). | |
setup_home() { | |
dnum1=${EX[1]:?'second display not connected'} | |
(( S[$dnum1.e]=1, S[$dnum0.x]=S[$dnum1.w], S[$dnum0.y]=680 )) | |
echo "Using dual-screen 'H' layout with elevated external monitor on left" | |
use_defaults=0 | |
} | |
# Set up for a general-purpose twin display, with the external monitor (any) to | |
# right of, and up 256px from the built-in display (LVDS1) | |
setup_twin() { | |
dnum1=${EX[1]:?'second display not connected'} | |
#rotate $dnum1 1 | |
(( S[$dnum1.x]=S[$dnum0.w], S[$dnum0.y]=256 )) | |
echo "Using dual-screen 'W' layout with elevated laptop & rotated external monitor" | |
use_defaults=0 | |
} | |
# Projector (any but usually VGA1) above built-in display (LVDS1) | |
setup_projector() { | |
dnum1=${EX[1]:?'second display not connected'} | |
(( S[$dnum1.e]=1, S[$dnum1.y]=S[$dnum0.y]-S[$dnum1.h], S[$dnum1.x]=0 )) | |
echo "Using dual-screen 'P' layout with projector above laptop" | |
use_defaults=0 | |
} | |
# LVDS1 to right of Ext but overlapping by 400px, and down 560px | |
setup_demo5() { | |
dnum1=${EX[1]:?'second display not connected'} | |
(( S[$dnum1.e]=1, S[$dnum0.x]=S[$dnum1.w]-400, S[$dnum0.y]=560 )) | |
setup_only2 | |
echo "Using example overlapping dual-screen 'P' layout" | |
use_defaults=0 | |
} | |
# turn off internal (LVDS1) and only use first external | |
setup_demo6() { | |
dnum1=${EX[1]:?'second display not connected'} | |
(( S[$dnum1.e]=1, S[0.e]=0 )) | |
setup_only2 | |
echo "Using only external ${EX[1]} screen" | |
use_defaults=0 | |
} | |
use_defaults=1 | |
while (($#)) ; do | |
case ${1#"${1%%[^-]*}"} in | |
# preset groups | |
([0M]|mirror) setup_mirror ;; | |
([1A]|away) setup_single ;; | |
([2W]|work) setup_twin ;; | |
([3H]|home) setup_home ;; | |
([4P]|proj) setup_projector ;; | |
([5Q]|plus) setup_demo5 ;; | |
([6E]|ext) setup_demo6 ;; | |
# rotate individual screens | |
(u|u[0-9]) rotate "${EX[${1#*u}+0]}" 0 ;; # upright | |
(r|r[0-9]) rotate "${EX[${1#*r}+0]}" 1 ;; # rotated right | |
(i|i[0-9]) rotate "${EX[${1#*i}+0]}" 2 ;; # inverted | |
(l|l[0-9]) rotate "${EX[${1#*l}+0]}" 3 ;; # left-rotated | |
(r*-*=[uril]*) rotate "${EX[${1#*r*-}+0]}" "${1#*=}" ;; | |
(r*-[uril]*=*) rotate "${EX[${1#*=}+0]}" "${1#*r*-}" ;; | |
# driver initialisation | |
(xds|init) xds=true ;; | |
# choose which screen is "primary" and thus holds the window manager's menu bar etc | |
(p|p*=first) make_primary=${EX[0]} ;; | |
(p[0-9]) make_primary=${EX[${1#p}+0]} ;; | |
(pl|p*=left) make_primary=left ;; | |
(pr|p*=right) make_primary=right ;; | |
(pt|p*=top) make_primary=top ;; | |
(pb|p*=bottom) make_primary=bottom ;; | |
(p[0-9]|p*=[0-9]) make_primary=${1##*[p=]} ;; | |
(p*) make_primary=${1##*[p=]} ; make_primary=${DX[$make_primary]?"No device '$make_primary'"} ;; | |
# debuggins | |
(n|dryrun|dry-run|no-act) dry_run=true ;; | |
(notdryrun|not-dry-run|act) dry_run=false ;; | |
(v|verbose) verbose=true ;; | |
(q|quiet) verbose=false ;; | |
(*) | |
echo >&2 "Invalid option '$1'; usage: $0 [away|home|work] [xds] [dryrun] [v|q] [--primary={NUM|top|bottom|left|right] [--rotate-{displayname}={upright|right|inverted|left}]" | |
exit 64 ;; | |
esac | |
shift | |
done | |
# If there was no hint on the command line, examine what devices are attached and | |
# make a best-effort guess at what the user wants. | |
if ((use_defaults)) | |
then | |
case ${#EX[@]} in | |
(2) case ${S[${EX[1]}.n]}:${S[${EX[1]}.w]}:${S[${EX[1]}.h]} in | |
(DVI1:1600:1200) setup_home ;; | |
(*) setup_twin ;; | |
esac ;; | |
(1) setup_single ;; | |
(*) setup_mirror ;; | |
esac | |
fi | |
# Find the x & y offsets of the extreme edges of all screens. | |
bw=0 # right-most | |
bh=0 # bottom-most | |
ox=$maxw # left-most | |
oy=$maxh # top-most | |
for dnum in "${DX[@]}" | |
do | |
(( S[$dnum.e] )) || continue # skip disabled displays | |
(( (q = S[$dnum.x]) < ox && (ox = q) )) | |
(( (q = S[$dnum.y]) < oy && (oy = q) )) | |
# Adjust frame buffer size to encompass each screen | |
(( (q = S[$dnum.x]+S[$dnum.w]) > bw && (bw = q), | |
(q = S[$dnum.y]+S[$dnum.h]) > bh && (bh = q) )) | |
done | |
# Keeping their relative positions, move all the screens so that the | |
# x-offset of the furthest-left screen and the y-offset of the | |
# furthest-up screen are both zero. | |
for dnum in "${DX[@]}" | |
do | |
(( S[$dnum.e] )) || continue # skip disabled displays | |
(( S[$dnum.x] -= ox, | |
S[$dnum.y] -= oy )) | |
done | |
(( | |
bw -= ox, | |
bw>maxw && (bw=maxw), | |
bh -= oy, | |
bh>maxh && (bh=maxh) | |
)) | |
((bw && bh)) || die "zero-sized display" | |
xrandr_args=( --fb ${bw}x${bh} ) # xrandr args, to be determined | |
primary_done=0 | |
for dname in ${!DX[@]} | |
do | |
dnum="${DX[$dname]}" | |
[[ $dname = ${S[$dnum.n]} ]] || die "Mismatch of name for display #$dnum between '$dname' and '${S[$dnum.n]}'" | |
xrandr_args+=( --output $dname ) | |
if (( !S[$dnum.e] )) # || [[ $dname != ${S[$dnum.n]} ]] | |
then | |
# | |
# The keys in the DX hash are a superset of the S[$dnum.n] values, since the | |
# latter are only defined when the corresponding display is physically | |
# connected and enabled. | |
# | |
# Turn off any unused displays | |
xrandr_args+=( --off ) | |
continue # skip everything else for a disabled display | |
fi | |
# Prepare xrandr args for Device, Position, and Rotation | |
xrandr_args+=( --pos ${S[$dnum.x]:-0}x${S[$dnum.y]:-0} --mode ${S[$dnum.uw]:-0}x${S[$dnum.uh]:-0} --rotate ${R[${S[$dnum.r]:-0}&3]} ) | |
# Choose a "primary" display (where the menu appears) | |
if case $make_primary in | |
(left) ((S[$dnum.x] == 0)) ;; | |
(top) ((S[$dnum.y] == 0)) ;; | |
(right) ((S[$dnum.x]+S[$dnum.w] == bw)) ;; # (right) ((S[$dnum.x] != 0)) ;; | |
(bottom) ((S[$dnum.y]+S[$dnum.h] == bh)) ;; # (bottom) ((S[$dnum.y] != 0)) ;; | |
([0-9]) ((dnum == make_primary)) ;; | |
(*) false ;; | |
esac && | |
((! primary_done++)) | |
then | |
xrandr_args+=( --preferred ) | |
fi | |
done | |
if $dry_run ; then vv() { echo "$@"; } | |
declare -p DX EX S | |
elif $verbose ; then vv() { echo "$@"; "$@"; } | |
xrandr_args+=( --verbose ) | |
else vv() { "$@"; } | |
fi | |
$xds && vv xfce4-display-settings --minimal | |
vv xrandr "${xrandr_args[@]}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
When I ran this I found that unless I changed (on line 59)
(( maxw = words[i+1], maxh = words[i+3] ))
to
(( maxw = ${words[i+1]}, maxh = ${words[i+3]} ))
bash itself segfaulted.