#!/bin/bash # Common library functions for linux-configuration scripts # Source this file at the beginning of scripts that need shared functionality # # Usage: source "$(dirname "$(readlink -f "$0")")/../lib/common.sh" # Or: source "/path/to/scripts/lib/common.sh" # Prevent multiple sourcing [[ -n ${_LIB_COMMON_LOADED:-} ]] && return 0 _LIB_COMMON_LOADED=1 # ============================================================================= # LOGGING FUNCTIONS # ============================================================================= # Log message with timestamp to stderr and optionally to a file # Usage: log_message "message" [log_file] log_message() { local msg="$1" local log_file="${2:-}" local formatted printf -v formatted '%(%Y-%m-%d %H:%M:%S)T - %s' -1 "$msg" echo "$formatted" >&2 if [[ -n $log_file ]]; then echo "$formatted" >>"$log_file" 2>/dev/null || true fi } # Simple log with timestamp (no file output) # Usage: log "message" log() { local _ts printf -v _ts '%(%Y-%m-%d %H:%M:%S)T' -1 printf '[%s] %s\n' "$_ts" "$*" } # ============================================================================= # SUDO / ROOT HANDLING # ============================================================================= # Check if running as root, if not re-exec with sudo # Usage: require_root "$@" require_root() { if [[ $EUID -ne 0 ]]; then echo "This script requires root privileges." echo "Requesting sudo access..." exec sudo "$0" "$@" fi } # Get the actual user even when running with sudo # Usage: ACTUAL_USER=$(get_actual_user) get_actual_user() { echo "${SUDO_USER:-$USER}" } # Get the actual user's home directory # Usage: USER_HOME=$(get_actual_user_home) get_actual_user_home() { local user user=$(get_actual_user) if [[ $user == "root" ]]; then echo "/root" else echo "/home/$user" fi } # Set both ACTUAL_USER and USER_HOME variables (common pattern) # Usage: set_actual_user_vars # echo "$ACTUAL_USER" # => the actual user # echo "$USER_HOME" # => /home/username set_actual_user_vars() { ACTUAL_USER=$(get_actual_user) USER_HOME=$(get_actual_user_home) export ACTUAL_USER USER_HOME } # ============================================================================= # ARGUMENT PARSING HELPERS # ============================================================================= # Parse common --interactive/-i and --help/-h flags # Sets INTERACTIVE_MODE variable (exported for use by calling scripts) # Usage: parse_common_args "$@" # shift "$COMMON_ARGS_SHIFT" export INTERACTIVE_MODE=false export COMMON_ARGS_SHIFT=0 parse_interactive_args() { INTERACTIVE_MODE=false COMMON_ARGS_SHIFT=0 local script_name="${0##*/}" while [[ $# -gt 0 ]]; do case $1 in -i | --interactive) INTERACTIVE_MODE=true ((COMMON_ARGS_SHIFT++)) shift ;; -h | --help) echo "Usage: $script_name [OPTIONS]" echo "Options:" echo " -i, --interactive Enable interactive prompts (default: auto-yes)" echo " -h, --help Show this help message" exit 0 ;; *) # Stop parsing at first unknown argument break ;; esac done } # Handle common argument patterns for scripts with custom usage functions # Usage: handle_arg_help_or_unknown "$1" usage_function err_function # Returns: 0 if argument was handled (caller should continue), 1 if not our concern # Exits: on -h/--help (exit 0) or unknown arg starting with - (exit 2) handle_arg_help_or_unknown() { local arg="$1" local usage_fn="${2:-usage}" local err_fn="${3:-err}" case "$arg" in -h | --help) "$usage_fn" exit 0 ;; -*) "$err_fn" "Unknown argument: $arg" "$usage_fn" exit 2 ;; *) return 1 # Not a flag, let caller handle it ;; esac return 0 } # Initialize a setup script with common boilerplate # Usage: init_setup_script "Script Title" "$@" # This combines: parse_interactive_args, shift, require_root, print_setup_header init_setup_script() { local title="$1" shift parse_interactive_args "$@" shift "$COMMON_ARGS_SHIFT" require_root "$@" print_setup_header "$title" } # ============================================================================= # FOCUS APP DETECTION (for digital wellbeing scripts) # ============================================================================= # Default focus apps - can be overridden before calling is_focus_app_running FOCUS_APPS_WINDOWS=( "Visual Studio Code" "VSCodium" "Cursor" "IntelliJ IDEA" "PyCharm" "WebStorm" "CLion" "Rider" "Sublime Text" "Blender" "Godot" "Unity" "Unreal Editor" ) FOCUS_APPS_PROCESSES=( "steam_app_" "gamescope" ) # Check if any focus app is running (window-based detection) # Returns 0 if focus app found, 1 otherwise # Echoes the name of the found app is_focus_app_running() { # One xdotool call with a combined regex instead of N separate calls if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then local regex wid printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}" regex="${regex%|}" # strip trailing | while IFS= read -r wid; do [[ -n $wid ]] || continue echo "focus app" return 0 done < <(xdotool search --name "$regex" 2>/dev/null) fi # Check specific processes via /proc (no fork) local app comm for app in "${FOCUS_APPS_PROCESSES[@]}"; do for comm in /proc/[0-9]*/comm; do [[ -r $comm ]] || continue read -r _proc_comm < "$comm" 2>/dev/null || continue if [[ $_proc_comm == *"$app"* ]]; then echo "$_proc_comm" return 0 fi done done return 1 } # ============================================================================= # COMMAND AVAILABILITY # ============================================================================= # Check if a command exists # Usage: if require_command ffmpeg; then ... require_command() { local cmd="$1" local pkg="${2:-$1}" if ! command -v "$cmd" >/dev/null 2>&1; then echo "Error: '$cmd' is not installed or not in PATH." >&2 echo "Install with: sudo pacman -S $pkg" >&2 return 1 fi return 0 } # Check for ImageMagick and display helpful installation message # Usage: require_imagemagick [optional: "magick" or "convert"] # Returns: Sets MAGICK_CMD variable to available command require_imagemagick() { local preferred="${1:-}" if [[ $preferred == "magick" ]] || [[ -z $preferred ]]; then if command -v magick &>/dev/null; then MAGICK_CMD="magick" export MAGICK_CMD return 0 fi fi if [[ $preferred == "convert" ]] || [[ -z $preferred ]]; then if command -v convert &>/dev/null; then MAGICK_CMD="convert" export MAGICK_CMD return 0 fi fi echo "Error: ImageMagick is not installed." >&2 echo "Install it with:" >&2 echo " Arch Linux: sudo pacman -S imagemagick" >&2 echo " Ubuntu/Debian: sudo apt install imagemagick" >&2 return 1 } # Install missing pacman packages # Usage: install_missing_pacman_packages pkg1 pkg2 pkg3 ... # Returns 0 if all packages installed successfully, 1 otherwise install_missing_pacman_packages() { local packages=("$@") local missing=() for pkg in "${packages[@]}"; do if ! pacman -Qi "$pkg" >/dev/null 2>&1; then missing+=("$pkg") fi done if [[ ${#missing[@]} -eq 0 ]]; then echo "[INFO] All required packages are already installed." return 0 fi echo "[INFO] Installing missing packages: ${missing[*]}" if ! sudo pacman -S --needed --noconfirm "${missing[@]}"; then echo "[ERROR] Failed to install packages" >&2 return 1 fi return 0 } # ============================================================================= # NOTIFICATION # ============================================================================= # Send desktop notification (fails silently if notify-send not available) # Usage: notify "Title" "Message" [urgency: low/normal/critical] [timeout_ms] notify() { local title="$1" local message="$2" local urgency="${3:-normal}" local timeout="${4:-5000}" if command -v notify-send &>/dev/null; then notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2>/dev/null || true fi } # ============================================================================= # FILE/PATH UTILITIES # ============================================================================= # Get the directory containing the calling script # Usage: SCRIPT_DIR=$(get_script_dir) get_script_dir() { dirname "$(readlink -f "${BASH_SOURCE[1]:-$0}")" } # Ensure a directory exists # Usage: ensure_dir "/path/to/dir" ensure_dir() { local dir="$1" if [[ ! -d $dir ]]; then mkdir -p "$dir" fi } # ============================================================================= # SYSTEMD HELPERS # ============================================================================= # Internal helper for running systemctl with optional --user flag _systemctl_cmd() { local user_flag="$1" shift if [[ $user_flag == "--user" ]]; then systemctl --user "$@" else systemctl "$@" fi } # Enable and start a systemd service (user or system) # Usage: enable_service "service-name" [--user] enable_service() { local service="$1" local user_flag="${2:-}" _systemctl_cmd "$user_flag" daemon-reload _systemctl_cmd "$user_flag" enable --now "$service" } # Check if a systemd service is active # Usage: if is_service_active "service-name" [--user]; then ... is_service_active() { _systemctl_cmd "${2:-}" is-active --quiet "$1" } # Check if a systemd service is enabled # Usage: if is_service_enabled "service-name" [--user]; then ... is_service_enabled() { _systemctl_cmd "${2:-}" is-enabled --quiet "$1" 2>/dev/null } # ============================================================================= # COLORED LOGGING (for scripts that need colored output) # ============================================================================= # ANSI color codes declare -g COLOR_RED='\033[1;31m' declare -g COLOR_GREEN='\033[1;32m' declare -g COLOR_YELLOW='\033[1;33m' declare -g COLOR_BLUE='\033[1;34m' declare -g COLOR_NC='\033[0m' log_info() { printf "${COLOR_BLUE}[INFO]${COLOR_NC} %s\n" "$*" } log_ok() { printf "${COLOR_GREEN}[ OK ]${COLOR_NC} %s\n" "$*" } log_warn() { printf "${COLOR_YELLOW}[WARN]${COLOR_NC} %s\n" "$*" >&2 } log_error() { printf "${COLOR_RED}[ERROR]${COLOR_NC} %s\n" "$*" >&2 } # Alias for compatibility warn() { log_warn "$@"; } err() { log_error "$@"; } # ============================================================================= # EFFICIENT TIME FUNCTIONS (zero-fork bash builtins) # ============================================================================= # These functions use printf '%(...)'T' bash builtin (NO external commands) # to avoid fork-storm anti-patterns in polling scripts. # See: .github/skills/efficient-polling-scripts/SKILL.md # Get current Unix timestamp (seconds since epoch) # Usage: ts=$(get_timestamp) # FORK-FREE: uses bash builtin printf %s (sec_since_epoch) get_timestamp() { printf '%(%s)T' -1 } # Get current date in YYYY-MM-DD format # Usage: date=$(get_date) get_date() { printf '%(%Y-%m-%d)T' -1 } # Get current time in HH:MM:SS format # Usage: time=$(get_time) get_time() { printf '%(%H:%M:%S)T' -1 } # Get current date-time in YYYY-MM-DD HH:MM:SS format # Usage: dt=$(get_datetime) get_datetime() { printf '%(%Y-%m-%d %H:%M:%S)T' -1 } # Get day of week (1=Monday, 7=Sunday) # Usage: dow=$(get_day_of_week) get_day_of_week() { printf '%(%u)T' -1 } # Get day name (Monday, Tuesday, ...) # Usage: day=$(get_day_name) get_day_name() { printf '%(%A)T' -1 } # Get current hour (00-23) # Usage: hour=$(get_hour) get_hour() { printf '%(%H)T' -1 } # Get current minute (00-59) # Usage: minute=$(get_minute) get_minute() { printf '%(%M)T' -1 } # Get current second (00-59) # Usage: second=$(get_second) get_second() { printf '%(%S)T' -1 } # Get Unix timestamp from boot (uptime in seconds) # Usage: boot_seconds=$(get_uptime_seconds) get_uptime_seconds() { read -r uptime_with_fraction _ < /proc/uptime printf '%.*f\n' 0 "$uptime_with_fraction" } # Get boot time in YYYY-MM-DD HH:MM:SS format # Usage: boot_time=$(get_boot_datetime) # Calculates: current_time - uptime_seconds get_boot_datetime() { local uptime_seconds uptime_seconds=$(get_uptime_seconds) local boot_ts=$(($(get_timestamp) - uptime_seconds)) printf '%(%Y-%m-%d %H:%M:%S)T' "$boot_ts" } # Get boot time date only (YYYY-MM-DD) # Usage: boot_date=$(get_boot_date) get_boot_date() { local uptime_seconds uptime_seconds=$(get_uptime_seconds) local boot_ts=$(($(get_timestamp) - uptime_seconds)) printf '%(%Y-%m-%d)T' "$boot_ts" } # Get boot time hour only (00-23) # Usage: boot_hour=$(get_boot_hour) get_boot_hour() { local uptime_seconds uptime_seconds=$(get_uptime_seconds) local boot_ts=$(($(get_timestamp) - uptime_seconds)) printf '%(%H)T' "$boot_ts" } # Check if current time is within a given hour range # Usage: if is_hour_in_range 5 8; then ... # 5AM-8AM is_hour_in_range() { local start_hour=$1 local end_hour=$2 local current_hour current_hour=$(get_hour) local current_hour_num=$((10#$current_hour)) [[ $current_hour_num -ge $start_hour ]] && [[ $current_hour_num -lt $end_hour ]] } # Check if current day is a specific day of week # Usage: if is_day_of_week 1 5 6 7; then ... # Monday, Friday, Saturday, Sunday is_day_of_week() { local target_day target_day=$(get_day_of_week) for day in "$@"; do [[ $target_day -eq $day ]] && return 0 done return 1 } # ============================================================================= # INTERACTIVE PROMPTS # ============================================================================= # Ask yes/no question, returns 0 for yes, 1 for no # Usage: if ask_yes_no "Continue?"; then ... ask_yes_no() { local prompt="$1" local ans read -r -p "$prompt [y/N]: " ans || true case "${ans:-}" in y | Y | yes | YES) return 0 ;; *) return 1 ;; esac } # Check if a command is available # Usage: if has_cmd git; then ... has_cmd() { command -v "$1" >/dev/null 2>&1 } # ============================================================================= # STANDARD SETUP HEADER # ============================================================================= # Print a standard setup header for scripts # Usage: print_setup_header "Script Name" print_setup_header() { local title="$1" local current_datetime current_datetime=$(get_datetime) echo "$title" printf '=%.0s' $(seq 1 ${#title}) echo "" echo "Current Date: $current_datetime" echo "User: $USER" echo "Original user: $(get_actual_user)" if [[ $INTERACTIVE_MODE == "true" ]]; then echo "Mode: Interactive (prompts enabled)" else echo "Mode: Automatic (auto-yes, use --interactive for prompts)" fi } # ============================================================================= # MOUNT/UNMOUNT HELPERS (for hosts guard and similar) # ============================================================================= # Count mount layers for a path # Usage: count=$(mount_layers_count "/etc/hosts") mount_layers_count() { local target="$1" awk -v t="$target" '$5==t{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0 } # Collapse all bind mount layers for a path # Usage: collapse_mounts "/etc/hosts" [max_iterations] collapse_mounts() { local target="$1" local max_iter="${2:-20}" local i=0 if has_cmd mountpoint; then while mountpoint -q "$target"; do umount -l "$target" >/dev/null 2>&1 || break i=$((i + 1)) ((i >= max_iter)) && break done else local cnt cnt=$(mount_layers_count "$target") while ((cnt > 1)); do umount -l "$target" >/dev/null 2>&1 || break i=$((i + 1)) ((i >= max_iter)) && break cnt=$(mount_layers_count "$target") done fi } # ============================================================================= # RESOLUTION/FORMAT VALIDATION # ============================================================================= # Validate resolution format (WIDTHxHEIGHT) # Usage: if validate_resolution "1920x1080"; then ... validate_resolution() { local res="$1" [[ $res =~ ^[0-9]+x[0-9]+$ ]] } # Generate output filename with suffix # Usage: output=$(generate_output_filename "input.jpg" "_resized") generate_output_filename() { local input="$1" local suffix="$2" local ext="${3:-}" local basename dirname filename extension basename=$(basename "$input") dirname=$(dirname "$input") filename="${basename%.*}" extension="${basename##*.}" # Handle files without extension if [[ $filename == "$extension" ]]; then extension="${ext:-jpg}" fi echo "${dirname}/${filename}${suffix}.${extension}" }