diff --git a/docs/superpowers/contracts/linux-config-common-lib-split-2026-08-16.json b/docs/superpowers/contracts/linux-config-common-lib-split-2026-08-16.json new file mode 100644 index 00000000..8d69fc68 --- /dev/null +++ b/docs/superpowers/contracts/linux-config-common-lib-split-2026-08-16.json @@ -0,0 +1,18 @@ +{ + "title": "Split scripts/lib/common.sh under the 250-line cap", + "objective": "Bring the shared library that 49 scripts source under the cap, without changing any name it exports or requiring a single consumer to be edited.", + "acceptance_criteria": [ + "common.sh and all three new parts are at most 250 lines.", + "common.sh remains the single path every consumer sources; it sources its own parts, so no caller changes.", + "Sourcing common.sh yields exactly the same set of function names as before the split, proven by diffing `declare -F` against a detached worktree at HEAD. A passing lint is not sufficient evidence for a library with this many consumers.", + "Any array or constant a moved function reads moves with it, rather than being left behind and suppressed.", + "The pre-commit shellcheck hook passes on every file, with no new suppression.", + "The parts are sourced in the original definition order, so a consumer relying on ordering is unaffected." + ], + "out_of_scope": [ + "Executing any of the 49 consumer scripts.", + "Changing what any helper does.", + "The other over-cap linux_configuration scripts." + ], + "verifier": "A subshell that sources common.sh and prints `declare -F`, diffed against the same run from a detached worktree at HEAD; plus array-length checks, shellcheck -x, meta/scripts/verify_shell_split.sh on a real consumer, and check_file_length.sh." +} diff --git a/docs/superpowers/evidence/linux-config-common-lib-split-2026-08-16.json b/docs/superpowers/evidence/linux-config-common-lib-split-2026-08-16.json new file mode 100644 index 00000000..18264bfd --- /dev/null +++ b/docs/superpowers/evidence/linux-config-common-lib-split-2026-08-16.json @@ -0,0 +1,43 @@ +{ + "intent": "Split scripts/lib/common.sh, the library 49 scripts source, without changing a single name it exports.", + "scope": [ + "linux_configuration/scripts/lib/common.sh (620 -> 163)", + "lib/common_packages.sh, common_services.sh, common_datetime.sh (new)", + "Non-goal: no behaviour change, no change to the public surface" + ], + "changes": [ + "common.sh stays the single entry point and now sources its own three parts, exactly the shape the handoff prescribes for phone_focus_mode/config.sh. All 49 consumers keep sourcing one path and need no edit.", + "Split by concern: user/root/setup helpers stay, focus-app detection and package installation go to common_packages.sh, service control and the log_* family to common_services.sh, and the date/time/uptime helpers to common_datetime.sh.", + "The FOCUS_APPS_WINDOWS and FOCUS_APPS_PROCESSES arrays moved with is_focus_app_running, the only function that reads them - leaving them behind produced two SC2034 warnings." + ], + "verification": [ + { + "command": "source common.sh in a subshell and diff `declare -F` against the same from a detached worktree at HEAD", + "result": "pass", + "evidence": "Identical 48-function set before and after. For a library with 49 consumers this is the check that matters: it proves the public surface is unchanged rather than inferring it from a passing lint." + }, + { + "command": "source common.sh and print the array lengths", + "result": "pass", + "evidence": "WINDOWS=13 PROCESSES=2 - the arrays are populated, so moving them did not break is_focus_app_running." + }, + { + "command": "shellcheck -x common.sh and shellcheck on each part", + "result": "fail then pass", + "evidence": "Two SC2034 warnings for the arrays left behind; zero after moving them. No suppression added." + }, + { + "command": "meta/scripts/verify_shell_split.sh on convert_video.sh, a real consumer", + "result": "pass", + "evidence": "'libs sourced, functions defined' - a script that sources common.sh from two directories up still resolves everything through the new chain." + } + ], + "risks": [ + "common.sh is sourced by 49 scripts and by systemd-invoked ones; the function-set diff covers the names, but nothing here executes those 49 scripts. A consumer that relied on a definition ORDER rather than a name would not be caught - the parts are sourced in the original order to keep that safe.", + "_COMMON_DIR uses ${BASH_SOURCE[0]}, which is required here: common.sh is always sourced, never executed, so $0 would point at the calling script." + ], + "rollback": [ + "git revert the commit; common.sh returns to 620 lines and the three parts disappear.", + "After rollback, re-run the declare -F diff and expect the same 48 functions." + ] +} diff --git a/linux_configuration/scripts/lib/common.sh b/linux_configuration/scripts/lib/common.sh index 9e1c8647..dda3799e 100755 --- a/linux_configuration/scripts/lib/common.sh +++ b/linux_configuration/scripts/lib/common.sh @@ -154,467 +154,10 @@ init_setup_script() { 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 _ /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}" -} +_COMMON_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +# shellcheck source=common_services.sh +source "$_COMMON_DIR/common_packages.sh" +# shellcheck source=common_services.sh +source "$_COMMON_DIR/common_services.sh" +# shellcheck source=common_datetime.sh +source "$_COMMON_DIR/common_datetime.sh" diff --git a/linux_configuration/scripts/lib/common_datetime.sh b/linux_configuration/scripts/lib/common_datetime.sh new file mode 100755 index 00000000..a653e0e1 --- /dev/null +++ b/linux_configuration/scripts/lib/common_datetime.sh @@ -0,0 +1,237 @@ +#!/bin/bash +# Date, time, uptime and boot-time helpers. +# +# Sourced by common.sh, which stays the single entry point every script +# sources -- 49 of them, so the public surface must not change. + +# ============================================================================= +# 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 _ /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}" +} diff --git a/linux_configuration/scripts/lib/common_packages.sh b/linux_configuration/scripts/lib/common_packages.sh new file mode 100755 index 00000000..75e1802c --- /dev/null +++ b/linux_configuration/scripts/lib/common_packages.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# Focus-app detection, command checks and package installation. +# +# Sourced by common.sh, which stays the single entry point every script +# sources -- 49 of them, so the public surface must not change. + +# 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 +# ============================================================================= +# 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" +) + + +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 +} diff --git a/linux_configuration/scripts/lib/common_services.sh b/linux_configuration/scripts/lib/common_services.sh new file mode 100755 index 00000000..ef60db6a --- /dev/null +++ b/linux_configuration/scripts/lib/common_services.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Service control, directory helpers and the log_* family. +# +# Sourced by common.sh, which stays the single entry point every script +# sources -- 49 of them, so the public surface must not change. + +# ============================================================================= +# 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 "$@"; }