1#!/bin/sh 2# 3# The MIT License (MIT) 4# 5# Copyright (c) 2017-2018 Thomas "Ventto" Venriès <thomas.venries@gmail.com> 6# 7# Permission is hereby granted, free of charge, to any person obtaining a copy of 8# this software and associated documentation files (the "Software"), to deal in 9# the Software without restriction, including without limitation the rights to 10# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11# the Software, and to permit persons to whom the Software is furnished to do so, 12# subject to the following conditions: 13# 14# The above copyright notice and this permission notice shall be included in all 15# copies or substantial portions of the Software. 16# 17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23usage() { 24 echo 'Usage: mons [OPTION]... 25 26Without argument, it prints connected monitors list with their names and ids. 27Options are exclusive and can be used in conjunction with extra options. 28 29Information: 30 -h Prints this help and exits. 31 -v Prints version and exits. 32 33Two monitors: 34 -o Primary monitor only. 35 -s Second monitor only. 36 -d Duplicates the primary monitor. 37 -m Mirrors the primary monitor. 38 -e <side> 39 Extends the primary monitor to the selected side 40 [ top | left | right | bottom ]. 41 -n <side> 42 This mode selects the previous ones, one after another. The argument 43 sets the side for the extend mode. 44 45More monitors: 46 -O <mon> 47 Only enables the monitor with a specified id. 48 -S <mon1>,<mon2>:<pos> 49 Only enables two monitors with specified ids. The specified position 50 places the second monitor on the right (R) or at the top (T). 51 52Extra (in-conjunction or alone): 53 --dpi <dpi> 54 Set the DPI, a strictly positive value within the range [0 ; 27432]. 55 --primary <mon_name> 56 Select a connected monitor as the primary output. Run the script 57 without argument to print monitors information, the names are in the 58 second column between ids and status. The primary monitor is marked 59 by an asterisk. 60 61Daemon mode: 62 -a Performs an automatic display if it detects only one monitor. 63' 64} 65 66version() { 67 echo 'Mons 0.8.2 68Copyright (C) 2017 Thomas "Ventto" Venries. 69 70License MIT: <https://opensource.org/licenses/MIT>. 71 72THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 73IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 74FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 75' 76} 77 78# Helps to generate manpage with help2man before installing the library 79[ "$1" = '-h' ] && { usage; exit; } 80[ "$1" = '-v' ] && { version; exit; } 81lib='/usr/local/lib/libshlist/liblist.sh' 82[ ! -r "$lib" ] && { "$lib: library not found."; exit 1; } 83. "${lib}" 84 85arg_err() { 86 usage ; exit 2 87} 88 89enable_mon() { 90 "${XRANDR}" --output "${1}" --auto --dpi "${dpi}" 91} 92 93disable_mons() { 94 for mon in $@; do "${XRANDR}" --output "${mon}" --off ; done 95} 96 97arg2xrandr() { 98 case $1 in 99 left) echo '--left-of' ;; 100 right) echo '--right-of' ;; 101 bottom) echo '--below' ;; 102 top) echo '--above' ;; 103 esac 104} 105 106whichmode() { 107 if [ "$(list_size "${disp_mons}")" -eq 1 ]; then 108 if echo "${enabled_out}" | grep prima > /dev/null 2>&1; then 109 echo 'primary' 110 else 111 echo 'second' 112 fi 113 else 114 if [ "$(list_size "${plug_mons}")" -gt 2 ] ; then 115 echo 'selection'; return 0 116 fi 117 118 enabled_out="$(echo "${enabled_out}" | \ 119 sed 's/^.*\( [0-9]\+\x[0-9]\++[0-9]\++[0-9]\+\).*/\1/')" 120 121 echo "${enabled_out}" | head -n1 | sed -e 's/+/ /g' | \ 122 while read -r trash x1 y1; do 123 echo "${enabled_out}" | tail -n1 | sed -e 's/x/ /' -e 's/+/ /g' | \ 124 while read -r w2 h2 x2 y2; do 125 echo "${xrandr_out}" | \ 126 awk "/^$(list_get 1 "${plug_mons}")/{nr[NR+1]}; NR in nr" | \ 127 awk '{print $1;}' | sed -e 's/x/ /' | \ 128 while read -r wi2 hi2; do 129 if [ "$x1" = "$x2" ] && [ "$y1" = "$y2" ]; then 130 if [ "$w2" != "$wi2" ] || [ "$h2" != "$hi2" ]; then 131 echo 'mirror' 132 else 133 echo 'duplicate' 134 fi 135 else 136 echo 'extend' 137 fi 138 done 139 done 140 done 141 fi 142} 143 144main() { 145 aFlag=false 146 dFlag=false 147 eFlag=false 148 mFlag=false 149 nFlag=false 150 oFlag=false 151 sFlag=false 152 OFlag=false 153 SFlag=false 154 pFlag=false 155 iFlag=false 156 is_flag=false 157 # X has assumed 96 DPI and this is fine for many traditional monitors. 158 dpi=96 159 primary= 160 161 # getopts does not support long options. We convert them to short one. 162 for arg in "$@"; do 163 shift 164 case "$arg" in 165 --dpi) set -- "$@" '-i' ;; 166 --primary) set -- "$@" '-p' ;; 167 *) set -- "$@" "$arg" 168 esac 169 done 170 171 while getopts 'hvamosde:n:O:S:i:p:' opt; do 172 case $opt in 173 # Long options 174 i) 175 if ! echo "${OPTARG}" | \ 176 grep -E '^[1-9][0-9]*$' > /dev/null 2>&1; then 177 arg_err 178 fi 179 iFlag=true; dpi="$OPTARG" 180 ;; 181 p) if ! echo "${OPTARG}" | \ 182 grep -E '^[a-zA-Z][a-zA-Z0-9\-]+' > /dev/null 2>&1; then 183 arg_err 184 fi 185 pFlag=true; primary="$OPTARG" 186 ;; 187 # Short options 188 a) $is_flag && arg_err 189 aFlag=true ; is_flag=true 190 ;; 191 m) $is_flag && arg_err 192 mFlag=true ; is_flag=true 193 ;; 194 o) $is_flag && arg_err 195 oFlag=true ; is_flag=true 196 ;; 197 s) $is_flag && arg_err 198 sFlag=true ; is_flag=true 199 ;; 200 d) $is_flag && arg_err 201 dFlag=true ; is_flag=true 202 ;; 203 e|n) $is_flag && arg_err 204 case ${OPTARG} in 205 left | right | bottom | top) ;; 206 *) arg_err ;; 207 esac 208 eArg=$OPTARG 209 [ "$opt" = "e" ] && eFlag=true || nFlag=true ; is_flag=true 210 ;; 211 O) $is_flag && arg_err 212 ! echo "${OPTARG}" | grep -E '^[0-9]+$' > /dev/null && arg_err 213 OArg=$OPTARG 214 OFlag=true ; is_flag=true 215 ;; 216 S) $is_flag && arg_err 217 idx1="$(echo "${OPTARG}" | cut -d',' -f1)" 218 idx2="$(echo "${OPTARG}" | cut -d',' -f2)" 219 area="$(echo "${idx2}" | cut -d ':' -f2)" 220 idx2="$(echo "${idx2}" | cut -d ':' -f1)" 221 ! echo "${idx1}" | grep -E '^[0-9]+$' > /dev/null && arg_err 222 ! echo "${idx2}" | grep -E '^[0-9]+$' > /dev/null && arg_err 223 ! echo "${area}" | grep -E '^[RT]$' > /dev/null && arg_err 224 [ "${idx1}" = "${idx2}" ] && arg_err 225 SFlag=true ; is_flag=true 226 ;; 227 h) usage ; exit ;; 228 v) version ; exit ;; 229 \?) arg_err ;; 230 :) arg_err ;; 231 esac 232 done 233 234 [ -z "${DISPLAY}" ] && { echo 'DISPLAY: no variable set.'; exit 1; } 235 236 XRANDR="$(command -v xrandr)" 237 [ "$?" -ne 0 ] && { echo 'xrandr: command not found.'; exit 1; } 238 239 # DPI set 240 $iFlag && [ "$#" -eq 2 ] && { "${XRANDR}" --dpi "$dpi"; exit; } 241 242 # Daemon mode 243 if $aFlag ; then 244 prev=0; i=0 245 while true; do 246 for status in /sys/class/drm/*/status; do 247 [ "$(<"$status")" = 'connected' ] && i=$((i+1)) 248 done 249 if [ "$i" -eq 1 ] && [ "$i" != "$prev" ]; then 250 "${XRANDR}" --auto --dpi "${dpi}" 251 fi 252 prev="$i"; i=0 253 sleep 2 254 done 255 fi 256 257 # List all outputs (except primary one) 258 xrandr_out="$("${XRANDR}")" 259 enabled_out="$(echo "${xrandr_out}" | grep 'connect')" 260 [ -z "${enabled_out}" ] && { echo 'No monitor output detected.'; exit; } 261 mons="$(echo "${enabled_out}" | cut -d' ' -f1)" 262 263 # List plugged-in and turned-on outputs 264 enabled_out="$(echo "${enabled_out}" | grep ' connect')" 265 [ -z "${enabled_out}" ] && { echo 'No plugged-in monitor detected.'; exit 1; } 266 plug_mons="$(echo "${enabled_out}" | cut -d' ' -f1)" 267 268 # Set primary output 269 if $pFlag; then 270 if ! list_contains "${primary}" "${plug_mons}"; then 271 echo "${primary}: output not connected." 272 exit 1 273 fi 274 "${XRANDR}" --output "${primary}" --primary 275 [ "$#" -eq 2 ] && exit 276 else 277 primary="$(echo "${enabled_out}" | grep 'primary' | cut -d' ' -f1)" 278 fi 279 280 # Move the primary monitor to the head if connected otherwise the first 281 # connected monitor that appears in the xrandr output is considerate as 282 # the primary one. 283 if [ -n "${primary}" ]; then 284 plug_mons="$(list_erase "${primary}" "${plug_mons}")" 285 plug_mons="$(list_insert "${primary}" 0 "${plug_mons}")" 286 fi 287 288 enabled_out="$(echo "${enabled_out}" | grep -E '\+[0-9]{1,4}\+[0-9]{1,4}')" 289 disp_mons="$(echo "${enabled_out}" | cut -d' ' -f1)" 290 291 if [ "$#" -eq 0 ]; then 292 echo "Monitors: $(list_size "${plug_mons}")" 293 echo "Mode: $(whichmode)" 294 295 i=0 296 for mon in ${mons}; do 297 if echo "${plug_mons}" | grep "^${mon}$" > /dev/null; then 298 if echo "${disp_mons}" | grep "^${mon}$" > /dev/null; then 299 state='(enabled)' 300 fi 301 if [ "${mon}" = "${primary}" ]; then 302 printf '%-4s %-8s %-8s %-8s\n' "${i}:*" "${mon}" "${state}" 303 else 304 printf '%-4s %-8s %-8s\n' "${i}:" "${mon}" "${state}" 305 fi 306 fi 307 i=$((i+1)) 308 state= 309 done 310 exit 311 fi 312 313 if $nFlag ; then 314 case "$(whichmode)" in 315 primary) sFlag=true;; 316 second) eFlag=true;; 317 extend) mFlag=true;; 318 mirror) dFlag=true;; 319 duplicate) oFlag=true;; 320 esac 321 fi 322 323 if [ "$(list_size "${plug_mons}")" -eq 1 ] ; then 324 if $oFlag ; then 325 # After unplugging each monitor, the last preferred one might be 326 # still turned off or the window manager might need the monitor 327 # reset to cause the reconfiguration of the layout placement. 328 "${XRANDR}" --auto --dpi "${dpi}" 329 else 330 echo 'Only one monitor detected.' 331 fi 332 exit 333 fi 334 335 if $oFlag ; then 336 if [ "$(list_size "${disp_mons}")" -eq 1 ]; then 337 if [ "$(list_front "${disp_mons}")" = "$(list_front "${plug_mons}")" ]; then 338 exit 339 fi 340 fi 341 disp_mons="$(list_erase "$(list_front "${plug_mons}")" "$disp_mons")" 342 disable_mons "${disp_mons}" 343 enable_mon "$(list_front "${plug_mons}")" 344 exit 345 fi 346 347 if $OFlag ; then 348 if [ "${OArg}" -ge "$(list_size "${mons}")" ] ; then 349 echo "Monitor ID '${OArg}' does not exist." 350 echo 'Try without option to get monitor ID list.' 351 exit 2 352 fi 353 mons_elt="$(list_get "${OArg}" "${mons}")" 354 if ! list_contains "${mons_elt}" "${plug_mons}"; then 355 echo "Monitor ID '${OArg}' not plugged in." 356 echo 'Try without option to get monitor ID list.' 357 exit 2 358 fi 359 360 disp_mons="$(list_erase "${mons_elt}" "${disp_mons}")" 361 disable_mons "${disp_mons}" 362 enable_mon "${mons_elt}" 363 exit 364 fi 365 366 if $SFlag ; then 367 if [ "${idx1}" -ge "$(list_size "${mons}")" ] || \ 368 [ "${idx2}" -ge "$(list_size "${mons}")" ]; then 369 echo 'One or both monitor IDs do not exist.' 370 echo 'Try without option to get monitor ID list.' 371 exit 2 372 fi 373 if ! list_contains "$(list_get "${idx1}" "${mons}")" "${plug_mons}" || \ 374 ! list_contains "$(list_get "${idx2}" "${mons}")" "${plug_mons}" ; then 375 echo 'One or both monitor IDs are not plugged in.' 376 echo 'Try without option to get monitor ID list.' 377 exit 2 378 fi 379 380 [ "${area}" = 'R' ] && area="--right-of" || area="--above" 381 382 mon1="$(list_get "${idx1}" "${mons}")" 383 mon2="$(list_get "${idx2}" "${mons}")" 384 disp_mons="$(list_erase "${mon1}" "${disp_mons}")" 385 disp_mons="$(list_erase "${mon2}" "${disp_mons}")" 386 disable_mons "${disp_mons}" 387 enable_mon "${mon1}" 388 enable_mon "${mon2}" 389 "${XRANDR}" --output "${mon2}" "${area}" "${mon1}" 390 exit 391 fi 392 393 if [ "$(list_size "${plug_mons}")" -eq 2 ]; then 394 if $sFlag ; then 395 if [ "$(list_size "${disp_mons}")" -eq 1 ] ; then 396 if [ "$(list_front "${disp_mons}")" = "$(list_get 1 "${plug_mons}")" ] ; then 397 enable_mon "$(list_get 1 "${plug_mons}")" 398 exit 399 fi 400 fi 401 enable_mon "$(list_get 1 "${plug_mons}")" 402 disable_mons "$(list_front "${disp_mons}")" 403 exit 404 fi 405 406 # Resets the screen configuration 407 disable_mons "$(list_get 1 "${plug_mons}")" 408 "${XRANDR}" --auto --dpi "${dpi}" 409 410 if $dFlag ; then 411 "${XRANDR}" --output "$(list_get 1 "${plug_mons}")" \ 412 --same-as "$(list_front "${plug_mons}")" 413 exit $? 414 fi 415 416 if $mFlag ; then 417 xrandr_out="$(echo "${xrandr_out}" | \ 418 awk "/primary/{nr[NR]; nr[NR+1]}; NR in nr")" 419 if echo "${xrandr_out}" | \ 420 grep -E 'primary [0-9]+x[0-9]+' >/dev/null 2>&1; then 421 size="$(echo "${xrandr_out}" | head -n1 | cut -d' ' -f4 | \ 422 cut -d'+' -f1)" 423 else 424 size="$(echo "${xrandr_out}" | tail -n1 | awk '{ print $1 }')" 425 fi 426 427 "${XRANDR}" --output "$(list_get 1 "${plug_mons}")" \ 428 --auto --scale-from "${size}" \ 429 --output "$(list_front "${plug_mons}")" 430 exit $? 431 fi 432 433 if $eFlag ; then 434 "${XRANDR}" --output "$(list_get 1 "${plug_mons}")" \ 435 "$(arg2xrandr "$eArg")" "$(list_front "${plug_mons}")" 436 exit $? 437 fi 438 else 439 echo 'At most two plugged monitors for this option.' 440 fi 441} 442 443main "$@" 444