Skip to content

Instantly share code, notes, and snippets.

@ocpu
Last active January 12, 2021 20:41
Show Gist options
  • Save ocpu/ac0558f9013cc15dfe1d5a30518e1331 to your computer and use it in GitHub Desktop.
Save ocpu/ac0558f9013cc15dfe1d5a30518e1331 to your computer and use it in GitHub Desktop.
use rofi/dmenu to configure/disconnect/modify a monitor with xrandr
#!/bin/bash
# Provides an interactive xrandr experience with dmenu or dmenu
# like programs
# If the Window Manager requires a restart / reload put the
# command here or leave blank
RESTART_WM_CMD="i3-msg restart"
# Any command that takes a pipe of newline separated options
# Such as for example dmenu and rofi -dmenu
D_MENU_CMD="rofi -dmenu"
# All configurable labels
LABEL_MONITOR_AVAILABLE="Configure "
LABEL_MONITOR_AVAILABLE_CONFIRM="Activate "
LABEL_MONITOR_MONIFY="Modify "
LABEL_MONITOR_MONIFY_CONFIRM="Save Settings for "
LABEL_MONITOR_OFF="Disconnect "
LABEL_CONFIG_RESOLUTION="Resolution: "
LABEL_CONFIG_REFRESH_RATE="Refresh Rate: "
LABEL_CONFIG_REFRESH_RATE_POSTFIX="hz"
LABEL_CONFIG_POSITION="Position "
LABEL_CONFIG_DENY="Cancel"
LABEL_POSITION_RIGHT_OF="right of "
LABEL_POSITION_LEFT_OF="left of "
LABEL_POSITION_ABOVE="above "
LABEL_POSITION_BELOW="below "
LABEL_POSITION_SAME_AS="same as "
# -------------------------------------------------------------- #
SCRIPT_PATH="`realpath $0`"
SCRIPT_FILENAME="`basename $SCRIPT_PATH`"
SCRIPT_DIR="`dirname $SCRIPT_PATH`"
# Get active and unactivated monitors
ACTIVE_MONITORS=($(xrandr --listactivemonitors | tail -n +2 | tr -s ' ' | cut -d ' ' -f 5))
TEMP_MONITORS=($(xrandr | grep -w connected | cut -d ' ' -f 1))
# Delete active monitors from the temp list
for MONITOR in "${ACTIVE_MONITORS[@]}"; do
for i in "${!TEMP_MONITORS[@]}"; do
if [[ ${TEMP_MONITORS[i]} = $MONITOR ]]; then
unset 'TEMP_MONITORS[i]'
fi
done
done
# Build new monitor array from the temp list
for i in "${!TEMP_MONITORS[@]}"; do
if test -n "$i"; then
MONITORS+=( "${TEMP_MONITORS[i]}" )
fi
done
# Basic select function that takes a variable amount of args and
# passes them to the selection program.
#
# Usage: select-action ACTION...
select-action() {
[ "$#" -eq "0" ] && return
for action in "$@"; do echo $action; done | $D_MENU_CMD 2> /dev/null
}
# Get all possible resulutions any monitor accepts.
# Usage: get-resolutions MONITOR
get-resolutions() {
xrandr | awk -v monitor="^$1 connected" '/connected/ {p = 0} $0 ~ monitor {p = 1} p' | tail -n +2 | cut -d ' ' -f 4
}
# Get all refresh rate possibilities for a specific resolution on
# a monitor.
#
# Usage: get-update-rates-for-resolution MONITOR RESOLUTION
get-update-rates-for-resolution() {
local res=`xrandr | awk -v monitor="^$1 connected" '/connected/ {p = 0} $0 ~ monitor {p = 1} p' | grep -w "$2" | sed 's/^ *//;s/ *$//' | sed -n -e "/^$2/p" | sed "s/^$2 *//;s/ *$//"`
res="${res/\*}"
res="${res/+}"
rates=("${res[@]}")
for rate in $rates; do
echo $rate
done
}
get-relative-position-to-label() {
case "$1" in
"right-of") echo "$LABEL_POSITION_RIGHT_OF";;
"left-of") echo "$LABEL_POSITION_LEFT_OF";;
"above") echo "$LABEL_POSITION_ABOVE";;
"below") echo "$LABEL_POSITION_BELOW";;
"same-as") echo "$LABEL_POSITION_SAME_AS";;
*) echo "$LABEL_POSITION_RIGHT_OF";;
esac
}
get-relative-position-from-label() {
if test "`echo $LABEL_POSITION_RIGHT_OF | sed 's/^ *| *$//g'`" = "$*"; then echo "right-of"; return; fi
if test "`echo $LABEL_POSITION_LEFT_OF | sed 's/^ *| *$//g'`" = "$*"; then echo "left-of"; return; fi
if test "`echo $LABEL_POSITION_ABOVE | sed 's/^ *| *$//g'`" = "$*"; then echo "above"; return; fi
if test "`echo $LABEL_POSITION_BELOW | sed 's/^ *| *$//g'`" = "$*"; then echo "below"; return; fi
if test "`echo $LABEL_POSITION_SAME_AS | sed 's/^ *| *$//g'`" = "$*"; then echo "same-as"; return; fi
echo "right-of"
}
# Starts the interactive selector for monitor resolution,
# refresh rate, and monitor position. The position is
# a string that starts with either "same-as", "left-of",
# "right-of", "above", or "below" and then the monitor the
# position would be relative to like "right-of eDP-1".
#
# If INITIAL_POSITION is left out the selector will not
# present an option for the position arg.
#
# Usage: update-monitor EXECUTE_LABEL INITIAL_RESOLUTION INITIAL_REFRESH_RATE [INITIAL_POSITION]
update-monitor() {
local EXECUTE_LABEL="$1"
local RES="$2"
local RATE="$3"
read -a POS <<< "$4"
local POSITIONS_LIST_LABELS=(
"$LABEL_POSITION_SAME_AS"
"$LABEL_POSITION_LEFT_OF"
"$LABEL_POSITION_RIGHT_OF"
"$LABEL_POSITION_ABOVE"
"$LABEL_POSITION_BELOW"
)
local ACTIONS=(
"$LABEL_CONFIG_RESOLUTION$RES"
"$LABEL_CONFIG_REFRESH_RATE${RATE}$LABEL_CONFIG_REFRESH_RATE_POSTFIX"
)
[ "${#POS[@]}" != "0" ] && ACTIONS+=("$LABEL_CONFIG_POSITION`get-relative-position-to-label ${POS[0]}` ${POS[1]}")
while :; do
case `select-action "$EXECUTE_LABEL" "${ACTIONS[@]}" "$LABEL_CONFIG_DENY"` in
"$EXECUTE_LABEL")
xrandr --output "$MONITOR" --mode "$RES" --rate "$RATE" --${POS[0]} "${POS[1]}"
if [ -n "$RESTART_WM_CMD" ]; then
sleep 1
$RESTART_WM_CMD
fi
exit
;;
"$LABEL_CONFIG_RESOLUTION$RES")
local res=`select-action $(get-resolutions $MONITOR)`
if test -n "$res"; then
RES="$res"
RATE=`get-update-rates-for-resolution $MONITOR $RES | head -n1`
ACTIONS=(
"$LABEL_CONFIG_RESOLUTION$RES"
"$LABEL_CONFIG_REFRESH_RATE${RATE}$LABEL_CONFIG_REFRESH_RATE_POSTFIX"
)
[ "${#POS[@]}" != "0" ] && ACTIONS+=("$LABEL_CONFIG_POSITION`get-relative-position-to-label ${POS[0]}`${POS[1]}")
fi
;;
"$LABEL_CONFIG_REFRESH_RATE${RATE}$LABEL_CONFIG_REFRESH_RATE_POSTFIX")
local rate=`for rate in $(get-update-rates-for-resolution $MONITOR $RES); do echo "${rate}hz"; done | $D_MENU_CMD 2> /dev/null`
if test -n "$rate"; then
RATE="${rate%hz}"
ACTIONS=(
"$LABEL_CONFIG_RESOLUTION$RES"
"$LABEL_CONFIG_REFRESH_RATE${RATE}$LABEL_CONFIG_REFRESH_RATE_POSTFIX"
)
[ "${#POS[@]}" != "0" ] && ACTIONS+=("$LABEL_CONFIG_POSITION`get-relative-position-to-label ${POS[0]}`${POS[1]}")
fi
;;
"$LABEL_CONFIG_POSITION`get-relative-position-to-label ${POS[0]}`${POS[1]}")
local ACTIVE_MONITOR_LIST=("${ACTIVE_MONITORS[@]}")
for index in "${!ACTIVE_MONITOR_LIST[@]}"; do
if test "${ACTIVE_MONITOR_LIST[index]}" == "$MONITOR"; then
unset 'ACTIVE_MONITOR_LIST[index]'
fi
done
local MONITOR_LIST=()
for i in "${!ACTIVE_MONITOR_LIST[@]}"; do
if test -n "$i"; then
MONITOR_LIST+=( "${ACTIVE_MONITOR_LIST[i]}" )
fi
done
if test "${#MONITOR_LIST[@]}" -ne "0"; then
local pos=`select-action "${POSITIONS_LIST_LABELS[@]}"`
echo "'$pos' '`get-relative-position-from-label $pos`'"
if test -n "$pos"; then
if test "${#MONITOR_LIST[@]}" -eq "1"; then
POS=("`get-relative-position-from-label $pos`" "${MONITOR_LIST[0]}")
ACTIONS=(
"$LABEL_CONFIG_RESOLUTION$RES"
"$LABEL_CONFIG_REFRESH_RATE${RATE}$LABEL_CONFIG_REFRESH_RATE_POSTFIX"
"$LABEL_CONFIG_POSITION`get-relative-position-to-label ${POS[0]}`${POS[1]}"
)
else
local monitor=`select-action "${MONITOR_LIST[@]}"`
if test -n "$monitor"; then
POS=("`get-relative-position-from-label $pos`" "$monitor")
ACTIONS=(
"$LABEL_CONFIG_RESOLUTION$RES"
"$LABEL_CONFIG_REFRESH_RATE${RATE}$LABEL_CONFIG_REFRESH_RATE_POSTFIX"
"$LABEL_CONFIG_POSITION`get-relative-position-to-label ${POS[0]}`${POS[1]}"
)
fi
fi
fi
fi
;;
*) exit;;
esac
done
}
# Gets the first or specified regex capture group in regex result.
#
# Source: based on some Stack Overflow answer don't remember.
# Usage: echo "text" | regex1 REGEX [CAPTURE_GROUP]
function regex1 { gawk "match(\$0,/$1/, ary) {print ary[${2:-1}]}"; }
# Tries to figure out the relative position of a monitor to
# another.
#
# Usage: get-monitor-relative-position MONITOR
get-monitor-relative-position() {
local MONITOR="$1"
local MONITOR_ORDER=(`xrandr | grep -P '\b(\d+\+\d+)\b' | regex1 '^([^ ]+)'`)
local MONITOR_POSITIONS=(`xrandr | grep -oP '\b(\d+\+\d+)\b'`)
local MONITOR_RESOLUTIONS=()
for i in "${!MONITOR_ORDER[@]}"; do
local resolution=`xrandr | awk -v monitor="^${MONITOR_ORDER[i]} connected" '/connected/ {p = 0} $0 ~ monitor {p = 1} p' | grep '*' | cut -d ' ' -f4`
if [[ ${MONITOR_ORDER[i]} = $MONITOR ]]; then
local MONITOR_POSITION="${MONITOR_POSITIONS[i]}"
local MONITOR_RESOLUTION="${resolution}"
unset 'MONITOR_ORDER[i]'
unset 'MONITOR_POSITIONS[i]'
else
local MONITOR_RESOLUTIONS+=("${resolution}")
fi
done
IFS="x" read -a MONITOR_RESOLUTION_PARTS <<< $MONITOR_RESOLUTION
IFS="+" read -a MONITOR_POSITION_PARTS <<< $MONITOR_POSITION
for i in "${!MONITOR_ORDER[@]}"; do
IFS="x" read -a resolutionParts <<< ${MONITOR_RESOLUTIONS[i]}
IFS="+" read -a positionParts <<< ${MONITOR_POSITIONS[i]}
if test "${MONITOR_POSITION_PARTS[0]}" -eq "${positionParts[0]}" -a "${MONITOR_POSITION_PARTS[1]}" -eq "${positionParts[1]}"; then
echo "same-as ${MONITOR_ORDER[i]}"
return
elif test "`expr ${MONITOR_POSITION_PARTS[0]} + ${resolutionParts[0]}`" -eq "${MONITOR_POSITION_PARTS[0]}" -a "${MONITOR_POSITION_PARTS[1]}" -eq "${positionParts[1]}"; then
echo "left-of ${MONITOR_ORDER[i]}"
return
elif test "`expr ${MONITOR_POSITION_PARTS[0]} - ${resolutionParts[0]}`" -eq "${MONITOR_POSITION_PARTS[0]}" -a "${MONITOR_POSITION_PARTS[1]}" -eq "${positionParts[1]}"; then
echo "left-of ${MONITOR_ORDER[i]}"
return
elif test "`expr ${MONITOR_POSITION_PARTS[0]} + ${resolutionParts[0]}`" -eq "${positionParts[0]}" -a "${MONITOR_POSITION_PARTS[1]}" -eq "${positionParts[1]}"; then
echo "right-of ${MONITOR_ORDER[i]}"
return
elif test "`expr ${MONITOR_POSITION_PARTS[0]} - ${resolutionParts[0]}`" -eq "${positionParts[0]}" -a "${MONITOR_POSITION_PARTS[1]}" -eq "${positionParts[1]}"; then
echo "right-of ${MONITOR_ORDER[i]}"
return
elif test "${MONITOR_POSITION_PARTS[0]}" -eq "${positionParts[0]}" -a "${MONITOR_POSITION_PARTS[1]}" -eq "${resolutionParts[1]}"; then
echo "below ${MONITOR_ORDER[i]}"
return
elif test "${MONITOR_POSITION_PARTS[0]}" -eq "${positionParts[0]}" -a "`expr ${MONITOR_POSITION_PARTS[1]} + ${resolutionParts[1]}`" -eq "${positionParts[1]}"; then
echo "above ${MONITOR_ORDER[i]}"
return
else
continue
fi
done
}
ACTIONS=()
for monitor in "${MONITORS[@]}"; do
ACTIONS+=( "$LABEL_MONITOR_AVAILABLE $monitor" )
done
for monitor in "${ACTIVE_MONITORS[@]}"; do
ACTIONS+=( "$LABEL_MONITOR_OFF $monitor" )
done
for monitor in "${ACTIVE_MONITORS[@]}"; do
ACTIONS+=( "$LABEL_MONITOR_MONIFY $monitor" )
done
ACTION=`select-action "${ACTIONS[@]}" "$LABEL_CONFIG_DENY"`
case $ACTION in
$LABEL_MONITOR_AVAILABLE*)
MONITOR="${ACTION#$LABEL_MONITOR_AVAILABLE}"
RES=`get-resolutions $MONITOR | head -n1`
RATE=`get-update-rates-for-resolution $MONITOR $RES | head -n1`
# TODO: Save for monitor type
POSITIONS_LIST=("same-as" "left-of" "right-of" "above" "below")
POS="${POSITIONS_LIST[2]} ${ACTIVE_MONITORS[0]}"
update-monitor "$LABEL_MONITOR_AVAILABLE_CONFIRM$MONITOR" "$RES" "$RATE" "$POS"
;;
$LABEL_MONITOR_OFF*)
xrandr --output "${ACTION#$LABEL_MONITOR_OFF}" --off
if [ -n "$RESTART_WM_CMD" ]; then
sleep 1
$RESTART_WM_CMD
fi
;;
$LABEL_MONITOR_MONIFY*)
MONITOR="${ACTION#$LABEL_MONITOR_MONIFY}"
RES_RATE=`xrandr | awk -v monitor="^$MONITOR connected" '/connected/ {p = 0} $0 ~ monitor {p = 1} p' | grep '*'`
RES=`cat <<< $RES_RATE | cut -d ' ' -f4`
read -a array <<< $RES_RATE
RATE=`for action in "${array[@]}"; do echo $action; done | grep '*'`
RATE="${RATE/\*}"
RATE="${RATE/+}"
update-monitor "$LABEL_MONITOR_MONIFY_CONFIRM$MONITOR" "$RES" "$RATE" "`get-monitor-relative-position $MONITOR`"
;;
*) ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment