Split scripts/lib/common.sh, the library 49 scripts source
Some checks are pending
billsplit CI / quality (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

620 -> 163, with common.sh staying the single entry point that sources
its own three parts - the shape the handoff prescribes for config.sh, so
all 49 consumers are untouched.

Verified by diffing `declare -F` after sourcing it against the same from
a detached worktree at HEAD: an identical 48-function set. For a library
this widely sourced that is the check that matters, not a passing lint.

The FOCUS_APPS_* arrays moved with is_focus_app_running, the only
function that reads them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 21:14:48 +02:00
parent 43c502c940
commit 51e3945507
6 changed files with 549 additions and 464 deletions

View File

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

View File

@ -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."
]
}

View File

@ -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 _ </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}"
}
_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"

View File

@ -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 _ </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}"
}

View File

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

View File

@ -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 "$@"; }