diff --git a/.local/bin/xmonconf b/.local/bin/xmonconf index 73d9938..750b1c4 100755 --- a/.local/bin/xmonconf +++ b/.local/bin/xmonconf @@ -1,331 +1,17 @@ #!/bin/bash +# shellcheck disable=SC1090 # This script is used to apply monitor profiles based on the connected monitors and USB devices. set -euo pipefail -_monitors=() # Cache for the xrandr output - -# Get the list of connected and disconnected monitors. -# Includes information about the primary monitor -# E.g: -# eDP-1,connected,primary,1920x1080+0+0,enabled -# HDMI-1,disconnected,1920x1080+1920+0,enabled -# DP-1,disconnected,disabled -_get_monitors() { - local monitors enabled_regex="[[:digit:]]+x[[:digit:]]+\+[[:digit:]]+\+[[:digit:]]+" # Regex for enabled monitors - - # Use caching to avoid multiple calls to xrandr - if [ ${#_monitors[@]} -eq 0 ]; then - # Get the list of connected and disconnected monitors - monitors=$(xrandr | grep -Eo "^.* ((dis|)connected)( primary|) ($enabled_regex|)" | sed 's/ /,/g' | sed 's/,$//g') - - for monitor in $monitors; do - # Set "enabled" if it matches the enabled regex and remove the matching string, else set "disabled" - if [[ "$monitor" =~ $enabled_regex ]]; then - monitor="$monitor,enabled" - else - monitor="$monitor,disabled" - fi - - # Add to the list of monitors - _monitors+=("$monitor") - done - - # _monitors="$monitors" - fi - - for monitor in "${_monitors[@]}"; do - echo "$monitor" - done -} - -# Function that gets all monitors and takes an optional amount of arguments to filter the list. -# Filters are applied as an OR operation. -# E.g: -# get_monitors connected primary -# get_monitors connected primary eDP-1 -get_monitors() { - local monitors - monitors=$(_get_monitors) - - # If no filters are set, return all monitors - if [ "$#" -eq 0 ]; then - echo "$monitors" - return 0 - fi - - for monitor in $monitors; do - for filter in "$@"; do - for part in ${monitor//,/ }; do - if [ "$part" = "$filter" ]; then - echo "$monitor" - continue 3 # Continue to the next monitor - fi - done - done - done -} - -# monitor_exist checks if the specified monitor is connected to the system -# $1: The monitor name to search for -monitor_exist() { - local monitor=${1:?Monitor not set} - local connected_monitors - connected_monitors=$(get_monitors connected) - - for connected_monitor in $connected_monitors; do - if [ "$(cut -d',' -f1 <<<"$connected_monitor")" = "$monitor" ]; then - return 0 - fi - done - - return 1 -} - -# monitors_exist checks if the specified monitors are connected to the system -# $1..$n: The monitor names to search for -monitors_exist() { - local monitor matches=0 - - for monitor in "$@"; do - if monitor_exist "$monitor"; then - echo "$monitor" - matches=$((matches + 1)) - fi - done - - if [ "$matches" -ne "$#" ]; then - echo "Amount of devices does not match the amount of arguments" 1>&2 - return 1 - fi - - return 0 -} - -# Disable all monitors that are not in the list of connected monitors -disable_disconnected_monitors() { - local connected_monitors - connected_monitors=$(get_monitors disconnected) - - echo "Disabling disconnected monitors" - - for monitor in $connected_monitors; do - echo "Disabling monitor $(cut -d',' -f1 <<<"$monitor")" - xrandr --output "$(cut -d',' -f1 <<<"$monitor")" --off - done -} - -# Get the list of USB devices using lsusb, outputs for example "06cb:00f0 Synaptics, Inc." -get_usb_devices() { - lsusb | grep -Eo ' ID .*' | cut -d' ' -f3- -} - -# Get the list of USB device names using lsusb, outputs for example "Synaptics, Inc." -get_usb_device_names() { - get_usb_devices | cut -d' ' -f2- -} - -# Get the list of USB devices that match the name -get_usb_devices_by_name() { - local name=${1:?Name not set} - get_usb_device_names | while read -r device; do - if [ "$device" = "$name" ]; then - echo "$device" - fi - done -} - -# Get the list of USB devices that match the pattern -get_usb_devices_by_pattern() { - local pattern=${1:?Pattern not set} - get_usb_device_names | grep -E "$pattern" -} - -# Returns the list of USB devices that match the criteria -# $1: Required, either "all" or "any" -# $2: Mode, either "exact" or "pattern" -# $3..$n: The name or pattern to search for -usb_devices_exist() { - local required=${1:?Required not set} - local mode=${2:?Mode not set} - shift 2 - local devices matches=0 - - # Check lenght of arguments - if [ "$#" -eq 0 ]; then - echo "No patterns set" 1>&2 - return 1 - fi - - # Check if required and mode args are valid - if ! [[ "$required" =~ ^(all|any)$ ]]; then - echo "Invalid 'required' argument: $required" 1>&2 - return 1 - elif ! [[ "$mode" =~ ^(exact|pattern)$ ]]; then - echo "Invalid 'mode' argument: $mode" 1>&2 - return 1 - fi - - # Process each pattern - for pattern in "$@"; do - if [ -z "$pattern" ]; then - echo "Got empty pattern" 1>&2 - return 1 - fi - - if [ "$mode" = "exact" ]; then - devices="$(get_usb_devices_by_name "$pattern")" - elif [ "$mode" = "pattern" ]; then - devices="$(get_usb_devices_by_pattern "$pattern")" - fi - - # Check if at least one device was found - if [ -n "$devices" ]; then - matches=$((matches + 1)) - echo "$devices" - fi - done - - # Check if all patterns had a match - if [ "$required" = "all" ]; then - if [ "$matches" -eq "$#" ]; then - return 0 - else - echo "Amount of devices does not match the amount of patterns" 1>&2 - return 1 - fi - # Check if at least one pattern had a match - elif [ "$required" = "any" ]; then - if [ "$matches" -ge "0" ]; then - return 0 - else - echo "Amount of devices does not match the amount of patterns" 1>&2 - return 1 - fi - fi -} - -# Get the list of empty i3 workspaces -get_empty_i3_workspaces() { - i3-msg -t get_tree | jq -r ' - recurse(.nodes[]?) | # Recursively descend into "nodes" arrays - select(.type == "workspace") | # Select elements where "type" is "workspace" - select(.name | test("^[^_].*")) | # Select elements where "name" does not start with "_" - select(.nodes | length == 0) | # Further select those where "nodes" array is empty - .name # Output the "name" of these workspaces - ' -} - -i3_workspace_is_empty() { - local workspace_name=${1:?Workspace name not set} - local empty_workspace - empty_workspace=$(get_empty_i3_workspaces) - - for workspace in $empty_workspace; do - if [ "$workspace" = "$workspace_name" ]; then - return 0 - fi - done - - return 1 -} - -# Get a comma separated list of i3 workspace names and the output they are on -get_i3_workspaces() { - i3-msg -t get_workspaces | jq -r '.[] | .name + "," + .output' -} - -get_i3_workspace_prop() { - local workspace_name=${1:?Workspace name not set} - local prop=${2:?Property not set} - i3-msg -t get_workspaces | jq -r ".[] | select(.name == \"$workspace_name\") | .$prop" -} - -i3_workspace_exists() { - local workspace_name=${1:?Workspace name not set} - local workspaces name - - if [[ -n "$(get_i3_workspace_prop "$workspace_name" name)" ]]; then - return 0 - fi - - return 1 -} - -# This function selects a workspace by name and moves it to the desired monitor. -# If the workspace name is an integer, it will allow incrementing the workspace name by the given number -move_i3_workspace() { - local workspace_name=${1:?Workspace name not set} - local monitor_name=${2:?Monitor name not set} - local increment=${3:-0} - local new_workspace_name="$workspace_name" - - if ! i3_workspace_exists "$workspace_name"; then - echo "Workspace $workspace_name not found" 1>&2 - return 1 - elif ! monitor_exist "$monitor_name"; then - echo "Monitor $monitor_name not found" 1>&2 - return 1 - fi - - # Ensure the new output is not the same as the current output - if [[ "$(get_i3_workspace_prop "$workspace_name" output)" == "$monitor_name" ]]; then - echo "Workspace $workspace_name is already on monitor $monitor_name" 1>&2 - return 1 - fi - - # Increment the workspace name if it is an integer - if [[ "$workspace_name" =~ ^[0-9]+$ ]]; then - new_workspace_name=$((workspace_name + increment)) - elif [ "$increment" -ne 0 ]; then - echo "Workspace name is not an integer. Unable to increment/decrement." 1>&2 - return 1 - fi - - # Check if the new workspace name already exists and is not the same as the current workspace name - if i3_workspace_exists "$new_workspace_name"; then - if [[ "$new_workspace_name" != "$workspace_name" ]]; then - echo "Workspace $new_workspace_name already exists" 1>&2 - return 1 - fi - fi - - echo "Moving workspace $workspace_name to monitor $monitor_name as $new_workspace_name" - - i3-msg "workspace $workspace_name" 1>/dev/null # Select the workspace - i3-msg "rename workspace $workspace_name to $new_workspace_name" 1>/dev/null - i3-msg "move workspace to output $monitor_name" 1>/dev/null -} - -# This function moves all workspaces that are not on the given monitor to the given monitor -move_nonexistent_i3_workspaces() { - local monitor_name=${1:?Monitor name not set} - local increment=${2:-0} - local workspaces - workspaces=$(get_i3_workspaces) - - echo "Moving workspaces to monitor $monitor_name" - - for workspace in $workspaces; do - local workspace_name - local workspace_output - workspace_name=$(echo "$workspace" | cut -d',' -f1) - workspace_output=$(echo "$workspace" | cut -d',' -f2) - - if i3_workspace_is_empty "$workspace_name"; then - echo "Skipping empty workspace $workspace_name" - continue - fi - - if ! monitor_exist "$workspace_output"; then - move_i3_workspace "$workspace_name" "$monitor_name" "$increment" - fi - done -} +# Source the utility functions +source ~/.local/share/dabruh/libs/bash/x.sh +source ~/.local/share/dabruh/libs/bash/i3.sh +source ~/.local/share/dabruh/libs/bash/usb.sh main() { - local primary_monitor monitor_count monitors=() profile_found=false exists + local primary_monitor monitor_count monitors=() profile_found=false primary_monitor=$(get_monitors primary | cut -d',' -f1) monitor_count=$(get_monitors connected | wc -l) echo "Monitors connected: $monitor_count" diff --git a/.local/share/dabruh/libs/bash/i3.sh b/.local/share/dabruh/libs/bash/i3.sh new file mode 100644 index 0000000..76a5fee --- /dev/null +++ b/.local/share/dabruh/libs/bash/i3.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +# This script contains utility functions related to i3 workspaces. + +# Ensure required functions exist +if ! command -v monitor_exist >/dev/null 2>&1; then + echo "monitor_exist function is required but not found. Please source x.sh before i3.sh." 1>&2 + exit 1 +fi + +# Get the list of empty i3 workspaces. +# Usage: get_empty_i3_workspaces +get_empty_i3_workspaces() { + set -euo pipefail + i3-msg -t get_tree | jq -r ' + recurse(.nodes[]?) | + select(.type == "workspace") | + select(.name | test("^[^_].*")) | + select(.nodes | length == 0) | + .name + ' +} + +# Check if the i3 workspace is empty. +# Usage: i3_workspace_is_empty +i3_workspace_is_empty() { + set -euo pipefail + local workspace_name=${1:?Workspace name not set} + local empty_workspace + empty_workspace=$(get_empty_i3_workspaces) + + for workspace in $empty_workspace; do + if [ "$workspace" = "$workspace_name" ]; then + return 0 + fi + done + + return 1 +} + +# Get a comma separated list of i3 workspace names and the output they are on. +# Usage: get_i3_workspaces +get_i3_workspaces() { + set -euo pipefail + i3-msg -t get_workspaces | jq -r '.[] | .name + "," + .output' +} + +# Get a property of an i3 workspace. +# Usage: get_i3_workspace_prop +get_i3_workspace_prop() { + set -euo pipefail + local workspace_name=${1:?Workspace name not set} + local prop=${2:?Property not set} + i3-msg -t get_workspaces | jq -r ".[] | select(.name == \"$workspace_name\") | .$prop" +} + +# Check if an i3 workspace exists. +# Usage: i3_workspace_exists +i3_workspace_exists() { + set -euo pipefail + local workspace_name=${1:?Workspace name not set} + if [[ -n "$(get_i3_workspace_prop "$workspace_name" name)" ]]; then + return 0 + fi + return 1 +} + +# Move an i3 workspace to a different monitor. +# Usage: move_i3_workspace [increment] +move_i3_workspace() { + set -euo pipefail + local workspace_name=${1:?Workspace name not set} + local monitor_name=${2:?Monitor name not set} + local increment=${3:-0} + local new_workspace_name="$workspace_name" + + if ! i3_workspace_exists "$workspace_name"; then + echo "Workspace $workspace_name not found" 1>&2 + return 1 + elif ! monitor_exist "$monitor_name"; then + echo "Monitor $monitor_name not found" 1>&2 + return 1 + fi + + if [[ "$(get_i3_workspace_prop "$workspace_name" output)" == "$monitor_name" ]]; then + echo "Workspace $workspace_name is already on monitor $monitor_name" 1>&2 + return 1 + fi + + if [[ "$workspace_name" =~ ^[0-9]+$ ]]; then + new_workspace_name=$((workspace_name + increment)) + elif [ "$increment" -ne 0 ]; then + echo "Workspace name is not an integer. Unable to increment/decrement." 1>&2 + return 1 + fi + + if i3_workspace_exists "$new_workspace_name"; then + if [[ "$new_workspace_name" != "$workspace_name" ]]; then + echo "Workspace $new_workspace_name already exists" 1>&2 + return 1 + fi + fi + + echo "Moving workspace $workspace_name to monitor $monitor_name as $new_workspace_name" + + i3-msg "workspace $workspace_name" 1>/dev/null + i3-msg "rename workspace $workspace_name to $new_workspace_name" 1>/dev/null + i3-msg "move workspace to output $monitor_name" 1>/dev/null +} + +# Move all workspaces to a specified monitor. +# Usage: move_nonexistent_i3_workspaces [increment] +move_nonexistent_i3_workspaces() { + set -euo pipefail + local monitor_name=${1:?Monitor name not set} + local increment=${2:-0} + local workspaces + workspaces=$(get_i3_workspaces) + + echo "Moving workspaces to monitor $monitor_name" + + for workspace in $workspaces; do + local workspace_name + local workspace_output + workspace_name=$(echo "$workspace" | cut -d',' -f1) + workspace_output=$(echo "$workspace" | cut -d',' -f2) + + if i3_workspace_is_empty "$workspace_name"; then + echo "Skipping empty workspace $workspace_name" + continue + fi + + if ! monitor_exist "$workspace_output"; then + move_i3_workspace "$workspace_name" "$monitor_name" "$increment" + fi + done +} diff --git a/.local/share/dabruh/libs/bash/usb.sh b/.local/share/dabruh/libs/bash/usb.sh new file mode 100644 index 0000000..3315b72 --- /dev/null +++ b/.local/share/dabruh/libs/bash/usb.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# This script contains utility functions related to USB devices. + +# Get the list of USB devices. +# Usage: get_usb_devices +get_usb_devices() { + set -euo pipefail + lsusb | grep -Eo ' ID .*' | cut -d' ' -f3- +} + +# Get the list of USB device names. +# Usage: get_usb_device_names +get_usb_device_names() { + set -euo pipefail + get_usb_devices | cut -d' ' -f2- +} + +# Get the list of USB devices that match the name. +# Usage: get_usb_devices_by_name +get_usb_devices_by_name() { + set -euo pipefail + local name=${1:?Name not set} + get_usb_device_names | while read -r device; do + if [ "$device" = "$name" ]; then + echo "$device" + fi + done +} + +# Get the list of USB devices that match the pattern. +# Usage: get_usb_devices_by_pattern +get_usb_devices_by_pattern() { + set -euo pipefail + local pattern=${1:?Pattern not set} + get_usb_device_names | grep -E "$pattern" +} + +# Returns the list of USB devices that match the criteria. +# Usage: usb_devices_exist [pattern2] ... +usb_devices_exist() { + set -euo pipefail + local required=${1:?Required not set} + local mode=${2:?Mode not set} + shift 2 + local devices matches=0 + + if [ "$#" -eq 0 ]; then + echo "No patterns set" 1>&2 + return 1 + fi + + if ! [[ "$required" =~ ^(all|any)$ ]]; then + echo "Invalid 'required' argument: $required" 1>&2 + return 1 + elif ! [[ "$mode" =~ ^(exact|pattern)$ ]]; then + echo "Invalid 'mode' argument: $mode" 1>&2 + return 1 + fi + + for pattern in "$@"; do + if [ -z "$pattern" ]; then + echo "Got empty pattern" 1>&2 + return 1 + fi + + if [ "$mode" = "exact" ]; then + devices="$(get_usb_devices_by_name "$pattern")" + elif [ "$mode" = "pattern" ]; then + devices="$(get_usb_devices_by_pattern "$pattern")" + fi + + if [ -n "$devices" ]; then + matches=$((matches + 1)) + echo "$devices" + fi + done + + if [ "$required" = "all" ]; then + if [ "$matches" -eq "$#" ]; then + return 0 + else + echo "Amount of devices does not match the amount of patterns" 1>&2 + return 1 + fi + elif [ "$required" = "any" ]; then + if [ "$matches" -ge "0" ]; then + return 0 + else + echo "Amount of devices does not match the amount of patterns" 1>&2 + return 1 + fi + fi +} diff --git a/.local/share/dabruh/libs/bash/x.sh b/.local/share/dabruh/libs/bash/x.sh new file mode 100644 index 0000000..b732844 --- /dev/null +++ b/.local/share/dabruh/libs/bash/x.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# This script contains utility functions related to X monitors. + +_monitors=() # Cache for the xrandr output + +# Get the list of connected and disconnected monitors. +# Usage: _get_monitors +_get_monitors() { + set -euo pipefail + local monitors enabled_regex="[[:digit:]]+x[[:digit:]]+\+[[:digit:]]+\+[[:digit:]]+" # Regex for enabled monitors + + if [ ${#_monitors[@]} -eq 0 ]; then + monitors=$(xrandr | grep -Eo "^.* ((dis|)connected)( primary|) ($enabled_regex|)" | sed 's/ /,/g' | sed 's/,$//g') + for monitor in $monitors; do + if [[ "$monitor" =~ $enabled_regex ]]; then + monitor="$monitor,enabled" + else + monitor="$monitor,disabled" + fi + _monitors+=("$monitor") + done + fi + + for monitor in "${_monitors[@]}"; do + echo "$monitor" + done +} + +# Function to get all monitors with optional filters. +# Usage: get_monitors [filter1] [filter2] ... +get_monitors() { + set -euo pipefail + local monitors + monitors=$(_get_monitors) + + if [ "$#" -eq 0 ]; then + echo "$monitors" + return 0 + fi + + for monitor in $monitors; do + for filter in "$@"; do + for part in ${monitor//,/ }; do + if [ "$part" = "$filter" ]; then + echo "$monitor" + continue 3 # Continue to the next monitor + fi + done + done + done +} + +# Check if the specified monitor is connected. +# Usage: monitor_exist +monitor_exist() { + set -euo pipefail + local monitor=${1:?Monitor not set} + local connected_monitors + connected_monitors=$(get_monitors connected) + + for connected_monitor in $connected_monitors; do + if [ "$(cut -d',' -f1 <<<"$connected_monitor")" = "$monitor" ]; then + return 0 + fi + done + + return 1 +} + +# Check if specified monitors are connected. +# Usage: monitors_exist [monitor2] ... +monitors_exist() { + set -euo pipefail + local monitor matches=0 + + for monitor in "$@"; do + if monitor_exist "$monitor"; then + echo "$monitor" + matches=$((matches + 1)) + fi + done + + if [ "$matches" -ne "$#" ]; then + echo "Amount of devices does not match the amount of arguments ($#: $*)" 1>&2 + return 1 + fi + + return 0 +} + +# Disable all monitors that are not connected. +# Usage: disable_disconnected_monitors +disable_disconnected_monitors() { + set -euo pipefail + local connected_monitors + connected_monitors=$(get_monitors disconnected) + + echo "Disabling disconnected monitors" + + for monitor in $connected_monitors; do + echo "Disabling monitor $(cut -d',' -f1 <<<"$monitor")" + xrandr --output "$(cut -d',' -f1 <<<"$monitor")" --off + done +}