refactor: reduce code duplication from 1.97% to 0.76%

- Add common.sh library functions: require_imagemagick, install_missing_pacman_packages, handle_arg_help_or_unknown
- Create android.sh shared library for Android utilities
- Create hosts-guard-common.sh for pacman hooks shared functions
- Update multiple scripts to source common.sh and use shared helpers
- Add print_shutdown_schedule helper in setup_midnight_shutdown.sh
- Remove duplicate log(), usage(), install_packages patterns across scripts
- Format all shell scripts with shfmt (2-space indent)
This commit is contained in:
Krzysztof kuhy Rudnicki 2025-12-11 18:32:15 +01:00
parent 3e336d4958
commit 5b032891c5
37 changed files with 4355 additions and 2616 deletions

View File

@ -61,7 +61,7 @@ printf 'Running duplicate code detection...\n'
JSCPD_BIN="${HOME}/.local/node_modules/.bin/jscpd"
# Install jscpd if not present
if [[ ! -x "$JSCPD_BIN" ]]; then
if [[ ! -x $JSCPD_BIN ]]; then
printf ' → jscpd not found, installing...\n'
if ! npm install --prefix ~/.local jscpd 2>&1; then
printf '\nCommit aborted: failed to install jscpd.\n' >&2

View File

@ -75,6 +75,17 @@ install_from_aur() {
fi
}
# Helper: try to install from AUR and log result to done.txt/failed.txt
try_aur_install() {
local repo_url="$1"
local pkg_name="$2"
if install_from_aur "$repo_url" "$pkg_name"; then
echo "$pkg_name" >>done.txt
else
echo "$pkg_name" >>failed.txt
fi
}
process_packages() {
local file_path
file_path="$1"
@ -105,18 +116,10 @@ process_packages() {
echo "$pkg_name" >>failed.txt
fi
else
if install_from_aur "$repo_url" "$pkg_name"; then
echo "$pkg_name" >> done.txt
else
echo "$pkg_name" >> failed.txt
fi
try_aur_install "$repo_url" "$pkg_name"
fi
else
if install_from_aur "$repo_url" "$pkg_name"; then
echo "$pkg_name" >> done.txt
else
echo "$pkg_name" >> failed.txt
fi
try_aur_install "$repo_url" "$pkg_name"
fi
done <"$file_path"
}
@ -174,6 +177,18 @@ while IFS= read -r line; do
fi
done <"aur_packages.txt"
# Helper: Check if all subpackages are installed
# Returns 0 if ALL subpackages are installed, 1 otherwise
all_subpackages_installed() {
local -n sub_pkgs_ref=$1
for subpkg in "${sub_pkgs_ref[@]}"; do
if ! pacman -Qi "$subpkg" &>/dev/null; then
return 1
fi
done
return 0
}
# Read pacman packages from file
declare -a pacman_packages
while IFS= read -r line; do
@ -191,21 +206,15 @@ for pkg in "${pacman_packages[@]}"; do
fi
# Check for texlive subpackages
if [ "$pkg" == "texlive" ]; then
sub_pkgs=(
# shellcheck disable=SC2034 # Used via nameref in all_subpackages_installed
texlive_sub_pkgs=(
texlive-basic texlive-bibtexextra texlive-binextra texlive-context texlive-fontsextra
texlive-fontsrecommended texlive-fontutils texlive-formatsextra texlive-games texlive-humanities
texlive-latex texlive-latexextra texlive-latexrecommended texlive-luatex texlive-mathscience
texlive-metapost texlive-music texlive-pictures texlive-plaingeneric texlive-pstricks
texlive-publishers texlive-xetex
)
all_installed=true
for subpkg in "${sub_pkgs[@]}"; do
if ! pacman -Qi "$subpkg" &> /dev/null; then
all_installed=false
break
fi
done
if [ "$all_installed" = true ]; then
if all_subpackages_installed texlive_sub_pkgs; then
echo "All texlive subpackages are installed, skipping texlive"
continue
fi
@ -213,21 +222,15 @@ for pkg in "${pacman_packages[@]}"; do
# Check for texlive-lang subpackages
if [ "$pkg" == "texlive-lang" ]; then
sub_pkgs=(
# shellcheck disable=SC2034 # Used via nameref in all_subpackages_installed
texlive_lang_sub_pkgs=(
texlive-langarabic texlive-langchinese texlive-langcjk texlive-langcyrillic
texlive-langczechslovak texlive-langenglish texlive-langeuropean texlive-langfrench
texlive-langgerman texlive-langgreek texlive-langitalian texlive-langjapanese
texlive-langkorean texlive-langother texlive-langpolish texlive-langportuguese
texlive-langspanish
)
all_installed=true
for subpkg in "${sub_pkgs[@]}"; do
if ! pacman -Qi "$subpkg" &> /dev/null; then
all_installed=false
break
fi
done
if [ "$all_installed" = true ]; then
if all_subpackages_installed texlive_lang_sub_pkgs; then
echo "All texlive-lang subpackages are installed, skipping texlive-lang"
continue
fi

View File

@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Shared functions for hosts-guard pacman hooks
# This file is sourced by pacman-pre-unlock-hosts.sh and pacman-post-relock-hosts.sh
TARGET=/etc/hosts
LOGTAG=hosts-guard-hook
# Check if target has a read-only mount
is_ro_mount() {
findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro
}
# Count mount layers for the target
mount_layers_count() {
awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0
}
# Collapse all bind mount layers
collapse_mounts() {
local i=0
if command -v mountpoint >/dev/null 2>&1; then
while mountpoint -q "$TARGET"; do
umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1))
((i > 20)) && break
done
else
local cnt
cnt=$(mount_layers_count)
while ((cnt > 1)); do
umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1))
((i > 20)) && break
cnt=$(mount_layers_count)
done
fi
}
# Stop systemd units related to hosts guard
stop_units_if_present() {
local units=(hosts-bind-mount.service hosts-guard.path)
for u in "${units[@]}"; do
if command -v systemctl >/dev/null 2>&1; then
if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then
systemctl stop "$u" >/dev/null 2>&1 || true
fi
fi
done
}
# Remove immutable/append-only attributes
remove_host_attrs() {
if command -v lsattr >/dev/null 2>&1; then
local attrs
attrs=$(lsattr -d "$TARGET" 2>/dev/null || true)
if echo "$attrs" | grep -q " i "; then
chattr -i "$TARGET" >/dev/null 2>&1 || true
fi
if echo "$attrs" | grep -q " a "; then
chattr -a "$TARGET" >/dev/null 2>&1 || true
fi
fi
}
# Apply immutable attribute
apply_immutable() {
if command -v chattr >/dev/null 2>&1; then
chattr +i "$TARGET" >/dev/null 2>&1 || true
fi
}
# Apply a single read-only bind mount layer
apply_ro_bind_mount() {
mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true
mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true
}
# Start the path watcher service
start_path_watcher() {
if command -v systemctl >/dev/null 2>&1; then
systemctl start hosts-guard.path >/dev/null 2>&1 || true
fi
}
# Log to system logger and run log file
log_hook() {
local phase="$1"
local state="$2"
logger -t "$LOGTAG" "$phase: $state"
echo "$(date -Is) $phase-$state" >>/run/hosts-guard-hook.log 2>/dev/null || true
}

View File

@ -3,54 +3,30 @@
set -euo pipefail
TARGET=/etc/hosts
# Source shared functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=hosts-guard-common.sh
source "$SCRIPT_DIR/hosts-guard-common.sh"
ENFORCE=/usr/local/sbin/enforce-hosts.sh
LOGTAG=hosts-guard-hook
mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0; }
collapse_mounts() {
local i=0
if command -v mountpoint >/dev/null 2>&1; then
while mountpoint -q "$TARGET"; do
umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1))
((i > 20)) && break
done
else
local cnt
cnt=$(mount_layers_count)
while ((cnt > 1)); do
umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1))
((i > 20)) && break
cnt=$(mount_layers_count)
done
fi
}
log_hook "post" "relocking(start)"
# Ensure we end with a single read-only bind mount layer
logger -t "$LOGTAG" "post: relocking /etc/hosts (starting)"
echo "$(date -Is) post-relock(start)" >>/run/hosts-guard-hook.log 2>/dev/null || true
# Collapse any stacked mounts first
collapse_mounts
# Run enforcement script if available
if [[ -x $ENFORCE ]]; then
"$ENFORCE" >/dev/null 2>&1 || true
fi
if command -v chattr >/dev/null 2>&1; then
chattr +i "$TARGET" >/dev/null 2>&1 || true
fi
# Apply protections
apply_immutable
apply_ro_bind_mount
# Apply exactly one ro bind layer
mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true
mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true
# Start the path watcher
start_path_watcher
# Start only the path watcher; avoid bind-mount service (we already bound once)
if command -v systemctl >/dev/null 2>&1; then
systemctl start hosts-guard.path >/dev/null 2>&1 || true
fi
logger -t "$LOGTAG" "post: relocking /etc/hosts (done)"
echo "$(date -Is) post-relock(done)" >>/run/hosts-guard-hook.log 2>/dev/null || true
log_hook "post" "relocking(done)"
exit 0

View File

@ -3,69 +3,27 @@
set -euo pipefail
TARGET=/etc/hosts
LOGTAG=hosts-guard-hook
# Source shared functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=hosts-guard-common.sh
source "$SCRIPT_DIR/hosts-guard-common.sh"
stop_units_if_present() {
local units=(hosts-bind-mount.service hosts-guard.path)
for u in "${units[@]}"; do
if command -v systemctl >/dev/null 2>&1; then
if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then
systemctl stop "$u" >/dev/null 2>&1 || true
fi
fi
done
}
is_ro_mount() { findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro; }
mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0; }
cleanup_mount_stacks() {
local i=0
# Unmount bind layers until /etc/hosts is no longer a mountpoint
if command -v mountpoint >/dev/null 2>&1; then
while mountpoint -q "$TARGET"; do
umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1))
((i > 20)) && break
done
else
# Fallback to best-effort using mountinfo count
local cnt
cnt=$(mount_layers_count)
while ((cnt > 1)); do
umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1))
((i > 20)) && break
cnt=$(mount_layers_count)
done
fi
}
# Drop protective attributes if present
if command -v lsattr >/dev/null 2>&1; then
attrs=$(lsattr -d "$TARGET" 2>/dev/null || true)
if echo "$attrs" | grep -q " i "; then
chattr -i "$TARGET" >/dev/null 2>&1 || true
fi
if echo "$attrs" | grep -q " a "; then
chattr -a "$TARGET" >/dev/null 2>&1 || true
fi
fi
# Remove protective attributes
remove_host_attrs
# Stop guard services
stop_units_if_present
logger -t "$LOGTAG" "pre: unlocking /etc/hosts (starting)"
echo "$(date -Is) pre-unlock" >>/run/hosts-guard-hook.log 2>/dev/null || true
log_hook "pre" "unlocking(start)"
# Always collapse any existing layers; we'll operate on the plain file
cleanup_mount_stacks
# Collapse any existing mount layers
collapse_mounts
# If someone managed a ro single-layer mount, ensure rw by remounting or collapsing again
# Ensure writable by remounting if still read-only
if is_ro_mount; then
mount -o remount,rw "$TARGET" >/dev/null 2>&1 || cleanup_mount_stacks
mount -o remount,rw "$TARGET" >/dev/null 2>&1 || collapse_mounts
fi
logger -t "$LOGTAG" "pre: unlocking /etc/hosts (done)"
log_hook "pre" "unlocking(done)"
exit 0

View File

@ -50,7 +50,7 @@ extract_custom_entries_from_script() {
# Extract custom entries from the current /etc/hosts (entries after "# Custom blocking entries" marker)
extract_custom_entries_from_hosts() {
local hosts_file="$1"
if [[ ! -f "$hosts_file" ]]; then
if [[ ! -f $hosts_file ]]; then
return
fi
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
@ -61,7 +61,7 @@ extract_custom_entries_from_hosts() {
# Load previously saved custom entries state
load_saved_custom_entries() {
if [[ -f "$CUSTOM_ENTRIES_STATE_FILE" ]]; then
if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
sort -u "$CUSTOM_ENTRIES_STATE_FILE"
fi
}
@ -77,7 +77,7 @@ save_custom_entries_state() {
# Helper function to count non-empty lines
count_lines() {
local input="$1"
if [[ -z "$input" ]]; then
if [[ -z $input ]]; then
echo 0
else
echo "$input" | grep -c . 2>/dev/null || echo 0
@ -98,7 +98,7 @@ check_custom_entries_protection() {
# Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
local saved_entries
saved_entries=$(load_saved_custom_entries)
if [[ -z "$saved_entries" ]]; then
if [[ -z $saved_entries ]]; then
# First run or state file missing - extract from current /etc/hosts if it has our marker
saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts")
fi

1793
report/jscpd-report.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -120,6 +120,48 @@ declare -A SERVICE_STATUS
ISSUES_FOUND=0
FIXES_APPLIED=0
######################################################################
# Report issues and optionally run fix script
# Usage: report_and_fix issues_array status_var status_key fix_note setup_script verify_service [args...]
######################################################################
report_and_fix() {
local -n _issues=$1
local -n _status=$2
local status_key="$3"
local fix_note="$4"
local setup_script="$5"
local verify_service="${6:-}"
shift 6
local script_args=("$@")
if [[ $_status != "ok" ]]; then
for issue in "${_issues[@]}"; do
if [[ $_status == "error" ]]; then
err "$issue"
else
warn "$issue"
fi
done
((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 && $_status == "error" ]]; then
note "$fix_note"
if [[ -f $setup_script ]]; then
run bash "$setup_script" "${script_args[@]}"
((FIXES_APPLIED++)) || true
# Re-verify after fix
if [[ $DRY_RUN -eq 0 && -n $verify_service ]] && systemctl is-enabled "$verify_service" &>/dev/null; then
_status="ok"
fi
else
err "Setup script not found: $setup_script"
fi
fi
fi
SERVICE_STATUS["$status_key"]=$_status
}
######################################################################
# Check functions
######################################################################
@ -134,7 +176,7 @@ check_pacman_wrapper() {
if [[ -L /usr/bin/pacman ]]; then
local target
target=$(readlink -f /usr/bin/pacman)
if [[ "$target" == "/usr/local/bin/pacman_wrapper" ]]; then
if [[ $target == "/usr/local/bin/pacman_wrapper" ]]; then
msg "Pacman symlink points to wrapper"
else
issues+=("Pacman symlink points to: $target (expected /usr/local/bin/pacman_wrapper)")
@ -171,7 +213,7 @@ check_pacman_wrapper() {
done
# Report and fix
if [[ "$status" == "error" ]]; then
if [[ $status == "error" ]]; then
for issue in "${issues[@]}"; do
err "$issue"
done
@ -179,7 +221,7 @@ check_pacman_wrapper() {
if [[ $STATUS_ONLY -eq 0 ]]; then
note "Installing pacman wrapper..."
if [[ -f "$PACMAN_WRAPPER_INSTALL" ]]; then
if [[ -f $PACMAN_WRAPPER_INSTALL ]]; then
run bash "$PACMAN_WRAPPER_INSTALL"
((FIXES_APPLIED++)) || true
# Re-verify after fix
@ -232,33 +274,11 @@ check_midnight_shutdown() {
status="error"
fi
# Report and fix
if [[ "$status" != "ok" ]]; then
for issue in "${issues[@]}"; do
if [[ "$status" == "error" ]]; then
err "$issue"
else
warn "$issue"
fi
done
((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 && "$status" == "error" ]]; then
note "Setting up midnight shutdown..."
if [[ -f "$MIDNIGHT_SHUTDOWN_SCRIPT" ]]; then
run bash "$MIDNIGHT_SHUTDOWN_SCRIPT" enable
((FIXES_APPLIED++)) || true
# Re-verify after fix
if [[ $DRY_RUN -eq 0 ]] && systemctl is-enabled day-specific-shutdown.timer &>/dev/null; then
status="ok"
fi
else
err "Setup script not found: $MIDNIGHT_SHUTDOWN_SCRIPT"
fi
fi
fi
SERVICE_STATUS["midnight_shutdown"]=$status
report_and_fix issues status "midnight_shutdown" \
"Setting up midnight shutdown..." \
"$MIDNIGHT_SHUTDOWN_SCRIPT" \
"day-specific-shutdown.timer" \
enable
}
check_startup_monitor() {
@ -298,33 +318,10 @@ check_startup_monitor() {
status="error"
fi
# Report and fix
if [[ "$status" != "ok" ]]; then
for issue in "${issues[@]}"; do
if [[ "$status" == "error" ]]; then
err "$issue"
else
warn "$issue"
fi
done
((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 && "$status" == "error" ]]; then
note "Setting up startup monitor..."
if [[ -f "$STARTUP_MONITOR_SCRIPT" ]]; then
run bash "$STARTUP_MONITOR_SCRIPT"
((FIXES_APPLIED++)) || true
# Re-verify after fix
if [[ $DRY_RUN -eq 0 ]] && systemctl is-enabled pc-startup-monitor.timer &>/dev/null; then
status="ok"
fi
else
err "Setup script not found: $STARTUP_MONITOR_SCRIPT"
fi
fi
fi
SERVICE_STATUS["startup_monitor"]=$status
report_and_fix issues status "startup_monitor" \
"Setting up startup monitor..." \
"$STARTUP_MONITOR_SCRIPT" \
"pc-startup-monitor.timer"
}
check_periodic_systems() {
@ -379,33 +376,10 @@ check_periodic_systems() {
status="error"
fi
# Report and fix
if [[ "$status" != "ok" ]]; then
for issue in "${issues[@]}"; do
if [[ "$status" == "error" ]]; then
err "$issue"
else
warn "$issue"
fi
done
((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 && "$status" == "error" ]]; then
note "Setting up periodic systems..."
if [[ -f "$PERIODIC_SYSTEM_SCRIPT" ]]; then
run bash "$PERIODIC_SYSTEM_SCRIPT"
((FIXES_APPLIED++)) || true
# Re-verify after fix
if [[ $DRY_RUN -eq 0 ]] && systemctl is-enabled periodic-system-maintenance.timer &>/dev/null; then
status="ok"
fi
else
err "Setup script not found: $PERIODIC_SYSTEM_SCRIPT"
fi
fi
fi
SERVICE_STATUS["periodic_systems"]=$status
report_and_fix issues status "periodic_systems" \
"Setting up periodic systems..." \
"$PERIODIC_SYSTEM_SCRIPT" \
"periodic-system-maintenance.timer"
}
check_hosts() {
@ -432,7 +406,7 @@ check_hosts() {
# Check if hosts file is immutable
local attrs
attrs=$(lsattr /etc/hosts 2>/dev/null | cut -d' ' -f1 || echo "")
if [[ "$attrs" == *"i"* ]]; then
if [[ $attrs == *"i"* ]]; then
msg "/etc/hosts has immutable attribute set"
else
issues+=("/etc/hosts is not immutable")
@ -503,9 +477,9 @@ check_hosts() {
fi
# Report issues
if [[ "$status" != "ok" ]]; then
if [[ $status != "ok" ]]; then
for issue in "${issues[@]}"; do
if [[ "$status" == "error" ]]; then
if [[ $status == "error" ]]; then
err "$issue"
else
warn "$issue"
@ -517,7 +491,7 @@ check_hosts() {
# Run hosts install first
if [[ ! -f /etc/hosts ]] || [[ $(wc -l </etc/hosts) -lt 100 ]]; then
note "Installing hosts file..."
if [[ -f "$HOSTS_INSTALL_SCRIPT" ]]; then
if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then
run bash "$HOSTS_INSTALL_SCRIPT"
((FIXES_APPLIED++)) || true
else
@ -528,7 +502,7 @@ check_hosts() {
# Run hosts guard setup
if ! systemctl is-enabled hosts-guard.path &>/dev/null || [[ ! -f /usr/local/sbin/enforce-hosts.sh ]]; then
note "Setting up hosts guard..."
if [[ -f "$HOSTS_GUARD_SCRIPT" ]]; then
if [[ -f $HOSTS_GUARD_SCRIPT ]]; then
run bash "$HOSTS_GUARD_SCRIPT"
((FIXES_APPLIED++)) || true
else
@ -539,7 +513,7 @@ check_hosts() {
# Install pacman hooks if missing
if [[ ! -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then
note "Installing pacman hooks..."
if [[ -f "$HOSTS_PACMAN_HOOKS_SCRIPT" ]]; then
if [[ -f $HOSTS_PACMAN_HOOKS_SCRIPT ]]; then
run bash "$HOSTS_PACMAN_HOOKS_SCRIPT"
((FIXES_APPLIED++)) || true
else

View File

@ -66,10 +66,10 @@ was_opened_this_hour() {
local current_hour
current_hour=$(get_hour_key)
if [[ -f "$state_file" ]]; then
if [[ -f $state_file ]]; then
local last_hour
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
if [[ "$last_hour" == "$current_hour" ]]; then
if [[ $last_hour == "$current_hour" ]]; then
return 0 # Was opened this hour
fi
fi
@ -152,13 +152,13 @@ install_wrapper() {
fi
# Check if wrapper location exists (file or symlink)
if [[ ! -e "$wrapper_path" && ! -L "$wrapper_path" ]]; then
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
echo "$app not installed ($wrapper_path not found)"
return 1
fi
# Check if real binary exists
if [[ ! -x "$real_binary" ]]; then
if [[ ! -x $real_binary ]]; then
echo "$app real binary not found ($real_binary)"
return 1
fi
@ -166,7 +166,7 @@ install_wrapper() {
echo " Installing wrapper for $app..."
# Handle symlinks: save the symlink itself, not the target
if [[ -L "$wrapper_path" ]]; then
if [[ -L $wrapper_path ]]; then
local link_target
link_target=$(readlink "$wrapper_path")
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
@ -207,7 +207,7 @@ uninstall_wrapper() {
# Check if it was a symlink (stored as SYMLINK:target in .orig)
local orig_content
orig_content=$(cat "${wrapper_path}.orig" 2>/dev/null || echo "")
if [[ "$orig_content" == SYMLINK:* ]]; then
if [[ $orig_content == SYMLINK:* ]]; then
local link_target="${orig_content#SYMLINK:}"
echo " Restoring symlink $wrapper_path -> $link_target"
ln -s "$link_target" "$wrapper_path"
@ -229,7 +229,7 @@ install_all() {
script_path="$(readlink -f "$0")"
local install_path="/usr/local/bin/block-compulsive-opening.sh"
if [[ "$script_path" != "$install_path" ]]; then
if [[ $script_path != "$install_path" ]]; then
echo "Installing main script to $install_path..."
cp "$script_path" "$install_path"
chmod +x "$install_path"
@ -287,10 +287,10 @@ show_status() {
local status="not opened this hour"
local icon="○"
if [[ -f "$state_file" ]]; then
if [[ -f $state_file ]]; then
local last_hour
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
if [[ "$last_hour" == "$current_hour" ]]; then
if [[ $last_hour == "$current_hour" ]]; then
status="already opened (blocked until next hour)"
icon="●"
else
@ -303,7 +303,7 @@ show_status() {
local wrapper_path="${APPS[$app]}"
if [[ -f "${wrapper_path}.orig" ]]; then
wrapped="wrapped"
elif [[ -f "$wrapper_path" ]]; then
elif [[ -f $wrapper_path ]]; then
wrapped="installed (not wrapped)"
fi
@ -320,7 +320,7 @@ reset_app() {
local state_file
state_file=$(get_state_file "$app")
if [[ -f "$state_file" ]]; then
if [[ -f $state_file ]]; then
rm -f "$state_file"
echo "Reset $app - can be opened again this hour"
log_message "RESET: $app state cleared by user"
@ -392,7 +392,7 @@ main() {
show_status
;;
reset)
if [[ -z "${2:-}" ]]; then
if [[ -z ${2:-} ]]; then
echo "Error: specify app to reset"
echo "Apps: ${!APPS[*]}"
exit 1
@ -403,7 +403,7 @@ main() {
reset_all
;;
wrapper)
if [[ -z "${2:-}" ]]; then
if [[ -z ${2:-} ]]; then
echo "Error: wrapper requires app name"
exit 1
fi

View File

@ -107,12 +107,12 @@ kill_music_services() {
local yt_music_windows
yt_music_windows=$(xdotool search --name "YouTube Music" 2>/dev/null || true)
for wid in $yt_music_windows; do
if [[ -n "$wid" ]]; then
if [[ -n $wid ]]; then
# Get window name for logging
local wname
wname=$(xdotool getwindowname "$wid" 2>/dev/null || echo "unknown")
# Only close if it's YouTube Music, not regular YouTube
if [[ "$wname" == *"YouTube Music"* ]] || [[ "$wname" == *"music.youtube.com"* ]]; then
if [[ $wname == *"YouTube Music"* ]] || [[ $wname == *"music.youtube.com"* ]]; then
log_message "Closing YouTube Music window: $wname (ID: $wid)"
xdotool windowclose "$wid" 2>/dev/null || true
killed=true
@ -152,7 +152,7 @@ kill_music_services() {
local windows
windows=$(xdotool search --name "$pattern" 2>/dev/null || true)
for wid in $windows; do
if [[ -n "$wid" ]]; then
if [[ -n $wid ]]; then
local wname
wname=$(xdotool getwindowname "$wid" 2>/dev/null || echo "unknown")
log_message "Closing music service window: $wname (ID: $wid)"

View File

@ -220,8 +220,6 @@ function is_greylisted_package_name() {
return 1
}
# Helper: detect if current invocation includes --noconfirm
# Helper: detect if current invocation includes --noconfirm
function has_noconfirm_flag() {
for arg in "$@"; do
@ -232,6 +230,27 @@ function has_noconfirm_flag() {
return 1
}
# Helper: get list of PIDs holding a lock file (excluding our own PID)
# Populates the $holders array
get_lock_holders() {
local lock_file="$1"
holders=()
if command -v fuser >/dev/null 2>&1; then
mapfile -t holders < <(fuser "$lock_file" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true)
elif command -v lsof >/dev/null 2>&1; then
mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null | grep -E '^[0-9]+$' || true)
fi
# Filter out our own PID
if [[ ${#holders[@]} -gt 0 ]]; then
local -a filtered=()
for pid in "${holders[@]}"; do
[[ $pid -eq $$ ]] && continue
filtered+=("$pid")
done
holders=("${filtered[@]}")
fi
}
# Handle stale pacman database lock if present and no package managers are running
check_and_handle_db_lock() {
local lock_file="/var/lib/pacman/db.lck"
@ -242,23 +261,7 @@ check_and_handle_db_lock() {
# Determine which processes actually have the lock open
local -a holders=()
if command -v fuser >/dev/null 2>&1; then
mapfile -t holders < <(fuser "$lock_file" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true)
elif command -v lsof >/dev/null 2>&1; then
mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null | grep -E '^[0-9]+$' || true)
else
holders=()
fi
# Filter out our own PID if it somehow appears
if [[ ${#holders[@]} -gt 0 ]]; then
local -a filtered=()
for pid in "${holders[@]}"; do
[[ $pid -eq $$ ]] && continue
filtered+=("$pid")
done
holders=("${filtered[@]}")
fi
get_lock_holders "$lock_file"
if [[ ${#holders[@]} -gt 0 ]]; then
local pac_holder=0
@ -292,12 +295,7 @@ check_and_handle_db_lock() {
sleep 1
# Re-check holders
holders=()
if command -v fuser >/dev/null 2>&1; then
mapfile -t holders < <(fuser "$lock_file" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true)
elif command -v lsof >/dev/null 2>&1; then
mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null | grep -E '^[0-9]+$' || true)
fi
get_lock_holders "$lock_file"
if [[ ${#holders[@]} -gt 0 ]]; then
echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2
return 1
@ -305,6 +303,16 @@ check_and_handle_db_lock() {
fi
fi
# Helper to remove a file with sudo if needed
remove_file_as_root() {
local f="$1"
if [[ $EUID -ne 0 ]]; then
sudo rm -f "$f"
else
rm -f "$f"
fi
}
# Decide whether to remove the lock
local now epoch age
if epoch=$(stat -c %Y "$lock_file" 2>/dev/null); then
@ -317,11 +325,7 @@ check_and_handle_db_lock() {
# Auto-remove in non-interactive mode (--noconfirm) or if the lock is older than 10 minutes
if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then
echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2
if [[ $EUID -ne 0 ]]; then
sudo rm -f "$lock_file" || return 1
else
rm -f "$lock_file" || return 1
fi
remove_file_as_root "$lock_file" || return 1
return 0
fi
@ -330,25 +334,23 @@ check_and_handle_db_lock() {
echo -e "${CYAN}Lock path:${NC} $lock_file (age: ${age}s)" >&2
read -r -t 15 -p $'Remove stale lock and continue? [y/N]: ' reply || reply="n"
if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then
if [[ $EUID -ne 0 ]]; then
sudo rm -f "$lock_file" || return 1
else
rm -f "$lock_file" || return 1
fi
remove_file_as_root "$lock_file" || return 1
return 0
fi
echo -e "${RED}Aborting due to existing pacman lock. Close other updaters and retry, or run with --noconfirm to auto-clear stale locks.${NC}" >&2
return 1
}
# Cleanup: remove any installed blocked packages (in addition to the queued operation)
function remove_installed_blocked_packages() {
# args not used; kept for future policy extension
# List installed package names
# Generic function to remove installed packages matching a filter
# Args: check_function label_prefix
function remove_installed_packages_matching() {
local check_function="$1"
local label="$2"
mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null)
local to_remove=()
for name in "${installed_names[@]}"; do
if is_blocked_package_name "$name"; then
if "$check_function" "$name"; then
to_remove+=("$name")
fi
done
@ -357,83 +359,59 @@ function remove_installed_blocked_packages() {
return 0
fi
echo -e "${YELLOW}Policy cleanup:${NC} Removing blocked installed packages: ${BOLD}${to_remove[*]}${NC}" >&2
local remove_cmd=("$PACMAN_BIN" -Rns --noconfirm)
"${remove_cmd[@]}" "${to_remove[@]}"
echo -e "${YELLOW}${label} cleanup:${NC} Removing packages: ${BOLD}${to_remove[*]}${NC}" >&2
"$PACMAN_BIN" -Rns --noconfirm "${to_remove[@]}"
local rc=$?
if [[ $rc -ne 0 ]]; then
echo -e "${RED}Cleanup removal failed with exit code ${rc}.${NC}" >&2
echo -e "${RED}${label} cleanup removal failed with exit code ${rc}.${NC}" >&2
else
echo -e "${GREEN}Cleanup removal completed for: ${to_remove[*]}${NC}" >&2
echo -e "${GREEN}${label} cleanup removal completed for: ${to_remove[*]}${NC}" >&2
fi
return $rc
}
# Cleanup: remove any installed greylisted packages (challenge-required packages)
function remove_installed_greylisted_packages() {
# List installed package names
mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null)
local to_remove=()
for name in "${installed_names[@]}"; do
if is_greylisted_package_name "$name"; then
to_remove+=("$name")
fi
done
# Cleanup: remove any installed blocked packages
function remove_installed_blocked_packages() {
remove_installed_packages_matching is_blocked_package_name "Policy"
}
if [[ ${#to_remove[@]} -eq 0 ]]; then
# Cleanup: remove any installed greylisted packages
function remove_installed_greylisted_packages() {
remove_installed_packages_matching is_greylisted_package_name "Greylist"
}
# Helper: Check if this is an install command and run a filter on each package name
# Usage: check_install_for filter_func "$@"
# Returns 0 if filter_func matches any package
function check_install_for() {
local filter_func="$1"
shift
# Check if the command is an installation command
if [[ ${1:-} == "-S" || ${1:-} == "-Sy" || ${1:-} == "-Syu" || ${1:-} == "-Syyu" || ${1:-} == "-U" ]]; then
for arg in "$@"; do
# Strip repository prefix if present (like extra/ or community/)
local package_name="${arg##*/}"
if "$filter_func" "$package_name"; then
return 0
fi
echo -e "${YELLOW}Greylist cleanup:${NC} Removing greylisted installed packages: ${BOLD}${to_remove[*]}${NC}" >&2
local remove_cmd=("$PACMAN_BIN" -Rns --noconfirm)
"${remove_cmd[@]}" "${to_remove[@]}"
local rc=$?
if [[ $rc -ne 0 ]]; then
echo -e "${RED}Greylist cleanup removal failed with exit code ${rc}.${NC}" >&2
else
echo -e "${GREEN}Greylist cleanup removal completed for: ${to_remove[*]}${NC}" >&2
done
fi
return $rc
return 1
}
# Function to check if user is trying to install packages that are always blocked
function check_for_always_blocked() {
# Check if the command is an installation command
if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then
# Check all arguments
for arg in "$@"; do
# Strip repository prefix if present (like extra/ or community/)
local package_name="${arg##*/}"
if is_blocked_package_name "$package_name"; then
return 0 # Always blocked package found
fi
done
fi
return 1 # No always blocked package found
check_install_for is_blocked_package_name "$@"
}
# Helper to check if a package name is steam
function is_steam_package() {
[[ $1 == "steam" ]]
}
# Function to check if user is trying to install steam (challenge-eligible package)
function check_for_steam() {
# List of packages that require challenge (only steam in this case)
local steam_packages=("steam")
# Check if the command is an installation command
if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then
# Check all arguments
for arg in "$@"; do
# Strip repository prefix if present (like extra/ or community/)
local package_name="${arg##*/}"
# Check if argument matches steam
for package in "${steam_packages[@]}"; do
if [[ $arg == "$package" || $arg == *"/$package-"* || $arg == *"/$package/"* ||
$arg == *"/$package" || $package_name == "$package" ]]; then
return 0 # Steam package found
fi
done
done
fi
return 1 # No steam package found
check_install_for is_steam_package "$@"
}
# Function to check if current day is a weekday (after 4PM Friday until midnight Sunday)
@ -459,6 +437,121 @@ function is_weekday() {
fi
}
# Unified word unscrambling challenge function
# Args: challenge_name word_length words_count timeout_seconds initial_delay_max post_delay_min post_delay_range
function run_word_challenge() {
local challenge_name="$1"
local word_length="$2"
local words_count="$3"
local timeout_seconds="$4"
local initial_delay_max="${5:-20}"
local post_delay_min="${6:-0}"
local post_delay_range="${7:-20}"
echo -e "${YELLOW}${challenge_name} challenge will begin shortly...${NC}"
# Initial delay
local sleep_duration=$((RANDOM % initial_delay_max))
sleep "$sleep_duration"
# Load words file
local script_dir words_file
script_dir="$(dirname "$(readlink -f "$0")")"
words_file="$script_dir/words.txt"
if [[ ! -f $words_file ]]; then
echo -e "${RED}Error: words.txt file not found at $words_file${NC}"
return 1
fi
echo -e "${CYAN}Challenge: Words with ${word_length} letters${NC}"
# Load random words of specified length
local -a selected_words
mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count")
if [[ ${#selected_words[@]} -lt $words_count ]]; then
echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}"
words_count=${#selected_words[@]}
if [[ $words_count -eq 0 ]]; then
echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}"
return 1
fi
fi
# Convert to uppercase
for i in "${!selected_words[@]}"; do
selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]')
done
echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}"
# Display words in grid
for ((i = 0; i < words_count; i++)); do
printf "${BLUE}%-15s${NC}" "${selected_words[i]}"
if (((i + 1) % 4 == 0)); then
echo ""
fi
done
# Select and scramble a word
local target_index target_word scrambled_word
target_index=$((RANDOM % words_count))
target_word="${selected_words[target_index]}"
scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n')
if [[ $scrambled_word == "$target_word" ]]; then
scrambled_word=$(echo "$target_word" | rev)
fi
echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}"
echo -e "${YELLOW}Unscramble the word to proceed (you have $timeout_seconds seconds):${NC}"
# Timer display background process
(
local start_time current_time elapsed remaining
start_time=$(date +%s)
while true; do
current_time=$(date +%s)
elapsed=$((current_time - start_time))
remaining=$((timeout_seconds - elapsed))
if [[ $remaining -le 0 ]]; then
echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} "
break
fi
echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} "
sleep 1
done
) &
local display_pid=$!
# Read input with timeout
local user_input read_status
read -t "$timeout_seconds" -r user_input
read_status=$?
kill "$display_pid" 2>/dev/null
wait "$display_pid" 2>/dev/null
echo
if [[ $read_status -ne 0 ]]; then
echo -e "${RED}Time's up! Challenge failed. The correct word was '$target_word'.${NC}"
return 1
fi
user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs)
if [[ $user_input == "$target_word" ]]; then
echo -e "${GREEN}Correct! Proceeding with installation...${NC}"
local post_challenge_sleep=$((RANDOM % post_delay_range + post_delay_min))
[[ $post_challenge_sleep -gt 0 ]] && sleep "$post_challenge_sleep"
return 0
else
echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}"
return 1
fi
}
# Function to prompt for solving a word unscrambling challenge (only for steam)
function prompt_for_steam_challenge() {
echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}"
@ -472,259 +565,20 @@ function prompt_for_steam_challenge() {
return 1
fi
echo -e "${YELLOW}Weekend Steam challenge will begin shortly...${NC}"
# Sleep for random 20-40 seconds
# sleep_duration=$((RANDOM % 20 + 20))
sleep_duration=$((RANDOM % 20))
sleep "$sleep_duration"
# Define path to words.txt (in the same directory as the script)
script_dir="$(dirname "$(readlink -f "$0")")"
words_file="$script_dir/words.txt"
# Check if words.txt exists
if [[ ! -f $words_file ]]; then
echo -e "${RED}Error: words.txt file not found at $words_file${NC}"
return 1
fi
# Choose a specific word length (5, 6, 7, or 8 characters)
#
word_length=5
echo -e "${CYAN}Today's challenge: Words with ${word_length} letters${NC}"
# Filter words by the specific chosen length and load random words
words_count=160
mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count")
# If we couldn't get enough words of the right length
if [[ ${#selected_words[@]} -lt $words_count ]]; then
echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}"
words_count=${#selected_words[@]}
if [[ $words_count -eq 0 ]]; then
echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}"
return 1
fi
fi
# Convert all words to uppercase
for i in "${!selected_words[@]}"; do
selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]')
done
echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}"
# Display the words in a grid (4 columns)
for ((i = 0; i < words_count; i++)); do
printf "${BLUE}%-15s${NC}" "${selected_words[i]}"
if (((i + 1) % 4 == 0)); then
echo ""
fi
done
# Select a random word to scramble (already in uppercase)
target_index=$((RANDOM % words_count))
target_word="${selected_words[target_index]}"
# Scramble the word
scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n')
# Ensure scrambled word is different from original
if [[ $scrambled_word == "$target_word" ]]; then
# Use simple reversal as fallback
scrambled_word=$(echo "$target_word" | rev)
fi
echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}"
echo -e "${YELLOW}Unscramble the word to proceed with installation (you have 2 minutes):${NC}"
# Set up a background process to display the timer
(
start_time=$(date +%s)
while true; do
current_time=$(date +%s)
elapsed=$((current_time - start_time))
remaining=$((60 - elapsed))
if [[ $remaining -le 0 ]]; then
echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} "
break
fi
echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} "
sleep 1
done
) &
display_pid=$!
# Read user input with timeout
read -t 60 -r user_input
read_status=$?
# Kill the timer display
kill "$display_pid" 2>/dev/null
wait "$display_pid" 2>/dev/null
echo # Add a newline after the timer
# Check if read timed out
if [[ $read_status -ne 0 ]]; then
echo -e "${RED}Time's up! Challenge failed. The correct word was '$target_word'.${NC}"
return 1
fi
# Convert user input to uppercase and trim whitespaces
user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs)
if [[ $user_input == "$target_word" ]]; then
echo -e "${GREEN}Correct! Proceeding with installation...${NC}"
# Add sleep after successful challenge completion (20-40 seconds)
# post_challenge_sleep=$((RANDOM % 20 + 20))
post_challenge_sleep=$((RANDOM % 20))
sleep "$post_challenge_sleep"
return 0
else
echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}"
return 1
fi
# word_length=5, words_count=160, timeout=60s, initial_delay=20, post_delay=0-20
run_word_challenge "Weekend Steam" 5 160 60 20 0 20
}
function check_for_greylisted() {
# Check if the command is an installation command
if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then
# Check all arguments
for arg in "$@"; do
# Strip repository prefix if present
local package_name="${arg##*/}"
# Check if package name matches any greylisted keyword
if is_greylisted_package_name "$package_name"; then
return 0 # Greylisted package found
fi
done
fi
return 1 # No greylisted package found
check_install_for is_greylisted_package_name "$@"
}
# Function to prompt for solving a word unscrambling challenge (for greylisted packages - always active)
function prompt_for_greylist_challenge() {
echo -e "${YELLOW}WARNING: You are trying to install a greylisted package.${NC}"
echo -e "${YELLOW}Greylist challenge will begin shortly...${NC}"
# Sleep for random 10-30 seconds
sleep_duration=$((RANDOM % 20 + 10))
sleep "$sleep_duration"
# Define path to words.txt (in the same directory as the script)
script_dir="$(dirname "$(readlink -f "$0")")"
words_file="$script_dir/words.txt"
# Check if words.txt exists
if [[ ! -f $words_file ]]; then
echo -e "${RED}Error: words.txt file not found at $words_file${NC}"
return 1
fi
# Choose a specific word length (6 characters for greylist challenge)
word_length=6
echo -e "${CYAN}Greylist challenge: Words with ${word_length} letters${NC}"
# Filter words by the specific chosen length and load random words
words_count=120
mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count")
# If we couldn't get enough words of the right length
if [[ ${#selected_words[@]} -lt $words_count ]]; then
echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}"
words_count=${#selected_words[@]}
if [[ $words_count -eq 0 ]]; then
echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}"
return 1
fi
fi
# Convert all words to uppercase
for i in "${!selected_words[@]}"; do
selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]')
done
echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}"
# Display the words in a grid (4 columns)
for ((i = 0; i < words_count; i++)); do
printf "${BLUE}%-15s${NC}" "${selected_words[i]}"
if (((i + 1) % 4 == 0)); then
echo ""
fi
done
# Select a random word to scramble (already in uppercase)
target_index=$((RANDOM % words_count))
target_word="${selected_words[target_index]}"
# Scramble the word
scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n')
# Ensure scrambled word is different from original
if [[ $scrambled_word == "$target_word" ]]; then
# Use simple reversal as fallback
scrambled_word=$(echo "$target_word" | rev)
fi
echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}"
echo -e "${YELLOW}Unscramble the word to proceed with installation (you have 90 seconds):${NC}"
# Set up a background process to display the timer
(
start_time=$(date +%s)
while true; do
current_time=$(date +%s)
elapsed=$((current_time - start_time))
remaining=$((90 - elapsed))
if [[ $remaining -le 0 ]]; then
echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} "
break
fi
echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} "
sleep 1
done
) &
display_pid=$!
# Read user input with timeout (90 seconds for VirtualBox)
read -t 90 -r user_input
read_status=$?
# Kill the timer display
kill "$display_pid" 2>/dev/null
wait "$display_pid" 2>/dev/null
echo # Add a newline after the timer
# Check if read timed out
if [[ $read_status -ne 0 ]]; then
echo -e "${RED}Time's up! Greylist challenge failed. The correct word was '$target_word'.${NC}"
return 1
fi
# Convert user input to uppercase and trim whitespaces
user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs)
if [[ $user_input == "$target_word" ]]; then
echo -e "${GREEN}Correct! Proceeding with installation...${NC}"
# Add sleep after successful challenge completion (15-35 seconds)
post_challenge_sleep=$((RANDOM % 20 + 15))
sleep "$post_challenge_sleep"
return 0
else
echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}"
return 1
fi
# word_length=6, words_count=120, timeout=90s, initial_delay=30, post_delay=15-35
run_word_challenge "Greylist" 6 120 90 30 15 20
}
# Check for wrapper-specific commands

View File

@ -6,6 +6,11 @@
set -e # Exit on any error
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Function to show usage
show_usage() {
echo "Day-Specific Auto-Shutdown Setup for Arch Linux"
@ -35,13 +40,7 @@ check_sudo() {
}
# Get the actual user (even when running with sudo)
if [[ -n $SUDO_USER ]]; then
ACTUAL_USER="$SUDO_USER"
USER_HOME="/home/$SUDO_USER"
else
ACTUAL_USER="$USER"
USER_HOME="$HOME"
fi
set_actual_user_vars
# Function to show current status
show_current_status() {
@ -603,6 +602,13 @@ test_setup() {
systemctl list-timers day-specific-shutdown.timer --no-pager 2>/dev/null | head -5 | grep day-specific-shutdown || echo "Timer information not available"
}
# Display the shutdown schedule (used in multiple places)
print_shutdown_schedule() {
echo "Shutdown Schedule:"
echo " Monday-Wednesday: 21:00-05:00 (9:00 PM to 5:00 AM)"
echo " Thursday-Sunday: 22:00-05:00 (10:00 PM to 5:00 AM)"
}
# Function to show final instructions
show_instructions() {
echo ""
@ -618,9 +624,7 @@ show_instructions() {
echo "✓ Monitor service installed (protects timer from being disabled)"
echo "✓ Watchdog timer installed (restarts monitor if stopped)"
echo ""
echo "Shutdown Schedule:"
echo " Monday-Wednesday: 21:00-05:00 (9:00 PM to 5:00 AM)"
echo " Thursday-Sunday: 22:00-05:00 (10:00 PM to 5:00 AM)"
print_shutdown_schedule
echo ""
echo "Management commands:"
echo " sudo day-specific-shutdown-manager.sh status - Check status"
@ -646,9 +650,7 @@ confirm_setup() {
echo "==============================================="
echo "This will set up your PC to automatically shutdown during specific time windows."
echo ""
echo "Shutdown Schedule:"
echo " Monday-Wednesday: 21:00-05:00 (9:00 PM to 5:00 AM)"
echo " Thursday-Sunday: 22:00-05:00 (10:00 PM to 5:00 AM)"
print_shutdown_schedule
echo ""
echo "Important considerations:"
echo "- Any unsaved work will be lost during shutdown windows"

View File

@ -10,14 +10,13 @@ set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# ---------- User/paths ----------
if [[ -n ${SUDO_USER:-} ]]; then
ACTUAL_USER="$SUDO_USER"
USER_HOME="/home/$SUDO_USER"
else
ACTUAL_USER="$USER"
USER_HOME="$HOME"
fi
set_actual_user_vars
INSTALL_ROOT_DEFAULT="$USER_HOME/.local/share/unreal-mcp"
INSTALL_ROOT="$INSTALL_ROOT_DEFAULT"

View File

@ -13,7 +13,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/.raspberry_pi.conf"
# Load configuration from gitignored config file if it exists
if [[ -f "$CONFIG_FILE" ]]; then
if [[ -f $CONFIG_FILE ]]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
fi
@ -93,7 +93,7 @@ generate_password() {
}
auto_generate_pi_password() {
if [[ -z "$PI_PASSWORD" ]]; then
if [[ -z $PI_PASSWORD ]]; then
PI_PASSWORD=$(generate_password 16)
log_info "Auto-generated Pi password (will be saved to config file)"
fi
@ -150,7 +150,7 @@ discover_remote_laptop() {
nmap -sn -T4 "$network" &>/dev/null || true
ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u)
if [[ -z "$ssh_hosts" ]]; then
if [[ -z $ssh_hosts ]]; then
die "No SSH-enabled devices found on network"
fi
@ -163,7 +163,7 @@ discover_remote_laptop() {
for u in "${common_users[@]}"; do
local is_dup=0
for existing in "${users[@]}"; do
if [[ "$u" == "$existing" ]]; then
if [[ $u == "$existing" ]]; then
is_dup=1
break
fi
@ -182,7 +182,7 @@ discover_remote_laptop() {
for ip in $ssh_hosts; do
idx=$((idx + 1))
if [[ "$ip" == "$gateway" ]]; then
if [[ $ip == "$gateway" ]]; then
log_info "[$idx/$host_count] Skipping $ip (gateway)"
continue
fi
@ -198,13 +198,13 @@ discover_remote_laptop() {
local has_sd
has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2>/dev/null || true)
if [[ -n "$has_sd" ]]; then
if [[ -n $has_sd ]]; then
log_success "[$idx/$host_count] $ip - Found SD card: $has_sd"
found_laptop="$ip"
break 2
else
log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..."
if [[ -z "$found_laptop" ]]; then
if [[ -z $found_laptop ]]; then
found_laptop="$ip"
fi
fi
@ -213,19 +213,19 @@ discover_remote_laptop() {
done
done
if [[ -z "$found_laptop" ]] || [[ -z "$found_user" ]]; then
if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then
log_warning "No device with passwordless SSH found using common usernames."
found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1)
if [[ -z "$found_laptop" ]]; then
if [[ -z $found_laptop ]]; then
die "Could not find any suitable SSH-enabled device"
fi
log_info "Found SSH host at $found_laptop but need credentials."
read -r -p "Enter username for $found_laptop: " found_user
if [[ -z "$found_user" ]]; then
if [[ -z $found_user ]]; then
die "No username provided"
fi
fi
@ -279,16 +279,16 @@ download_raspberry_pi_os() {
mkdir -p "$download_dir"
if [[ -f "$extracted_image" ]]; then
if [[ -f $extracted_image ]]; then
log_info "Using existing image at $extracted_image"
echo "$extracted_image"
return
fi
if [[ -f "$image_file" ]]; then
if [[ -f $image_file ]]; then
local actual_size
actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0)
if [[ "$actual_size" -lt "$expected_size" ]]; then
if [[ $actual_size -lt $expected_size ]]; then
log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..."
rm -f "$image_file"
else
@ -296,7 +296,7 @@ download_raspberry_pi_os() {
fi
fi
if [[ ! -f "$image_file" ]]; then
if [[ ! -f $image_file ]]; then
log_info "Downloading Raspberry Pi OS Lite (64-bit)..."
log_info "This may take a while depending on your internet connection..."
@ -312,7 +312,7 @@ download_raspberry_pi_os() {
local actual_size
actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0)
if [[ "$actual_size" -lt "$expected_size" ]]; then
if [[ $actual_size -lt $expected_size ]]; then
die "Download incomplete: got $actual_size bytes, expected $expected_size"
fi
log_success "Download complete: $actual_size bytes"
@ -321,7 +321,7 @@ download_raspberry_pi_os() {
log_info "Extracting image..."
xz -dk "$image_file"
if [[ ! -f "$extracted_image" ]]; then
if [[ ! -f $extracted_image ]]; then
die "Failed to extract image"
fi
@ -342,7 +342,7 @@ phase_flash_local() {
local devices
devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}')
if [[ -z "$devices" ]]; then
if [[ -z $devices ]]; then
log_warning "No removable devices detected automatically."
lsblk -d -o NAME,SIZE,TYPE,RM,TRAN
read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE
@ -352,13 +352,13 @@ phase_flash_local() {
read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE
fi
if [[ ! -b "$SD_CARD_DEVICE" ]]; then
if [[ ! -b $SD_CARD_DEVICE ]]; then
die "Device $SD_CARD_DEVICE does not exist or is not a block device"
fi
local root_device
root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//')
if [[ "$SD_CARD_DEVICE" == "$root_device" ]]; then
if [[ $SD_CARD_DEVICE == "$root_device" ]]; then
die "Cannot flash to the system drive!"
fi
@ -375,7 +375,7 @@ phase_flash_local() {
log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE"
read -r -p "Are you sure you want to continue? (yes/no): " confirm
if [[ "$confirm" != "yes" ]]; then
if [[ $confirm != "yes" ]]; then
die "Aborted by user"
fi
@ -423,7 +423,7 @@ phase_flash_local() {
root_partition="${SD_CARD_DEVICE}p2"
fi
if [[ -n "$root_partition" ]]; then
if [[ -n $root_partition ]]; then
local root_mount="/tmp/rpi-root"
mkdir -p "$root_mount"
mount "$root_partition" "$root_mount"
@ -475,7 +475,7 @@ phase_flash_remote() {
local sd_device
sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2>/dev/null || true)
if [[ -z "$sd_device" ]]; then
if [[ -z $sd_device ]]; then
die "No SD card detected on remote laptop. Please insert an SD card and try again."
fi
@ -530,7 +530,7 @@ phase_execute_remote() {
log_info "=== Executing Flash on Remote Laptop ==="
if [[ -z "$SD_CARD_DEVICE" ]]; then
if [[ -z $SD_CARD_DEVICE ]]; then
die "SD_CARD_DEVICE not set"
fi
@ -570,7 +570,7 @@ phase_execute_remote() {
touch "$boot_mount/ssh"
log_success "SSH enabled"
if [[ -n "$encrypted_password" ]]; then
if [[ -n $encrypted_password ]]; then
echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt"
log_success "User '$PI_USER' configured"
fi
@ -582,7 +582,7 @@ phase_execute_remote() {
root_partition="${SD_CARD_DEVICE}p2"
fi
if [[ -n "$root_partition" ]]; then
if [[ -n $root_partition ]]; then
local root_mount="/tmp/rpi-root"
mkdir -p "$root_mount"
mount "$root_partition" "$root_mount"

View File

@ -14,7 +14,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/.raspberry_pi.conf"
# Load configuration from gitignored config file if it exists
if [[ -f "$CONFIG_FILE" ]]; then
if [[ -f $CONFIG_FILE ]]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
fi
@ -102,7 +102,7 @@ generate_password() {
}
auto_generate_nextcloud_password() {
if [[ -z "$NEXTCLOUD_ADMIN_PASSWORD" ]]; then
if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then
NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20)
log_info "Auto-generated Nextcloud admin password (will be saved to config file)"
fi
@ -180,11 +180,11 @@ discover_raspberry_pi() {
# Try resolving hostname directly
pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true
if [[ -z "$pi_ip" ]]; then
if [[ -z $pi_ip ]]; then
pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true
fi
if [[ -n "$pi_ip" ]]; then
if [[ -n $pi_ip ]]; then
log_success "Found Pi by hostname: $pi_ip"
echo "$pi_ip"
return
@ -196,7 +196,7 @@ discover_raspberry_pi() {
local ssh_hosts
ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) || true
if [[ -z "$ssh_hosts" ]]; then
if [[ -z $ssh_hosts ]]; then
die "No SSH-enabled devices found. Is the Pi connected and booted?"
fi
@ -599,7 +599,7 @@ phase_fix_issues() {
# Generate server certificate signed by our CA
local regenerate="${1:-}"
if [[ ! -f "$ssl_dir/server.crt" ]] || [[ "$regenerate" == "--regenerate" ]]; then
if [[ ! -f "$ssl_dir/server.crt" ]] || [[ $regenerate == "--regenerate" ]]; then
log_info "Generating server certificate signed by CA..."
# Generate server private key
@ -718,7 +718,7 @@ EOF
# Enable SVG in ImageMagick policy
local policy_file="/etc/ImageMagick-6/policy.xml"
if [[ -f "$policy_file" ]]; then
if [[ -f $policy_file ]]; then
# Remove SVG restrictions if present
sed -i 's/<policy domain="coder" rights="none" pattern="SVG" \/>/<policy domain="coder" rights="read|write" pattern="SVG" \/>/' "$policy_file"
# If no SVG policy exists, add one allowing it
@ -777,7 +777,7 @@ phase_setup_ssl() {
log_info "=== Setting up Let's Encrypt SSL with DuckDNS ==="
# Check if DuckDNS is configured
if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]]; then
if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then
echo
log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain."
log_info "1. Go to https://www.duckdns.org/ and sign in with Google/GitHub/etc."
@ -789,7 +789,7 @@ phase_setup_ssl() {
read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN
read -r -p "Enter your email (for Let's Encrypt notifications): " LETSENCRYPT_EMAIL
if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]] || [[ -z "$LETSENCRYPT_EMAIL" ]]; then
if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]] || [[ -z $LETSENCRYPT_EMAIL ]]; then
die "All fields are required"
fi
fi
@ -817,7 +817,7 @@ phase_setup_ssl() {
echo
read -r -p "Have you set up port forwarding? (yes/no): " port_forward_done
if [[ "$port_forward_done" != "yes" ]]; then
if [[ $port_forward_done != "yes" ]]; then
log_info "Please set up port forwarding and run this command again."
log_info "Without port forwarding, Let's Encrypt cannot verify your domain."
exit 0
@ -829,7 +829,7 @@ phase_setup_ssl() {
# When ip= is empty, DuckDNS auto-detects the public IP
duckdns_response=$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=")
if [[ "$duckdns_response" != "OK" ]]; then
if [[ $duckdns_response != "OK" ]]; then
die "Failed to update DuckDNS: $duckdns_response"
fi
log_success "DuckDNS updated to public IP"
@ -855,14 +855,14 @@ DUCKEOF
log_info "Waiting for DNS propagation (this may take a minute)..."
local dns_ip=""
local attempts=0
while [[ "$dns_ip" != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do
while [[ $dns_ip != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do
sleep 5
dns_ip=$(dig +short "$full_domain" 2>/dev/null | tail -1) || true
attempts=$((attempts + 1))
log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)"
done
if [[ "$dns_ip" != "$public_ip" ]]; then
if [[ $dns_ip != "$public_ip" ]]; then
log_warning "DNS may not have propagated yet. Continuing anyway..."
else
log_success "DNS verified: $full_domain -> $public_ip"
@ -961,19 +961,19 @@ EOF
phase_setup_ssl_remote() {
log_info "=== Setting up Let's Encrypt SSL via SSH ==="
if [[ -z "$PI_PASSWORD" ]]; then
if [[ -z $PI_PASSWORD ]]; then
die "PI_PASSWORD not set. Run install-remote first."
fi
local pi_ip
pi_ip=$(discover_raspberry_pi)
if [[ -z "$pi_ip" ]]; then
if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi"
fi
# Get DuckDNS credentials if not set
if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]]; then
if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then
echo
log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain."
log_info "1. Go to https://www.duckdns.org/ and sign in"
@ -1012,14 +1012,14 @@ phase_setup_ssl_remote() {
phase_install_remote() {
log_info "=== Installing Nextcloud via SSH ==="
if [[ -z "$PI_PASSWORD" ]]; then
if [[ -z $PI_PASSWORD ]]; then
die "PI_PASSWORD not set. Did you run flash script first?"
fi
local pi_ip
pi_ip=$(discover_raspberry_pi)
if [[ -z "$pi_ip" ]]; then
if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi"
fi
@ -1070,14 +1070,14 @@ phase_install_remote() {
phase_install_ca() {
log_info "=== Installing Nextcloud CA Certificate ==="
if [[ -z "$PI_PASSWORD" ]]; then
if [[ -z $PI_PASSWORD ]]; then
die "PI_PASSWORD not set. Run this after running install-remote or flash."
fi
local pi_ip
pi_ip=$(discover_raspberry_pi)
if [[ -z "$pi_ip" ]]; then
if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi"
fi
@ -1089,7 +1089,7 @@ phase_install_ca() {
sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no \
"${PI_USER}@${pi_ip}" "echo '$PI_PASSWORD' | sudo -S cat /etc/ssl/nextcloud/ca.crt" >"$ca_file" 2>/dev/null
if [[ ! -f "$ca_file" ]] || [[ ! -s "$ca_file" ]]; then
if [[ ! -f $ca_file ]] || [[ ! -s $ca_file ]]; then
die "Failed to download CA certificate"
fi
@ -1146,7 +1146,7 @@ phase_install_ca() {
if [[ -d ~/.mozilla/firefox ]]; then
local installed=0
for profile_dir in ~/.mozilla/firefox/*.default* ~/.mozilla/firefox/*.esr*; do
if [[ -d "$profile_dir" ]]; then
if [[ -d $profile_dir ]]; then
if ! certutil -d sql:"$profile_dir" -L 2>/dev/null | grep -q "Nextcloud"; then
certutil -d sql:"$profile_dir" -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null &&
installed=1

View File

@ -5,6 +5,11 @@
set -e # Exit on any error
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Function to check and request sudo privileges for package installation
check_sudo() {
if [[ $EUID -ne 0 ]] && [[ $1 == "install" ]]; then
@ -14,20 +19,13 @@ check_sudo() {
fi
}
# Get the actual user (even when running with sudo)
set_actual_user_vars
echo "ActivityWatch Setup for Arch Linux + i3"
echo "======================================="
echo "Current Date: $(date)"
echo "User: ${SUDO_USER:-$USER}"
# Get the actual user (even when running with sudo)
if [[ -n $SUDO_USER ]]; then
ACTUAL_USER="$SUDO_USER"
USER_HOME="/home/$SUDO_USER"
else
ACTUAL_USER="$USER"
USER_HOME="$HOME"
fi
echo "User: $ACTUAL_USER"
echo "Target user: $ACTUAL_USER"
echo "User home: $USER_HOME"

View File

@ -19,7 +19,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/.nextcloud_raspberry.conf"
# Load configuration from gitignored config file if it exists
if [[ -f "$CONFIG_FILE" ]]; then
if [[ -f $CONFIG_FILE ]]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
fi
@ -108,14 +108,14 @@ generate_password() {
}
auto_generate_pi_password() {
if [[ -z "$PI_PASSWORD" ]]; then
if [[ -z $PI_PASSWORD ]]; then
PI_PASSWORD=$(generate_password 16)
log_info "Auto-generated Pi password (will be saved to config file)"
fi
}
auto_generate_nextcloud_password() {
if [[ -z "$NEXTCLOUD_ADMIN_PASSWORD" ]]; then
if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then
NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20)
log_info "Auto-generated Nextcloud admin password (will be saved to config file)"
fi
@ -133,8 +133,8 @@ prompt_password() {
read -r -s -p "Confirm password: " password_confirm
echo
if [[ "$password" == "$password_confirm" ]]; then
if [[ -z "$password" ]]; then
if [[ $password == "$password_confirm" ]]; then
if [[ -z $password ]]; then
log_warning "Password cannot be empty. Please try again."
continue
fi
@ -157,7 +157,7 @@ detect_sd_card() {
local devices
devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}')
if [[ -z "$devices" ]]; then
if [[ -z $devices ]]; then
log_warning "No removable devices detected automatically."
log_info "Available block devices:"
lsblk -d -o NAME,SIZE,TYPE,RM,TRAN
@ -171,14 +171,14 @@ detect_sd_card() {
fi
# Validate device exists
if [[ ! -b "$SD_CARD_DEVICE" ]]; then
if [[ ! -b $SD_CARD_DEVICE ]]; then
die "Device $SD_CARD_DEVICE does not exist or is not a block device"
fi
# Safety check - don't flash system drive
local root_device
root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//')
if [[ "$SD_CARD_DEVICE" == "$root_device" ]]; then
if [[ $SD_CARD_DEVICE == "$root_device" ]]; then
die "Cannot flash to the system drive!"
fi
}
@ -192,17 +192,17 @@ download_raspberry_pi_os() {
mkdir -p "$download_dir"
if [[ -f "$extracted_image" ]]; then
if [[ -f $extracted_image ]]; then
log_info "Using existing image at $extracted_image"
echo "$extracted_image"
return
fi
# Check if download exists and is complete
if [[ -f "$image_file" ]]; then
if [[ -f $image_file ]]; then
local actual_size
actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0)
if [[ "$actual_size" -lt "$expected_size" ]]; then
if [[ $actual_size -lt $expected_size ]]; then
log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..."
rm -f "$image_file"
else
@ -210,7 +210,7 @@ download_raspberry_pi_os() {
fi
fi
if [[ ! -f "$image_file" ]]; then
if [[ ! -f $image_file ]]; then
log_info "Downloading Raspberry Pi OS Lite (64-bit)..."
log_info "This may take a while depending on your internet connection..."
@ -229,7 +229,7 @@ download_raspberry_pi_os() {
# Verify download size
local actual_size
actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0)
if [[ "$actual_size" -lt "$expected_size" ]]; then
if [[ $actual_size -lt $expected_size ]]; then
die "Download incomplete: got $actual_size bytes, expected $expected_size"
fi
log_success "Download complete: $actual_size bytes"
@ -238,7 +238,7 @@ download_raspberry_pi_os() {
log_info "Extracting image..."
xz -dk "$image_file"
if [[ ! -f "$extracted_image" ]]; then
if [[ ! -f $extracted_image ]]; then
die "Failed to extract image"
fi
@ -251,7 +251,7 @@ flash_sd_card() {
log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE"
read -r -p "Are you sure you want to continue? (yes/no): " confirm
if [[ "$confirm" != "yes" ]]; then
if [[ $confirm != "yes" ]]; then
die "Aborted by user"
fi
@ -300,7 +300,7 @@ configure_headless_boot() {
# Configure WiFi (optional)
read -r -p "Do you want to configure WiFi? (y/n): " configure_wifi
if [[ "$configure_wifi" == "y" ]]; then
if [[ $configure_wifi == "y" ]]; then
read -r -p "WiFi SSID: " wifi_ssid
read -r -s -p "WiFi Password: " wifi_password
echo
@ -320,7 +320,7 @@ EOF
fi
# Create userconf.txt for first user (Raspberry Pi OS Bookworm+)
if [[ -z "$PI_PASSWORD" ]]; then
if [[ -z $PI_PASSWORD ]]; then
prompt_password "Enter password for Pi user '$PI_USER'" PI_PASSWORD
fi
@ -337,7 +337,7 @@ EOF
root_partition="${SD_CARD_DEVICE}p2"
fi
if [[ -n "$root_partition" ]]; then
if [[ -n $root_partition ]]; then
local root_mount="/tmp/rpi-root"
mkdir -p "$root_mount"
mount "$root_partition" "$root_mount"
@ -474,7 +474,7 @@ discover_remote_laptop() {
# Extract IPs from nmap output - grep for report lines then extract IP
ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u)
if [[ -z "$ssh_hosts" ]]; then
if [[ -z $ssh_hosts ]]; then
die "No SSH-enabled devices found on network"
fi
@ -489,7 +489,7 @@ discover_remote_laptop() {
for u in "${common_users[@]}"; do
local is_dup=0
for existing in "${users[@]}"; do
if [[ "$u" == "$existing" ]]; then
if [[ $u == "$existing" ]]; then
is_dup=1
break
fi
@ -510,7 +510,7 @@ discover_remote_laptop() {
idx=$((idx + 1))
# Skip gateway
if [[ "$ip" == "$gateway" ]]; then
if [[ $ip == "$gateway" ]]; then
log_info "[$idx/$host_count] Skipping $ip (gateway)"
continue
fi
@ -528,13 +528,13 @@ discover_remote_laptop() {
local has_sd
has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2>/dev/null || true)
if [[ -n "$has_sd" ]]; then
if [[ -n $has_sd ]]; then
log_success "[$idx/$host_count] $ip - Found SD card: $has_sd"
found_laptop="$ip"
break 2 # Break out of both loops
else
log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..."
if [[ -z "$found_laptop" ]]; then
if [[ -z $found_laptop ]]; then
found_laptop="$ip"
fi
fi
@ -542,26 +542,26 @@ discover_remote_laptop() {
fi
done
if [[ -z "$found_user" ]]; then
if [[ -z $found_user ]]; then
log_info "[$idx/$host_count] $ip - No SSH key access with any common username"
fi
done
# If no passwordless access found, prompt user for username
if [[ -z "$found_laptop" ]] || [[ -z "$found_user" ]]; then
if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then
log_warning "No device with passwordless SSH found using common usernames."
# Pick first available SSH host
found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1)
if [[ -z "$found_laptop" ]]; then
if [[ -z $found_laptop ]]; then
die "Could not find any suitable SSH-enabled device"
fi
log_info "Found SSH host at $found_laptop but need credentials."
read -r -p "Enter username for $found_laptop: " found_user
if [[ -z "$found_user" ]]; then
if [[ -z $found_user ]]; then
die "No username provided"
fi
fi
@ -596,7 +596,7 @@ phase_flash_remote() {
local sd_device
sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2>/dev/null || true)
if [[ -z "$sd_device" ]]; then
if [[ -z $sd_device ]]; then
die "No SD card detected on remote laptop. Please insert an SD card and try again."
fi
@ -657,7 +657,7 @@ phase_flash_remote_execute() {
log_info "=== Executing Flash on Remote Laptop ==="
if [[ -z "$SD_CARD_DEVICE" ]]; then
if [[ -z $SD_CARD_DEVICE ]]; then
die "SD_CARD_DEVICE not set"
fi
@ -703,7 +703,7 @@ phase_flash_remote_execute() {
log_success "SSH enabled"
# Create userconf.txt for first user
if [[ -n "$encrypted_password" ]]; then
if [[ -n $encrypted_password ]]; then
echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt"
log_success "User '$PI_USER' configured"
fi
@ -716,7 +716,7 @@ phase_flash_remote_execute() {
root_partition="${SD_CARD_DEVICE}p2"
fi
if [[ -n "$root_partition" ]]; then
if [[ -n $root_partition ]]; then
local root_mount="/tmp/rpi-root"
mkdir -p "$root_mount"
mount "$root_partition" "$root_mount"
@ -951,7 +951,7 @@ download_nextcloud() {
local download_dir="/tmp"
local nc_zip="$download_dir/nextcloud.zip"
if [[ -f "$nc_zip" ]]; then
if [[ -f $nc_zip ]]; then
log_info "Nextcloud archive already downloaded"
else
wget -O "$nc_zip" "$nc_url"
@ -1069,7 +1069,7 @@ install_nextcloud() {
local db_password
db_password=$(cat /root/.nextcloud_db_password)
if [[ -z "$NEXTCLOUD_ADMIN_PASSWORD" ]]; then
if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then
prompt_password "Enter Nextcloud admin password" NEXTCLOUD_ADMIN_PASSWORD
fi
@ -1206,11 +1206,11 @@ discover_raspberry_pi() {
# Try resolving hostname directly
pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true
if [[ -z "$pi_ip" ]]; then
if [[ -z $pi_ip ]]; then
pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true
fi
if [[ -n "$pi_ip" ]]; then
if [[ -n $pi_ip ]]; then
log_success "Found Pi by hostname: $pi_ip"
echo "$pi_ip"
return
@ -1224,7 +1224,7 @@ discover_raspberry_pi() {
local ssh_hosts
ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | grep -vw "$REMOTE_LAPTOP_IP" 2>/dev/null | sort -u) || true
if [[ -z "$ssh_hosts" ]]; then
if [[ -z $ssh_hosts ]]; then
die "No new SSH-enabled devices found. Is the Pi connected and booted?"
fi
@ -1259,14 +1259,14 @@ phase_all_remote() {
local pi_ip
pi_ip=$(discover_raspberry_pi)
if [[ -z "$pi_ip" ]]; then
if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi"
fi
log_info "Using Raspberry Pi at: $pi_ip"
# PI_PASSWORD should already be set from config file
if [[ -z "$PI_PASSWORD" ]]; then
if [[ -z $PI_PASSWORD ]]; then
die "PI_PASSWORD not set. Did you run flash-remote first?"
fi

View File

@ -2,35 +2,22 @@
set -euo pipefail
# Source common library
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
on_error() {
local exit_code=$?
local line_number=$1
printf '\033[1;31m[ERROR]\033[0m Unexpected failure at line %s (exit code %s).\n' "${line_number}" "${exit_code}" >&2
log_error "Unexpected failure at line ${line_number} (exit code ${exit_code})."
}
trap 'on_error ${LINENO}' ERR
log_info() {
printf '\033[1;34m[INFO]\033[0m %s\n' "$*"
}
log_warn() {
printf '\033[1;33m[WARN]\033[0m %s\n' "$*"
}
log_error() {
printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2
exit 1
}
require_root() {
if [[ ${EUID} -ne 0 ]]; then
log_error "This script must be run as root (try again with sudo)."
fi
}
require_pacman() {
if ! command -v pacman > /dev/null 2>&1; then
if ! has_cmd pacman; then
log_error "pacman not found. This script is intended for Arch Linux systems."
exit 1
fi
}

View File

@ -17,16 +17,7 @@ shift "$COMMON_ARGS_SHIFT"
# Check for sudo privileges
require_root "$@"
echo "NVIDIA Comprehensive Troubleshooter & GSP Disabler"
echo "=================================================="
echo "Current Date: $(date)"
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
print_setup_header "NVIDIA Comprehensive Troubleshooter & GSP Disabler"
# Check if nvidia module is loaded
if ! lsmod | grep -q nvidia; then

50
scripts/lib/android.sh Normal file
View File

@ -0,0 +1,50 @@
#!/bin/bash
# Shared functions for Android-related scripts
# Source this file after sourcing common.sh
# Prevent multiple sourcing
[[ -n ${_LIB_ANDROID_LOADED:-} ]] && return 0
_LIB_ANDROID_LOADED=1
ANDROID_WORK_DIR="${HOME}/.cache/android-adblock"
ensure_dir "$ANDROID_WORK_DIR"
# Exit with error message
die() {
echo "[ERROR] $*" >&2
exit 1
}
# Print section header
print_header() {
echo
echo "========================================"
echo " $1"
echo "========================================"
echo
}
# Check if ADB device is connected
check_adb_device() {
log "Checking device connection..."
if ! adb devices | grep -q "device$"; then
die "No device connected. Enable USB debugging and connect your phone."
fi
log "Device connected"
}
# Check if device has root access
check_adb_root() {
log "Checking root access..."
if ! adb shell "su -c 'echo test'" 2>/dev/null | grep -q "test"; then
die "Root access not available. Make sure Magisk is installed and grant root to Shell."
fi
log "Root access confirmed"
}
# Re-exec with sudo if needed to read /etc/hosts
require_hosts_readable() {
if [[ $EUID -ne 0 ]] && [[ ! -r /etc/hosts ]]; then
exec sudo -E bash "$0" "$@"
fi
}

View File

@ -21,7 +21,7 @@ log_message() {
local formatted
formatted="$(date '+%Y-%m-%d %H:%M:%S') - $msg"
echo "$formatted" >&2
if [[ -n "$log_file" ]]; then
if [[ -n $log_file ]]; then
echo "$formatted" >>"$log_file" 2>/dev/null || true
fi
}
@ -57,13 +57,23 @@ get_actual_user() {
get_actual_user_home() {
local user
user=$(get_actual_user)
if [[ "$user" == "root" ]]; then
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
# =============================================================================
@ -102,6 +112,32 @@ parse_interactive_args() {
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
}
# =============================================================================
# FOCUS APP DETECTION (for digital wellbeing scripts)
# =============================================================================
@ -172,6 +208,61 @@ require_command() {
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
# =============================================================================
@ -203,7 +294,7 @@ get_script_dir() {
# Usage: ensure_dir "/path/to/dir"
ensure_dir() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
if [[ ! -d $dir ]]; then
mkdir -p "$dir"
fi
}
@ -212,30 +303,176 @@ ensure_dir() {
# 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:-}"
if [[ "$user_flag" == "--user" ]]; then
systemctl --user daemon-reload
systemctl --user enable --now "$service"
else
systemctl daemon-reload
systemctl enable --now "$service"
fi
_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() {
local service="$1"
local user_flag="${2:-}"
_systemctl_cmd "${2:-}" is-active --quiet "$1"
}
if [[ "$user_flag" == "--user" ]]; then
systemctl --user is-active --quiet "$service"
# 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 "$@"; }
# =============================================================================
# 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"
echo "$title"
printf '=%.0s' $(seq 1 ${#title})
echo ""
echo "Current Date: $(date)"
echo "User: $USER"
echo "Original user: $(get_actual_user)"
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Mode: Interactive (prompts enabled)"
else
systemctl is-active --quiet "$service"
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

@ -17,6 +17,11 @@
set -uo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
# Source common library for log_info, log_warn, log_error
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
DEFAULT_ROOT=$(cd -- "$SCRIPT_DIR/../../" && pwd)
ROOT_DIR="$DEFAULT_ROOT"
@ -25,18 +30,6 @@ INSTALL_ONLY="false"
LIST_ONLY="false"
VERBOSE="false"
log_info() {
printf '\033[1;34m[INFO]\033[0m %s\n' "$*"
}
log_warn() {
printf '\033[1;33m[WARN]\033[0m %s\n' "$*"
}
log_error() {
printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2
}
usage() {
cat <<EOF
Usage: $(basename "$0") [options]
@ -47,7 +40,9 @@ Options:
--install-only Only install linters, do not scan
--list-only Only list discovered shell files, do not run linters
--verbose Print additional details while running
-h, --help Show this helpLinters used:
-h, --help Show this help
Linters used:
Required: shellcheck, shfmt
Optional (if available): checkbashisms, bashate
Syntax checks: bash -n, zsh -n (if installed), sh/dash -n

View File

@ -18,15 +18,10 @@
set -euo pipefail
IFS=$'\n\t'
GREEN="\033[1;32m"
YELLOW="\033[1;33m"
RED="\033[1;31m"
BLUE="\033[1;34m"
NC="\033[0m"
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; }
# Source common library for log functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
DO_POLICY=false
SET_DEFAULT=false

View File

@ -23,16 +23,11 @@ set -euo pipefail
IFS=$'\n\t'
SCRIPT_NAME="$(basename "$0")"
GREEN="\033[1;32m"
YELLOW="\033[1;33m"
RED="\033[1;31m"
BLUE="\033[1;34m"
NC="\033[0m"
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; }
# Source common library for log functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
usage() {
cat <<EOF

View File

@ -11,15 +11,10 @@ set -euo pipefail
# Bash/get_rnnoise_model.sh # interactive download
# RN_TARGET_DIR=./models Bash/get_rnnoise_model.sh --yes
ask_yes_no() {
read -r -p "$1 [y/N]: " ans || true
case "${ans:-}" in
y | Y | yes | YES) return 0 ;;
*) return 1 ;;
esac
}
has_cmd() { command -v "$1" > /dev/null 2>&1; }
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
YES=false
while [[ $# -gt 0 ]]; do
@ -58,21 +53,31 @@ if ! has_cmd curl && ! has_cmd wget; then
exit 3
fi
# Priority 1: explicit URL
if [[ -n ${RN_URL:-} ]]; then
echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2
# Helper: try to download a URL to destination, exit 0 on success
# Usage: try_download_model URL DEST
try_download_model() {
local url="$1"
local dest="$2"
local tmp
tmp=$(mktemp)
echo "Attempting to download RNNoise model from: $url" >&2
if has_cmd curl; then
curl -fsSL "$RN_URL" -o "$tmp"
curl -fsSL "$url" -o "$tmp" 2>/dev/null || true
else
wget -qO "$tmp" "$RN_URL"
wget -qO "$tmp" "$url" 2>/dev/null || true
fi
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
rm -f "$tmp" || true
}
# Priority 1: explicit URL
if [[ -n ${RN_URL:-} ]]; then
echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2
try_download_model "$RN_URL" "$dest"
echo "Warning: RN_URL download failed; continuing to fallback sources." >&2
fi
@ -85,26 +90,7 @@ NU_URLS=(
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/cb.rnnn"
)
for u in "${NU_URLS[@]}"; do
echo "Attempting to download RNNoise model from: $u" >&2
tmp=$(mktemp)
if has_cmd curl; then
if curl -fsSL "$u" -o "$tmp"; then
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
fi
else
if wget -qO "$tmp" "$u"; then
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
fi
fi
rm -f "$tmp" || true
try_download_model "$u" "$dest"
done
# Priority 2b: arnndn-models fallback (richardpl)
@ -112,26 +98,7 @@ RNNDN_URLS=(
"https://raw.githubusercontent.com/richardpl/arnndn-models/master/sh.rnnn"
)
for u in "${RNNDN_URLS[@]}"; do
echo "Attempting to download RNNoise model from: $u" >&2
tmp=$(mktemp)
if has_cmd curl; then
if curl -fsSL "$u" -o "$tmp"; then
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
fi
else
if wget -qO "$tmp" "$u"; then
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
fi
fi
rm -f "$tmp" || true
try_download_model "$u" "$dest"
done
# Priority 3: repo archives (rnnoise-nu and arnndn-models)

View File

@ -7,20 +7,15 @@ set -euo pipefail
# Tries distro packages first; if not suitable, offers to build from source.
# This script prints commands and asks for confirmation before building.
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
print_info() {
echo "[info] $*"
}
ask_yes_no() {
read -r -p "$1 [y/N]: " ans || true
case "${ans:-}" in
y | Y | yes | YES) return 0 ;;
*) return 1 ;;
esac
}
has_cmd() { command -v "$1" > /dev/null 2>&1; }
detect_distro() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release

View File

@ -2,6 +2,11 @@
set -euo pipefail
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
SCRIPT_NAME="$(basename "$0")"
RED="\033[31m"
@ -21,35 +26,8 @@ error() {
printf "%b[%s]%b %s\n" "$RED" "$SCRIPT_NAME" "$RESET" "$*" >&2
}
require_command() {
local cmd="$1"
local package_hint="${2:-}"
if ! command -v "$cmd" > /dev/null 2>&1; then
if [[ -n $package_hint ]]; then
error "Missing command '$cmd'. Try installing the package: $package_hint"
else
error "Missing command '$cmd'."
fi
exit 1
fi
}
ensure_pacman_packages() {
local packages=("python" "git" "curl" "jq" "code")
local missing=()
for pkg in "${packages[@]}"; do
if ! pacman -Qi "$pkg" > /dev/null 2>&1; then
missing+=("$pkg")
fi
done
if ((${#missing[@]} > 0)); then
info "Installing required packages with pacman: ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}"
else
info "All required pacman packages are already installed."
fi
install_missing_pacman_packages python git curl jq code
}
install_uv() {

View File

@ -17,16 +17,7 @@ shift "$COMMON_ARGS_SHIFT"
# Check for sudo privileges
require_root "$@"
echo "Periodic System Setup - Pacman Wrapper & Hosts File"
echo "==================================================="
echo "Current Date: $(date)"
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
print_setup_header "Periodic System Setup - Pacman Wrapper & Hosts File"
# Get the directory where this script is located
CONFIG_DIR="$(dirname "$SCRIPT_DIR")"

View File

@ -16,16 +16,7 @@ shift "$COMMON_ARGS_SHIFT"
# Check for sudo privileges
require_root "$@"
echo "Thorium Browser Auto-Startup Setup"
echo "=================================="
echo "Current Date: $(date)"
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
print_setup_header "Thorium Browser Auto-Startup Setup"
# Target URL
TARGET_URL="https://www.fitatu.com/app/planner"

View File

@ -7,6 +7,11 @@ set -euo pipefail
# Convert video files to a target format (mp4 or webm) using ffmpeg.
# Accepts either a single video file or a directory (will recurse into subdirectories).
# Source common library
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Default settings
TARGET_FORMAT="mp4"
CRF="" # Will be set based on format if not specified
@ -17,10 +22,6 @@ TARGET_PATH=""
# Video extensions to search for
ALL_VIDEO_EXTENSIONS=("mp4" "webm" "mkv" "avi" "mov" "wmv" "flv" "m4v" "mpg" "mpeg" "3gp" "ogv" "ts" "mts" "m2ts" "vob" "asf" "rm" "rmvb" "divx" "f4v")
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
usage() {
cat <<EOF
Usage:
@ -56,7 +57,7 @@ get_video_extensions_except() {
local exclude="$1"
local exts=()
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ "${ext,,}" != "${exclude,,}" ]]; then
if [[ ${ext,,} != "${exclude,,}" ]]; then
exts+=("$ext")
fi
done
@ -69,7 +70,7 @@ is_video_file() {
ext="${ext,,}" # lowercase
for video_ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ "$ext" == "${video_ext,,}" ]]; then
if [[ $ext == "${video_ext,,}" ]]; then
return 0
fi
done
@ -81,7 +82,7 @@ convert_video() {
local output_file="${input_file%.*}.${TARGET_FORMAT}"
# Skip if output already exists
if [[ -f "$output_file" ]]; then
if [[ -f $output_file ]]; then
log "Skipping '$input_file': output '$output_file' already exists"
return 0
fi
@ -91,12 +92,12 @@ convert_video() {
local ffmpeg_args=()
ffmpeg_args+=(-hide_banner -loglevel warning -i "$input_file")
if [[ "$TARGET_FORMAT" == "mp4" ]]; then
if [[ $TARGET_FORMAT == "mp4" ]]; then
# H.264 codec for video and AAC for audio (maximum compatibility)
ffmpeg_args+=(-c:v libx264 -crf "$CRF" -preset "$PRESET")
ffmpeg_args+=(-c:a aac -b:a 192k)
ffmpeg_args+=(-movflags +faststart)
elif [[ "$TARGET_FORMAT" == "webm" ]]; then
elif [[ $TARGET_FORMAT == "webm" ]]; then
# VP9 codec for video and Opus for audio
ffmpeg_args+=(-c:v libvpx-vp9 -crf "$CRF" -b:v 0)
ffmpeg_args+=(-c:a libopus -b:a 128k)
@ -107,13 +108,13 @@ convert_video() {
if ffmpeg "${ffmpeg_args[@]}"; then
log "Successfully converted '$input_file'"
if [[ "$DELETE_ORIGINAL" == true ]]; then
if [[ $DELETE_ORIGINAL == true ]]; then
log "Deleting original: '$input_file'"
rm "$input_file"
fi
else
log "Error converting '$input_file'"
[[ -f "$output_file" ]] && rm "$output_file"
[[ -f $output_file ]] && rm "$output_file"
return 1
fi
}
@ -129,8 +130,8 @@ process_directory() {
local find_args=(-type f \()
local first=true
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ "${ext,,}" != "${TARGET_FORMAT,,}" ]]; then
if [[ "$first" == true ]]; then
if [[ ${ext,,} != "${TARGET_FORMAT,,}" ]]; then
if [[ $first == true ]]; then
first=false
else
find_args+=(-o)
@ -159,7 +160,7 @@ parse_args() {
case "$opt" in
f)
TARGET_FORMAT="${OPTARG,,}"
if [[ "$TARGET_FORMAT" != "mp4" && "$TARGET_FORMAT" != "webm" ]]; then
if [[ $TARGET_FORMAT != "mp4" && $TARGET_FORMAT != "webm" ]]; then
echo "Error: Format must be 'mp4' or 'webm'" >&2
exit 1
fi
@ -194,8 +195,8 @@ parse_args() {
TARGET_PATH="$1"
# Set default CRF based on format if not specified
if [[ -z "$CRF" ]]; then
if [[ "$TARGET_FORMAT" == "mp4" ]]; then
if [[ -z $CRF ]]; then
if [[ $TARGET_FORMAT == "mp4" ]]; then
CRF=23
else
CRF=30
@ -207,14 +208,14 @@ main() {
ensure_ffmpeg
parse_args "$@"
if [[ ! -e "$TARGET_PATH" ]]; then
if [[ ! -e $TARGET_PATH ]]; then
echo "Error: Path '$TARGET_PATH' does not exist." >&2
exit 1
fi
if [[ -f "$TARGET_PATH" ]]; then
if [[ -f $TARGET_PATH ]]; then
# Single file
if [[ "${TARGET_PATH,,}" == *."$TARGET_FORMAT" ]]; then
if [[ ${TARGET_PATH,,} == *."$TARGET_FORMAT" ]]; then
log "File '$TARGET_PATH' is already in $TARGET_FORMAT format, skipping."
exit 0
fi
@ -225,7 +226,7 @@ main() {
echo "Error: '$TARGET_PATH' is not a recognized video file." >&2
exit 1
fi
elif [[ -d "$TARGET_PATH" ]]; then
elif [[ -d $TARGET_PATH ]]; then
process_directory "$TARGET_PATH"
else
echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2

View File

@ -5,6 +5,11 @@
set -euo pipefail
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Default resolution
DEFAULT_RESOLUTION="320x240"
@ -30,13 +35,7 @@ EOF
}
# Check if ImageMagick is installed
if ! command -v convert &> /dev/null; then
echo "Error: ImageMagick (convert) is not installed."
echo "Install it with:"
echo " Arch Linux: sudo pacman -S imagemagick"
echo " Ubuntu/Debian: sudo apt install imagemagick"
exit 1
fi
require_imagemagick "convert" || exit 1
# Parse arguments
if [[ $# -lt 1 ]]; then
@ -55,7 +54,7 @@ if [[ ! -f ${INPUT_IMAGE} ]]; then
fi
# Validate resolution format (WIDTHxHEIGHT)
if [[ ! ${RESOLUTION} =~ ^[0-9]+x[0-9]+$ ]]; then
if ! validate_resolution "$RESOLUTION"; then
echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1
@ -63,19 +62,7 @@ fi
# Generate output filename if not provided
if [[ -z ${OUTPUT_IMAGE} ]]; then
# Extract filename without extension and extension
BASENAME=$(basename "${INPUT_IMAGE}")
FILENAME="${BASENAME%.*}"
EXTENSION="${BASENAME##*.}"
# If no extension (single name file), default to jpg
if [[ ${FILENAME} == "${EXTENSION}" ]]; then
EXTENSION="jpg"
fi
# Create output filename with resolution suffix
DIRNAME=$(dirname "${INPUT_IMAGE}")
OUTPUT_IMAGE="${DIRNAME}/${FILENAME}_${RESOLUTION}.${EXTENSION}"
OUTPUT_IMAGE=$(generate_output_filename "${INPUT_IMAGE}" "_${RESOLUTION}")
fi
# Perform the conversion

View File

@ -7,14 +7,15 @@ set -euo pipefail
# Convert one or more PDF files to image files using ImageMagick v7 `magick`.
# Default output format is jpg, but can be changed with -f.
# Source common library
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
OUTPUT_DIR=""
OUTPUT_FORMAT="jpg"
PDF_FILES=()
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
usage() {
cat <<EOF
Usage:
@ -35,10 +36,7 @@ EOF
}
ensure_magick() {
if ! command -v magick > /dev/null 2>&1; then
echo "Error: 'magick' (ImageMagick v7) is not installed or not in PATH." >&2
exit 1
fi
require_imagemagick "magick" || exit 1
}
parse_args() {

View File

@ -214,7 +214,7 @@ setup_udev_rules() {
# Install MTKClient udev rules if mtkclient is present
if [[ -d "${WORK_DIR}/mtkclient" ]]; then
log "Installing MTKClient udev rules..."
if [[ -d "$mtk_udev_dir" ]]; then
if [[ -d $mtk_udev_dir ]]; then
sudo cp "$mtk_udev_dir"/*.rules /etc/udev/rules.d/ 2>/dev/null || warn "Failed to copy MTKClient rules"
fi
fi
@ -634,7 +634,7 @@ install_mtkclient() {
local mtk_dir="${WORK_DIR}/mtkclient"
if [[ -d "$mtk_dir" && -f "$mtk_dir/mtk.py" ]]; then
if [[ -d $mtk_dir && -f "$mtk_dir/mtk.py" ]]; then
log "MTKClient already installed at $mtk_dir"
return 0
fi
@ -667,7 +667,7 @@ extract_boot_with_mtkclient() {
local boot_a_img="$WORK_DIR/boot_a.img"
local vbmeta_a_img="$WORK_DIR/vbmeta_a.img"
if [[ ! -d "$mtk_dir" ]]; then
if [[ ! -d $mtk_dir ]]; then
error "MTKClient not installed. Run: $SCRIPT_NAME install-mtk"
return 1
fi

View File

@ -6,43 +6,13 @@ set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/android.sh
source "$SCRIPT_DIR/../lib/android.sh"
# Re-run with sudo if needed for reading /etc/hosts
if [[ $EUID -ne 0 ]] && [[ ! -r /etc/hosts ]]; then
exec sudo -E bash "$0" "$@"
fi
require_hosts_readable "$@"
WORK_DIR="${HOME}/.cache/android-adblock"
ensure_dir "$WORK_DIR"
die() {
echo "[ERROR] $*" >&2
exit 1
}
print_header() {
echo
echo "========================================"
echo " $1"
echo "========================================"
echo
}
check_device() {
log "Checking device connection..."
if ! adb devices | grep -q "device$"; then
die "No device connected. Enable USB debugging and connect your phone."
fi
log "Device connected"
}
check_root() {
log "Checking root access..."
if ! adb shell "su -c 'echo test'" 2>/dev/null | grep -q "test"; then
die "Root access not available. Make sure Magisk is installed and grant root to Shell."
fi
log "Root access confirmed"
}
WORK_DIR="$ANDROID_WORK_DIR"
install_adaway() {
print_header "Installing AdAway"

View File

@ -2,6 +2,11 @@
set -euo pipefail
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Configuration -----------------------------------------------------------------
TARGET_SESSION_NAME="Xfce Session"
TARGET_PACKAGES=(
@ -20,42 +25,15 @@ error() {
exit 1
}
require_command() {
local cmd="$1" pkg_hint="${2:-}"
if ! command -v "$cmd" > /dev/null 2>&1; then
if [[ -n $pkg_hint ]]; then
warn "Install '$pkg_hint' to obtain the '$cmd' command."
fi
error "Required command '$cmd' not found."
fi
}
ensure_pacman() {
require_command pacman "pacman"
require_command pacman "pacman" || error "Required command 'pacman' not found."
if ! grep -qi "arch" /etc/os-release 2>/dev/null; then
warn "This script was designed for Arch Linux; continuing anyway."
fi
}
install_packages() {
local missing=()
for pkg in "${TARGET_PACKAGES[@]}"; do
if ! pacman -Qi "$pkg" > /dev/null 2>&1; then
missing+=("$pkg")
fi
done
if [[ ${#missing[@]} -eq 0 ]]; then
info "All target packages are already installed."
return
fi
if ! command -v sudo > /dev/null 2>&1; then
error "sudo is required to install packages. Install sudo or run this script as root."
fi
info "Installing missing packages: ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}"
install_missing_pacman_packages "${TARGET_PACKAGES[@]}"
}
print_post_install_tips() {

View File

@ -6,6 +6,11 @@
set -euo pipefail
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Default resolution
DEFAULT_RESOLUTION="320x240"
@ -30,17 +35,7 @@ EOF
}
# Check if ImageMagick is installed and determine which command to use
if command -v magick &> /dev/null; then
MAGICK_CMD="magick"
elif command -v convert &> /dev/null; then
MAGICK_CMD="convert"
else
echo "Error: ImageMagick is not installed."
echo "Install it with:"
echo " Arch Linux: sudo pacman -S imagemagick"
echo " Ubuntu/Debian: sudo apt install imagemagick"
exit 1
fi
require_imagemagick || exit 1
# Parse arguments
if [[ $# -lt 1 ]]; then
@ -59,7 +54,7 @@ if [[ ! -f ${INPUT_FILE} ]]; then
fi
# Validate resolution format (WIDTHxHEIGHT)
if [[ ! ${RESOLUTION} =~ ^[0-9]+x[0-9]+$ ]]; then
if ! validate_resolution "$RESOLUTION"; then
echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1

View File

@ -6,31 +6,21 @@ set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/android.sh
source "$SCRIPT_DIR/../lib/android.sh"
# Re-run with sudo if needed for reading /etc/hosts
if [[ $EUID -ne 0 ]] && [[ ! -r /etc/hosts ]]; then
exec sudo -E bash "$0" "$@"
fi
require_hosts_readable "$@"
WORK_DIR="${HOME}/.cache/android-adblock"
ensure_dir "$WORK_DIR"
die() {
echo "[ERROR] $*" >&2
exit 1
}
WORK_DIR="$ANDROID_WORK_DIR"
log "Updating Android hosts file from Linux configuration..."
# Check device connection
if ! adb devices | grep -q "device$"; then
die "No device connected. Enable USB debugging and connect your phone."
fi
check_adb_device
# Check root access
if ! adb shell "su -c 'echo test'" 2>/dev/null | grep -q "test"; then
die "Root access not available. Make sure Magisk is installed and grant root to Shell."
fi
check_adb_root
# Use the StevenBlack cache or /etc/hosts
HOSTS_FILE="$WORK_DIR/hosts"