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" JSCPD_BIN="${HOME}/.local/node_modules/.bin/jscpd"
# Install jscpd if not present # Install jscpd if not present
if [[ ! -x "$JSCPD_BIN" ]]; then if [[ ! -x $JSCPD_BIN ]]; then
printf ' → jscpd not found, installing...\n' printf ' → jscpd not found, installing...\n'
if ! npm install --prefix ~/.local jscpd 2>&1; then if ! npm install --prefix ~/.local jscpd 2>&1; then
printf '\nCommit aborted: failed to install jscpd.\n' >&2 printf '\nCommit aborted: failed to install jscpd.\n' >&2

View File

@ -46,13 +46,13 @@ install_from_aur() {
fi fi
cd "$repo_dir" || return 1 cd "$repo_dir" || return 1
if pacman -Qi "$pkg_name" > /dev/null 2>&1; then if pacman -Qi "$pkg_name" >/dev/null 2>&1; then
echo "$pkg_name is already installed" echo "$pkg_name is already installed"
return 0 return 0
fi fi
echo "Cleaning old package artifacts to avoid duplicate -U targets" echo "Cleaning old package artifacts to avoid duplicate -U targets"
find . -maxdepth 1 -type f -name '*.pkg.tar.*' -delete 2> /dev/null || true find . -maxdepth 1 -type f -name '*.pkg.tar.*' -delete 2>/dev/null || true
echo "Building $pkg_name (clean build)" echo "Building $pkg_name (clean build)"
# -c (clean up work dirs after) -C (clean build - remove src/ and pkg/ first) # -c (clean up work dirs after) -C (clean build - remove src/ and pkg/ first)
@ -75,11 +75,22 @@ install_from_aur() {
fi 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() { process_packages() {
local file_path local file_path
file_path="$1" file_path="$1"
: > failed.txt : >failed.txt
: > done.txt : >done.txt
while IFS= read -r pkg_name; do while IFS= read -r pkg_name; do
if [ -z "$pkg_name" ]; then if [ -z "$pkg_name" ]; then
@ -100,25 +111,17 @@ process_packages() {
if [ -d "$repo_dir" ] && [ -z "$(ls -A "$repo_dir")" ]; then if [ -d "$repo_dir" ] && [ -z "$(ls -A "$repo_dir")" ]; then
echo "Repository $repo_dir is empty, trying to install with pacman" echo "Repository $repo_dir is empty, trying to install with pacman"
if sudo pacman -Sy --noconfirm "$pkg_name"; then if sudo pacman -Sy --noconfirm "$pkg_name"; then
echo "$pkg_name" >> done.txt echo "$pkg_name" >>done.txt
else else
echo "$pkg_name" >> failed.txt echo "$pkg_name" >>failed.txt
fi fi
else else
if install_from_aur "$repo_url" "$pkg_name"; then try_aur_install "$repo_url" "$pkg_name"
echo "$pkg_name" >> done.txt
else
echo "$pkg_name" >> failed.txt
fi
fi fi
else else
if install_from_aur "$repo_url" "$pkg_name"; then try_aur_install "$repo_url" "$pkg_name"
echo "$pkg_name" >> done.txt
else
echo "$pkg_name" >> failed.txt
fi fi
fi done <"$file_path"
done < "$file_path"
} }
sudo cp /etc/makepkg.conf /etc/makepkg.conf.bak sudo cp /etc/makepkg.conf /etc/makepkg.conf.bak
@ -129,19 +132,19 @@ sudo cp ./pacman.conf /etc/pacman.conf
# sudo cp ./mkinitcpio.conf /etc/mkinitcpio.conf # sudo cp ./mkinitcpio.conf /etc/mkinitcpio.conf
# mkinitcpio -P # mkinitcpio -P
# Reflector install / service management (idempotent & resilient) # Reflector install / service management (idempotent & resilient)
if pacman -Qi reflector > /dev/null 2>&1; then if pacman -Qi reflector >/dev/null 2>&1; then
echo "reflector already installed" echo "reflector already installed"
else else
yes | sudo pacman -Sy --noconfirm reflector || echo "Warning: reflector install failed (continuing)" yes | sudo pacman -Sy --noconfirm reflector || echo "Warning: reflector install failed (continuing)"
fi fi
# Prefer timer over service (Arch default) # Prefer timer over service (Arch default)
if systemctl list-unit-files | grep -q '^reflector.timer'; then if systemctl list-unit-files | grep -q '^reflector.timer'; then
if systemctl is-enabled reflector.timer > /dev/null 2>&1; then if systemctl is-enabled reflector.timer >/dev/null 2>&1; then
echo "reflector.timer already enabled" echo "reflector.timer already enabled"
else else
sudo systemctl enable reflector.timer || echo "Warning: could not enable reflector.timer" sudo systemctl enable reflector.timer || echo "Warning: could not enable reflector.timer"
fi fi
if systemctl is-active reflector.timer > /dev/null 2>&1; then if systemctl is-active reflector.timer >/dev/null 2>&1; then
echo "reflector.timer already active" echo "reflector.timer already active"
else else
if ! sudo systemctl start reflector.timer; then if ! sudo systemctl start reflector.timer; then
@ -149,12 +152,12 @@ if systemctl list-unit-files | grep -q '^reflector.timer'; then
fi fi
fi fi
elif systemctl list-unit-files | grep -q '^reflector.service'; then elif systemctl list-unit-files | grep -q '^reflector.service'; then
if systemctl is-enabled reflector.service > /dev/null 2>&1; then if systemctl is-enabled reflector.service >/dev/null 2>&1; then
echo "reflector.service already enabled" echo "reflector.service already enabled"
else else
sudo systemctl enable reflector.service || echo "Warning: could not enable reflector.service" sudo systemctl enable reflector.service || echo "Warning: could not enable reflector.service"
fi fi
if systemctl is-active reflector.service > /dev/null 2>&1; then if systemctl is-active reflector.service >/dev/null 2>&1; then
echo "reflector.service already running" echo "reflector.service already running"
else else
if ! sudo systemctl start reflector.service; then if ! sudo systemctl start reflector.service; then
@ -172,7 +175,19 @@ while IFS= read -r line; do
aur_packages+=("$line") aur_packages+=("$line")
aur_package_names+=("${line%% *}") aur_package_names+=("${line%% *}")
fi fi
done < "aur_packages.txt" 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 # Read pacman packages from file
declare -a pacman_packages declare -a pacman_packages
@ -181,7 +196,7 @@ while IFS= read -r line; do
if [[ -n $line && $line =~ ^[a-z0-9] ]]; then if [[ -n $line && $line =~ ^[a-z0-9] ]]; then
pacman_packages+=("$line") pacman_packages+=("$line")
fi fi
done < "pacman_packages.txt" done <"pacman_packages.txt"
for pkg in "${pacman_packages[@]}"; do for pkg in "${pacman_packages[@]}"; do
# Skip NVIDIA packages if GPU is not NVIDIA # Skip NVIDIA packages if GPU is not NVIDIA
@ -191,21 +206,15 @@ for pkg in "${pacman_packages[@]}"; do
fi fi
# Check for texlive subpackages # Check for texlive subpackages
if [ "$pkg" == "texlive" ]; then 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-basic texlive-bibtexextra texlive-binextra texlive-context texlive-fontsextra
texlive-fontsrecommended texlive-fontutils texlive-formatsextra texlive-games texlive-humanities texlive-fontsrecommended texlive-fontutils texlive-formatsextra texlive-games texlive-humanities
texlive-latex texlive-latexextra texlive-latexrecommended texlive-luatex texlive-mathscience texlive-latex texlive-latexextra texlive-latexrecommended texlive-luatex texlive-mathscience
texlive-metapost texlive-music texlive-pictures texlive-plaingeneric texlive-pstricks texlive-metapost texlive-music texlive-pictures texlive-plaingeneric texlive-pstricks
texlive-publishers texlive-xetex texlive-publishers texlive-xetex
) )
all_installed=true if all_subpackages_installed texlive_sub_pkgs; then
for subpkg in "${sub_pkgs[@]}"; do
if ! pacman -Qi "$subpkg" &> /dev/null; then
all_installed=false
break
fi
done
if [ "$all_installed" = true ]; then
echo "All texlive subpackages are installed, skipping texlive" echo "All texlive subpackages are installed, skipping texlive"
continue continue
fi fi
@ -213,27 +222,21 @@ for pkg in "${pacman_packages[@]}"; do
# Check for texlive-lang subpackages # Check for texlive-lang subpackages
if [ "$pkg" == "texlive-lang" ]; then 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-langarabic texlive-langchinese texlive-langcjk texlive-langcyrillic
texlive-langczechslovak texlive-langenglish texlive-langeuropean texlive-langfrench texlive-langczechslovak texlive-langenglish texlive-langeuropean texlive-langfrench
texlive-langgerman texlive-langgreek texlive-langitalian texlive-langjapanese texlive-langgerman texlive-langgreek texlive-langitalian texlive-langjapanese
texlive-langkorean texlive-langother texlive-langpolish texlive-langportuguese texlive-langkorean texlive-langother texlive-langpolish texlive-langportuguese
texlive-langspanish texlive-langspanish
) )
all_installed=true if all_subpackages_installed texlive_lang_sub_pkgs; then
for subpkg in "${sub_pkgs[@]}"; do
if ! pacman -Qi "$subpkg" &> /dev/null; then
all_installed=false
break
fi
done
if [ "$all_installed" = true ]; then
echo "All texlive-lang subpackages are installed, skipping texlive-lang" echo "All texlive-lang subpackages are installed, skipping texlive-lang"
continue continue
fi fi
fi fi
if ! pacman -Qi "$pkg" &> /dev/null; then if ! pacman -Qi "$pkg" &>/dev/null; then
if ! printf '%s if ! printf '%s
' "${aur_package_names[@]}" | grep -Fxq "$pkg"; then ' "${aur_package_names[@]}" | grep -Fxq "$pkg"; then
yes | sudo pacman -Sy --noconfirm "$pkg" yes | sudo pacman -Sy --noconfirm "$pkg"
@ -244,7 +247,7 @@ for pkg in "${pacman_packages[@]}"; do
echo "$pkg is already installed" echo "$pkg is already installed"
fi fi
done done
if ! command -v nvm &> /dev/null; then if ! command -v nvm &>/dev/null; then
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
else else
echo "nvm is already installed" echo "nvm is already installed"
@ -256,7 +259,7 @@ if [ -s "$NVM_DIR/nvm.sh" ]; then
else else
echo "nvm.sh not found at $NVM_DIR/nvm.sh" >&2 echo "nvm.sh not found at $NVM_DIR/nvm.sh" >&2
fi fi
if command -v nvm &> /dev/null; then if command -v nvm &>/dev/null; then
nvm i v18.20.5 nvm i v18.20.5
nvm install --lts nvm install --lts
else else

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 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 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; } log_hook "post" "relocking(start)"
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
}
# Ensure we end with a single read-only bind mount layer # Collapse any stacked mounts first
logger -t "$LOGTAG" "post: relocking /etc/hosts (starting)"
echo "$(date -Is) post-relock(start)" >>/run/hosts-guard-hook.log 2>/dev/null || true
collapse_mounts collapse_mounts
# Run enforcement script if available
if [[ -x $ENFORCE ]]; then if [[ -x $ENFORCE ]]; then
"$ENFORCE" >/dev/null 2>&1 || true "$ENFORCE" >/dev/null 2>&1 || true
fi fi
if command -v chattr >/dev/null 2>&1; then # Apply protections
chattr +i "$TARGET" >/dev/null 2>&1 || true apply_immutable
fi apply_ro_bind_mount
# Apply exactly one ro bind layer # Start the path watcher
mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true start_path_watcher
mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true
# Start only the path watcher; avoid bind-mount service (we already bound once) log_hook "post" "relocking(done)"
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
exit 0 exit 0

View File

@ -3,69 +3,27 @@
set -euo pipefail set -euo pipefail
TARGET=/etc/hosts # Source shared functions
LOGTAG=hosts-guard-hook 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() { # Remove protective attributes
local units=(hosts-bind-mount.service hosts-guard.path) remove_host_attrs
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
# Stop guard services
stop_units_if_present stop_units_if_present
logger -t "$LOGTAG" "pre: unlocking /etc/hosts (starting)" log_hook "pre" "unlocking(start)"
echo "$(date -Is) pre-unlock" >>/run/hosts-guard-hook.log 2>/dev/null || true
# Always collapse any existing layers; we'll operate on the plain file # Collapse any existing mount layers
cleanup_mount_stacks 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 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 fi
logger -t "$LOGTAG" "pre: unlocking /etc/hosts (done)" log_hook "pre" "unlocking(done)"
exit 0 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 the current /etc/hosts (entries after "# Custom blocking entries" marker)
extract_custom_entries_from_hosts() { extract_custom_entries_from_hosts() {
local hosts_file="$1" local hosts_file="$1"
if [[ ! -f "$hosts_file" ]]; then if [[ ! -f $hosts_file ]]; then
return return
fi fi
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" | sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
@ -61,7 +61,7 @@ extract_custom_entries_from_hosts() {
# Load previously saved custom entries state # Load previously saved custom entries state
load_saved_custom_entries() { load_saved_custom_entries() {
if [[ -f "$CUSTOM_ENTRIES_STATE_FILE" ]]; then if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
sort -u "$CUSTOM_ENTRIES_STATE_FILE" sort -u "$CUSTOM_ENTRIES_STATE_FILE"
fi fi
} }
@ -77,7 +77,7 @@ save_custom_entries_state() {
# Helper function to count non-empty lines # Helper function to count non-empty lines
count_lines() { count_lines() {
local input="$1" local input="$1"
if [[ -z "$input" ]]; then if [[ -z $input ]]; then
echo 0 echo 0
else else
echo "$input" | grep -c . 2>/dev/null || echo 0 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) # Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
local saved_entries local saved_entries
saved_entries=$(load_saved_custom_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 # 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") saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts")
fi 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 ISSUES_FOUND=0
FIXES_APPLIED=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 # Check functions
###################################################################### ######################################################################
@ -134,7 +176,7 @@ check_pacman_wrapper() {
if [[ -L /usr/bin/pacman ]]; then if [[ -L /usr/bin/pacman ]]; then
local target local target
target=$(readlink -f /usr/bin/pacman) 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" msg "Pacman symlink points to wrapper"
else else
issues+=("Pacman symlink points to: $target (expected /usr/local/bin/pacman_wrapper)") issues+=("Pacman symlink points to: $target (expected /usr/local/bin/pacman_wrapper)")
@ -171,7 +213,7 @@ check_pacman_wrapper() {
done done
# Report and fix # Report and fix
if [[ "$status" == "error" ]]; then if [[ $status == "error" ]]; then
for issue in "${issues[@]}"; do for issue in "${issues[@]}"; do
err "$issue" err "$issue"
done done
@ -179,7 +221,7 @@ check_pacman_wrapper() {
if [[ $STATUS_ONLY -eq 0 ]]; then if [[ $STATUS_ONLY -eq 0 ]]; then
note "Installing pacman wrapper..." note "Installing pacman wrapper..."
if [[ -f "$PACMAN_WRAPPER_INSTALL" ]]; then if [[ -f $PACMAN_WRAPPER_INSTALL ]]; then
run bash "$PACMAN_WRAPPER_INSTALL" run bash "$PACMAN_WRAPPER_INSTALL"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
# Re-verify after fix # Re-verify after fix
@ -232,33 +274,11 @@ check_midnight_shutdown() {
status="error" status="error"
fi fi
# Report and fix report_and_fix issues status "midnight_shutdown" \
if [[ "$status" != "ok" ]]; then "Setting up midnight shutdown..." \
for issue in "${issues[@]}"; do "$MIDNIGHT_SHUTDOWN_SCRIPT" \
if [[ "$status" == "error" ]]; then "day-specific-shutdown.timer" \
err "$issue" enable
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
} }
check_startup_monitor() { check_startup_monitor() {
@ -298,33 +318,10 @@ check_startup_monitor() {
status="error" status="error"
fi fi
# Report and fix report_and_fix issues status "startup_monitor" \
if [[ "$status" != "ok" ]]; then "Setting up startup monitor..." \
for issue in "${issues[@]}"; do "$STARTUP_MONITOR_SCRIPT" \
if [[ "$status" == "error" ]]; then "pc-startup-monitor.timer"
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
} }
check_periodic_systems() { check_periodic_systems() {
@ -379,33 +376,10 @@ check_periodic_systems() {
status="error" status="error"
fi fi
# Report and fix report_and_fix issues status "periodic_systems" \
if [[ "$status" != "ok" ]]; then "Setting up periodic systems..." \
for issue in "${issues[@]}"; do "$PERIODIC_SYSTEM_SCRIPT" \
if [[ "$status" == "error" ]]; then "periodic-system-maintenance.timer"
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
} }
check_hosts() { check_hosts() {
@ -432,7 +406,7 @@ check_hosts() {
# Check if hosts file is immutable # Check if hosts file is immutable
local attrs local attrs
attrs=$(lsattr /etc/hosts 2>/dev/null | cut -d' ' -f1 || echo "") 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" msg "/etc/hosts has immutable attribute set"
else else
issues+=("/etc/hosts is not immutable") issues+=("/etc/hosts is not immutable")
@ -503,9 +477,9 @@ check_hosts() {
fi fi
# Report issues # Report issues
if [[ "$status" != "ok" ]]; then if [[ $status != "ok" ]]; then
for issue in "${issues[@]}"; do for issue in "${issues[@]}"; do
if [[ "$status" == "error" ]]; then if [[ $status == "error" ]]; then
err "$issue" err "$issue"
else else
warn "$issue" warn "$issue"
@ -517,7 +491,7 @@ check_hosts() {
# Run hosts install first # Run hosts install first
if [[ ! -f /etc/hosts ]] || [[ $(wc -l </etc/hosts) -lt 100 ]]; then if [[ ! -f /etc/hosts ]] || [[ $(wc -l </etc/hosts) -lt 100 ]]; then
note "Installing hosts file..." note "Installing hosts file..."
if [[ -f "$HOSTS_INSTALL_SCRIPT" ]]; then if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then
run bash "$HOSTS_INSTALL_SCRIPT" run bash "$HOSTS_INSTALL_SCRIPT"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
else else
@ -528,7 +502,7 @@ check_hosts() {
# Run hosts guard setup # Run hosts guard setup
if ! systemctl is-enabled hosts-guard.path &>/dev/null || [[ ! -f /usr/local/sbin/enforce-hosts.sh ]]; then if ! systemctl is-enabled hosts-guard.path &>/dev/null || [[ ! -f /usr/local/sbin/enforce-hosts.sh ]]; then
note "Setting up hosts guard..." note "Setting up hosts guard..."
if [[ -f "$HOSTS_GUARD_SCRIPT" ]]; then if [[ -f $HOSTS_GUARD_SCRIPT ]]; then
run bash "$HOSTS_GUARD_SCRIPT" run bash "$HOSTS_GUARD_SCRIPT"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
else else
@ -539,7 +513,7 @@ check_hosts() {
# Install pacman hooks if missing # Install pacman hooks if missing
if [[ ! -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then if [[ ! -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then
note "Installing pacman hooks..." note "Installing pacman hooks..."
if [[ -f "$HOSTS_PACMAN_HOOKS_SCRIPT" ]]; then if [[ -f $HOSTS_PACMAN_HOOKS_SCRIPT ]]; then
run bash "$HOSTS_PACMAN_HOOKS_SCRIPT" run bash "$HOSTS_PACMAN_HOOKS_SCRIPT"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
else else

View File

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

View File

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

View File

@ -220,8 +220,6 @@ function is_greylisted_package_name() {
return 1 return 1
} }
# Helper: detect if current invocation includes --noconfirm
# Helper: detect if current invocation includes --noconfirm # Helper: detect if current invocation includes --noconfirm
function has_noconfirm_flag() { function has_noconfirm_flag() {
for arg in "$@"; do for arg in "$@"; do
@ -232,6 +230,27 @@ function has_noconfirm_flag() {
return 1 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 # Handle stale pacman database lock if present and no package managers are running
check_and_handle_db_lock() { check_and_handle_db_lock() {
local lock_file="/var/lib/pacman/db.lck" 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 # Determine which processes actually have the lock open
local -a holders=() local -a holders=()
if command -v fuser >/dev/null 2>&1; then get_lock_holders "$lock_file"
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
if [[ ${#holders[@]} -gt 0 ]]; then if [[ ${#holders[@]} -gt 0 ]]; then
local pac_holder=0 local pac_holder=0
@ -292,12 +295,7 @@ check_and_handle_db_lock() {
sleep 1 sleep 1
# Re-check holders # Re-check holders
holders=() get_lock_holders "$lock_file"
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
if [[ ${#holders[@]} -gt 0 ]]; then if [[ ${#holders[@]} -gt 0 ]]; then
echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2 echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2
return 1 return 1
@ -305,6 +303,16 @@ check_and_handle_db_lock() {
fi fi
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 # Decide whether to remove the lock
local now epoch age local now epoch age
if epoch=$(stat -c %Y "$lock_file" 2>/dev/null); then 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 # Auto-remove in non-interactive mode (--noconfirm) or if the lock is older than 10 minutes
if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then
echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2 echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2
if [[ $EUID -ne 0 ]]; then remove_file_as_root "$lock_file" || return 1
sudo rm -f "$lock_file" || return 1
else
rm -f "$lock_file" || return 1
fi
return 0 return 0
fi fi
@ -330,25 +334,23 @@ check_and_handle_db_lock() {
echo -e "${CYAN}Lock path:${NC} $lock_file (age: ${age}s)" >&2 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" read -r -t 15 -p $'Remove stale lock and continue? [y/N]: ' reply || reply="n"
if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then
if [[ $EUID -ne 0 ]]; then remove_file_as_root "$lock_file" || return 1
sudo rm -f "$lock_file" || return 1
else
rm -f "$lock_file" || return 1
fi
return 0 return 0
fi 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 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 return 1
} }
# Cleanup: remove any installed blocked packages (in addition to the queued operation) # Generic function to remove installed packages matching a filter
function remove_installed_blocked_packages() { # Args: check_function label_prefix
# args not used; kept for future policy extension function remove_installed_packages_matching() {
# List installed package names local check_function="$1"
local label="$2"
mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null) mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null)
local to_remove=() local to_remove=()
for name in "${installed_names[@]}"; do for name in "${installed_names[@]}"; do
if is_blocked_package_name "$name"; then if "$check_function" "$name"; then
to_remove+=("$name") to_remove+=("$name")
fi fi
done done
@ -357,83 +359,59 @@ function remove_installed_blocked_packages() {
return 0 return 0
fi fi
echo -e "${YELLOW}Policy cleanup:${NC} Removing blocked installed packages: ${BOLD}${to_remove[*]}${NC}" >&2 echo -e "${YELLOW}${label} cleanup:${NC} Removing packages: ${BOLD}${to_remove[*]}${NC}" >&2
local remove_cmd=("$PACMAN_BIN" -Rns --noconfirm) "$PACMAN_BIN" -Rns --noconfirm "${to_remove[@]}"
"${remove_cmd[@]}" "${to_remove[@]}"
local rc=$? local rc=$?
if [[ $rc -ne 0 ]]; then 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 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 fi
return $rc return $rc
} }
# Cleanup: remove any installed greylisted packages (challenge-required packages) # Cleanup: remove any installed blocked packages
function remove_installed_greylisted_packages() { function remove_installed_blocked_packages() {
# List installed package names remove_installed_packages_matching is_blocked_package_name "Policy"
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
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 return 0
fi fi
done
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
fi fi
return $rc return 1
} }
# Function to check if user is trying to install packages that are always blocked # Function to check if user is trying to install packages that are always blocked
function check_for_always_blocked() { function check_for_always_blocked() {
# Check if the command is an installation command check_install_for is_blocked_package_name "$@"
if [[ $1 == "-S" || $1 == "-Sy" || $1 == "-Syu" || $1 == "-Syyu" || $1 == "-U" ]]; then }
# Check all arguments
for arg in "$@"; do # Helper to check if a package name is steam
# Strip repository prefix if present (like extra/ or community/) function is_steam_package() {
local package_name="${arg##*/}" [[ $1 == "steam" ]]
if is_blocked_package_name "$package_name"; then
return 0 # Always blocked package found
fi
done
fi
return 1 # No always blocked package found
} }
# Function to check if user is trying to install steam (challenge-eligible package) # Function to check if user is trying to install steam (challenge-eligible package)
function check_for_steam() { function check_for_steam() {
# List of packages that require challenge (only steam in this case) check_install_for is_steam_package "$@"
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
} }
# Function to check if current day is a weekday (after 4PM Friday until midnight Sunday) # Function to check if current day is a weekday (after 4PM Friday until midnight Sunday)
@ -459,6 +437,121 @@ function is_weekday() {
fi 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 to prompt for solving a word unscrambling challenge (only for steam)
function prompt_for_steam_challenge() { function prompt_for_steam_challenge() {
echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}" echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}"
@ -472,259 +565,20 @@ function prompt_for_steam_challenge() {
return 1 return 1
fi fi
echo -e "${YELLOW}Weekend Steam challenge will begin shortly...${NC}" # 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
# 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
} }
function check_for_greylisted() { function check_for_greylisted() {
# Check if the command is an installation command check_install_for is_greylisted_package_name "$@"
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
} }
# Function to prompt for solving a word unscrambling challenge (for greylisted packages - always active) # Function to prompt for solving a word unscrambling challenge (for greylisted packages - always active)
function prompt_for_greylist_challenge() { function prompt_for_greylist_challenge() {
echo -e "${YELLOW}WARNING: You are trying to install a greylisted package.${NC}" 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 # word_length=6, words_count=120, timeout=90s, initial_delay=30, post_delay=15-35
sleep_duration=$((RANDOM % 20 + 10)) run_word_challenge "Greylist" 6 120 90 30 15 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 (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
} }
# Check for wrapper-specific commands # Check for wrapper-specific commands

View File

@ -6,6 +6,11 @@
set -e # Exit on any error 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 # Function to show usage
show_usage() { show_usage() {
echo "Day-Specific Auto-Shutdown Setup for Arch Linux" echo "Day-Specific Auto-Shutdown Setup for Arch Linux"
@ -35,13 +40,7 @@ check_sudo() {
} }
# Get the actual user (even when running with sudo) # Get the actual user (even when running with sudo)
if [[ -n $SUDO_USER ]]; then set_actual_user_vars
ACTUAL_USER="$SUDO_USER"
USER_HOME="/home/$SUDO_USER"
else
ACTUAL_USER="$USER"
USER_HOME="$HOME"
fi
# Function to show current status # Function to show current status
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" 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 # Function to show final instructions
show_instructions() { show_instructions() {
echo "" echo ""
@ -618,9 +624,7 @@ show_instructions() {
echo "✓ Monitor service installed (protects timer from being disabled)" echo "✓ Monitor service installed (protects timer from being disabled)"
echo "✓ Watchdog timer installed (restarts monitor if stopped)" echo "✓ Watchdog timer installed (restarts monitor if stopped)"
echo "" echo ""
echo "Shutdown Schedule:" print_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)"
echo "" echo ""
echo "Management commands:" echo "Management commands:"
echo " sudo day-specific-shutdown-manager.sh status - Check status" echo " sudo day-specific-shutdown-manager.sh status - Check status"
@ -646,9 +650,7 @@ confirm_setup() {
echo "===============================================" echo "==============================================="
echo "This will set up your PC to automatically shutdown during specific time windows." echo "This will set up your PC to automatically shutdown during specific time windows."
echo "" echo ""
echo "Shutdown Schedule:" print_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)"
echo "" echo ""
echo "Important considerations:" echo "Important considerations:"
echo "- Any unsaved work will be lost during shutdown windows" echo "- Any unsaved work will be lost during shutdown windows"

View File

@ -10,14 +10,13 @@ set -euo pipefail
SCRIPT_NAME="$(basename "$0")" 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 ---------- # ---------- User/paths ----------
if [[ -n ${SUDO_USER:-} ]]; then set_actual_user_vars
ACTUAL_USER="$SUDO_USER"
USER_HOME="/home/$SUDO_USER"
else
ACTUAL_USER="$USER"
USER_HOME="$HOME"
fi
INSTALL_ROOT_DEFAULT="$USER_HOME/.local/share/unreal-mcp" INSTALL_ROOT_DEFAULT="$USER_HOME/.local/share/unreal-mcp"
INSTALL_ROOT="$INSTALL_ROOT_DEFAULT" 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" CONFIG_FILE="${SCRIPT_DIR}/.raspberry_pi.conf"
# Load configuration from gitignored config file if it exists # Load configuration from gitignored config file if it exists
if [[ -f "$CONFIG_FILE" ]]; then if [[ -f $CONFIG_FILE ]]; then
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$CONFIG_FILE" source "$CONFIG_FILE"
fi fi
@ -93,7 +93,7 @@ generate_password() {
} }
auto_generate_pi_password() { auto_generate_pi_password() {
if [[ -z "$PI_PASSWORD" ]]; then if [[ -z $PI_PASSWORD ]]; then
PI_PASSWORD=$(generate_password 16) PI_PASSWORD=$(generate_password 16)
log_info "Auto-generated Pi password (will be saved to config file)" log_info "Auto-generated Pi password (will be saved to config file)"
fi fi
@ -150,7 +150,7 @@ discover_remote_laptop() {
nmap -sn -T4 "$network" &>/dev/null || true 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) 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" die "No SSH-enabled devices found on network"
fi fi
@ -163,7 +163,7 @@ discover_remote_laptop() {
for u in "${common_users[@]}"; do for u in "${common_users[@]}"; do
local is_dup=0 local is_dup=0
for existing in "${users[@]}"; do for existing in "${users[@]}"; do
if [[ "$u" == "$existing" ]]; then if [[ $u == "$existing" ]]; then
is_dup=1 is_dup=1
break break
fi fi
@ -182,7 +182,7 @@ discover_remote_laptop() {
for ip in $ssh_hosts; do for ip in $ssh_hosts; do
idx=$((idx + 1)) idx=$((idx + 1))
if [[ "$ip" == "$gateway" ]]; then if [[ $ip == "$gateway" ]]; then
log_info "[$idx/$host_count] Skipping $ip (gateway)" log_info "[$idx/$host_count] Skipping $ip (gateway)"
continue continue
fi fi
@ -198,13 +198,13 @@ discover_remote_laptop() {
local has_sd 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) 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" log_success "[$idx/$host_count] $ip - Found SD card: $has_sd"
found_laptop="$ip" found_laptop="$ip"
break 2 break 2
else else
log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." 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" found_laptop="$ip"
fi fi
fi fi
@ -213,19 +213,19 @@ discover_remote_laptop() {
done done
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." log_warning "No device with passwordless SSH found using common usernames."
found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) 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" die "Could not find any suitable SSH-enabled device"
fi fi
log_info "Found SSH host at $found_laptop but need credentials." log_info "Found SSH host at $found_laptop but need credentials."
read -r -p "Enter username for $found_laptop: " found_user read -r -p "Enter username for $found_laptop: " found_user
if [[ -z "$found_user" ]]; then if [[ -z $found_user ]]; then
die "No username provided" die "No username provided"
fi fi
fi fi
@ -279,16 +279,16 @@ download_raspberry_pi_os() {
mkdir -p "$download_dir" mkdir -p "$download_dir"
if [[ -f "$extracted_image" ]]; then if [[ -f $extracted_image ]]; then
log_info "Using existing image at $extracted_image" log_info "Using existing image at $extracted_image"
echo "$extracted_image" echo "$extracted_image"
return return
fi fi
if [[ -f "$image_file" ]]; then if [[ -f $image_file ]]; then
local actual_size local actual_size
actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) 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..." log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..."
rm -f "$image_file" rm -f "$image_file"
else else
@ -296,7 +296,7 @@ download_raspberry_pi_os() {
fi fi
fi fi
if [[ ! -f "$image_file" ]]; then if [[ ! -f $image_file ]]; then
log_info "Downloading Raspberry Pi OS Lite (64-bit)..." log_info "Downloading Raspberry Pi OS Lite (64-bit)..."
log_info "This may take a while depending on your internet connection..." log_info "This may take a while depending on your internet connection..."
@ -312,7 +312,7 @@ download_raspberry_pi_os() {
local actual_size local actual_size
actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) 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" die "Download incomplete: got $actual_size bytes, expected $expected_size"
fi fi
log_success "Download complete: $actual_size bytes" log_success "Download complete: $actual_size bytes"
@ -321,7 +321,7 @@ download_raspberry_pi_os() {
log_info "Extracting image..." log_info "Extracting image..."
xz -dk "$image_file" xz -dk "$image_file"
if [[ ! -f "$extracted_image" ]]; then if [[ ! -f $extracted_image ]]; then
die "Failed to extract image" die "Failed to extract image"
fi fi
@ -342,7 +342,7 @@ phase_flash_local() {
local devices local devices
devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}') 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_warning "No removable devices detected automatically."
lsblk -d -o NAME,SIZE,TYPE,RM,TRAN lsblk -d -o NAME,SIZE,TYPE,RM,TRAN
read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE 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 read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE
fi 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" die "Device $SD_CARD_DEVICE does not exist or is not a block device"
fi fi
local root_device local root_device
root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//') 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!" die "Cannot flash to the system drive!"
fi fi
@ -375,7 +375,7 @@ phase_flash_local() {
log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE" log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE"
read -r -p "Are you sure you want to continue? (yes/no): " confirm 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" die "Aborted by user"
fi fi
@ -423,7 +423,7 @@ phase_flash_local() {
root_partition="${SD_CARD_DEVICE}p2" root_partition="${SD_CARD_DEVICE}p2"
fi fi
if [[ -n "$root_partition" ]]; then if [[ -n $root_partition ]]; then
local root_mount="/tmp/rpi-root" local root_mount="/tmp/rpi-root"
mkdir -p "$root_mount" mkdir -p "$root_mount"
mount "$root_partition" "$root_mount" mount "$root_partition" "$root_mount"
@ -475,7 +475,7 @@ phase_flash_remote() {
local sd_device 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) 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." die "No SD card detected on remote laptop. Please insert an SD card and try again."
fi fi
@ -530,7 +530,7 @@ phase_execute_remote() {
log_info "=== Executing Flash on Remote Laptop ===" 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" die "SD_CARD_DEVICE not set"
fi fi
@ -570,7 +570,7 @@ phase_execute_remote() {
touch "$boot_mount/ssh" touch "$boot_mount/ssh"
log_success "SSH enabled" log_success "SSH enabled"
if [[ -n "$encrypted_password" ]]; then if [[ -n $encrypted_password ]]; then
echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt"
log_success "User '$PI_USER' configured" log_success "User '$PI_USER' configured"
fi fi
@ -582,7 +582,7 @@ phase_execute_remote() {
root_partition="${SD_CARD_DEVICE}p2" root_partition="${SD_CARD_DEVICE}p2"
fi fi
if [[ -n "$root_partition" ]]; then if [[ -n $root_partition ]]; then
local root_mount="/tmp/rpi-root" local root_mount="/tmp/rpi-root"
mkdir -p "$root_mount" mkdir -p "$root_mount"
mount "$root_partition" "$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" CONFIG_FILE="${SCRIPT_DIR}/.raspberry_pi.conf"
# Load configuration from gitignored config file if it exists # Load configuration from gitignored config file if it exists
if [[ -f "$CONFIG_FILE" ]]; then if [[ -f $CONFIG_FILE ]]; then
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$CONFIG_FILE" source "$CONFIG_FILE"
fi fi
@ -102,7 +102,7 @@ generate_password() {
} }
auto_generate_nextcloud_password() { auto_generate_nextcloud_password() {
if [[ -z "$NEXTCLOUD_ADMIN_PASSWORD" ]]; then if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then
NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20) NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20)
log_info "Auto-generated Nextcloud admin password (will be saved to config file)" log_info "Auto-generated Nextcloud admin password (will be saved to config file)"
fi fi
@ -180,11 +180,11 @@ discover_raspberry_pi() {
# Try resolving hostname directly # Try resolving hostname directly
pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true 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 pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true
fi fi
if [[ -n "$pi_ip" ]]; then if [[ -n $pi_ip ]]; then
log_success "Found Pi by hostname: $pi_ip" log_success "Found Pi by hostname: $pi_ip"
echo "$pi_ip" echo "$pi_ip"
return return
@ -196,7 +196,7 @@ discover_raspberry_pi() {
local ssh_hosts 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 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?" die "No SSH-enabled devices found. Is the Pi connected and booted?"
fi fi
@ -599,7 +599,7 @@ phase_fix_issues() {
# Generate server certificate signed by our CA # Generate server certificate signed by our CA
local regenerate="${1:-}" 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..." log_info "Generating server certificate signed by CA..."
# Generate server private key # Generate server private key
@ -718,7 +718,7 @@ EOF
# Enable SVG in ImageMagick policy # Enable SVG in ImageMagick policy
local policy_file="/etc/ImageMagick-6/policy.xml" local policy_file="/etc/ImageMagick-6/policy.xml"
if [[ -f "$policy_file" ]]; then if [[ -f $policy_file ]]; then
# Remove SVG restrictions if present # 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" 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 # 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 ===" log_info "=== Setting up Let's Encrypt SSL with DuckDNS ==="
# Check if DuckDNS is configured # Check if DuckDNS is configured
if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]]; then if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then
echo echo
log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." 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." 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 DuckDNS token: " DUCKDNS_TOKEN
read -r -p "Enter your email (for Let's Encrypt notifications): " LETSENCRYPT_EMAIL 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" die "All fields are required"
fi fi
fi fi
@ -817,7 +817,7 @@ phase_setup_ssl() {
echo echo
read -r -p "Have you set up port forwarding? (yes/no): " port_forward_done 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 "Please set up port forwarding and run this command again."
log_info "Without port forwarding, Let's Encrypt cannot verify your domain." log_info "Without port forwarding, Let's Encrypt cannot verify your domain."
exit 0 exit 0
@ -829,7 +829,7 @@ phase_setup_ssl() {
# When ip= is empty, DuckDNS auto-detects the public IP # 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=") 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" die "Failed to update DuckDNS: $duckdns_response"
fi fi
log_success "DuckDNS updated to public IP" log_success "DuckDNS updated to public IP"
@ -855,14 +855,14 @@ DUCKEOF
log_info "Waiting for DNS propagation (this may take a minute)..." log_info "Waiting for DNS propagation (this may take a minute)..."
local dns_ip="" local dns_ip=""
local attempts=0 local attempts=0
while [[ "$dns_ip" != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do while [[ $dns_ip != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do
sleep 5 sleep 5
dns_ip=$(dig +short "$full_domain" 2>/dev/null | tail -1) || true dns_ip=$(dig +short "$full_domain" 2>/dev/null | tail -1) || true
attempts=$((attempts + 1)) attempts=$((attempts + 1))
log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)" log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)"
done done
if [[ "$dns_ip" != "$public_ip" ]]; then if [[ $dns_ip != "$public_ip" ]]; then
log_warning "DNS may not have propagated yet. Continuing anyway..." log_warning "DNS may not have propagated yet. Continuing anyway..."
else else
log_success "DNS verified: $full_domain -> $public_ip" log_success "DNS verified: $full_domain -> $public_ip"
@ -961,19 +961,19 @@ EOF
phase_setup_ssl_remote() { phase_setup_ssl_remote() {
log_info "=== Setting up Let's Encrypt SSL via SSH ===" 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." die "PI_PASSWORD not set. Run install-remote first."
fi fi
local pi_ip local pi_ip
pi_ip=$(discover_raspberry_pi) pi_ip=$(discover_raspberry_pi)
if [[ -z "$pi_ip" ]]; then if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi" die "Failed to discover Raspberry Pi"
fi fi
# Get DuckDNS credentials if not set # Get DuckDNS credentials if not set
if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]]; then if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then
echo echo
log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." 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" log_info "1. Go to https://www.duckdns.org/ and sign in"
@ -1012,14 +1012,14 @@ phase_setup_ssl_remote() {
phase_install_remote() { phase_install_remote() {
log_info "=== Installing Nextcloud via SSH ===" 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?" die "PI_PASSWORD not set. Did you run flash script first?"
fi fi
local pi_ip local pi_ip
pi_ip=$(discover_raspberry_pi) pi_ip=$(discover_raspberry_pi)
if [[ -z "$pi_ip" ]]; then if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi" die "Failed to discover Raspberry Pi"
fi fi
@ -1070,14 +1070,14 @@ phase_install_remote() {
phase_install_ca() { phase_install_ca() {
log_info "=== Installing Nextcloud CA Certificate ===" 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." die "PI_PASSWORD not set. Run this after running install-remote or flash."
fi fi
local pi_ip local pi_ip
pi_ip=$(discover_raspberry_pi) pi_ip=$(discover_raspberry_pi)
if [[ -z "$pi_ip" ]]; then if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi" die "Failed to discover Raspberry Pi"
fi fi
@ -1089,7 +1089,7 @@ phase_install_ca() {
sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no \ 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 "${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" die "Failed to download CA certificate"
fi fi
@ -1146,7 +1146,7 @@ phase_install_ca() {
if [[ -d ~/.mozilla/firefox ]]; then if [[ -d ~/.mozilla/firefox ]]; then
local installed=0 local installed=0
for profile_dir in ~/.mozilla/firefox/*.default* ~/.mozilla/firefox/*.esr*; do 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 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 && certutil -d sql:"$profile_dir" -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null &&
installed=1 installed=1

View File

@ -5,6 +5,11 @@
set -e # Exit on any error 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 # Function to check and request sudo privileges for package installation
check_sudo() { check_sudo() {
if [[ $EUID -ne 0 ]] && [[ $1 == "install" ]]; then if [[ $EUID -ne 0 ]] && [[ $1 == "install" ]]; then
@ -14,20 +19,13 @@ check_sudo() {
fi fi
} }
# Get the actual user (even when running with sudo)
set_actual_user_vars
echo "ActivityWatch Setup for Arch Linux + i3" echo "ActivityWatch Setup for Arch Linux + i3"
echo "=======================================" echo "======================================="
echo "Current Date: $(date)" echo "Current Date: $(date)"
echo "User: ${SUDO_USER:-$USER}" echo "User: $ACTUAL_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 "Target user: $ACTUAL_USER" echo "Target user: $ACTUAL_USER"
echo "User home: $USER_HOME" echo "User home: $USER_HOME"
@ -38,7 +36,7 @@ check_activitywatch_installed() {
echo "========================================" echo "========================================"
# Check if activitywatch-bin is installed via pacman # Check if activitywatch-bin is installed via pacman
if pacman -Qi activitywatch-bin &> /dev/null; then if pacman -Qi activitywatch-bin &>/dev/null; then
echo "✓ activitywatch-bin package is installed" echo "✓ activitywatch-bin package is installed"
return 0 return 0
fi fi
@ -78,7 +76,7 @@ install_activitywatch() {
local helper_found="" local helper_found=""
for helper in "${aur_helpers[@]}"; do for helper in "${aur_helpers[@]}"; do
if command -v "$helper" &> /dev/null; then if command -v "$helper" &>/dev/null; then
helper_found="$helper" helper_found="$helper"
break break
fi fi
@ -110,7 +108,7 @@ install_activitywatch_manual() {
cd "$temp_dir" cd "$temp_dir"
# Download PKGBUILD # Download PKGBUILD
if command -v git &> /dev/null; then if command -v git &>/dev/null; then
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git . sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
else else
echo "Installing git..." echo "Installing git..."
@ -133,13 +131,13 @@ check_activitywatch_running() {
echo "==================================" echo "=================================="
# Check for aw-qt process # Check for aw-qt process
if pgrep -f "aw-qt" > /dev/null; then if pgrep -f "aw-qt" >/dev/null; then
echo "✓ ActivityWatch (aw-qt) is running" echo "✓ ActivityWatch (aw-qt) is running"
return 0 return 0
fi fi
# Check for aw-server process # Check for aw-server process
if pgrep -f "aw-server" > /dev/null; then if pgrep -f "aw-server" >/dev/null; then
echo "✓ ActivityWatch server is running" echo "✓ ActivityWatch server is running"
return 0 return 0
fi fi
@ -157,7 +155,7 @@ start_activitywatch() {
# Find aw-qt executable # Find aw-qt executable
local aw_qt_path="" local aw_qt_path=""
if command -v aw-qt &> /dev/null; then if command -v aw-qt &>/dev/null; then
aw_qt_path="$(which aw-qt)" aw_qt_path="$(which aw-qt)"
elif [[ -x "/usr/bin/aw-qt" ]]; then elif [[ -x "/usr/bin/aw-qt" ]]; then
aw_qt_path="/usr/bin/aw-qt" aw_qt_path="/usr/bin/aw-qt"
@ -181,7 +179,7 @@ start_activitywatch() {
# Give it time to start # Give it time to start
sleep 3 sleep 3
if check_activitywatch_running > /dev/null 2>&1; then if check_activitywatch_running >/dev/null 2>&1; then
echo "✓ ActivityWatch started successfully" echo "✓ ActivityWatch started successfully"
else else
echo "! ActivityWatch may be starting (check system tray)" echo "! ActivityWatch may be starting (check system tray)"
@ -206,7 +204,7 @@ setup_autostart() {
fi fi
# Create desktop file for autostart # Create desktop file for autostart
cat > "$desktop_file" << EOF cat >"$desktop_file" <<EOF
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=ActivityWatch Name=ActivityWatch
@ -245,7 +243,7 @@ EOF"
printf '\n' printf '\n'
printf '# Auto-start ActivityWatch\n' printf '# Auto-start ActivityWatch\n'
printf 'exec --no-startup-id aw-qt\n' printf 'exec --no-startup-id aw-qt\n'
} >> "$i3_config" } >>"$i3_config"
fi fi
echo "✓ Added ActivityWatch to i3 config autostart" echo "✓ Added ActivityWatch to i3 config autostart"
@ -274,7 +272,7 @@ create_i3blocks_status() {
fi fi
# Create the status script # Create the status script
cat > "$status_script" << 'EOF' cat >"$status_script" <<'EOF'
#!/bin/bash #!/bin/bash
# ActivityWatch status script for i3blocks # ActivityWatch status script for i3blocks
# Shows ActivityWatch installation and running status # Shows ActivityWatch installation and running status
@ -352,14 +350,14 @@ test_setup() {
echo "==================" echo "=================="
echo "Installation status:" echo "Installation status:"
if check_activitywatch_installed > /dev/null 2>&1; then if check_activitywatch_installed >/dev/null 2>&1; then
echo "✓ ActivityWatch is installed" echo "✓ ActivityWatch is installed"
else else
echo "✗ ActivityWatch is not installed" echo "✗ ActivityWatch is not installed"
fi fi
echo "Running status:" echo "Running status:"
if check_activitywatch_running > /dev/null 2>&1; then if check_activitywatch_running >/dev/null 2>&1; then
echo "✓ ActivityWatch is running" echo "✓ ActivityWatch is running"
else else
echo "✗ ActivityWatch is not running" echo "✗ ActivityWatch is not running"

View File

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

View File

@ -2,35 +2,22 @@
set -euo pipefail 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() { on_error() {
local exit_code=$? local exit_code=$?
local line_number=$1 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 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() { 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." log_error "pacman not found. This script is intended for Arch Linux systems."
exit 1
fi fi
} }
@ -57,7 +44,7 @@ collect_kernel_headers() {
local -a headers=() local -a headers=()
local kernel_pkg header_pkg local kernel_pkg header_pkg
for kernel_pkg in linux linux-lts linux-zen linux-hardened; do for kernel_pkg in linux linux-lts linux-zen linux-hardened; do
if pacman -Q "${kernel_pkg}" > /dev/null 2>&1; then if pacman -Q "${kernel_pkg}" >/dev/null 2>&1; then
header_pkg="${kernel_pkg}-headers" header_pkg="${kernel_pkg}-headers"
headers+=("${header_pkg}") headers+=("${header_pkg}")
fi fi
@ -72,7 +59,7 @@ maybe_remove_conflicting_host_packages() {
local -a candidates=("virtualbox-host-dkms" "virtualbox-host-modules-arch" "virtualbox-host-modules-lts") local -a candidates=("virtualbox-host-dkms" "virtualbox-host-modules-arch" "virtualbox-host-modules-lts")
local pkg local pkg
for pkg in "${candidates[@]}"; do for pkg in "${candidates[@]}"; do
if [[ ${pkg} != "${selected_package}" ]] && pacman -Q "${pkg}" > /dev/null 2>&1; then if [[ ${pkg} != "${selected_package}" ]] && pacman -Q "${pkg}" >/dev/null 2>&1; then
log_warn "Removing conflicting package ${pkg} before installing ${selected_package}." log_warn "Removing conflicting package ${pkg} before installing ${selected_package}."
pacman -Rsn "${PACMAN_REMOVE_FLAGS[@]}" "${pkg}" pacman -Rsn "${PACMAN_REMOVE_FLAGS[@]}" "${pkg}"
fi fi
@ -101,7 +88,7 @@ install_packages() {
rebuild_virtualbox_modules() { rebuild_virtualbox_modules() {
local host_package=$1 local host_package=$1
if [[ ${host_package} == "virtualbox-host-dkms" ]]; then if [[ ${host_package} == "virtualbox-host-dkms" ]]; then
if command -v dkms > /dev/null 2>&1; then if command -v dkms >/dev/null 2>&1; then
log_info "Rebuilding VirtualBox DKMS modules for all installed kernels." log_info "Rebuilding VirtualBox DKMS modules for all installed kernels."
dkms autoinstall dkms autoinstall
else else
@ -122,7 +109,7 @@ reload_virtualbox_modules() {
local mod local mod
for mod in "${modules[@]}"; do for mod in "${modules[@]}"; do
if ! lsmod | awk '{print $1}' | grep -Fxq "${mod}"; then if ! lsmod | awk '{print $1}' | grep -Fxq "${mod}"; then
if ! modprobe "${mod}" > /dev/null 2>&1; then if ! modprobe "${mod}" >/dev/null 2>&1; then
log_warn "Module ${mod} failed to load; check dmesg for details." log_warn "Module ${mod} failed to load; check dmesg for details."
fi fi
fi fi
@ -137,10 +124,10 @@ reload_virtualbox_modules() {
warn_if_secure_boot_enabled() { warn_if_secure_boot_enabled() {
local secure_boot_file local secure_boot_file
if [[ -d /sys/firmware/efi/efivars ]]; then if [[ -d /sys/firmware/efi/efivars ]]; then
secure_boot_file=$(find /sys/firmware/efi/efivars -maxdepth 1 -name 'SecureBoot-*' -print -quit 2> /dev/null || true) secure_boot_file=$(find /sys/firmware/efi/efivars -maxdepth 1 -name 'SecureBoot-*' -print -quit 2>/dev/null || true)
if [[ -n ${secure_boot_file} && -r ${secure_boot_file} ]]; then if [[ -n ${secure_boot_file} && -r ${secure_boot_file} ]]; then
local state local state
state=$(hexdump -n 1 -s 4 -e '1 "%d"' "${secure_boot_file}" 2> /dev/null || echo "0") state=$(hexdump -n 1 -s 4 -e '1 "%d"' "${secure_boot_file}" 2>/dev/null || echo "0")
if [[ ${state} == "1" ]]; then if [[ ${state} == "1" ]]; then
log_warn "EFI Secure Boot appears to be enabled. You may need to sign VirtualBox modules manually." log_warn "EFI Secure Boot appears to be enabled. You may need to sign VirtualBox modules manually."
fi fi

View File

@ -17,16 +17,7 @@ shift "$COMMON_ARGS_SHIFT"
# Check for sudo privileges # Check for sudo privileges
require_root "$@" require_root "$@"
echo "NVIDIA Comprehensive Troubleshooter & GSP Disabler" print_setup_header "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
# Check if nvidia module is loaded # Check if nvidia module is loaded
if ! lsmod | grep -q nvidia; then 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 local formatted
formatted="$(date '+%Y-%m-%d %H:%M:%S') - $msg" formatted="$(date '+%Y-%m-%d %H:%M:%S') - $msg"
echo "$formatted" >&2 echo "$formatted" >&2
if [[ -n "$log_file" ]]; then if [[ -n $log_file ]]; then
echo "$formatted" >>"$log_file" 2>/dev/null || true echo "$formatted" >>"$log_file" 2>/dev/null || true
fi fi
} }
@ -57,13 +57,23 @@ get_actual_user() {
get_actual_user_home() { get_actual_user_home() {
local user local user
user=$(get_actual_user) user=$(get_actual_user)
if [[ "$user" == "root" ]]; then if [[ $user == "root" ]]; then
echo "/root" echo "/root"
else else
echo "/home/$user" echo "/home/$user"
fi 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 # ARGUMENT PARSING HELPERS
# ============================================================================= # =============================================================================
@ -102,6 +112,32 @@ parse_interactive_args() {
done 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) # FOCUS APP DETECTION (for digital wellbeing scripts)
# ============================================================================= # =============================================================================
@ -172,6 +208,61 @@ require_command() {
return 0 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 # NOTIFICATION
# ============================================================================= # =============================================================================
@ -203,7 +294,7 @@ get_script_dir() {
# Usage: ensure_dir "/path/to/dir" # Usage: ensure_dir "/path/to/dir"
ensure_dir() { ensure_dir() {
local dir="$1" local dir="$1"
if [[ ! -d "$dir" ]]; then if [[ ! -d $dir ]]; then
mkdir -p "$dir" mkdir -p "$dir"
fi fi
} }
@ -212,30 +303,176 @@ ensure_dir() {
# SYSTEMD HELPERS # 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) # Enable and start a systemd service (user or system)
# Usage: enable_service "service-name" [--user] # Usage: enable_service "service-name" [--user]
enable_service() { enable_service() {
local service="$1" local service="$1"
local user_flag="${2:-}" local user_flag="${2:-}"
_systemctl_cmd "$user_flag" daemon-reload
if [[ "$user_flag" == "--user" ]]; then _systemctl_cmd "$user_flag" enable --now "$service"
systemctl --user daemon-reload
systemctl --user enable --now "$service"
else
systemctl daemon-reload
systemctl enable --now "$service"
fi
} }
# Check if a systemd service is active # Check if a systemd service is active
# Usage: if is_service_active "service-name" [--user]; then ... # Usage: if is_service_active "service-name" [--user]; then ...
is_service_active() { is_service_active() {
local service="$1" _systemctl_cmd "${2:-}" is-active --quiet "$1"
local user_flag="${2:-}" }
if [[ "$user_flag" == "--user" ]]; then # Check if a systemd service is enabled
systemctl --user is-active --quiet "$service" # 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 else
systemctl is-active --quiet "$service" echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
fi 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 set -uo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) 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) DEFAULT_ROOT=$(cd -- "$SCRIPT_DIR/../../" && pwd)
ROOT_DIR="$DEFAULT_ROOT" ROOT_DIR="$DEFAULT_ROOT"
@ -25,20 +30,8 @@ INSTALL_ONLY="false"
LIST_ONLY="false" LIST_ONLY="false"
VERBOSE="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() { usage() {
cat << EOF cat <<EOF
Usage: $(basename "$0") [options] Usage: $(basename "$0") [options]
Options: Options:
@ -47,7 +40,9 @@ Options:
--install-only Only install linters, do not scan --install-only Only install linters, do not scan
--list-only Only list discovered shell files, do not run linters --list-only Only list discovered shell files, do not run linters
--verbose Print additional details while running --verbose Print additional details while running
-h, --help Show this helpLinters used: -h, --help Show this help
Linters used:
Required: shellcheck, shfmt Required: shellcheck, shfmt
Optional (if available): checkbashisms, bashate Optional (if available): checkbashisms, bashate
Syntax checks: bash -n, zsh -n (if installed), sh/dash -n Syntax checks: bash -n, zsh -n (if installed), sh/dash -n
@ -93,7 +88,7 @@ if [[ ! -d $ROOT_DIR ]]; then
exit 2 exit 2
fi fi
is_cmd() { command -v "$1" > /dev/null 2>&1; } is_cmd() { command -v "$1" >/dev/null 2>&1; }
is_arch() { is_cmd pacman; } is_arch() { is_cmd pacman; }
have_aur_helper() { is_cmd yay || is_cmd paru; } have_aur_helper() { is_cmd yay || is_cmd paru; }
@ -136,7 +131,7 @@ install_linters() {
# checkbashisms may be in repos or AUR; try pacman first, then AUR helper # checkbashisms may be in repos or AUR; try pacman first, then AUR helper
if ! is_cmd checkbashisms; then if ! is_cmd checkbashisms; then
if is_arch; then if is_arch; then
if ! sudo pacman -S --needed --noconfirm checkbashisms 2> /dev/null; then if ! sudo pacman -S --needed --noconfirm checkbashisms 2>/dev/null; then
if have_aur_helper; then if have_aur_helper; then
log_info "Installing checkbashisms from AUR (requires yay/paru)..." log_info "Installing checkbashisms from AUR (requires yay/paru)..."
if is_cmd yay; then yay -S --noconfirm checkbashisms || true; fi if is_cmd yay; then yay -S --noconfirm checkbashisms || true; fi
@ -182,7 +177,7 @@ discover_shell_files() {
local -a all local -a all
all=() all=()
if git -C "$base" rev-parse --is-inside-work-tree > /dev/null 2>&1; then if git -C "$base" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
while IFS= read -r -d '' f; do all+=("$f"); done < <(git -C "$base" ls-files -z) while IFS= read -r -d '' f; do all+=("$f"); done < <(git -C "$base" ls-files -z)
while IFS= read -r -d '' f; do all+=("$f"); done < <(git -C "$base" ls-files --others --exclude-standard -z) while IFS= read -r -d '' f; do all+=("$f"); done < <(git -C "$base" ls-files --others --exclude-standard -z)
else else
@ -215,7 +210,7 @@ discover_shell_files() {
# Check shebang # Check shebang
local first local first
first=$(head -n 1 -- "$abs" 2> /dev/null || true) first=$(head -n 1 -- "$abs" 2>/dev/null || true)
if [[ $first =~ ^#! && $first =~ (ba|z|d|k)?sh ]]; then if [[ $first =~ ^#! && $first =~ (ba|z|d|k)?sh ]]; then
shells+=("$rel") shells+=("$rel")
continue continue
@ -230,38 +225,38 @@ discover_shell_files() {
done done
# write lists # write lists
: > "$REL_FILES_Z" : >"$REL_FILES_Z"
: > "$ABS_FILES_Z" : >"$ABS_FILES_Z"
for rel in "${shells[@]}"; do for rel in "${shells[@]}"; do
printf '%s\0' "$rel" >> "$REL_FILES_Z" printf '%s\0' "$rel" >>"$REL_FILES_Z"
printf '%s\0' "$base/$rel" >> "$ABS_FILES_Z" printf '%s\0' "$base/$rel" >>"$ABS_FILES_Z"
done done
} }
print_file_list() { print_file_list() {
local count local count
count=$(tr -cd '\0' < "$REL_FILES_Z" | wc -c) count=$(tr -cd '\0' <"$REL_FILES_Z" | wc -c)
log_info "Discovered $count shell file(s) under $ROOT_DIR" log_info "Discovered $count shell file(s) under $ROOT_DIR"
if [[ $VERBOSE == "true" ]]; then if [[ $VERBOSE == "true" ]]; then
tr '\0' '\n' < "$REL_FILES_Z" | sed 's/^/ - /' tr '\0' '\n' <"$REL_FILES_Z" | sed 's/^/ - /'
fi fi
} }
run_linters() { run_linters() {
local issues=0 local issues=0
local count local count
count=$(tr -cd '\0' < "$ABS_FILES_Z" | wc -c) count=$(tr -cd '\0' <"$ABS_FILES_Z" | wc -c)
if [[ $count -eq 0 ]]; then if [[ $count -eq 0 ]]; then
log_warn "No shell files found to lint." log_warn "No shell files found to lint."
return 0 return 0
fi fi
mapfile -d '' -t FILES < "$ABS_FILES_Z" mapfile -d '' -t FILES <"$ABS_FILES_Z"
log_info "Running shellcheck..." log_info "Running shellcheck..."
local sc_out="$TMPDIR/shellcheck.txt" local sc_out="$TMPDIR/shellcheck.txt"
if is_cmd shellcheck; then if is_cmd shellcheck; then
if ! shellcheck -x -S style "${FILES[@]}" > "$sc_out" 2>&1; then if ! shellcheck -x -S style "${FILES[@]}" >"$sc_out" 2>&1; then
issues=$((issues + 1)) issues=$((issues + 1))
fi fi
else else
@ -271,7 +266,7 @@ run_linters() {
log_info "Running shfmt (diff mode)..." log_info "Running shfmt (diff mode)..."
local shfmt_out="$TMPDIR/shfmt.diff" local shfmt_out="$TMPDIR/shfmt.diff"
if is_cmd shfmt; then if is_cmd shfmt; then
if ! shfmt -d -i 2 -ci -sr -s "${FILES[@]}" > "$shfmt_out" 2>&1; then if ! shfmt -d -i 2 -ci -sr -s "${FILES[@]}" >"$shfmt_out" 2>&1; then
# shfmt returns non-zero when diff exists # shfmt returns non-zero when diff exists
issues=$((issues + 1)) issues=$((issues + 1))
fi fi
@ -289,7 +284,7 @@ run_linters() {
CBI_FILES=() CBI_FILES=()
for f in "${FILES[@]}"; do for f in "${FILES[@]}"; do
local first local first
first=$(head -n 1 -- "$f" 2> /dev/null || true) first=$(head -n 1 -- "$f" 2>/dev/null || true)
if [[ $first =~ bash || $first =~ zsh ]]; then if [[ $first =~ bash || $first =~ zsh ]]; then
continue continue
fi fi
@ -297,9 +292,9 @@ run_linters() {
done done
if [[ ${#CBI_FILES[@]} -gt 0 ]]; then if [[ ${#CBI_FILES[@]} -gt 0 ]]; then
# checkbashisms exits 0 if OK, 1 if issues, other codes for tool warnings # checkbashisms exits 0 if OK, 1 if issues, other codes for tool warnings
checkbashisms "${CBI_FILES[@]}" > "$cbi_out" 2>&1 checkbashisms "${CBI_FILES[@]}" >"$cbi_out" 2>&1
else else
: > "$cbi_out" : >"$cbi_out"
fi fi
cbi_status=$? cbi_status=$?
if [[ $cbi_status -eq 1 ]]; then if [[ $cbi_status -eq 1 ]]; then
@ -323,7 +318,7 @@ run_linters() {
SH_FILES=() SH_FILES=()
for f in "${FILES[@]}"; do for f in "${FILES[@]}"; do
local first local first
first=$(head -n 1 -- "$f" 2> /dev/null || true) first=$(head -n 1 -- "$f" 2>/dev/null || true)
if [[ $first =~ bash ]]; then if [[ $first =~ bash ]]; then
BASH_FILES+=("$f") BASH_FILES+=("$f")
elif [[ $first =~ zsh ]]; then elif [[ $first =~ zsh ]]; then
@ -334,23 +329,23 @@ run_linters() {
done done
if [[ ${#BASH_FILES[@]} -gt 0 ]] && is_cmd bash; then if [[ ${#BASH_FILES[@]} -gt 0 ]] && is_cmd bash; then
if ! bash -n "${BASH_FILES[@]}" 2> "$bash_out"; then if ! bash -n "${BASH_FILES[@]}" 2>"$bash_out"; then
issues=$((issues + 1)) issues=$((issues + 1))
fi fi
fi fi
if [[ ${#ZSH_FILES[@]} -gt 0 ]] && is_cmd zsh; then if [[ ${#ZSH_FILES[@]} -gt 0 ]] && is_cmd zsh; then
if ! zsh -n "${ZSH_FILES[@]}" 2> "$zsh_out"; then if ! zsh -n "${ZSH_FILES[@]}" 2>"$zsh_out"; then
issues=$((issues + 1)) issues=$((issues + 1))
fi fi
fi fi
# prefer dash if present for /bin/sh style # prefer dash if present for /bin/sh style
if [[ ${#SH_FILES[@]} -gt 0 ]]; then if [[ ${#SH_FILES[@]} -gt 0 ]]; then
if is_cmd dash; then if is_cmd dash; then
if ! dash -n "${SH_FILES[@]}" 2> "$sh_out"; then if ! dash -n "${SH_FILES[@]}" 2>"$sh_out"; then
issues=$((issues + 1)) issues=$((issues + 1))
fi fi
elif is_cmd sh; then elif is_cmd sh; then
if ! sh -n "${SH_FILES[@]}" 2> "$sh_out"; then if ! sh -n "${SH_FILES[@]}" 2>"$sh_out"; then
issues=$((issues + 1)) issues=$((issues + 1))
fi fi
fi fi

View File

@ -18,22 +18,17 @@
set -euo pipefail set -euo pipefail
IFS=$'\n\t' IFS=$'\n\t'
GREEN="\033[1;32m" # Source common library for log functions
YELLOW="\033[1;33m" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
RED="\033[1;31m" # shellcheck source=../../lib/common.sh
BLUE="\033[1;34m" source "$SCRIPT_DIR/../../lib/common.sh"
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; }
DO_POLICY=false DO_POLICY=false
SET_DEFAULT=false SET_DEFAULT=false
DO_RESTART=false DO_RESTART=false
usage() { usage() {
cat << EOF cat <<EOF
fix_thorium_unity.sh - Auto-allow unityhub:// from Unity origins in Thorium/Chromium fix_thorium_unity.sh - Auto-allow unityhub:// from Unity origins in Thorium/Chromium
Options: Options:
@ -75,7 +70,7 @@ while [[ $# -gt 0 ]]; do
done done
ensure_sudo() { ensure_sudo() {
if ! command -v sudo > /dev/null 2>&1; then if ! command -v sudo >/dev/null 2>&1; then
log_error "sudo not found; cannot install system policy. Use --set-default or run from root." log_error "sudo not found; cannot install system policy. Use --set-default or run from root."
exit 1 exit 1
fi fi
@ -94,7 +89,7 @@ install_policy() {
log_info "Installing policy into: $target" log_info "Installing policy into: $target"
sudo mkdir -p "$target" sudo mkdir -p "$target"
local policy_file="$target/unityhub-policy.json" local policy_file="$target/unityhub-policy.json"
sudo tee "$policy_file" > /dev/null << 'JSON' sudo tee "$policy_file" >/dev/null <<'JSON'
{ {
"AutoLaunchProtocolsFromOrigins": [ "AutoLaunchProtocolsFromOrigins": [
{ "protocol": "unityhub", "origin": "https://id.unity.com", "allow": true }, { "protocol": "unityhub", "origin": "https://id.unity.com", "allow": true },
@ -116,7 +111,7 @@ JSON
} }
set_default_browser() { set_default_browser() {
if command -v xdg-settings > /dev/null 2>&1; then if command -v xdg-settings >/dev/null 2>&1; then
# Prefer the upstream desktop id if it exists # Prefer the upstream desktop id if it exists
local desktop="thorium-browser.desktop" local desktop="thorium-browser.desktop"
if [[ ! -f "/usr/share/applications/$desktop" && -f "$HOME/.local/share/applications/$desktop" ]]; then if [[ ! -f "/usr/share/applications/$desktop" && -f "$HOME/.local/share/applications/$desktop" ]]; then
@ -127,7 +122,7 @@ set_default_browser() {
fi fi
log_info "Setting default browser to $desktop" log_info "Setting default browser to $desktop"
xdg-settings set default-web-browser "$desktop" || log_warn "Failed to set default browser via xdg-settings" xdg-settings set default-web-browser "$desktop" || log_warn "Failed to set default browser via xdg-settings"
log_ok "Default browser set to: $(xdg-settings get default-web-browser 2> /dev/null || echo "$desktop")" log_ok "Default browser set to: $(xdg-settings get default-web-browser 2>/dev/null || echo "$desktop")"
else else
log_warn "xdg-settings not found; cannot set default browser automatically." log_warn "xdg-settings not found; cannot set default browser automatically."
fi fi
@ -136,12 +131,12 @@ set_default_browser() {
restart_thorium() { restart_thorium() {
# Kill Thorium processes and start fresh # Kill Thorium processes and start fresh
log_info "Restarting Thorium..." log_info "Restarting Thorium..."
pkill -9 -f 'thorium-browser' 2> /dev/null || true pkill -9 -f 'thorium-browser' 2>/dev/null || true
# Also kill unityhub-bin's embedded Chromium if any leftover (harmless) # Also kill unityhub-bin's embedded Chromium if any leftover (harmless)
pkill -9 -f 'unityhub-bin' 2> /dev/null || true pkill -9 -f 'unityhub-bin' 2>/dev/null || true
# Start Thorium detached if available # Start Thorium detached if available
if command -v thorium-browser > /dev/null 2>&1; then if command -v thorium-browser >/dev/null 2>&1; then
nohup thorium-browser > /dev/null 2>&1 & nohup thorium-browser >/dev/null 2>&1 &
disown || true disown || true
fi fi
log_ok "Thorium restart attempted." log_ok "Thorium restart attempted."
@ -152,7 +147,7 @@ main() {
$SET_DEFAULT && set_default_browser $SET_DEFAULT && set_default_browser
$DO_RESTART && restart_thorium $DO_RESTART && restart_thorium
cat << 'NEXT' cat <<'NEXT'
--- ---
Next steps: Next steps:
- Open Unity Hub, click Sign in, complete in Thorium; when prompted, allow the unityhub link to open the app. - Open Unity Hub, click Sign in, complete in Thorium; when prompted, allow the unityhub link to open the app.

View File

@ -23,19 +23,14 @@ set -euo pipefail
IFS=$'\n\t' IFS=$'\n\t'
SCRIPT_NAME="$(basename "$0")" 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} $*"; } # Source common library for log functions
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; } SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } # shellcheck source=../../lib/common.sh
log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; } source "$SCRIPT_DIR/../../lib/common.sh"
usage() { usage() {
cat << EOF cat <<EOF
${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler ${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler
Options: Options:
@ -74,7 +69,7 @@ while [[ $# -gt 0 ]]; do
done done
require_cmd() { require_cmd() {
if ! command -v "$1" > /dev/null 2>&1; then if ! command -v "$1" >/dev/null 2>&1; then
return 1 return 1
fi fi
} }
@ -117,8 +112,8 @@ detect_unityhub() {
local install_type="UNKNOWN" exec_cmd="" local install_type="UNKNOWN" exec_cmd=""
# 1) Flatpak # 1) Flatpak
if command -v flatpak > /dev/null 2>&1; then if command -v flatpak >/dev/null 2>&1; then
if flatpak info com.unity.UnityHub > /dev/null 2>&1; then if flatpak info com.unity.UnityHub >/dev/null 2>&1; then
install_type="FLATPAK" install_type="FLATPAK"
exec_cmd="flatpak run com.unity.UnityHub %U" exec_cmd="flatpak run com.unity.UnityHub %U"
echo "$install_type|$exec_cmd" echo "$install_type|$exec_cmd"
@ -127,7 +122,7 @@ detect_unityhub() {
fi fi
# 2) Native binary in PATH # 2) Native binary in PATH
if command -v unityhub > /dev/null 2>&1; then if command -v unityhub >/dev/null 2>&1; then
local path local path
path="$(command -v unityhub)" path="$(command -v unityhub)"
install_type="NATIVE" install_type="NATIVE"
@ -150,8 +145,8 @@ detect_unityhub() {
local f local f
for f in "$d"/*.desktop; do for f in "$d"/*.desktop; do
[[ -e $f ]] || continue [[ -e $f ]] || continue
if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2> /dev/null || if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2>/dev/null ||
grep -qiE 'Exec=.*unityhub' "$f" 2> /dev/null; then grep -qiE 'Exec=.*unityhub' "$f" 2>/dev/null; then
local exec_line local exec_line
exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')" exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')"
if [[ -n $exec_line ]]; then if [[ -n $exec_line ]]; then
@ -203,7 +198,7 @@ create_handler_desktop() {
local exec_cmd="$1" local exec_cmd="$1"
local dest="$desktop_dir/unityhub-url-handler.desktop" local dest="$desktop_dir/unityhub-url-handler.desktop"
log_info "Writing handler desktop entry: $dest" log_info "Writing handler desktop entry: $dest"
cat > "$dest" << DESK cat >"$dest" <<DESK
[Desktop Entry] [Desktop Entry]
Name=Unity Hub URL Handler Name=Unity Hub URL Handler
Comment=Handle unityhub:// links for Unity Hub sign-in Comment=Handle unityhub:// links for Unity Hub sign-in
@ -223,14 +218,14 @@ DESK
register_mime_handler() { register_mime_handler() {
local desktop_file="$1" local desktop_file="$1"
# Update desktop database if available # Update desktop database if available
if command -v update-desktop-database > /dev/null 2>&1; then if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$desktop_dir" || true update-desktop-database "$desktop_dir" || true
else else
log_warn "update-desktop-database not found (install desktop-file-utils)." log_warn "update-desktop-database not found (install desktop-file-utils)."
fi fi
# Register as default handler for both schemes # Register as default handler for both schemes
if command -v xdg-mime > /dev/null 2>&1; then if command -v xdg-mime >/dev/null 2>&1; then
xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unityhub || true xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unityhub || true
xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true
else else
@ -243,8 +238,8 @@ register_mime_handler() {
verify_registration() { verify_registration() {
local expected cur1 cur2 local expected cur1 cur2
expected="$(basename "$1")" expected="$(basename "$1")"
cur1="$(xdg-mime query default x-scheme-handler/unityhub 2> /dev/null || true)" cur1="$(xdg-mime query default x-scheme-handler/unityhub 2>/dev/null || true)"
cur2="$(xdg-mime query default x-scheme-handler/unity 2> /dev/null || true)" cur2="$(xdg-mime query default x-scheme-handler/unity 2>/dev/null || true)"
log_info "Current handler (unityhub): ${cur1:-<none>}" log_info "Current handler (unityhub): ${cur1:-<none>}"
log_info "Current handler (unity): ${cur2:-<none>}" log_info "Current handler (unity): ${cur2:-<none>}"
if [[ $cur1 == "$expected" ]]; then if [[ $cur1 == "$expected" ]]; then
@ -257,8 +252,8 @@ verify_registration() {
maybe_test_open() { maybe_test_open() {
if [[ $RUN_TEST == true ]]; then if [[ $RUN_TEST == true ]]; then
log_info "Opening test link: unityhub://v1/editor-signin" log_info "Opening test link: unityhub://v1/editor-signin"
if command -v xdg-open > /dev/null 2>&1; then if command -v xdg-open >/dev/null 2>&1; then
xdg-open 'unityhub://v1/editor-signin' > /dev/null 2>&1 || true xdg-open 'unityhub://v1/editor-signin' >/dev/null 2>&1 || true
log_ok "Test link invoked. Check if Unity Hub launches or focuses." log_ok "Test link invoked. Check if Unity Hub launches or focuses."
else else
log_warn "xdg-open not found; cannot run test automatically." log_warn "xdg-open not found; cannot run test automatically."
@ -291,7 +286,7 @@ main() {
register_mime_handler "$desktop_file" register_mime_handler "$desktop_file"
verify_registration "$desktop_file" verify_registration "$desktop_file"
cat << 'NOTE' cat <<'NOTE'
--- ---
Next steps: Next steps:
- Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub. - Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub.

View File

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

View File

@ -7,20 +7,15 @@ set -euo pipefail
# Tries distro packages first; if not suitable, offers to build from source. # Tries distro packages first; if not suitable, offers to build from source.
# This script prints commands and asks for confirmation before building. # 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() { print_info() {
echo "[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() { detect_distro() {
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
. /etc/os-release . /etc/os-release

View File

@ -2,6 +2,11 @@
set -euo pipefail 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")" SCRIPT_NAME="$(basename "$0")"
RED="\033[31m" RED="\033[31m"
@ -21,39 +26,12 @@ error() {
printf "%b[%s]%b %s\n" "$RED" "$SCRIPT_NAME" "$RESET" "$*" >&2 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() { ensure_pacman_packages() {
local packages=("python" "git" "curl" "jq" "code") install_missing_pacman_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_uv() { install_uv() {
if command -v uv > /dev/null 2>&1; then if command -v uv >/dev/null 2>&1; then
info "uv is already installed." info "uv is already installed."
return return
fi fi
@ -64,21 +42,21 @@ install_uv() {
local local_bin="$HOME/.local/bin" local local_bin="$HOME/.local/bin"
if [[ :$PATH: != *":$local_bin:"* ]]; then if [[ :$PATH: != *":$local_bin:"* ]]; then
warn "Adding $local_bin to PATH in ~/.profile and ~/.zshrc. Open a new shell to apply." warn "Adding $local_bin to PATH in ~/.profile and ~/.zshrc. Open a new shell to apply."
printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >> "$HOME/.profile" printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >>"$HOME/.profile"
printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >> "$HOME/.zshrc" printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >>"$HOME/.zshrc"
fi fi
} }
ensure_unity_hub() { ensure_unity_hub() {
if command -v unityhub > /dev/null 2>&1; then if command -v unityhub >/dev/null 2>&1; then
info "Unity Hub already installed." info "Unity Hub already installed."
return return
fi fi
if command -v yay > /dev/null 2>&1; then if command -v yay >/dev/null 2>&1; then
info "Installing Unity Hub from AUR using yay." info "Installing Unity Hub from AUR using yay."
yay -S --needed --noconfirm unityhub yay -S --needed --noconfirm unityhub
elif command -v flatpak > /dev/null 2>&1; then elif command -v flatpak >/dev/null 2>&1; then
warn "Unity Hub not found. Attempting Flatpak installation." warn "Unity Hub not found. Attempting Flatpak installation."
flatpak install -y com.unity.UnityHub || warn "Flatpak installation failed. Install Unity Hub manually via https://unity.com/download" flatpak install -y com.unity.UnityHub || warn "Flatpak installation failed. Install Unity Hub manually via https://unity.com/download"
else else
@ -142,14 +120,14 @@ configure_vscode_mcp() {
if [[ ! -f $mcp_config ]]; then if [[ ! -f $mcp_config ]]; then
info "Creating new VS Code MCP configuration at $mcp_config" info "Creating new VS Code MCP configuration at $mcp_config"
echo '{}' > "$mcp_config" echo '{}' >"$mcp_config"
else else
info "Updating existing VS Code MCP configuration at $mcp_config" info "Updating existing VS Code MCP configuration at $mcp_config"
fi fi
tmp="$(mktemp)" tmp="$(mktemp)"
if ! jq '.' "$mcp_config" > /dev/null 2>&1; then if ! jq '.' "$mcp_config" >/dev/null 2>&1; then
error "Existing $mcp_config is not valid JSON. Please fix it before running this script again." error "Existing $mcp_config is not valid JSON. Please fix it before running this script again."
exit 1 exit 1
fi fi
@ -162,7 +140,7 @@ configure_vscode_mcp() {
args: ["--directory", $path, "run", "server.py"], args: ["--directory", $path, "run", "server.py"],
type: "stdio" type: "stdio"
}' \ }' \
"$mcp_config" > "$tmp" "$mcp_config" >"$tmp"
mv "$tmp" "$mcp_config" mv "$tmp" "$mcp_config"
info "VS Code MCP server configuration updated for UnityMCP." info "VS Code MCP server configuration updated for UnityMCP."
@ -173,13 +151,13 @@ verify_python_version() {
require_command python "python" require_command python "python"
local version local version
version="$( version="$(
python - << 'PY' python - <<'PY'
import sys import sys
print("%d.%d.%d" % sys.version_info[:3]) print("%d.%d.%d" % sys.version_info[:3])
PY PY
)" )"
local major minor local major minor
IFS='.' read -r major minor _ <<< "$version" IFS='.' read -r major minor _ <<<"$version"
if ((major < 3 || (major == 3 && minor < 12))); then if ((major < 3 || (major == 3 && minor < 12))); then
error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing." error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing."
exit 1 exit 1
@ -188,7 +166,7 @@ PY
} }
print_next_steps() { print_next_steps() {
cat << 'EOT' cat <<'EOT'
Next steps: Next steps:
1. Launch Unity Hub and install a Unity Editor version 2021.3 LTS or newer. 1. Launch Unity Hub and install a Unity Editor version 2021.3 LTS or newer.

View File

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

View File

@ -16,16 +16,7 @@ shift "$COMMON_ARGS_SHIFT"
# Check for sudo privileges # Check for sudo privileges
require_root "$@" require_root "$@"
echo "Thorium Browser Auto-Startup Setup" print_setup_header "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
# Target URL # Target URL
TARGET_URL="https://www.fitatu.com/app/planner" 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. # 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). # 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 # Default settings
TARGET_FORMAT="mp4" TARGET_FORMAT="mp4"
CRF="" # Will be set based on format if not specified CRF="" # Will be set based on format if not specified
@ -17,10 +22,6 @@ TARGET_PATH=""
# Video extensions to search for # 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") 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() { usage() {
cat <<EOF cat <<EOF
Usage: Usage:
@ -56,7 +57,7 @@ get_video_extensions_except() {
local exclude="$1" local exclude="$1"
local exts=() local exts=()
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ "${ext,,}" != "${exclude,,}" ]]; then if [[ ${ext,,} != "${exclude,,}" ]]; then
exts+=("$ext") exts+=("$ext")
fi fi
done done
@ -69,7 +70,7 @@ is_video_file() {
ext="${ext,,}" # lowercase ext="${ext,,}" # lowercase
for video_ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do for video_ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ "$ext" == "${video_ext,,}" ]]; then if [[ $ext == "${video_ext,,}" ]]; then
return 0 return 0
fi fi
done done
@ -81,7 +82,7 @@ convert_video() {
local output_file="${input_file%.*}.${TARGET_FORMAT}" local output_file="${input_file%.*}.${TARGET_FORMAT}"
# Skip if output already exists # Skip if output already exists
if [[ -f "$output_file" ]]; then if [[ -f $output_file ]]; then
log "Skipping '$input_file': output '$output_file' already exists" log "Skipping '$input_file': output '$output_file' already exists"
return 0 return 0
fi fi
@ -91,12 +92,12 @@ convert_video() {
local ffmpeg_args=() local ffmpeg_args=()
ffmpeg_args+=(-hide_banner -loglevel warning -i "$input_file") 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) # H.264 codec for video and AAC for audio (maximum compatibility)
ffmpeg_args+=(-c:v libx264 -crf "$CRF" -preset "$PRESET") ffmpeg_args+=(-c:v libx264 -crf "$CRF" -preset "$PRESET")
ffmpeg_args+=(-c:a aac -b:a 192k) ffmpeg_args+=(-c:a aac -b:a 192k)
ffmpeg_args+=(-movflags +faststart) ffmpeg_args+=(-movflags +faststart)
elif [[ "$TARGET_FORMAT" == "webm" ]]; then elif [[ $TARGET_FORMAT == "webm" ]]; then
# VP9 codec for video and Opus for audio # VP9 codec for video and Opus for audio
ffmpeg_args+=(-c:v libvpx-vp9 -crf "$CRF" -b:v 0) ffmpeg_args+=(-c:v libvpx-vp9 -crf "$CRF" -b:v 0)
ffmpeg_args+=(-c:a libopus -b:a 128k) ffmpeg_args+=(-c:a libopus -b:a 128k)
@ -107,13 +108,13 @@ convert_video() {
if ffmpeg "${ffmpeg_args[@]}"; then if ffmpeg "${ffmpeg_args[@]}"; then
log "Successfully converted '$input_file'" log "Successfully converted '$input_file'"
if [[ "$DELETE_ORIGINAL" == true ]]; then if [[ $DELETE_ORIGINAL == true ]]; then
log "Deleting original: '$input_file'" log "Deleting original: '$input_file'"
rm "$input_file" rm "$input_file"
fi fi
else else
log "Error converting '$input_file'" log "Error converting '$input_file'"
[[ -f "$output_file" ]] && rm "$output_file" [[ -f $output_file ]] && rm "$output_file"
return 1 return 1
fi fi
} }
@ -129,8 +130,8 @@ process_directory() {
local find_args=(-type f \() local find_args=(-type f \()
local first=true local first=true
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ "${ext,,}" != "${TARGET_FORMAT,,}" ]]; then if [[ ${ext,,} != "${TARGET_FORMAT,,}" ]]; then
if [[ "$first" == true ]]; then if [[ $first == true ]]; then
first=false first=false
else else
find_args+=(-o) find_args+=(-o)
@ -159,7 +160,7 @@ parse_args() {
case "$opt" in case "$opt" in
f) f)
TARGET_FORMAT="${OPTARG,,}" 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 echo "Error: Format must be 'mp4' or 'webm'" >&2
exit 1 exit 1
fi fi
@ -194,8 +195,8 @@ parse_args() {
TARGET_PATH="$1" TARGET_PATH="$1"
# Set default CRF based on format if not specified # Set default CRF based on format if not specified
if [[ -z "$CRF" ]]; then if [[ -z $CRF ]]; then
if [[ "$TARGET_FORMAT" == "mp4" ]]; then if [[ $TARGET_FORMAT == "mp4" ]]; then
CRF=23 CRF=23
else else
CRF=30 CRF=30
@ -207,14 +208,14 @@ main() {
ensure_ffmpeg ensure_ffmpeg
parse_args "$@" parse_args "$@"
if [[ ! -e "$TARGET_PATH" ]]; then if [[ ! -e $TARGET_PATH ]]; then
echo "Error: Path '$TARGET_PATH' does not exist." >&2 echo "Error: Path '$TARGET_PATH' does not exist." >&2
exit 1 exit 1
fi fi
if [[ -f "$TARGET_PATH" ]]; then if [[ -f $TARGET_PATH ]]; then
# Single file # 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." log "File '$TARGET_PATH' is already in $TARGET_FORMAT format, skipping."
exit 0 exit 0
fi fi
@ -225,7 +226,7 @@ main() {
echo "Error: '$TARGET_PATH' is not a recognized video file." >&2 echo "Error: '$TARGET_PATH' is not a recognized video file." >&2
exit 1 exit 1
fi fi
elif [[ -d "$TARGET_PATH" ]]; then elif [[ -d $TARGET_PATH ]]; then
process_directory "$TARGET_PATH" process_directory "$TARGET_PATH"
else else
echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2 echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2

View File

@ -5,12 +5,17 @@
set -euo pipefail 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
DEFAULT_RESOLUTION="320x240" DEFAULT_RESOLUTION="320x240"
# Function to display usage # Function to display usage
usage() { usage() {
cat << EOF cat <<EOF
Usage: $0 <input_image> [resolution] [output_image] Usage: $0 <input_image> [resolution] [output_image]
Arguments: Arguments:
@ -30,13 +35,7 @@ EOF
} }
# Check if ImageMagick is installed # Check if ImageMagick is installed
if ! command -v convert &> /dev/null; then require_imagemagick "convert" || exit 1
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
# Parse arguments # Parse arguments
if [[ $# -lt 1 ]]; then if [[ $# -lt 1 ]]; then
@ -55,7 +54,7 @@ if [[ ! -f ${INPUT_IMAGE} ]]; then
fi fi
# Validate resolution format (WIDTHxHEIGHT) # Validate resolution format (WIDTHxHEIGHT)
if [[ ! ${RESOLUTION} =~ ^[0-9]+x[0-9]+$ ]]; then if ! validate_resolution "$RESOLUTION"; then
echo "Error: Invalid resolution format '${RESOLUTION}'" echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)" echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1 exit 1
@ -63,19 +62,7 @@ fi
# Generate output filename if not provided # Generate output filename if not provided
if [[ -z ${OUTPUT_IMAGE} ]]; then if [[ -z ${OUTPUT_IMAGE} ]]; then
# Extract filename without extension and extension OUTPUT_IMAGE=$(generate_output_filename "${INPUT_IMAGE}" "_${RESOLUTION}")
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}"
fi fi
# Perform the conversion # Perform the conversion

View File

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

View File

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

View File

@ -6,43 +6,13 @@ set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh # shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../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 # Re-run with sudo if needed for reading /etc/hosts
if [[ $EUID -ne 0 ]] && [[ ! -r /etc/hosts ]]; then require_hosts_readable "$@"
exec sudo -E bash "$0" "$@"
fi
WORK_DIR="${HOME}/.cache/android-adblock" WORK_DIR="$ANDROID_WORK_DIR"
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"
}
install_adaway() { install_adaway() {
print_header "Installing AdAway" print_header "Installing AdAway"

View File

@ -2,6 +2,11 @@
set -euo pipefail 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 ----------------------------------------------------------------- # Configuration -----------------------------------------------------------------
TARGET_SESSION_NAME="Xfce Session" TARGET_SESSION_NAME="Xfce Session"
TARGET_PACKAGES=( TARGET_PACKAGES=(
@ -20,46 +25,19 @@ error() {
exit 1 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() { 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 if ! grep -qi "arch" /etc/os-release 2>/dev/null; then
warn "This script was designed for Arch Linux; continuing anyway." warn "This script was designed for Arch Linux; continuing anyway."
fi fi
} }
install_packages() { install_packages() {
local missing=() install_missing_pacman_packages "${TARGET_PACKAGES[@]}"
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[@]}"
} }
print_post_install_tips() { print_post_install_tips() {
cat << EOF cat <<EOF
------------------------------------------------------------------------ ------------------------------------------------------------------------
XFCE session installed. XFCE session installed.
@ -78,13 +56,13 @@ EOF
logout_user() { logout_user() {
local session_id="${XDG_SESSION_ID:-}" local session_id="${XDG_SESSION_ID:-}"
if [[ -n $session_id ]] && loginctl show-session "$session_id" > /dev/null 2>&1; then if [[ -n $session_id ]] && loginctl show-session "$session_id" >/dev/null 2>&1; then
info "Terminating current session (ID: $session_id) via loginctl." info "Terminating current session (ID: $session_id) via loginctl."
loginctl terminate-session "$session_id" loginctl terminate-session "$session_id"
return return
fi fi
if loginctl list-sessions 2> /dev/null | awk '{print $1" "$3}' | grep -q " $USER$"; then if loginctl list-sessions 2>/dev/null | awk '{print $1" "$3}' | grep -q " $USER$"; then
info "Terminating all sessions for user '$USER' via loginctl." info "Terminating all sessions for user '$USER' via loginctl."
loginctl terminate-user "$USER" loginctl terminate-user "$USER"
return return

View File

@ -6,12 +6,17 @@
set -euo pipefail 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
DEFAULT_RESOLUTION="320x240" DEFAULT_RESOLUTION="320x240"
# Function to display usage # Function to display usage
usage() { usage() {
cat << EOF cat <<EOF
Usage: $0 <input_text_file> [resolution] [output_prefix] Usage: $0 <input_text_file> [resolution] [output_prefix]
Arguments: Arguments:
@ -30,17 +35,7 @@ EOF
} }
# Check if ImageMagick is installed and determine which command to use # Check if ImageMagick is installed and determine which command to use
if command -v magick &> /dev/null; then require_imagemagick || exit 1
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
# Parse arguments # Parse arguments
if [[ $# -lt 1 ]]; then if [[ $# -lt 1 ]]; then
@ -59,7 +54,7 @@ if [[ ! -f ${INPUT_FILE} ]]; then
fi fi
# Validate resolution format (WIDTHxHEIGHT) # Validate resolution format (WIDTHxHEIGHT)
if [[ ! ${RESOLUTION} =~ ^[0-9]+x[0-9]+$ ]]; then if ! validate_resolution "$RESOLUTION"; then
echo "Error: Invalid resolution format '${RESOLUTION}'" echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)" echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1 exit 1
@ -96,7 +91,7 @@ echo "Font size: ${FONT_SIZE}"
echo "Estimated lines per image: ${LINES_PER_IMAGE}" echo "Estimated lines per image: ${LINES_PER_IMAGE}"
# Read the file and count total lines # Read the file and count total lines
mapfile -t LINES < "${INPUT_FILE}" mapfile -t LINES <"${INPUT_FILE}"
TOTAL_LINES=${#LINES[@]} TOTAL_LINES=${#LINES[@]}
echo "Total lines in file: ${TOTAL_LINES}" echo "Total lines in file: ${TOTAL_LINES}"
@ -124,7 +119,7 @@ for ((i = 0; i < TOTAL_LINES; i += LINES_PER_IMAGE)); do
# Create chunk file # Create chunk file
CHUNK_FILE="${TEMP_DIR}/chunk_${IMAGE_COUNT}.txt" CHUNK_FILE="${TEMP_DIR}/chunk_${IMAGE_COUNT}.txt"
for ((j = i; j < END_LINE; j++)); do for ((j = i; j < END_LINE; j++)); do
echo "${LINES[$j]}" >> "${CHUNK_FILE}" echo "${LINES[$j]}" >>"${CHUNK_FILE}"
done done
# Determine output filename # Determine output filename

View File

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