#!/usr/bin/env bash # File: xdisplay / xrandr-tui # Description: A little helper around xrandr # Disclaimer: This code comes without any warrenty of any kind, use at your own risk! # GNU General Public License (GPL) 2016 by Simon Arjuna Erat (sea) (erat.simon æ gmail,com) # ------------------------------------------------------ # # Script Settings # Since it is a wrapper, lets define that all error messages are english # export LC_ALL=C #set -x # # Variables : Defaults # # Get defaults XDG_SOURCED=false HOME="$(eval echo ~${USER})" XCD="${XDG_CONFIG_DIR:-$HOME/.config}" XDG_DIRS="${XCD}/user-dirs.dirs" LOG="/tmp/xrandr-tui.log" [ -f "$XDG_DIRS" ] && source "$XDG_DIRS" && XDG_SOURCED=true # Repeate the leading 2 lines, since the initial variable might have changed XCD="${XDG_CONFIG_DIR:-$HOME/.config}" XDG_DIRS="${XCD}/user-dirs.dirs" ! $XDG_SOURCED && source "$XDG_DIRS" LIST_DEFAULT_RESOLUTIONS_BOX="640x480 800x600 1024x768 1280x1024" LIST_DEFAULT_RESOLUTIONS_HD="640x360 854x480 1280x720 1920x1080 2560x1440 3840x2160" LIST_DEFAULT_RESOLUTIONS_WIDE="720x400 720x576 1152x864 1280x768 1280x960 1368x768 1440x900 1400x1050 1600x900 1680x1050 1600x1200" DEFAULT_DISPLAY="" DEFAULT_RESOLUTION="" DEFAULT_LINEAR_SOURCE="" DEFAULT_LINEAR_TARGET="" DEFAULT_SHOW_HZ=false SKIP_CHECK=${SKIP_CHECK:-false} WAIT=2 # # Script info # script_name="xrandr-tui" script_version=0.4.1 script_author="Simon Arjuna Erat (sea)" script_homepage="https://github.com/sri-arjuna/xrandr-tui" script_authors="" # Add your names here, seperated by a comma ',' # # Variables : Messages # # Heading & Intro MSG_MENU_HEADER_LEFT="$script_name ($script_version)" MSG_MENU_HEADER_RIGHT="Brought to you by (sea)" # & ${script_authors:-noone else}" MSG_MENU_CONFIG_PREVIEW="Current setup" MSG_MENU_CONFIG_UNUSED="With these connected but unused displays:" MSG_MENU_TITLE="Welcome to the main menu" MSG_MENU_CONFIG_INTRO="What is your new setup?" MSG_MENU_UPDATE="Next screen update in:" # Configuration screen MSG_LABEL_CONFIG_TITLE="${script_name^} configuration" MSG_LABEL_CONFIG_DEFAULT_DISPLAY="Which is your default display when resetting?" MSG_LABEL_CONFIG_DEFAULT_RESOLUTION="What resolution shall be set when resetting?" MSG_LABEL_CONFIG_DEFAULT_HZ="Which is the default refresh rate for your default display?" MSG_LABEL_CONFIG_DEFAULT_SHOW_HZ="Use the HZ for the created modes as well?" MSG_LABEL_CONFIG_DEFAULT_LINEAR_BASE="What will be the output most left?" MSG_LABEL_CONFIG_DEFAULT_LINEAR_ORDER="What is the order of the monitors, from left to right:" MSG_LABEL_CONFIG_DEFAULT_MIRROR_SOURCE="What is the source display?" # Task : mirror MSG_TASK_MIRROR_NOTARGET="You have not selected a target for the mirror." MSG_TASK_MIRROR_TARGET="Add a target?" # Task : linear MSG_TASK_LINEAR_SET_PRIMARY="Set primary screen (left):" MSG_TASK_LINEAR_SET_ORDER="The following screens are:" MSG_TASK_LINEAR_NEXT="The previous display is left, next right one:" # Task : multi-line MSG_TASK_MULTILINE_NEWLINE="The previous display is last right, next new line starts with output slot:" MSG_TASK_MULTILINE_SET_BREAKPOINTS="Of not equaliy spread (2 lines x 3 outputs), provide the monitors at which $screen_name shall make a new line:" # Task : resolution MSG_TASK_RESOLUTION_DONE="Changed resolution:" # Task : switch MSG_TASK_SWITCH_DONE="Switched output to:" MSG_TASK_SWITCH_INVALID_COUNT='Invalid count of connected displays: !2 = $(list.screens.connected)' # Modes : add MSG_TASK_MODE_ADD_TITLE="Add a new mode to output:" MSG_TASK_MODE_ADD_KIND="What kind of mode would you like to add?" MSG_TASK_MODE_ADD_DISPLAY="Which display shall be used?" MSG_TASK_MODE_ADD_EXISTS="The mode you wanted to add does already exist" MSG_TASK_MODE_ADD_NEW="Please enter your desired resolution like: 1280x720" MSG_TASK_MODE_ADD_ASK="Are you sure to use this:" # Modes : remove MSG_MODE_RM_TITLE="Remove an old mode from slot:" MSG_MODE_RM_ASK="Are you sure to remove this?" # Shared MSG_SHARED_CORRECT="Is this correct?" MSG_SHARED_DISPLAY_CURRENT="The currently used display is:" MSG_SHARED_DISPLAY_FOUND="The found output display is:" MSG_SHARED_DISPLAY_NEW="The new output display will be:" MSG_SHARED_DISPLAY_CHANGE_INPUT="Please select the base display:" MSG_SHARED_DISPLAY_CHANGE_OUTPUT="Please select a new output display:" MSG_SHARED_DISPLAY_SELECTED="The selected display is:" MSG_SHARED_RESOLUTION_CHANGE="Please select a new resolution for:" # # # Variables # # Apps declare XR=\xrandr AWK=\gawk GREP=\grep SED=\sed LS=\ls WHICH=\which # Vars declare CONF="$XCD/$script_name.conf" RAW_DATA="" ORDER="" LISTLENGTH=15 # Booleans first=true # Menu tweak first_order=true # MSG toggle tweak when setting order # # Functions # # Data handling data.get() { export RAW_DATA="$(${XR}|$GREP -v Screen)" ; } data.show() { echo "$RAW_DATA" ; } data.cvt.get() { DATA_CVT=$(cvt ${res/x/" "} ${DEFAULT_HZ:-60}) ;} data.cvt.show() { echo "$DATA_CVT" ;} # Info of *-marked displays screen.name() { tmp=$(data.show | $GREP -B${LISTLENGTH} "*" | $GREP ^[a-zA-Z] | $AWK '{print $1}' | tail -n1);list.screens | $GREP $tmp; } screen.size() { data.show | $AWK '/*/ {print $1}' ; } # Lists list.screens() { data.show | $GREP ^[a-zA-Z] | $AWK '/ connected/ {print $1}' ; } list.screens.unused() { data.show | $GREP ^[a-zA-Z] | $GREP $(for f in $(screen.name);do echo " -ve $f ";done)|$AWK '/ connected/ {print $1}' ; } list.screens.connected() { data.show | $AWK '/ connected/ {print $1}' ;} list.modes() { data.show | $GREP "$1" -A${LISTLENGTH} | $GREP -v "$1" | while read data _ ;do echo "$data" | grep -q ^[a-zA-Z] && break || echo $data;done; } list.menu() { tmp=$($GREP "task.*()" "$($WHICH $0)"|$GREP -ve "list.menu" -ve "task.quit" -ve "#" -ve ^"#"|$AWK '{print $1}');echo "${tmp//task.}"|$SED s,\(\)*,,g; } # Task functions task.quit() { exit 0 ; } task.switch() { # Expects two connected displays # Switches the active to off, and uses the unused one # Get default values cur="$(screen.name)" new=$(echo $(list.screens.connected|$SED s,$cur,,g)) # Print title & values tui-title "${FUNCNAME/task\.}" tui-print -E "$MSG_SHARED_DISPLAY_CURRENT" "$cur" tui-print -E "$MSG_SHARED_DISPLAY_NEW" "$new" # Ask if these values are correct RET=0 ! tui-yesno "$MSG_SHARED_CORRECT" && \ tui-print -E "$MSG_SHARED_DISPLAY_CHANGE_INPUT ($cur)" && \ cur=$(tui-select $(list.screens.connected)) && \ tui-print -E "$MSG_SHARED_DISPLAY_CHANGE_OUTPUT ($new)" && \ RET=1 && \ new=$(tui-select $(list.screens.connected)) # Print selected target output tui-status $RET "$MSG_SHARED_DISPLAY_SELECTED $new" || exit 1 # Do the final step job.switch $cur $new } task.resolution() { # Change the resolution for the current active display # Select which display if more than one is active # Print title tui-title "${FUNCNAME/task\.}" # If there is more than one active screen, ask wich one to change [ $(screen.name|grep [a-zA-Z]|wc -w) -gt 1 ] && \ tui-print -E "$MSG_SHARED_RESOLUTION_CHANGE" && \ this=$(tui-select $(screen.name)) || \ this=$(screen.name) # Which output is used? tui-print -E "$MSG_SHARED_RESOLUTION_CHANGE" "$this" # Select resolution res=$(tui-select $(list.modes $this)) # Do the magic $XR --output $this --mode $res # Report status tui-status $? "$MSG_TASK_RESOLUTION_DONE" "$this @ $res" exit $? } task.mirror() { # SRC TRGT1 TRGT2 .. # Take the first argument as source screen # Mirror all following from that # Prepare variables ar_passed=($(echo "${@}")) src=${ar_passed[0]} unset ar_passed[0] # Print title tui-title "${FUNCNAME/task\.}" tui-print -S $? "${MSG_SCREEN_MIRROR_SRC} $src" # All good? ! tui-yesno "$MSG_SOURCE_CORRECT" && tui-print -E "$MSG_SOURCE_CORRECT_NOT" # Clear and load base output job.reset.all sleep 0.5 $XR --output $src --auto # Now load all mirror'd output for passed in ${ar_passed[@]} do thismode=$(list.modes $passed|head -n1) $XR --output $passed --same-as $src --mode $thismode tui-status $? "$MSG_DETECTED_SCREEN_TRGT $passed" done exit $? } task.linear() { # Take the first argument as source screen # Mirror all following from that # Prepare variables ar_passed=(${@}) src=${ar_passed[0]} unset ar_passed[0] # Print title tui-title "${FUNCNAME/task\.}" # Clear and load base output job.reset.all $XR --output $src --primary tui-print -S $? "$MSG_LINEAR_SET_PRIMARY $src" # Now order them all next to each other for passed in ${ar_passed[@]} do $XR --output $passed --right-of $src --mode $(list.modes $passed|head -n1) tui-print -S $? "$MSG_LINEAR_SET_LEFT $passed" done exit $? } task.mode.add() { # Add a new mode to current output # # Print title tui-title "$MSG_MODE_ADD_TITLE $(screen.name)" # Prepare variables local list="box hd wide" prefix=LIST_DEFAULT_RESOLUTIONS display=$(screen.name) # Select what kind of resolution list tui-print -E "$MSG_MODE_ADD_KIND" item=$(tui-select $list) # Expand list tmp="${prefix}_${item^^}" list_items=$(eval echo "\${${tmp}}" ) # Select new resolution tui-print -E tui-print -E "$MSG_SELECT_RES $display" res=$(tui-select $list_items custom) # Add custom resolution while [ "$res" = custom ] do tui-print -E "$MSG_MODE_ADD_NEW" res=$(tui-read) tui-yesno "$MSG_MODE_ADD_ASK $res" || res=custom done # Get mode parameters data.cvt.get mode_lbl="$(data.cvt.show|$AWK '/Modeline/ {print $2}'|$SED s,\",,g)" mode_numbers="$(data.cvt.show|$AWK '/Modeline/ {print $3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13}')" # Set mode-name according to preference $DEFAULT_SHOW_HZ || mode_lbl="${mode_lbl/_*}" # Check for new mode existence (double name) list.modes $(screen.name) | $GREP -q "${mode_lbl/_*}" && \ tui-print -S 1 "$MSG_MODE_ADD_EXISTS" && \ exit 1 # Where magic becomes reality $XR --newmode $mode_lbl $mode_numbers $XR --addmode $display $mode_lbl $XR --output $display --mode $mode_lbl } task.mode.rm() { # Remove a mode from current output # # Prepare variable display="$(screen.name)" # Print title tui-print -T "${FUNCNAME/task\.} $display" res_rm=$(tui-select $(list.modes $display)) # Really remove this? if tui-yesno "${MSG_MODE_RM_ASK} $res_rm ($display)" then (set -x;$XR --delmode $display $res_rm) (set -x;$XR --rmmode "$res_rm") fi return $? } #task.multiline() { tui-title "TODO ${FUNCNAME/task\.}" ; tui-list -1 $@ ; } task.reset() { job.reset.all ; $XR --output ${DEFAULT_DISPLAY:-$(screen.name)} --mode ${DEFAULT_RESOLUTION:-$(screen.size)} --primary ; } task.config() { # Show the configuration editor # Upon every call it rewrites the default values if none exists display="$(screen.name)" tui-conf-editor \ -H "$script_name ($script_version)" \ -T "$MSG_LABEL_CONFIG_TITLE" \ --option DEFAULT_DISPLAY \ --label "$MSG_LABEL_CONFIG_DEFAULT_DISPLAY" \ --values "$($0 --list-connected)" \ --default "$display" \ --option DEFAULT_RESOLUTION \ --label "$MSG_LABEL_CONFIG_DEFAULT_RESOLUTION" \ --values "$(list.modes $(screen.name))" \ --default "$(screen.size)" \ --option DEFAULT_MIRROR_SOURCE \ --label "$MSG_LABEL_CONFIG_DEFAULT_MIRROR_SOURCE" \ --values "$(list.screens.connected)" \ --default "$display" \ --option DEFAULT_LINEAR_SOURCE \ --label "$MSG_LABEL_CONFIG_DEFAULT_LINEAR_ORDER" \ --values "$(list.screens.connected)" \ --default "$display" \ --option DEFAULT_LINEAR_ORDER \ --label "$MSG_LABEL_CONFIG_DEFAULT_LINEAR_ORDER" \ --values "$(list.screens.connected)" \ --default "$(list.screens.connected|$SED s,$disply,,g)" \ --option DEFAULT_HZ \ --label "$MSG_LABEL_CONFIG_DEFAULT_HZ" \ --values "30 60 75 100" \ --default 60 \ --option DEFAULT_SHOW_HZ \ --label "$MSG_LABEL_CONFIG_DEFAULT_SHOW_HZ" \ --values "true false" \ --default "false" \ --write-no-exist "$CONF" } # # Jobs # job.firsttime() { tui-bol-dir "$XCD";touch "$CONF";task.config;} job.mirror.source() { list.screens.connected|$SED s,${DEFAULT_DISPLAY:-$(screen.name)},,g|$AWK '{print $1}' ; } job.reset.all() { for d in $(list.screens.connected);do $XR --output $d --off;done;} job.switch() { # CUR NEW [auto] # Expects two connected displays # Switches the the active to off, and activateses the off one # Even if called by arguments, cant switch an unequal amount of connected displays [ 2 -ne $(list.screens.connected|wc -w) ] && tui-print -S 1 "$MSG_TASK_SWITCH_INVALID_COUNT" && exit 1 if [ -z "$3" ] then # Ask for the screen resolution tui-print -E tui-print -E "$MSG_SHARED_RESOLUTION_CHANGE $2" MODE=$(tui-select $(list.modes $2 )) else # Get automatic MODE=$($XR | $GREP -A$LISTLENGTH $2 | $AWK '/+/ {print $1}'|head -n1) fi # The magic happens $XR --output $1 --off $XR --output $2 --mode $MODE RET=$? # Only print message if menu was used [ -z "$3" ] && tui-print -E && tui-status $RET "$MSG_TASK_SWITCH_DONE" "$2 @ $MODE" exit $RET } # # Show repeated or special stuff # show.config.current() { C=0;while [ -n "${AR_SCRN[$C]}" ] ;do tui-print -E "$MSG_SHARED_DISPLAY_CURRENT" "${AR_SCRN[$C]} @ ${AR_RES[$C]}";((C++));done;tui-print -E "$MSG_MENU_CONFIG_UNUSED" "$(list.screens.unused)";} show.version() { printf '%s\n' "$script_name, Version $script_version" "Copyright (C) 2015 by $script_author" "License GNU General Public License (GPL) or later " "" "This is free software; you are free to change and redistribute it." "There is NO WARRANTY, to the extent permitted by law.";exit 0;} show.help() { # Show help screen # Can be transformed to a manpage using txt2man cat <<-EO_SCREEN NAME $script_name SYNOPSIS $script_name [--help|--version] [-C|--config] [--reset] [--list-connected] [--list-unused] DESCRIPTION This is wrapper/wizard around the (xorg-)xrandr application. OPTIONS -h|--help Show this screen and exit --version Show the version and exit -C|--config Change the configuration -E|--edit Edit the configuration manually --list-connected List connected slots and exit --list-unused List free slots and exit --list-used List used slots and exit --reset Reset xrandr settings according to configuration --switch Switches the output of the current active one with the unused --mirror Mirrors all connected output from DEFAULT_DISPLAY ($DEFAULT_DISPLAY) SEE ALSO cvt(1), tui(1), xrandr(1) AUTHORS $script_author, $script_authors EO_SCREEN exit 0 } # # Preparations # data.get declare -a AR_SCRN=($(screen.name)) declare -a AR_RES=($(screen.size)) $SKIP_CHECK || { [ -f "$CONF" ] || job.firsttime ; } source "$CONF" # # Get options # GETOPT=$(getopt \ --options "hCE" \ --longoptions "help,version,edit,config,mirror,list-unused,list-used,list-connected,reset,switch" \ --name "${0##*/}" -- "${@}" \ ) eval set -- "${GETOPT}"; while true do case "$1" in -h|--help) show.help ;; --version) show.version ;; --) shift ; break ;; -C|--config) task.config exit $? ;; -E|--edit) tui-edit "$CONF" exit $? ;; # ----------------------------------------------- # The SKIP_CHECK is required to bypass check on first usage --list-connected) SKIP_CHECK=true list.screens.connected exit 0 ;; # The SKIP_CHECK is required to bypass check on first usage --list-unused) SKIP_CHECK=true list.screens.unused exit 0 ;; --list-used) screen.name exit 0 ;; # Use defaults from config, for WM integration --reset) task.reset exit $? ;; # Switch silently, to be used for WMs hotkey-binding as example --switch) cur="$(screen.name)" new="$(list.screens.connected|$SED s,$cur,,g)" job.switch $cur $new auto exit $? ;; # Use defaults from config, for WM integration --mirror) task.mirror ${DEFAULT_MIRROR_SOURCE} exit $? ;; # Use defaults from config, for WM integration --linear) task.linear $DEFAULT_DISPLAY $DEFAULT_LINEAR_ORDER exit $? ;; esac done # # Display # while :;do ! $first && tui-wait $WAIT "$MSG_MENU_UPDATE" || first=false ; clear # Print header and title tui-print -H "$MSG_MENU_HEADER_LEFT" "$MSG_MENU_HEADER_RIGHT" tui-print -T "$MSG_MENU_CONFIG_PREVIEW" # Show current setting show.config.current # Menu intro tui-print -T "$MSG_MENU_TITLE" tui-print -E "$MSG_MENU_CONFIG_INTRO" # Get menu action action=$(tui-select quit $(list.menu)) # Quit or define order of displays if [ "$action" = "quit" ] then # Back to terminal or close termnal window break # task.$action elif [ -z "$ORDER" ] then case "$action" in linear|multiline) item="" # Prepare order of screens while [ ! done = "$item" ] do ORDER+=" $item" $first_order && \ tui-print -E "$MSG_TASK_LINEAR_SET_PRIMARY" && \ first=false || \ tui-print -E "$MSG_TASK_LINEAR_SET_ORDER" item=$(tui-select done $(list.screens)) done ;; mirror) # Select base output tui-print -E "$MSG_SHARED_DISPLAY_CHANGE_INPUT" SOURCE=$(tui-select $(list.screens.connected)) # Select mirrors this="" while tui-print -E "$MSG_SHARED_DISPLAY_CHANGE_OUTPUT" tui-print -E "Src: $SOURCE" "Targets: $TARGET" this=$(tui-select done $(list.screens.connected|$SED s,"$SOURCE",,g)) [ ! done = $this ] do TARGET+=" $this";this="" done [ -z "$TARGET" ] && tui-print -S 1 "$MSG_TASK_MIRROR_NOTARGET" && exit 1 ORDER="$SOURCE $TARGET" ;; switch) # 2 connected outputs are required to switch [ 2 -ne $(list.screens.connected|wc -w) ] && \ tui-print -S 1 "$MSG_TASK_SWITCH_INVALID_COUNT" && \ exit 1 ;; esac fi # Execute the selected action task.$action $ORDER ORDER="" SOURCE="" TARGET="" done # # Clean up # set +x