Skip to content

Instantly share code, notes, and snippets.

@kurahaupo
Last active November 25, 2021 03:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kurahaupo/216a37ee0c6b4efaf158 to your computer and use it in GitHub Desktop.
Save kurahaupo/216a37ee0c6b4efaf158 to your computer and use it in GitHub Desktop.
Script using "xrandr" to configure multi-head display
#!/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[@]}"
@jglotzer
Copy link

jglotzer commented Apr 2, 2016

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment