Refactor xmonconf script by modularizing functions

- Moved i3-related functions to ~/.local/share/dabruh/libs/bash/i3.sh
- Moved USB-related functions to ~/.local/share/dabruh/libs/bash/usb.sh
- Moved xrandr-related functions to ~/.local/share/dabruh/libs/bash/x.sh
- Added checks in each library file to ensure required functions are sourced
- Updated xmonconf script to source these new library files
This commit is contained in:
dabruh 2024-05-24 10:42:28 +02:00
parent a71430f12e
commit 90b7fc35d2
4 changed files with 342 additions and 320 deletions

View file

@ -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"

View file

@ -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 <workspace_name>
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 <workspace_name> <property>
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 <workspace_name>
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 <workspace_name> <monitor_name> [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 <monitor_name> [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
}

View file

@ -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 <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 <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 <all|any> <exact|pattern> <pattern1> [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
}

View file

@ -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_name>
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 <monitor1> [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
}