refactor: reduce code duplication from 1.97% to 0.76%

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

View File

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

View File

@ -5,11 +5,11 @@ set -e
# Function to play a sound on error
play_error_sound() {
#pactl set-sink-volume @DEFAULT_SINK@ +50%
for _ in 1 2 3; do
paplay /usr/share/sounds/freedesktop/stereo/dialog-error.oga
done
#pactl set-sink-volume @DEFAULT_SINK@ -50%
#pactl set-sink-volume @DEFAULT_SINK@ +50%
for _ in 1 2 3; do
paplay /usr/share/sounds/freedesktop/stereo/dialog-error.oga
done
#pactl set-sink-volume @DEFAULT_SINK@ -50%
}
# Trap errors and call the play_error_sound function
@ -20,105 +20,108 @@ git config --global init.defaultBranch main
# GPU detection (now split vendor-specific logic)
if [ -f "./detect_gpu.sh" ]; then
# shellcheck source=./detect_gpu.sh disable=SC1091
. ./detect_gpu.sh
# shellcheck source=./detect_gpu.sh disable=SC1091
. ./detect_gpu.sh
elif [ -f "./detect_gpu_and_install.sh" ]; then
# shellcheck source=./detect_gpu_and_install.sh disable=SC1091
. ./detect_gpu_and_install.sh
# shellcheck source=./detect_gpu_and_install.sh disable=SC1091
. ./detect_gpu_and_install.sh
else
echo "GPU detection scripts not found; continuing without GPU specific installation."
echo "GPU detection scripts not found; continuing without GPU specific installation."
fi
install_from_aur() {
local repo_url pkg_name repo_dir
repo_url="$1"
pkg_name="$2"
local repo_url pkg_name repo_dir
repo_url="$1"
pkg_name="$2"
mkdir -p "$HOME/aur"
cd "$HOME/aur" || return 1
repo_dir="$(basename "$repo_url" .git)"
mkdir -p "$HOME/aur"
cd "$HOME/aur" || return 1
repo_dir="$(basename "$repo_url" .git)"
if [ ! -d "$repo_dir" ]; then
git clone "$repo_url"
else
echo "Repository $repo_dir already cloned; updating"
(cd "$repo_dir" && git fetch --all -q && git reset --hard origin/HEAD -q || git pull --ff-only || true)
fi
cd "$repo_dir" || return 1
if [ ! -d "$repo_dir" ]; then
git clone "$repo_url"
else
echo "Repository $repo_dir already cloned; updating"
(cd "$repo_dir" && git fetch --all -q && git reset --hard origin/HEAD -q || git pull --ff-only || true)
fi
cd "$repo_dir" || return 1
if pacman -Qi "$pkg_name" > /dev/null 2>&1; then
echo "$pkg_name is already installed"
return 0
fi
if pacman -Qi "$pkg_name" >/dev/null 2>&1; then
echo "$pkg_name is already installed"
return 0
fi
echo "Cleaning old package artifacts to avoid duplicate -U targets"
find . -maxdepth 1 -type f -name '*.pkg.tar.*' -delete 2> /dev/null || true
echo "Cleaning old package artifacts to avoid duplicate -U targets"
find . -maxdepth 1 -type f -name '*.pkg.tar.*' -delete 2>/dev/null || true
echo "Building $pkg_name (clean build)"
# -c (clean up work dirs after) -C (clean build - remove src/ and pkg/ first)
if ! yes | makepkg -s -c -C --noconfirm --nocheck --skipchecksums --skipinteg --skippgpcheck --needed; then
echo "Build failed for $pkg_name" >&2
return 1
fi
echo "Building $pkg_name (clean build)"
# -c (clean up work dirs after) -C (clean build - remove src/ and pkg/ first)
if ! yes | makepkg -s -c -C --noconfirm --nocheck --skipchecksums --skipinteg --skippgpcheck --needed; then
echo "Build failed for $pkg_name" >&2
return 1
fi
# Collect only the freshly built packages (should now be only current version)
mapfile -t built_pkgs < <(find . -maxdepth 1 -type f -name '*.pkg.tar.zst' -printf './%f\n')
if [ ${#built_pkgs[@]} -eq 0 ]; then
echo "No package files produced for $pkg_name" >&2
return 1
fi
# Collect only the freshly built packages (should now be only current version)
mapfile -t built_pkgs < <(find . -maxdepth 1 -type f -name '*.pkg.tar.zst' -printf './%f\n')
if [ ${#built_pkgs[@]} -eq 0 ]; then
echo "No package files produced for $pkg_name" >&2
return 1
fi
echo "Installing built package(s): ${built_pkgs[*]}"
if ! yes | sudo pacman -U --noconfirm "${built_pkgs[@]}"; then
echo "Installation failed for $pkg_name" >&2
return 1
fi
echo "Installing built package(s): ${built_pkgs[*]}"
if ! yes | sudo pacman -U --noconfirm "${built_pkgs[@]}"; then
echo "Installation failed for $pkg_name" >&2
return 1
fi
}
# Helper: try to install from AUR and log result to done.txt/failed.txt
try_aur_install() {
local repo_url="$1"
local pkg_name="$2"
if install_from_aur "$repo_url" "$pkg_name"; then
echo "$pkg_name" >>done.txt
else
echo "$pkg_name" >>failed.txt
fi
}
process_packages() {
local file_path
file_path="$1"
: > failed.txt
: > done.txt
local file_path
file_path="$1"
: >failed.txt
: >done.txt
while IFS= read -r pkg_name; do
if [ -z "$pkg_name" ]; then
continue
fi
while IFS= read -r pkg_name; do
if [ -z "$pkg_name" ]; then
continue
fi
local repo_url repo_dir
repo_url="https://aur.archlinux.org/${pkg_name}-git.git"
repo_dir="${pkg_name}-git"
local repo_url repo_dir
repo_url="https://aur.archlinux.org/${pkg_name}-git.git"
repo_dir="${pkg_name}-git"
git clone "$repo_url"
if [ -d "$repo_dir" ] && [ -z "$(ls -A "$repo_dir")" ]; then
echo "Repository $repo_dir is empty, trying without -git suffix"
repo_url="https://aur.archlinux.org/${pkg_name}.git"
repo_dir="${pkg_name}"
git clone "$repo_url"
if [ -d "$repo_dir" ] && [ -z "$(ls -A "$repo_dir")" ]; then
echo "Repository $repo_dir is empty, trying without -git suffix"
repo_url="https://aur.archlinux.org/${pkg_name}.git"
repo_dir="${pkg_name}"
git clone "$repo_url"
if [ -d "$repo_dir" ] && [ -z "$(ls -A "$repo_dir")" ]; then
echo "Repository $repo_dir is empty, trying to install with pacman"
if sudo pacman -Sy --noconfirm "$pkg_name"; then
echo "$pkg_name" >> done.txt
else
echo "$pkg_name" >> failed.txt
fi
else
if install_from_aur "$repo_url" "$pkg_name"; then
echo "$pkg_name" >> done.txt
else
echo "$pkg_name" >> failed.txt
fi
fi
else
if install_from_aur "$repo_url" "$pkg_name"; then
echo "$pkg_name" >> done.txt
else
echo "$pkg_name" >> failed.txt
fi
fi
done < "$file_path"
git clone "$repo_url"
if [ -d "$repo_dir" ] && [ -z "$(ls -A "$repo_dir")" ]; then
echo "Repository $repo_dir is empty, trying to install with pacman"
if sudo pacman -Sy --noconfirm "$pkg_name"; then
echo "$pkg_name" >>done.txt
else
echo "$pkg_name" >>failed.txt
fi
else
try_aur_install "$repo_url" "$pkg_name"
fi
else
try_aur_install "$repo_url" "$pkg_name"
fi
done <"$file_path"
}
sudo cp /etc/makepkg.conf /etc/makepkg.conf.bak
@ -129,161 +132,161 @@ sudo cp ./pacman.conf /etc/pacman.conf
# sudo cp ./mkinitcpio.conf /etc/mkinitcpio.conf
# mkinitcpio -P
# Reflector install / service management (idempotent & resilient)
if pacman -Qi reflector > /dev/null 2>&1; then
echo "reflector already installed"
if pacman -Qi reflector >/dev/null 2>&1; then
echo "reflector already installed"
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
# Prefer timer over service (Arch default)
if systemctl list-unit-files | grep -q '^reflector.timer'; then
if systemctl is-enabled reflector.timer > /dev/null 2>&1; then
echo "reflector.timer already enabled"
else
sudo systemctl enable reflector.timer || echo "Warning: could not enable reflector.timer"
fi
if systemctl is-active reflector.timer > /dev/null 2>&1; then
echo "reflector.timer already active"
else
if ! sudo systemctl start reflector.timer; then
echo "Warning: failed to start reflector.timer (check: systemctl status reflector.timer; journalctl -xeu reflector.timer)"
fi
fi
if systemctl is-enabled reflector.timer >/dev/null 2>&1; then
echo "reflector.timer already enabled"
else
sudo systemctl enable reflector.timer || echo "Warning: could not enable reflector.timer"
fi
if systemctl is-active reflector.timer >/dev/null 2>&1; then
echo "reflector.timer already active"
else
if ! sudo systemctl start reflector.timer; then
echo "Warning: failed to start reflector.timer (check: systemctl status reflector.timer; journalctl -xeu reflector.timer)"
fi
fi
elif systemctl list-unit-files | grep -q '^reflector.service'; then
if systemctl is-enabled reflector.service > /dev/null 2>&1; then
echo "reflector.service already enabled"
else
sudo systemctl enable reflector.service || echo "Warning: could not enable reflector.service"
fi
if systemctl is-active reflector.service > /dev/null 2>&1; then
echo "reflector.service already running"
else
if ! sudo systemctl start reflector.service; then
echo "Warning: failed to start reflector.service (check: systemctl status reflector.service; journalctl -xeu reflector.service)"
fi
fi
if systemctl is-enabled reflector.service >/dev/null 2>&1; then
echo "reflector.service already enabled"
else
sudo systemctl enable reflector.service || echo "Warning: could not enable reflector.service"
fi
if systemctl is-active reflector.service >/dev/null 2>&1; then
echo "reflector.service already running"
else
if ! sudo systemctl start reflector.service; then
echo "Warning: failed to start reflector.service (check: systemctl status reflector.service; journalctl -xeu reflector.service)"
fi
fi
else
echo "reflector systemd unit not found (neither timer nor service)"
echo "reflector systemd unit not found (neither timer nor service)"
fi
# Read AUR packages from file (needed before pacman processing)
declare -a aur_packages=()
declare -a aur_package_names=()
while IFS= read -r line; do
if [[ -n $line && $line =~ ^[a-z0-9] ]]; then
aur_packages+=("$line")
aur_package_names+=("${line%% *}")
fi
done < "aur_packages.txt"
if [[ -n $line && $line =~ ^[a-z0-9] ]]; then
aur_packages+=("$line")
aur_package_names+=("${line%% *}")
fi
done <"aur_packages.txt"
# Helper: Check if all subpackages are installed
# Returns 0 if ALL subpackages are installed, 1 otherwise
all_subpackages_installed() {
local -n sub_pkgs_ref=$1
for subpkg in "${sub_pkgs_ref[@]}"; do
if ! pacman -Qi "$subpkg" &>/dev/null; then
return 1
fi
done
return 0
}
# Read pacman packages from file
declare -a pacman_packages
while IFS= read -r line; do
# Skip empty lines and comments (lines not starting with alphanumeric characters)
if [[ -n $line && $line =~ ^[a-z0-9] ]]; then
pacman_packages+=("$line")
fi
done < "pacman_packages.txt"
# Skip empty lines and comments (lines not starting with alphanumeric characters)
if [[ -n $line && $line =~ ^[a-z0-9] ]]; then
pacman_packages+=("$line")
fi
done <"pacman_packages.txt"
for pkg in "${pacman_packages[@]}"; do
# Skip NVIDIA packages if GPU is not NVIDIA
if [ "$GPU_VENDOR" != "nvidia" ] && { [ "$pkg" = "nvidia" ] || [ "$pkg" = "nvidia-utils" ] || [ "$pkg" = "lib32-nvidia-utils" ]; }; then
echo "Skipping $pkg (GPU vendor: $GPU_VENDOR)"
continue
fi
# Check for texlive subpackages
if [ "$pkg" == "texlive" ]; then
sub_pkgs=(
texlive-basic texlive-bibtexextra texlive-binextra texlive-context texlive-fontsextra
texlive-fontsrecommended texlive-fontutils texlive-formatsextra texlive-games texlive-humanities
texlive-latex texlive-latexextra texlive-latexrecommended texlive-luatex texlive-mathscience
texlive-metapost texlive-music texlive-pictures texlive-plaingeneric texlive-pstricks
texlive-publishers texlive-xetex
)
all_installed=true
for subpkg in "${sub_pkgs[@]}"; do
if ! pacman -Qi "$subpkg" &> /dev/null; then
all_installed=false
break
fi
done
if [ "$all_installed" = true ]; then
echo "All texlive subpackages are installed, skipping texlive"
continue
fi
fi
# Skip NVIDIA packages if GPU is not NVIDIA
if [ "$GPU_VENDOR" != "nvidia" ] && { [ "$pkg" = "nvidia" ] || [ "$pkg" = "nvidia-utils" ] || [ "$pkg" = "lib32-nvidia-utils" ]; }; then
echo "Skipping $pkg (GPU vendor: $GPU_VENDOR)"
continue
fi
# Check for texlive subpackages
if [ "$pkg" == "texlive" ]; then
# shellcheck disable=SC2034 # Used via nameref in all_subpackages_installed
texlive_sub_pkgs=(
texlive-basic texlive-bibtexextra texlive-binextra texlive-context texlive-fontsextra
texlive-fontsrecommended texlive-fontutils texlive-formatsextra texlive-games texlive-humanities
texlive-latex texlive-latexextra texlive-latexrecommended texlive-luatex texlive-mathscience
texlive-metapost texlive-music texlive-pictures texlive-plaingeneric texlive-pstricks
texlive-publishers texlive-xetex
)
if all_subpackages_installed texlive_sub_pkgs; then
echo "All texlive subpackages are installed, skipping texlive"
continue
fi
fi
# Check for texlive-lang subpackages
if [ "$pkg" == "texlive-lang" ]; then
sub_pkgs=(
texlive-langarabic texlive-langchinese texlive-langcjk texlive-langcyrillic
texlive-langczechslovak texlive-langenglish texlive-langeuropean texlive-langfrench
texlive-langgerman texlive-langgreek texlive-langitalian texlive-langjapanese
texlive-langkorean texlive-langother texlive-langpolish texlive-langportuguese
texlive-langspanish
)
all_installed=true
for subpkg in "${sub_pkgs[@]}"; do
if ! pacman -Qi "$subpkg" &> /dev/null; then
all_installed=false
break
fi
done
if [ "$all_installed" = true ]; then
echo "All texlive-lang subpackages are installed, skipping texlive-lang"
continue
fi
fi
# Check for texlive-lang subpackages
if [ "$pkg" == "texlive-lang" ]; then
# shellcheck disable=SC2034 # Used via nameref in all_subpackages_installed
texlive_lang_sub_pkgs=(
texlive-langarabic texlive-langchinese texlive-langcjk texlive-langcyrillic
texlive-langczechslovak texlive-langenglish texlive-langeuropean texlive-langfrench
texlive-langgerman texlive-langgreek texlive-langitalian texlive-langjapanese
texlive-langkorean texlive-langother texlive-langpolish texlive-langportuguese
texlive-langspanish
)
if all_subpackages_installed texlive_lang_sub_pkgs; then
echo "All texlive-lang subpackages are installed, skipping texlive-lang"
continue
fi
fi
if ! pacman -Qi "$pkg" &> /dev/null; then
if ! printf '%s
if ! pacman -Qi "$pkg" &>/dev/null; then
if ! printf '%s
' "${aur_package_names[@]}" | grep -Fxq "$pkg"; then
yes | sudo pacman -Sy --noconfirm "$pkg"
else
echo "$pkg exists in AUR packages, skipping pacman installation"
fi
else
echo "$pkg is already installed"
fi
yes | sudo pacman -Sy --noconfirm "$pkg"
else
echo "$pkg exists in AUR packages, skipping pacman installation"
fi
else
echo "$pkg is already installed"
fi
done
if ! command -v nvm &> /dev/null; then
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
if ! command -v nvm &>/dev/null; then
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
else
echo "nvm is already installed"
echo "nvm is already installed"
fi
export NVM_DIR="$HOME/.nvm"
if [ -s "$NVM_DIR/nvm.sh" ]; then
# shellcheck source=/dev/null
. "$NVM_DIR/nvm.sh"
# shellcheck source=/dev/null
. "$NVM_DIR/nvm.sh"
else
echo "nvm.sh not found at $NVM_DIR/nvm.sh" >&2
echo "nvm.sh not found at $NVM_DIR/nvm.sh" >&2
fi
if command -v nvm &> /dev/null; then
nvm i v18.20.5
nvm install --lts
if command -v nvm &>/dev/null; then
nvm i v18.20.5
nvm install --lts
else
echo "nvm command unavailable; skipping Node installation" >&2
echo "nvm command unavailable; skipping Node installation" >&2
fi
sudo systemctl enable bluetooth.service
sudo systemctl start bluetooth.service
for entry in "${aur_packages[@]}"; do
pkg_name=${entry%% *}
repo_url=${entry#* }
if [ "$repo_url" = "$pkg_name" ] || [ -z "$repo_url" ]; then
repo_url="https://aur.archlinux.org/${pkg_name}.git"
fi
install_from_aur "$repo_url" "$pkg_name"
pkg_name=${entry%% *}
repo_url=${entry#* }
if [ "$repo_url" = "$pkg_name" ] || [ -z "$repo_url" ]; then
repo_url="https://aur.archlinux.org/${pkg_name}.git"
fi
install_from_aur "$repo_url" "$pkg_name"
done
cd ~/linux-configuration/fresh-install
if [ ! -d "$HOME/.config/mpv" ]; then
mkdir -p "$HOME/.config/mpv"
mkdir -p "$HOME/.config/mpv"
fi
cp mpv.conf "$HOME/.config/mpv/mpv.conf"
if [ ! -d "$HOME/.oh-my-zsh" ]; then
yes | sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
yes | sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
else
echo "Oh My Zsh is already installed"
echo "Oh My Zsh is already installed"
fi
cd ~/linux-configuration

View File

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

View File

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

View File

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

View File

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

1793
report/jscpd-report.json Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,208 +5,206 @@
set -e # Exit on any error
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Function to check and request sudo privileges for package installation
check_sudo() {
if [[ $EUID -ne 0 ]] && [[ $1 == "install" ]]; then
echo "Package installation requires sudo privileges."
echo "Requesting sudo access..."
exec sudo "$0" "$@"
fi
if [[ $EUID -ne 0 ]] && [[ $1 == "install" ]]; then
echo "Package installation requires sudo privileges."
echo "Requesting sudo access..."
exec sudo "$0" "$@"
fi
}
# Get the actual user (even when running with sudo)
set_actual_user_vars
echo "ActivityWatch Setup for Arch Linux + i3"
echo "======================================="
echo "Current Date: $(date)"
echo "User: ${SUDO_USER:-$USER}"
# Get the actual user (even when running with sudo)
if [[ -n $SUDO_USER ]]; then
ACTUAL_USER="$SUDO_USER"
USER_HOME="/home/$SUDO_USER"
else
ACTUAL_USER="$USER"
USER_HOME="$HOME"
fi
echo "User: $ACTUAL_USER"
echo "Target user: $ACTUAL_USER"
echo "User home: $USER_HOME"
# Function to check if ActivityWatch is installed
check_activitywatch_installed() {
echo ""
echo "1. Checking ActivityWatch Installation..."
echo "========================================"
echo ""
echo "1. Checking ActivityWatch Installation..."
echo "========================================"
# Check if activitywatch-bin is installed via pacman
if pacman -Qi activitywatch-bin &> /dev/null; then
echo "✓ activitywatch-bin package is installed"
return 0
fi
# Check if activitywatch-bin is installed via pacman
if pacman -Qi activitywatch-bin &>/dev/null; then
echo "✓ activitywatch-bin package is installed"
return 0
fi
# Check if aw-qt binary exists in common locations
local common_paths=(
"/usr/bin/aw-qt"
"/usr/local/bin/aw-qt"
"$USER_HOME/.local/bin/aw-qt"
"$USER_HOME/activitywatch/aw-qt"
)
# Check if aw-qt binary exists in common locations
local common_paths=(
"/usr/bin/aw-qt"
"/usr/local/bin/aw-qt"
"$USER_HOME/.local/bin/aw-qt"
"$USER_HOME/activitywatch/aw-qt"
)
for path in "${common_paths[@]}"; do
if [[ -x $path ]]; then
echo "✓ ActivityWatch found at: $path"
return 0
fi
done
for path in "${common_paths[@]}"; do
if [[ -x $path ]]; then
echo "✓ ActivityWatch found at: $path"
return 0
fi
done
echo "✗ ActivityWatch not found"
return 1
echo "✗ ActivityWatch not found"
return 1
}
# Function to install ActivityWatch
install_activitywatch() {
echo ""
echo "2. Installing ActivityWatch..."
echo "============================="
echo ""
echo "2. Installing ActivityWatch..."
echo "============================="
# Check if we need sudo for installation
check_sudo "install"
# Check if we need sudo for installation
check_sudo "install"
echo "Installing activitywatch-bin from AUR..."
echo "Installing activitywatch-bin from AUR..."
# Check if an AUR helper is available
local aur_helpers=("yay" "paru" "makepkg")
local helper_found=""
# Check if an AUR helper is available
local aur_helpers=("yay" "paru" "makepkg")
local helper_found=""
for helper in "${aur_helpers[@]}"; do
if command -v "$helper" &> /dev/null; then
helper_found="$helper"
break
fi
done
for helper in "${aur_helpers[@]}"; do
if command -v "$helper" &>/dev/null; then
helper_found="$helper"
break
fi
done
if [[ -n $helper_found && $helper_found != "makepkg" ]]; then
echo "Using AUR helper: $helper_found"
if [[ $EUID -eq 0 ]]; then
# Running as root, need to install as user
sudo -u "$ACTUAL_USER" "$helper_found" -S --noconfirm activitywatch-bin
else
"$helper_found" -S --noconfirm activitywatch-bin
fi
else
echo "No AUR helper found. Installing manually with makepkg..."
install_activitywatch_manual
fi
if [[ -n $helper_found && $helper_found != "makepkg" ]]; then
echo "Using AUR helper: $helper_found"
if [[ $EUID -eq 0 ]]; then
# Running as root, need to install as user
sudo -u "$ACTUAL_USER" "$helper_found" -S --noconfirm activitywatch-bin
else
"$helper_found" -S --noconfirm activitywatch-bin
fi
else
echo "No AUR helper found. Installing manually with makepkg..."
install_activitywatch_manual
fi
echo "✓ ActivityWatch installation completed"
echo "✓ ActivityWatch installation completed"
}
# Function to manually install ActivityWatch via makepkg
install_activitywatch_manual() {
local temp_dir="/tmp/activitywatch-install"
local original_user="$ACTUAL_USER"
local temp_dir="/tmp/activitywatch-install"
local original_user="$ACTUAL_USER"
# Create temp directory
mkdir -p "$temp_dir"
cd "$temp_dir"
# Create temp directory
mkdir -p "$temp_dir"
cd "$temp_dir"
# Download PKGBUILD
if command -v git &> /dev/null; then
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
else
echo "Installing git..."
pacman -S --noconfirm git
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
fi
# Download PKGBUILD
if command -v git &>/dev/null; then
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
else
echo "Installing git..."
pacman -S --noconfirm git
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
fi
# Build and install package
sudo -u "$original_user" makepkg -si --noconfirm
# Build and install package
sudo -u "$original_user" makepkg -si --noconfirm
# Cleanup
cd /
rm -rf "$temp_dir"
# Cleanup
cd /
rm -rf "$temp_dir"
}
# Function to check if ActivityWatch is running
check_activitywatch_running() {
echo ""
echo "3. Checking ActivityWatch Status..."
echo "=================================="
echo ""
echo "3. Checking ActivityWatch Status..."
echo "=================================="
# Check for aw-qt process
if pgrep -f "aw-qt" > /dev/null; then
echo "✓ ActivityWatch (aw-qt) is running"
return 0
fi
# Check for aw-qt process
if pgrep -f "aw-qt" >/dev/null; then
echo "✓ ActivityWatch (aw-qt) is running"
return 0
fi
# Check for aw-server process
if pgrep -f "aw-server" > /dev/null; then
echo "✓ ActivityWatch server is running"
return 0
fi
# Check for aw-server process
if pgrep -f "aw-server" >/dev/null; then
echo "✓ ActivityWatch server is running"
return 0
fi
echo "✗ ActivityWatch is not running"
return 1
echo "✗ ActivityWatch is not running"
return 1
}
# Function to start ActivityWatch
start_activitywatch() {
echo ""
echo "4. Starting ActivityWatch..."
echo "==========================="
echo ""
echo "4. Starting ActivityWatch..."
echo "==========================="
# Find aw-qt executable
local aw_qt_path=""
# Find aw-qt executable
local aw_qt_path=""
if command -v aw-qt &> /dev/null; then
aw_qt_path="$(which aw-qt)"
elif [[ -x "/usr/bin/aw-qt" ]]; then
aw_qt_path="/usr/bin/aw-qt"
else
echo "✗ Could not find aw-qt executable"
return 1
fi
if command -v aw-qt &>/dev/null; then
aw_qt_path="$(which aw-qt)"
elif [[ -x "/usr/bin/aw-qt" ]]; then
aw_qt_path="/usr/bin/aw-qt"
else
echo "✗ Could not find aw-qt executable"
return 1
fi
echo "Starting ActivityWatch as user: $ACTUAL_USER"
echo "Using aw-qt from: $aw_qt_path"
echo "Starting ActivityWatch as user: $ACTUAL_USER"
echo "Using aw-qt from: $aw_qt_path"
# Start as the actual user in the background
if [[ $EUID -eq 0 ]]; then
# Running as root, start as user
sudo -u "$ACTUAL_USER" env DISPLAY=:0 "$aw_qt_path" &
else
# Running as user
"$aw_qt_path" &
fi
# Start as the actual user in the background
if [[ $EUID -eq 0 ]]; then
# Running as root, start as user
sudo -u "$ACTUAL_USER" env DISPLAY=:0 "$aw_qt_path" &
else
# Running as user
"$aw_qt_path" &
fi
# Give it time to start
sleep 3
# Give it time to start
sleep 3
if check_activitywatch_running > /dev/null 2>&1; then
echo "✓ ActivityWatch started successfully"
else
echo "! ActivityWatch may be starting (check system tray)"
fi
if check_activitywatch_running >/dev/null 2>&1; then
echo "✓ ActivityWatch started successfully"
else
echo "! ActivityWatch may be starting (check system tray)"
fi
}
# Function to setup autostart
setup_autostart() {
echo ""
echo "5. Setting Up Autostart..."
echo "========================="
echo ""
echo "5. Setting Up Autostart..."
echo "========================="
local autostart_dir="$USER_HOME/.config/autostart"
local desktop_file="$autostart_dir/activitywatch.desktop"
local i3_config="$USER_HOME/.config/i3/config"
local autostart_dir="$USER_HOME/.config/autostart"
local desktop_file="$autostart_dir/activitywatch.desktop"
local i3_config="$USER_HOME/.config/i3/config"
# Method 1: XDG Autostart (works with most desktop environments)
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" mkdir -p "$autostart_dir"
else
mkdir -p "$autostart_dir"
fi
# Method 1: XDG Autostart (works with most desktop environments)
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" mkdir -p "$autostart_dir"
else
mkdir -p "$autostart_dir"
fi
# Create desktop file for autostart
cat > "$desktop_file" << EOF
# Create desktop file for autostart
cat >"$desktop_file" <<EOF
[Desktop Entry]
Type=Application
Name=ActivityWatch
@ -221,60 +219,60 @@ Terminal=false
Categories=Utility;
EOF
# Set proper ownership if running as root
if [[ $EUID -eq 0 ]]; then
chown "$ACTUAL_USER:$ACTUAL_USER" "$desktop_file"
fi
# Set proper ownership if running as root
if [[ $EUID -eq 0 ]]; then
chown "$ACTUAL_USER:$ACTUAL_USER" "$desktop_file"
fi
echo "✓ Created XDG autostart entry: $desktop_file"
echo "✓ Created XDG autostart entry: $desktop_file"
# Method 2: i3 config autostart (specific to i3)
if [[ -f $i3_config ]]; then
# Check if autostart entry already exists
if ! grep -q "aw-qt" "$i3_config"; then
# Add autostart entry to i3 config
if [[ $EUID -eq 0 ]]; then
# Running as root
sudo -u "$ACTUAL_USER" bash -c "cat <<'EOF' >> '$i3_config'
# Method 2: i3 config autostart (specific to i3)
if [[ -f $i3_config ]]; then
# Check if autostart entry already exists
if ! grep -q "aw-qt" "$i3_config"; then
# Add autostart entry to i3 config
if [[ $EUID -eq 0 ]]; then
# Running as root
sudo -u "$ACTUAL_USER" bash -c "cat <<'EOF' >> '$i3_config'
# Auto-start ActivityWatch
exec --no-startup-id aw-qt
EOF"
else
{
printf '\n'
printf '# Auto-start ActivityWatch\n'
printf 'exec --no-startup-id aw-qt\n'
} >> "$i3_config"
fi
else
{
printf '\n'
printf '# Auto-start ActivityWatch\n'
printf 'exec --no-startup-id aw-qt\n'
} >>"$i3_config"
fi
echo "✓ Added ActivityWatch to i3 config autostart"
else
echo "✓ ActivityWatch autostart already exists in i3 config"
fi
else
echo "! i3 config not found at $i3_config"
fi
echo "✓ Added ActivityWatch to i3 config autostart"
else
echo "✓ ActivityWatch autostart already exists in i3 config"
fi
else
echo "! i3 config not found at $i3_config"
fi
}
# Function to create i3blocks status script
create_i3blocks_status() {
echo ""
echo "6. Creating i3blocks Status Script..."
echo "===================================="
echo ""
echo "6. Creating i3blocks Status Script..."
echo "===================================="
local i3blocks_dir="$USER_HOME/.config/i3blocks"
local status_script="$i3blocks_dir/activitywatch_status.sh"
local i3blocks_dir="$USER_HOME/.config/i3blocks"
local status_script="$i3blocks_dir/activitywatch_status.sh"
# Create i3blocks directory if it doesn't exist
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" mkdir -p "$i3blocks_dir"
else
mkdir -p "$i3blocks_dir"
fi
# Create i3blocks directory if it doesn't exist
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" mkdir -p "$i3blocks_dir"
else
mkdir -p "$i3blocks_dir"
fi
# Create the status script
cat > "$status_script" << 'EOF'
# Create the status script
cat >"$status_script" <<'EOF'
#!/bin/bash
# ActivityWatch status script for i3blocks
# Shows ActivityWatch installation and running status
@ -325,134 +323,134 @@ else
fi
EOF
chmod +x "$status_script"
chmod +x "$status_script"
# Set proper ownership if running as root
if [[ $EUID -eq 0 ]]; then
chown "$ACTUAL_USER:$ACTUAL_USER" "$status_script"
fi
# Set proper ownership if running as root
if [[ $EUID -eq 0 ]]; then
chown "$ACTUAL_USER:$ACTUAL_USER" "$status_script"
fi
echo "✓ Created i3blocks status script: $status_script"
echo "✓ Created i3blocks status script: $status_script"
# Show configuration instructions
echo ""
echo "To add to your i3blocks config, add this block:"
echo ""
echo "[activitywatch]"
echo "command=~/.config/i3blocks/activitywatch_status.sh"
echo "interval=10"
echo "color=#FFFFFF"
echo ""
# Show configuration instructions
echo ""
echo "To add to your i3blocks config, add this block:"
echo ""
echo "[activitywatch]"
echo "command=~/.config/i3blocks/activitywatch_status.sh"
echo "interval=10"
echo "color=#FFFFFF"
echo ""
}
# Function to test the setup
test_setup() {
echo ""
echo "7. Testing Setup..."
echo "=================="
echo ""
echo "7. Testing Setup..."
echo "=================="
echo "Installation status:"
if check_activitywatch_installed > /dev/null 2>&1; then
echo "✓ ActivityWatch is installed"
else
echo "✗ ActivityWatch is not installed"
fi
echo "Installation status:"
if check_activitywatch_installed >/dev/null 2>&1; then
echo "✓ ActivityWatch is installed"
else
echo "✗ ActivityWatch is not installed"
fi
echo "Running status:"
if check_activitywatch_running > /dev/null 2>&1; then
echo "✓ ActivityWatch is running"
else
echo "✗ ActivityWatch is not running"
fi
echo "Running status:"
if check_activitywatch_running >/dev/null 2>&1; then
echo "✓ ActivityWatch is running"
else
echo "✗ ActivityWatch is not running"
fi
echo "Autostart files:"
if [[ -f "$USER_HOME/.config/autostart/activitywatch.desktop" ]]; then
echo "✓ XDG autostart file exists"
else
echo "✗ XDG autostart file missing"
fi
echo "Autostart files:"
if [[ -f "$USER_HOME/.config/autostart/activitywatch.desktop" ]]; then
echo "✓ XDG autostart file exists"
else
echo "✗ XDG autostart file missing"
fi
if [[ -f "$USER_HOME/.config/i3/config" ]] && grep -q "aw-qt" "$USER_HOME/.config/i3/config"; then
echo "✓ i3 autostart configured"
else
echo "! i3 autostart may not be configured"
fi
if [[ -f "$USER_HOME/.config/i3/config" ]] && grep -q "aw-qt" "$USER_HOME/.config/i3/config"; then
echo "✓ i3 autostart configured"
else
echo "! i3 autostart may not be configured"
fi
echo "i3blocks status script:"
if [[ -x "$USER_HOME/.config/i3blocks/activitywatch_status.sh" ]]; then
echo "✓ i3blocks status script created"
echo "Testing status script:"
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" "$USER_HOME/.config/i3blocks/activitywatch_status.sh"
else
"$USER_HOME/.config/i3blocks/activitywatch_status.sh"
fi
else
echo "✗ i3blocks status script missing"
fi
echo "i3blocks status script:"
if [[ -x "$USER_HOME/.config/i3blocks/activitywatch_status.sh" ]]; then
echo "✓ i3blocks status script created"
echo "Testing status script:"
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" "$USER_HOME/.config/i3blocks/activitywatch_status.sh"
else
"$USER_HOME/.config/i3blocks/activitywatch_status.sh"
fi
else
echo "✗ i3blocks status script missing"
fi
}
# Function to show final instructions
show_instructions() {
echo ""
echo "=========================================="
echo "ActivityWatch Setup Complete"
echo "=========================================="
echo "Summary:"
echo "✓ ActivityWatch installation checked/completed"
echo "✓ ActivityWatch startup configured"
echo "✓ Autostart configured (XDG + i3)"
echo "✓ i3blocks status script created"
echo ""
echo "Next steps:"
echo "1. Add the i3blocks configuration to your config file:"
echo " ~/.config/i3blocks/config"
echo ""
echo "2. Reload i3 configuration:"
echo " Super+Shift+R"
echo ""
echo "3. ActivityWatch web interface should be available at:"
echo " http://localhost:5600"
echo ""
echo "4. Check system tray for ActivityWatch icon"
echo ""
echo "Files created:"
echo " ~/.config/autostart/activitywatch.desktop"
echo " ~/.config/i3blocks/activitywatch_status.sh"
echo " ~/.config/i3/config (modified)"
echo ""
echo ""
echo "=========================================="
echo "ActivityWatch Setup Complete"
echo "=========================================="
echo "Summary:"
echo "✓ ActivityWatch installation checked/completed"
echo "✓ ActivityWatch startup configured"
echo "✓ Autostart configured (XDG + i3)"
echo "✓ i3blocks status script created"
echo ""
echo "Next steps:"
echo "1. Add the i3blocks configuration to your config file:"
echo " ~/.config/i3blocks/config"
echo ""
echo "2. Reload i3 configuration:"
echo " Super+Shift+R"
echo ""
echo "3. ActivityWatch web interface should be available at:"
echo " http://localhost:5600"
echo ""
echo "4. Check system tray for ActivityWatch icon"
echo ""
echo "Files created:"
echo " ~/.config/autostart/activitywatch.desktop"
echo " ~/.config/i3blocks/activitywatch_status.sh"
echo " ~/.config/i3/config (modified)"
echo ""
}
# Main execution flow
main() {
local need_install=false
local need_start=false
local need_install=false
local need_start=false
# Check installation
if ! check_activitywatch_installed; then
need_install=true
fi
# Check installation
if ! check_activitywatch_installed; then
need_install=true
fi
# Install if needed
if [[ $need_install == true ]]; then
install_activitywatch
fi
# Install if needed
if [[ $need_install == true ]]; then
install_activitywatch
fi
# Check if running
if ! check_activitywatch_running; then
need_start=true
fi
# Check if running
if ! check_activitywatch_running; then
need_start=true
fi
# Start if needed
if [[ $need_start == true ]]; then
start_activitywatch
fi
# Start if needed
if [[ $need_start == true ]]; then
start_activitywatch
fi
# Always set up autostart and i3blocks (in case they're missing)
setup_autostart
create_i3blocks_status
test_setup
show_instructions
# Always set up autostart and i3blocks (in case they're missing)
setup_autostart
create_i3blocks_status
test_setup
show_instructions
}
# Run main function

View File

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

View File

@ -2,195 +2,182 @@
set -euo pipefail
# Source common library
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
on_error() {
local exit_code=$?
local line_number=$1
printf '\033[1;31m[ERROR]\033[0m Unexpected failure at line %s (exit code %s).\n' "${line_number}" "${exit_code}" >&2
local exit_code=$?
local line_number=$1
log_error "Unexpected failure at line ${line_number} (exit code ${exit_code})."
}
trap 'on_error ${LINENO}' ERR
log_info() {
printf '\033[1;34m[INFO]\033[0m %s\n' "$*"
}
log_warn() {
printf '\033[1;33m[WARN]\033[0m %s\n' "$*"
}
log_error() {
printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2
exit 1
}
require_root() {
if [[ ${EUID} -ne 0 ]]; then
log_error "This script must be run as root (try again with sudo)."
fi
}
require_pacman() {
if ! command -v pacman > /dev/null 2>&1; then
log_error "pacman not found. This script is intended for Arch Linux systems."
fi
if ! has_cmd pacman; then
log_error "pacman not found. This script is intended for Arch Linux systems."
exit 1
fi
}
detect_kernel_release() {
uname -r
uname -r
}
select_host_package() {
local kernel_release=$1
case "${kernel_release}" in
*-lts)
echo "virtualbox-host-modules-lts"
;;
*-arch*)
echo "virtualbox-host-modules-arch"
;;
*)
echo "virtualbox-host-dkms"
;;
esac
local kernel_release=$1
case "${kernel_release}" in
*-lts)
echo "virtualbox-host-modules-lts"
;;
*-arch*)
echo "virtualbox-host-modules-arch"
;;
*)
echo "virtualbox-host-dkms"
;;
esac
}
collect_kernel_headers() {
local -a headers=()
local kernel_pkg header_pkg
for kernel_pkg in linux linux-lts linux-zen linux-hardened; do
if pacman -Q "${kernel_pkg}" > /dev/null 2>&1; then
header_pkg="${kernel_pkg}-headers"
headers+=("${header_pkg}")
fi
done
if [[ ${#headers[@]} -gt 0 ]]; then
printf '%s\n' "${headers[@]}"
fi
local -a headers=()
local kernel_pkg header_pkg
for kernel_pkg in linux linux-lts linux-zen linux-hardened; do
if pacman -Q "${kernel_pkg}" >/dev/null 2>&1; then
header_pkg="${kernel_pkg}-headers"
headers+=("${header_pkg}")
fi
done
if [[ ${#headers[@]} -gt 0 ]]; then
printf '%s\n' "${headers[@]}"
fi
}
maybe_remove_conflicting_host_packages() {
local selected_package=$1
local -a candidates=("virtualbox-host-dkms" "virtualbox-host-modules-arch" "virtualbox-host-modules-lts")
local pkg
for pkg in "${candidates[@]}"; do
if [[ ${pkg} != "${selected_package}" ]] && pacman -Q "${pkg}" > /dev/null 2>&1; then
log_warn "Removing conflicting package ${pkg} before installing ${selected_package}."
pacman -Rsn "${PACMAN_REMOVE_FLAGS[@]}" "${pkg}"
fi
done
local selected_package=$1
local -a candidates=("virtualbox-host-dkms" "virtualbox-host-modules-arch" "virtualbox-host-modules-lts")
local pkg
for pkg in "${candidates[@]}"; do
if [[ ${pkg} != "${selected_package}" ]] && pacman -Q "${pkg}" >/dev/null 2>&1; then
log_warn "Removing conflicting package ${pkg} before installing ${selected_package}."
pacman -Rsn "${PACMAN_REMOVE_FLAGS[@]}" "${pkg}"
fi
done
}
install_packages() {
local -a packages=()
local -a headers=()
local host_package=$1
shift
if [[ $# -gt 0 ]]; then
mapfile -t headers < <(printf '%s\n' "$@" | sort -u)
fi
packages+=("virtualbox" "virtualbox-guest-iso" "${host_package}")
if [[ ${host_package} == "virtualbox-host-dkms" ]]; then
packages+=("dkms")
fi
if [[ ${#headers[@]} -gt 0 ]]; then
packages+=("${headers[@]}")
fi
log_info "Installing packages: ${packages[*]}"
pacman -S "${PACMAN_INSTALL_FLAGS[@]}" "${packages[@]}"
local -a packages=()
local -a headers=()
local host_package=$1
shift
if [[ $# -gt 0 ]]; then
mapfile -t headers < <(printf '%s\n' "$@" | sort -u)
fi
packages+=("virtualbox" "virtualbox-guest-iso" "${host_package}")
if [[ ${host_package} == "virtualbox-host-dkms" ]]; then
packages+=("dkms")
fi
if [[ ${#headers[@]} -gt 0 ]]; then
packages+=("${headers[@]}")
fi
log_info "Installing packages: ${packages[*]}"
pacman -S "${PACMAN_INSTALL_FLAGS[@]}" "${packages[@]}"
}
rebuild_virtualbox_modules() {
local host_package=$1
if [[ ${host_package} == "virtualbox-host-dkms" ]]; then
if command -v dkms > /dev/null 2>&1; then
log_info "Rebuilding VirtualBox DKMS modules for all installed kernels."
dkms autoinstall
else
log_warn "dkms command not found; skipping DKMS rebuild."
fi
fi
local host_package=$1
if [[ ${host_package} == "virtualbox-host-dkms" ]]; then
if command -v dkms >/dev/null 2>&1; then
log_info "Rebuilding VirtualBox DKMS modules for all installed kernels."
dkms autoinstall
else
log_warn "dkms command not found; skipping DKMS rebuild."
fi
fi
}
reload_virtualbox_modules() {
log_info "Loading VirtualBox kernel modules."
if [[ -x /sbin/rcvboxdrv ]]; then
/sbin/rcvboxdrv setup || log_warn "rcvboxdrv reported an issue while setting up modules."
elif [[ -x /usr/lib/virtualbox/vboxdrv.sh ]]; then
/usr/lib/virtualbox/vboxdrv.sh setup || log_warn "vboxdrv.sh reported an issue while setting up modules."
fi
log_info "Loading VirtualBox kernel modules."
if [[ -x /sbin/rcvboxdrv ]]; then
/sbin/rcvboxdrv setup || log_warn "rcvboxdrv reported an issue while setting up modules."
elif [[ -x /usr/lib/virtualbox/vboxdrv.sh ]]; then
/usr/lib/virtualbox/vboxdrv.sh setup || log_warn "vboxdrv.sh reported an issue while setting up modules."
fi
local -a modules=(vboxdrv vboxnetflt vboxnetadp vboxpci)
local mod
for mod in "${modules[@]}"; do
if ! lsmod | awk '{print $1}' | grep -Fxq "${mod}"; then
if ! modprobe "${mod}" > /dev/null 2>&1; then
log_warn "Module ${mod} failed to load; check dmesg for details."
fi
fi
done
local -a modules=(vboxdrv vboxnetflt vboxnetadp vboxpci)
local mod
for mod in "${modules[@]}"; do
if ! lsmod | awk '{print $1}' | grep -Fxq "${mod}"; then
if ! modprobe "${mod}" >/dev/null 2>&1; then
log_warn "Module ${mod} failed to load; check dmesg for details."
fi
fi
done
if ! lsmod | awk '{print $1}' | grep -Fxq "vboxdrv"; then
log_error "VirtualBox kernel driver (vboxdrv) failed to load. Review /var/log and dmesg output for clues."
fi
log_info "VirtualBox kernel driver loaded successfully."
if ! lsmod | awk '{print $1}' | grep -Fxq "vboxdrv"; then
log_error "VirtualBox kernel driver (vboxdrv) failed to load. Review /var/log and dmesg output for clues."
fi
log_info "VirtualBox kernel driver loaded successfully."
}
warn_if_secure_boot_enabled() {
local secure_boot_file
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)
if [[ -n ${secure_boot_file} && -r ${secure_boot_file} ]]; then
local state
state=$(hexdump -n 1 -s 4 -e '1 "%d"' "${secure_boot_file}" 2> /dev/null || echo "0")
if [[ ${state} == "1" ]]; then
log_warn "EFI Secure Boot appears to be enabled. You may need to sign VirtualBox modules manually."
fi
fi
fi
local secure_boot_file
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)
if [[ -n ${secure_boot_file} && -r ${secure_boot_file} ]]; then
local state
state=$(hexdump -n 1 -s 4 -e '1 "%d"' "${secure_boot_file}" 2>/dev/null || echo "0")
if [[ ${state} == "1" ]]; then
log_warn "EFI Secure Boot appears to be enabled. You may need to sign VirtualBox modules manually."
fi
fi
fi
}
remind_group_membership() {
local invoking_user=${SUDO_USER:-}
if [[ -n ${invoking_user} && ${invoking_user} != "root" ]]; then
if ! id -nG "${invoking_user}" | grep -qw "vboxusers"; then
log_warn "User ${invoking_user} is not in the vboxusers group. Add them with: sudo gpasswd -a ${invoking_user} vboxusers"
else
log_info "User ${invoking_user} is already in the vboxusers group."
fi
fi
local invoking_user=${SUDO_USER:-}
if [[ -n ${invoking_user} && ${invoking_user} != "root" ]]; then
if ! id -nG "${invoking_user}" | grep -qw "vboxusers"; then
log_warn "User ${invoking_user} is not in the vboxusers group. Add them with: sudo gpasswd -a ${invoking_user} vboxusers"
else
log_info "User ${invoking_user} is already in the vboxusers group."
fi
fi
}
main() {
require_root
require_pacman
require_root
require_pacman
PACMAN_INSTALL_FLAGS=(--needed)
PACMAN_REMOVE_FLAGS=()
if [[ ${PACMAN_CONFIRM:-0} == "1" ]]; then
log_info "PACMAN_CONFIRM=1 detected; pacman will prompt for confirmation."
else
PACMAN_INSTALL_FLAGS+=(--noconfirm)
PACMAN_REMOVE_FLAGS+=(--noconfirm)
fi
PACMAN_INSTALL_FLAGS=(--needed)
PACMAN_REMOVE_FLAGS=()
if [[ ${PACMAN_CONFIRM:-0} == "1" ]]; then
log_info "PACMAN_CONFIRM=1 detected; pacman will prompt for confirmation."
else
PACMAN_INSTALL_FLAGS+=(--noconfirm)
PACMAN_REMOVE_FLAGS+=(--noconfirm)
fi
local kernel_release host_package
kernel_release=$(detect_kernel_release)
log_info "Detected running kernel: ${kernel_release}"
host_package=$(select_host_package "${kernel_release}")
log_info "Selected VirtualBox host package: ${host_package}"
local kernel_release host_package
kernel_release=$(detect_kernel_release)
log_info "Detected running kernel: ${kernel_release}"
host_package=$(select_host_package "${kernel_release}")
log_info "Selected VirtualBox host package: ${host_package}"
mapfile -t kernel_headers < <(collect_kernel_headers)
if [[ ${host_package} == "virtualbox-host-dkms" && ${#kernel_headers[@]} -eq 0 ]]; then
log_warn "No matching kernel headers detected. Ensure you've installed headers for your kernel so DKMS can build modules."
fi
mapfile -t kernel_headers < <(collect_kernel_headers)
if [[ ${host_package} == "virtualbox-host-dkms" && ${#kernel_headers[@]} -eq 0 ]]; then
log_warn "No matching kernel headers detected. Ensure you've installed headers for your kernel so DKMS can build modules."
fi
maybe_remove_conflicting_host_packages "${host_package}"
install_packages "${host_package}" "${kernel_headers[@]}"
rebuild_virtualbox_modules "${host_package}"
reload_virtualbox_modules
warn_if_secure_boot_enabled
remind_group_membership
maybe_remove_conflicting_host_packages "${host_package}"
install_packages "${host_package}" "${kernel_headers[@]}"
rebuild_virtualbox_modules "${host_package}"
reload_virtualbox_modules
warn_if_secure_boot_enabled
remind_group_membership
log_info "VirtualBox installation and driver setup complete."
log_info "VirtualBox installation and driver setup complete."
}
main "$@"

View File

@ -17,16 +17,7 @@ shift "$COMMON_ARGS_SHIFT"
# Check for sudo privileges
require_root "$@"
echo "NVIDIA Comprehensive Troubleshooter & GSP Disabler"
echo "=================================================="
echo "Current Date: $(date)"
echo "User: $USER"
echo "Original user: $(get_actual_user)"
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Mode: Interactive (prompts enabled)"
else
echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
fi
print_setup_header "NVIDIA Comprehensive Troubleshooter & GSP Disabler"
# Check if nvidia module is loaded
if ! lsmod | grep -q nvidia; then

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

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

View File

@ -21,7 +21,7 @@ log_message() {
local formatted
formatted="$(date '+%Y-%m-%d %H:%M:%S') - $msg"
echo "$formatted" >&2
if [[ -n "$log_file" ]]; then
if [[ -n $log_file ]]; then
echo "$formatted" >>"$log_file" 2>/dev/null || true
fi
}
@ -57,13 +57,23 @@ get_actual_user() {
get_actual_user_home() {
local user
user=$(get_actual_user)
if [[ "$user" == "root" ]]; then
if [[ $user == "root" ]]; then
echo "/root"
else
echo "/home/$user"
fi
}
# Set both ACTUAL_USER and USER_HOME variables (common pattern)
# Usage: set_actual_user_vars
# echo "$ACTUAL_USER" # => the actual user
# echo "$USER_HOME" # => /home/username
set_actual_user_vars() {
ACTUAL_USER=$(get_actual_user)
USER_HOME=$(get_actual_user_home)
export ACTUAL_USER USER_HOME
}
# =============================================================================
# ARGUMENT PARSING HELPERS
# =============================================================================
@ -102,6 +112,32 @@ parse_interactive_args() {
done
}
# Handle common argument patterns for scripts with custom usage functions
# Usage: handle_arg_help_or_unknown "$1" usage_function err_function
# Returns: 0 if argument was handled (caller should continue), 1 if not our concern
# Exits: on -h/--help (exit 0) or unknown arg starting with - (exit 2)
handle_arg_help_or_unknown() {
local arg="$1"
local usage_fn="${2:-usage}"
local err_fn="${3:-err}"
case "$arg" in
-h | --help)
"$usage_fn"
exit 0
;;
-*)
"$err_fn" "Unknown argument: $arg"
"$usage_fn"
exit 2
;;
*)
return 1 # Not a flag, let caller handle it
;;
esac
return 0
}
# =============================================================================
# FOCUS APP DETECTION (for digital wellbeing scripts)
# =============================================================================
@ -172,6 +208,61 @@ require_command() {
return 0
}
# Check for ImageMagick and display helpful installation message
# Usage: require_imagemagick [optional: "magick" or "convert"]
# Returns: Sets MAGICK_CMD variable to available command
require_imagemagick() {
local preferred="${1:-}"
if [[ $preferred == "magick" ]] || [[ -z $preferred ]]; then
if command -v magick &>/dev/null; then
MAGICK_CMD="magick"
export MAGICK_CMD
return 0
fi
fi
if [[ $preferred == "convert" ]] || [[ -z $preferred ]]; then
if command -v convert &>/dev/null; then
MAGICK_CMD="convert"
export MAGICK_CMD
return 0
fi
fi
echo "Error: ImageMagick is not installed." >&2
echo "Install it with:" >&2
echo " Arch Linux: sudo pacman -S imagemagick" >&2
echo " Ubuntu/Debian: sudo apt install imagemagick" >&2
return 1
}
# Install missing pacman packages
# Usage: install_missing_pacman_packages pkg1 pkg2 pkg3 ...
# Returns 0 if all packages installed successfully, 1 otherwise
install_missing_pacman_packages() {
local packages=("$@")
local missing=()
for pkg in "${packages[@]}"; do
if ! pacman -Qi "$pkg" >/dev/null 2>&1; then
missing+=("$pkg")
fi
done
if [[ ${#missing[@]} -eq 0 ]]; then
echo "[INFO] All required packages are already installed."
return 0
fi
echo "[INFO] Installing missing packages: ${missing[*]}"
if ! sudo pacman -S --needed --noconfirm "${missing[@]}"; then
echo "[ERROR] Failed to install packages" >&2
return 1
fi
return 0
}
# =============================================================================
# NOTIFICATION
# =============================================================================
@ -203,7 +294,7 @@ get_script_dir() {
# Usage: ensure_dir "/path/to/dir"
ensure_dir() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
if [[ ! -d $dir ]]; then
mkdir -p "$dir"
fi
}
@ -212,30 +303,176 @@ ensure_dir() {
# SYSTEMD HELPERS
# =============================================================================
# Internal helper for running systemctl with optional --user flag
_systemctl_cmd() {
local user_flag="$1"
shift
if [[ $user_flag == "--user" ]]; then
systemctl --user "$@"
else
systemctl "$@"
fi
}
# Enable and start a systemd service (user or system)
# Usage: enable_service "service-name" [--user]
enable_service() {
local service="$1"
local user_flag="${2:-}"
if [[ "$user_flag" == "--user" ]]; then
systemctl --user daemon-reload
systemctl --user enable --now "$service"
else
systemctl daemon-reload
systemctl enable --now "$service"
fi
_systemctl_cmd "$user_flag" daemon-reload
_systemctl_cmd "$user_flag" enable --now "$service"
}
# Check if a systemd service is active
# Usage: if is_service_active "service-name" [--user]; then ...
is_service_active() {
local service="$1"
local user_flag="${2:-}"
_systemctl_cmd "${2:-}" is-active --quiet "$1"
}
if [[ "$user_flag" == "--user" ]]; then
systemctl --user is-active --quiet "$service"
# Check if a systemd service is enabled
# Usage: if is_service_enabled "service-name" [--user]; then ...
is_service_enabled() {
_systemctl_cmd "${2:-}" is-enabled --quiet "$1" 2>/dev/null
}
# =============================================================================
# COLORED LOGGING (for scripts that need colored output)
# =============================================================================
# ANSI color codes
declare -g COLOR_RED='\033[1;31m'
declare -g COLOR_GREEN='\033[1;32m'
declare -g COLOR_YELLOW='\033[1;33m'
declare -g COLOR_BLUE='\033[1;34m'
declare -g COLOR_NC='\033[0m'
log_info() {
printf "${COLOR_BLUE}[INFO]${COLOR_NC} %s\n" "$*"
}
log_ok() {
printf "${COLOR_GREEN}[ OK ]${COLOR_NC} %s\n" "$*"
}
log_warn() {
printf "${COLOR_YELLOW}[WARN]${COLOR_NC} %s\n" "$*" >&2
}
log_error() {
printf "${COLOR_RED}[ERROR]${COLOR_NC} %s\n" "$*" >&2
}
# Alias for compatibility
warn() { log_warn "$@"; }
err() { log_error "$@"; }
# =============================================================================
# INTERACTIVE PROMPTS
# =============================================================================
# Ask yes/no question, returns 0 for yes, 1 for no
# Usage: if ask_yes_no "Continue?"; then ...
ask_yes_no() {
local prompt="$1"
local ans
read -r -p "$prompt [y/N]: " ans || true
case "${ans:-}" in
y | Y | yes | YES) return 0 ;;
*) return 1 ;;
esac
}
# Check if a command is available
# Usage: if has_cmd git; then ...
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
# =============================================================================
# STANDARD SETUP HEADER
# =============================================================================
# Print a standard setup header for scripts
# Usage: print_setup_header "Script Name"
print_setup_header() {
local title="$1"
echo "$title"
printf '=%.0s' $(seq 1 ${#title})
echo ""
echo "Current Date: $(date)"
echo "User: $USER"
echo "Original user: $(get_actual_user)"
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Mode: Interactive (prompts enabled)"
else
systemctl is-active --quiet "$service"
echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
fi
}
# =============================================================================
# MOUNT/UNMOUNT HELPERS (for hosts guard and similar)
# =============================================================================
# Count mount layers for a path
# Usage: count=$(mount_layers_count "/etc/hosts")
mount_layers_count() {
local target="$1"
awk -v t="$target" '$5==t{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0
}
# Collapse all bind mount layers for a path
# Usage: collapse_mounts "/etc/hosts" [max_iterations]
collapse_mounts() {
local target="$1"
local max_iter="${2:-20}"
local i=0
if has_cmd mountpoint; then
while mountpoint -q "$target"; do
umount -l "$target" >/dev/null 2>&1 || break
i=$((i + 1))
((i >= max_iter)) && break
done
else
local cnt
cnt=$(mount_layers_count "$target")
while ((cnt > 1)); do
umount -l "$target" >/dev/null 2>&1 || break
i=$((i + 1))
((i >= max_iter)) && break
cnt=$(mount_layers_count "$target")
done
fi
}
# =============================================================================
# RESOLUTION/FORMAT VALIDATION
# =============================================================================
# Validate resolution format (WIDTHxHEIGHT)
# Usage: if validate_resolution "1920x1080"; then ...
validate_resolution() {
local res="$1"
[[ $res =~ ^[0-9]+x[0-9]+$ ]]
}
# Generate output filename with suffix
# Usage: output=$(generate_output_filename "input.jpg" "_resized")
generate_output_filename() {
local input="$1"
local suffix="$2"
local ext="${3:-}"
local basename dirname filename extension
basename=$(basename "$input")
dirname=$(dirname "$input")
filename="${basename%.*}"
extension="${basename##*.}"
# Handle files without extension
if [[ $filename == "$extension" ]]; then
extension="${ext:-jpg}"
fi
echo "${dirname}/${filename}${suffix}.${extension}"
}

View File

@ -17,6 +17,11 @@
set -uo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
# Source common library for log_info, log_warn, log_error
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
DEFAULT_ROOT=$(cd -- "$SCRIPT_DIR/../../" && pwd)
ROOT_DIR="$DEFAULT_ROOT"
@ -25,20 +30,8 @@ INSTALL_ONLY="false"
LIST_ONLY="false"
VERBOSE="false"
log_info() {
printf '\033[1;34m[INFO]\033[0m %s\n' "$*"
}
log_warn() {
printf '\033[1;33m[WARN]\033[0m %s\n' "$*"
}
log_error() {
printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2
}
usage() {
cat << EOF
cat <<EOF
Usage: $(basename "$0") [options]
Options:
@ -47,7 +40,9 @@ Options:
--install-only Only install linters, do not scan
--list-only Only list discovered shell files, do not run linters
--verbose Print additional details while running
-h, --help Show this helpLinters used:
-h, --help Show this help
Linters used:
Required: shellcheck, shfmt
Optional (if available): checkbashisms, bashate
Syntax checks: bash -n, zsh -n (if installed), sh/dash -n
@ -55,120 +50,120 @@ EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--path)
ROOT_DIR="$2"
shift 2
;;
--skip-install)
SKIP_INSTALL="true"
shift
;;
--install-only)
INSTALL_ONLY="true"
shift
;;
--list-only)
LIST_ONLY="true"
shift
;;
--verbose)
VERBOSE="true"
shift
;;
-h | --help)
usage
exit 0
;;
*)
log_error "Unknown argument: $1"
usage
exit 2
;;
esac
case "$1" in
--path)
ROOT_DIR="$2"
shift 2
;;
--skip-install)
SKIP_INSTALL="true"
shift
;;
--install-only)
INSTALL_ONLY="true"
shift
;;
--list-only)
LIST_ONLY="true"
shift
;;
--verbose)
VERBOSE="true"
shift
;;
-h | --help)
usage
exit 0
;;
*)
log_error "Unknown argument: $1"
usage
exit 2
;;
esac
done
if [[ ! -d $ROOT_DIR ]]; then
log_error "Path not found: $ROOT_DIR"
exit 2
log_error "Path not found: $ROOT_DIR"
exit 2
fi
is_cmd() { command -v "$1" > /dev/null 2>&1; }
is_cmd() { command -v "$1" >/dev/null 2>&1; }
is_arch() { is_cmd pacman; }
have_aur_helper() { is_cmd yay || is_cmd paru; }
install_if_missing() {
local pkg cmd
pkg="$1"
cmd="$2"
if is_cmd "$cmd"; then
[[ $VERBOSE == "true" ]] && log_info "Found $cmd"
return 0
fi
local pkg cmd
pkg="$1"
cmd="$2"
if is_cmd "$cmd"; then
[[ $VERBOSE == "true" ]] && log_info "Found $cmd"
return 0
fi
if [[ $SKIP_INSTALL == "true" ]]; then
log_warn "Skipping install of $pkg ($cmd not found)"
return 1
fi
if [[ $SKIP_INSTALL == "true" ]]; then
log_warn "Skipping install of $pkg ($cmd not found)"
return 1
fi
if is_arch; then
log_info "Installing $pkg via pacman..."
if ! sudo pacman -S --needed --noconfirm "$pkg"; then
log_warn "Failed to install $pkg via pacman."
return 1
fi
return 0
else
log_warn "Non-Arch system detected. Please install '$pkg' manually."
return 1
fi
if is_arch; then
log_info "Installing $pkg via pacman..."
if ! sudo pacman -S --needed --noconfirm "$pkg"; then
log_warn "Failed to install $pkg via pacman."
return 1
fi
return 0
else
log_warn "Non-Arch system detected. Please install '$pkg' manually."
return 1
fi
}
install_linters() {
local ok=0
local ok=0
# Core linters
install_if_missing shellcheck shellcheck || ok=1
install_if_missing shfmt shfmt || ok=1
# Core linters
install_if_missing shellcheck shellcheck || ok=1
install_if_missing shfmt shfmt || ok=1
# Optional linters (best-effort)
# checkbashisms may be in repos or AUR; try pacman first, then AUR helper
if ! is_cmd checkbashisms; then
if is_arch; then
if ! sudo pacman -S --needed --noconfirm checkbashisms 2> /dev/null; then
if have_aur_helper; then
log_info "Installing checkbashisms from AUR (requires yay/paru)..."
if is_cmd yay; then yay -S --noconfirm checkbashisms || true; fi
if is_cmd paru; then paru -S --noconfirm checkbashisms || true; fi
else
log_warn "checkbashisms not installed (no AUR helper)."
fi
fi
fi
fi
# Optional linters (best-effort)
# checkbashisms may be in repos or AUR; try pacman first, then AUR helper
if ! is_cmd checkbashisms; then
if is_arch; then
if ! sudo pacman -S --needed --noconfirm checkbashisms 2>/dev/null; then
if have_aur_helper; then
log_info "Installing checkbashisms from AUR (requires yay/paru)..."
if is_cmd yay; then yay -S --noconfirm checkbashisms || true; fi
if is_cmd paru; then paru -S --noconfirm checkbashisms || true; fi
else
log_warn "checkbashisms not installed (no AUR helper)."
fi
fi
fi
fi
# bashate (python-based), typically available as python-bashate in AUR
if ! is_cmd bashate; then
if is_arch && have_aur_helper; then
log_info "Installing bashate from AUR (requires yay/paru)..."
if is_cmd yay; then yay -S --noconfirm python-bashate || true; fi
if is_cmd paru; then paru -S --noconfirm python-bashate || true; fi
else
# Try pip if user has it and wants to
if is_cmd pipx; then
log_info "Installing bashate via pipx..."
pipx install bashate || true
elif is_cmd pip3; then
log_info "Installing bashate via pip (user)..."
pip3 install --user bashate || true
else
log_warn "bashate not installed (no AUR helper or pip available)."
fi
fi
fi
# bashate (python-based), typically available as python-bashate in AUR
if ! is_cmd bashate; then
if is_arch && have_aur_helper; then
log_info "Installing bashate from AUR (requires yay/paru)..."
if is_cmd yay; then yay -S --noconfirm python-bashate || true; fi
if is_cmd paru; then paru -S --noconfirm python-bashate || true; fi
else
# Try pip if user has it and wants to
if is_cmd pipx; then
log_info "Installing bashate via pipx..."
pipx install bashate || true
elif is_cmd pip3; then
log_info "Installing bashate via pip (user)..."
pip3 install --user bashate || true
else
log_warn "bashate not installed (no AUR helper or pip available)."
fi
fi
fi
return "$ok"
return "$ok"
}
TMPDIR=$(mktemp -d)
@ -178,255 +173,255 @@ ABS_FILES_Z="$TMPDIR/files_abs.zlist"
REL_FILES_Z="$TMPDIR/files_rel.zlist"
discover_shell_files() {
local base="$1"
local -a all
all=()
local base="$1"
local -a all
all=()
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 --others --exclude-standard -z)
else
while IFS= read -r -d '' f; do
# trim leading ./ to keep consistent style with git paths
f="${f#./}"
f="${f#"${base}"/}"
all+=("$f")
done < <(find "$base" -type f -print0)
fi
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 --others --exclude-standard -z)
else
while IFS= read -r -d '' f; do
# trim leading ./ to keep consistent style with git paths
f="${f#./}"
f="${f#"${base}"/}"
all+=("$f")
done < <(find "$base" -type f -print0)
fi
local -a shells
shells=()
local -a shells
shells=()
for rel in "${all[@]}"; do
# skip binary-ish or huge files quickly by extension heuristic
case "$rel" in
*.png | *.jpg | *.jpeg | *.gif | *.ico | *.pdf | *.svg | *.zip | *.tar | *.gz | *.xz | *.7z | *.so | *.o | *.bin)
continue
;;
esac
for rel in "${all[@]}"; do
# skip binary-ish or huge files quickly by extension heuristic
case "$rel" in
*.png | *.jpg | *.jpeg | *.gif | *.ico | *.pdf | *.svg | *.zip | *.tar | *.gz | *.xz | *.7z | *.so | *.o | *.bin)
continue
;;
esac
local abs="$base/$rel"
[[ -f $abs && -r $abs ]] || continue
local abs="$base/$rel"
[[ -f $abs && -r $abs ]] || continue
if [[ $rel == *.sh || $rel == *.bash || $rel == *.zsh ]]; then
shells+=("$rel")
continue
fi
if [[ $rel == *.sh || $rel == *.bash || $rel == *.zsh ]]; then
shells+=("$rel")
continue
fi
# Check shebang
local first
first=$(head -n 1 -- "$abs" 2> /dev/null || true)
if [[ $first =~ ^#! && $first =~ (ba|z|d|k)?sh ]]; then
shells+=("$rel")
continue
fi
# Check shebang
local first
first=$(head -n 1 -- "$abs" 2>/dev/null || true)
if [[ $first =~ ^#! && $first =~ (ba|z|d|k)?sh ]]; then
shells+=("$rel")
continue
fi
# Also catch executable files with shell shebang even without extension
if [[ -x $abs ]]; then
if [[ $first =~ ^#! && $first =~ (ba|z|d|k)?sh ]]; then
shells+=("$rel")
fi
fi
done
# Also catch executable files with shell shebang even without extension
if [[ -x $abs ]]; then
if [[ $first =~ ^#! && $first =~ (ba|z|d|k)?sh ]]; then
shells+=("$rel")
fi
fi
done
# write lists
: > "$REL_FILES_Z"
: > "$ABS_FILES_Z"
for rel in "${shells[@]}"; do
printf '%s\0' "$rel" >> "$REL_FILES_Z"
printf '%s\0' "$base/$rel" >> "$ABS_FILES_Z"
done
# write lists
: >"$REL_FILES_Z"
: >"$ABS_FILES_Z"
for rel in "${shells[@]}"; do
printf '%s\0' "$rel" >>"$REL_FILES_Z"
printf '%s\0' "$base/$rel" >>"$ABS_FILES_Z"
done
}
print_file_list() {
local count
count=$(tr -cd '\0' < "$REL_FILES_Z" | wc -c)
log_info "Discovered $count shell file(s) under $ROOT_DIR"
if [[ $VERBOSE == "true" ]]; then
tr '\0' '\n' < "$REL_FILES_Z" | sed 's/^/ - /'
fi
local count
count=$(tr -cd '\0' <"$REL_FILES_Z" | wc -c)
log_info "Discovered $count shell file(s) under $ROOT_DIR"
if [[ $VERBOSE == "true" ]]; then
tr '\0' '\n' <"$REL_FILES_Z" | sed 's/^/ - /'
fi
}
run_linters() {
local issues=0
local count
count=$(tr -cd '\0' < "$ABS_FILES_Z" | wc -c)
if [[ $count -eq 0 ]]; then
log_warn "No shell files found to lint."
return 0
fi
local issues=0
local count
count=$(tr -cd '\0' <"$ABS_FILES_Z" | wc -c)
if [[ $count -eq 0 ]]; then
log_warn "No shell files found to lint."
return 0
fi
mapfile -d '' -t FILES < "$ABS_FILES_Z"
mapfile -d '' -t FILES <"$ABS_FILES_Z"
log_info "Running shellcheck..."
local sc_out="$TMPDIR/shellcheck.txt"
if is_cmd shellcheck; then
if ! shellcheck -x -S style "${FILES[@]}" > "$sc_out" 2>&1; then
issues=$((issues + 1))
fi
else
log_warn "shellcheck not found; skipping"
fi
log_info "Running shellcheck..."
local sc_out="$TMPDIR/shellcheck.txt"
if is_cmd shellcheck; then
if ! shellcheck -x -S style "${FILES[@]}" >"$sc_out" 2>&1; then
issues=$((issues + 1))
fi
else
log_warn "shellcheck not found; skipping"
fi
log_info "Running shfmt (diff mode)..."
local shfmt_out="$TMPDIR/shfmt.diff"
if is_cmd shfmt; then
if ! shfmt -d -i 2 -ci -sr -s "${FILES[@]}" > "$shfmt_out" 2>&1; then
# shfmt returns non-zero when diff exists
issues=$((issues + 1))
fi
else
log_warn "shfmt not found; skipping"
fi
log_info "Running shfmt (diff mode)..."
local shfmt_out="$TMPDIR/shfmt.diff"
if is_cmd shfmt; then
if ! shfmt -d -i 2 -ci -sr -s "${FILES[@]}" >"$shfmt_out" 2>&1; then
# shfmt returns non-zero when diff exists
issues=$((issues + 1))
fi
else
log_warn "shfmt not found; skipping"
fi
log_info "Running checkbashisms (optional)..."
local cbi_out="$TMPDIR/checkbashisms.txt"
local cbi_status=0
if is_cmd checkbashisms; then
# Only run checkbashisms on scripts that are intended for /bin/sh (or unspecified),
# skip explicit bash/zsh scripts to avoid false positives.
local -a CBI_FILES
CBI_FILES=()
for f in "${FILES[@]}"; do
local first
first=$(head -n 1 -- "$f" 2> /dev/null || true)
if [[ $first =~ bash || $first =~ zsh ]]; then
continue
fi
CBI_FILES+=("$f")
done
if [[ ${#CBI_FILES[@]} -gt 0 ]]; then
# checkbashisms exits 0 if OK, 1 if issues, other codes for tool warnings
checkbashisms "${CBI_FILES[@]}" > "$cbi_out" 2>&1
else
: > "$cbi_out"
fi
cbi_status=$?
if [[ $cbi_status -eq 1 ]]; then
issues=$((issues + 1))
elif [[ $cbi_status -ne 0 ]]; then
log_warn "checkbashisms exited with status $cbi_status (treated as warning)"
fi
else
log_warn "checkbashisms not found; skipping"
fi
log_info "Running checkbashisms (optional)..."
local cbi_out="$TMPDIR/checkbashisms.txt"
local cbi_status=0
if is_cmd checkbashisms; then
# Only run checkbashisms on scripts that are intended for /bin/sh (or unspecified),
# skip explicit bash/zsh scripts to avoid false positives.
local -a CBI_FILES
CBI_FILES=()
for f in "${FILES[@]}"; do
local first
first=$(head -n 1 -- "$f" 2>/dev/null || true)
if [[ $first =~ bash || $first =~ zsh ]]; then
continue
fi
CBI_FILES+=("$f")
done
if [[ ${#CBI_FILES[@]} -gt 0 ]]; then
# checkbashisms exits 0 if OK, 1 if issues, other codes for tool warnings
checkbashisms "${CBI_FILES[@]}" >"$cbi_out" 2>&1
else
: >"$cbi_out"
fi
cbi_status=$?
if [[ $cbi_status -eq 1 ]]; then
issues=$((issues + 1))
elif [[ $cbi_status -ne 0 ]]; then
log_warn "checkbashisms exited with status $cbi_status (treated as warning)"
fi
else
log_warn "checkbashisms not found; skipping"
fi
log_info "Running bash/zsh/sh syntax checks (-n)..."
local bash_out="$TMPDIR/bash_syntax.txt"
local zsh_out="$TMPDIR/zsh_syntax.txt"
local sh_out="$TMPDIR/sh_syntax.txt"
log_info "Running bash/zsh/sh syntax checks (-n)..."
local bash_out="$TMPDIR/bash_syntax.txt"
local zsh_out="$TMPDIR/zsh_syntax.txt"
local sh_out="$TMPDIR/sh_syntax.txt"
# Partition files by shebang for better accuracy
local -a BASH_FILES ZSH_FILES SH_FILES
BASH_FILES=()
ZSH_FILES=()
SH_FILES=()
for f in "${FILES[@]}"; do
local first
first=$(head -n 1 -- "$f" 2> /dev/null || true)
if [[ $first =~ bash ]]; then
BASH_FILES+=("$f")
elif [[ $first =~ zsh ]]; then
ZSH_FILES+=("$f")
else
SH_FILES+=("$f")
fi
done
# Partition files by shebang for better accuracy
local -a BASH_FILES ZSH_FILES SH_FILES
BASH_FILES=()
ZSH_FILES=()
SH_FILES=()
for f in "${FILES[@]}"; do
local first
first=$(head -n 1 -- "$f" 2>/dev/null || true)
if [[ $first =~ bash ]]; then
BASH_FILES+=("$f")
elif [[ $first =~ zsh ]]; then
ZSH_FILES+=("$f")
else
SH_FILES+=("$f")
fi
done
if [[ ${#BASH_FILES[@]} -gt 0 ]] && is_cmd bash; then
if ! bash -n "${BASH_FILES[@]}" 2> "$bash_out"; then
issues=$((issues + 1))
fi
fi
if [[ ${#ZSH_FILES[@]} -gt 0 ]] && is_cmd zsh; then
if ! zsh -n "${ZSH_FILES[@]}" 2> "$zsh_out"; then
issues=$((issues + 1))
fi
fi
# prefer dash if present for /bin/sh style
if [[ ${#SH_FILES[@]} -gt 0 ]]; then
if is_cmd dash; then
if ! dash -n "${SH_FILES[@]}" 2> "$sh_out"; then
issues=$((issues + 1))
fi
elif is_cmd sh; then
if ! sh -n "${SH_FILES[@]}" 2> "$sh_out"; then
issues=$((issues + 1))
fi
fi
fi
if [[ ${#BASH_FILES[@]} -gt 0 ]] && is_cmd bash; then
if ! bash -n "${BASH_FILES[@]}" 2>"$bash_out"; then
issues=$((issues + 1))
fi
fi
if [[ ${#ZSH_FILES[@]} -gt 0 ]] && is_cmd zsh; then
if ! zsh -n "${ZSH_FILES[@]}" 2>"$zsh_out"; then
issues=$((issues + 1))
fi
fi
# prefer dash if present for /bin/sh style
if [[ ${#SH_FILES[@]} -gt 0 ]]; then
if is_cmd dash; then
if ! dash -n "${SH_FILES[@]}" 2>"$sh_out"; then
issues=$((issues + 1))
fi
elif is_cmd sh; then
if ! sh -n "${SH_FILES[@]}" 2>"$sh_out"; then
issues=$((issues + 1))
fi
fi
fi
echo
log_info "========== Shell Lint Report =========="
echo
log_info "========== Shell Lint Report =========="
if [[ -s $sc_out ]]; then
printf '\n\033[1m-- shellcheck --\033[0m\n'
cat "$sc_out"
else
printf '\n\033[1;32m-- shellcheck: PASS (no issues) --\033[0m\n'
fi
if [[ -s $sc_out ]]; then
printf '\n\033[1m-- shellcheck --\033[0m\n'
cat "$sc_out"
else
printf '\n\033[1;32m-- shellcheck: PASS (no issues) --\033[0m\n'
fi
if [[ -s $shfmt_out ]]; then
printf '\n\033[1m-- shfmt (diffs found) --\033[0m\n'
cat "$shfmt_out"
else
printf '\n\033[1;32m-- shfmt: PASS (formatted) --\033[0m\n'
fi
if [[ -s $shfmt_out ]]; then
printf '\n\033[1m-- shfmt (diffs found) --\033[0m\n'
cat "$shfmt_out"
else
printf '\n\033[1;32m-- shfmt: PASS (formatted) --\033[0m\n'
fi
if [[ -s $cbi_out ]]; then
printf '\n\033[1m-- checkbashisms --\033[0m\n'
cat "$cbi_out"
else
printf '\n\033[1;32m-- checkbashisms: PASS (or skipped) --\033[0m\n'
fi
if [[ -s $cbi_out ]]; then
printf '\n\033[1m-- checkbashisms --\033[0m\n'
cat "$cbi_out"
else
printf '\n\033[1;32m-- checkbashisms: PASS (or skipped) --\033[0m\n'
fi
if [[ -s $bash_out ]]; then
printf '\n\033[1m-- bash -n (syntax) --\033[0m\n'
cat "$bash_out"
else
printf '\n\033[1;32m-- bash -n: PASS (or none) --\033[0m\n'
fi
if [[ -s $bash_out ]]; then
printf '\n\033[1m-- bash -n (syntax) --\033[0m\n'
cat "$bash_out"
else
printf '\n\033[1;32m-- bash -n: PASS (or none) --\033[0m\n'
fi
if [[ -s $zsh_out ]]; then
printf '\n\033[1m-- zsh -n (syntax) --\033[0m\n'
cat "$zsh_out"
else
printf '\n\033[1;32m-- zsh -n: PASS (or none) --\033[0m\n'
fi
if [[ -s $zsh_out ]]; then
printf '\n\033[1m-- zsh -n (syntax) --\033[0m\n'
cat "$zsh_out"
else
printf '\n\033[1;32m-- zsh -n: PASS (or none) --\033[0m\n'
fi
if [[ -s $sh_out ]]; then
printf '\n\033[1m-- sh/dash -n (syntax) --\033[0m\n'
cat "$sh_out"
else
printf '\n\033[1;32m-- sh/dash -n: PASS (or none) --\033[0m\n'
fi
if [[ -s $sh_out ]]; then
printf '\n\033[1m-- sh/dash -n (syntax) --\033[0m\n'
cat "$sh_out"
else
printf '\n\033[1;32m-- sh/dash -n: PASS (or none) --\033[0m\n'
fi
echo
if [[ $issues -gt 0 ]]; then
log_error "Linting completed with $issues tool(s) reporting issues."
return 1
else
log_info "All checks passed."
return 0
fi
echo
if [[ $issues -gt 0 ]]; then
log_error "Linting completed with $issues tool(s) reporting issues."
return 1
else
log_info "All checks passed."
return 0
fi
}
# Main
if [[ $INSTALL_ONLY == "true" ]]; then
install_linters
exit $?
install_linters
exit $?
fi
# Only attempt installs if not list-only
if [[ $LIST_ONLY != "true" ]]; then
install_linters || true
install_linters || true
fi
discover_shell_files "$ROOT_DIR"
print_file_list
if [[ $LIST_ONLY == "true" ]]; then
exit 0
exit 0
fi
run_linters

View File

@ -18,22 +18,17 @@
set -euo pipefail
IFS=$'\n\t'
GREEN="\033[1;32m"
YELLOW="\033[1;33m"
RED="\033[1;31m"
BLUE="\033[1;34m"
NC="\033[0m"
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; }
# Source common library for log functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
DO_POLICY=false
SET_DEFAULT=false
DO_RESTART=false
usage() {
cat << EOF
cat <<EOF
fix_thorium_unity.sh - Auto-allow unityhub:// from Unity origins in Thorium/Chromium
Options:
@ -49,52 +44,52 @@ EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--policy)
DO_POLICY=true
shift
;;
--set-default)
SET_DEFAULT=true
shift
;;
--restart)
DO_RESTART=true
shift
;;
-h | --help)
usage
exit 0
;;
*)
log_error "Unknown argument: $1"
usage
exit 1
;;
esac
case "$1" in
--policy)
DO_POLICY=true
shift
;;
--set-default)
SET_DEFAULT=true
shift
;;
--restart)
DO_RESTART=true
shift
;;
-h | --help)
usage
exit 0
;;
*)
log_error "Unknown argument: $1"
usage
exit 1
;;
esac
done
ensure_sudo() {
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."
exit 1
fi
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."
exit 1
fi
}
install_policy() {
ensure_sudo
# Candidate policy directories (most common for Chromium forks)
local candidates=(
"/etc/thorium-browser/policies/managed" # Thorium
"/etc/chromium/policies/managed" # Chromium
"/etc/opt/chrome/policies/managed" # Google Chrome
)
local wrote_any=false
for target in "${candidates[@]}"; do
log_info "Installing policy into: $target"
sudo mkdir -p "$target"
local policy_file="$target/unityhub-policy.json"
sudo tee "$policy_file" > /dev/null << 'JSON'
ensure_sudo
# Candidate policy directories (most common for Chromium forks)
local candidates=(
"/etc/thorium-browser/policies/managed" # Thorium
"/etc/chromium/policies/managed" # Chromium
"/etc/opt/chrome/policies/managed" # Google Chrome
)
local wrote_any=false
for target in "${candidates[@]}"; do
log_info "Installing policy into: $target"
sudo mkdir -p "$target"
local policy_file="$target/unityhub-policy.json"
sudo tee "$policy_file" >/dev/null <<'JSON'
{
"AutoLaunchProtocolsFromOrigins": [
{ "protocol": "unityhub", "origin": "https://id.unity.com", "allow": true },
@ -106,53 +101,53 @@ install_policy() {
]
}
JSON
# Some Chromium builds cache policies; no explicit reload on Linux. Restarting browser suffices.
log_ok "Policy written: $policy_file"
wrote_any=true
done
if [[ $wrote_any != true ]]; then
log_warn "Policy may not have been written. No candidate directories processed."
fi
# Some Chromium builds cache policies; no explicit reload on Linux. Restarting browser suffices.
log_ok "Policy written: $policy_file"
wrote_any=true
done
if [[ $wrote_any != true ]]; then
log_warn "Policy may not have been written. No candidate directories processed."
fi
}
set_default_browser() {
if command -v xdg-settings > /dev/null 2>&1; then
# Prefer the upstream desktop id if it exists
local desktop="thorium-browser.desktop"
if [[ ! -f "/usr/share/applications/$desktop" && -f "$HOME/.local/share/applications/$desktop" ]]; then
: # keep desktop as is
elif [[ ! -f "/usr/share/applications/$desktop" && ! -f "$HOME/.local/share/applications/$desktop" ]]; then
log_warn "thorium-browser.desktop not found; leaving default browser unchanged."
return
fi
log_info "Setting default browser to $desktop"
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")"
else
log_warn "xdg-settings not found; cannot set default browser automatically."
fi
if command -v xdg-settings >/dev/null 2>&1; then
# Prefer the upstream desktop id if it exists
local desktop="thorium-browser.desktop"
if [[ ! -f "/usr/share/applications/$desktop" && -f "$HOME/.local/share/applications/$desktop" ]]; then
: # keep desktop as is
elif [[ ! -f "/usr/share/applications/$desktop" && ! -f "$HOME/.local/share/applications/$desktop" ]]; then
log_warn "thorium-browser.desktop not found; leaving default browser unchanged."
return
fi
log_info "Setting default browser to $desktop"
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")"
else
log_warn "xdg-settings not found; cannot set default browser automatically."
fi
}
restart_thorium() {
# Kill Thorium processes and start fresh
log_info "Restarting Thorium..."
pkill -9 -f 'thorium-browser' 2> /dev/null || true
# Also kill unityhub-bin's embedded Chromium if any leftover (harmless)
pkill -9 -f 'unityhub-bin' 2> /dev/null || true
# Start Thorium detached if available
if command -v thorium-browser > /dev/null 2>&1; then
nohup thorium-browser > /dev/null 2>&1 &
disown || true
fi
log_ok "Thorium restart attempted."
# Kill Thorium processes and start fresh
log_info "Restarting Thorium..."
pkill -9 -f 'thorium-browser' 2>/dev/null || true
# Also kill unityhub-bin's embedded Chromium if any leftover (harmless)
pkill -9 -f 'unityhub-bin' 2>/dev/null || true
# Start Thorium detached if available
if command -v thorium-browser >/dev/null 2>&1; then
nohup thorium-browser >/dev/null 2>&1 &
disown || true
fi
log_ok "Thorium restart attempted."
}
main() {
$DO_POLICY && install_policy
$SET_DEFAULT && set_default_browser
$DO_RESTART && restart_thorium
$DO_POLICY && install_policy
$SET_DEFAULT && set_default_browser
$DO_RESTART && restart_thorium
cat << 'NEXT'
cat <<'NEXT'
---
Next steps:
- 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'
SCRIPT_NAME="$(basename "$0")"
GREEN="\033[1;32m"
YELLOW="\033[1;33m"
RED="\033[1;31m"
BLUE="\033[1;34m"
NC="\033[0m"
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; }
# Source common library for log functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
usage() {
cat << EOF
cat <<EOF
${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler
Options:
@ -52,158 +47,158 @@ AUTO_INSTALL=false
RUN_TEST=false
while [[ $# -gt 0 ]]; do
case "$1" in
-y | --yes)
AUTO_INSTALL=true
shift
;;
--test)
RUN_TEST=true
shift
;;
-h | --help)
usage
exit 0
;;
*)
log_error "Unknown argument: $1"
usage
exit 1
;;
esac
case "$1" in
-y | --yes)
AUTO_INSTALL=true
shift
;;
--test)
RUN_TEST=true
shift
;;
-h | --help)
usage
exit 0
;;
*)
log_error "Unknown argument: $1"
usage
exit 1
;;
esac
done
require_cmd() {
if ! command -v "$1" > /dev/null 2>&1; then
return 1
fi
if ! command -v "$1" >/dev/null 2>&1; then
return 1
fi
}
ensure_deps_arch() {
# Best-effort install for Arch-based systems
if [[ $AUTO_INSTALL != true ]]; then
log_warn "Skipping package installation (use -y to auto-install)."
return 0
fi
if ! require_cmd pacman; then
log_warn "Not an Arch-based system (pacman not found). Skipping auto-install."
return 0
fi
local pkgs=(xdg-utils desktop-file-utils xdg-desktop-portal xdg-desktop-portal-gtk)
log_info "Installing/ensuring packages: ${pkgs[*]}"
if ! require_cmd sudo; then
log_warn "sudo not found; attempting pacman directly (may fail)."
sudo_cmd=""
else
sudo_cmd="sudo"
fi
# Use --needed to avoid reinstalling
set +e
$sudo_cmd pacman -S --needed --noconfirm "${pkgs[@]}"
local rc=$?
set -e
if [[ $rc -ne 0 ]]; then
log_warn "Package install may have failed or been partial. Continuing anyway."
else
log_ok "Dependencies installed/verified."
fi
# Best-effort install for Arch-based systems
if [[ $AUTO_INSTALL != true ]]; then
log_warn "Skipping package installation (use -y to auto-install)."
return 0
fi
if ! require_cmd pacman; then
log_warn "Not an Arch-based system (pacman not found). Skipping auto-install."
return 0
fi
local pkgs=(xdg-utils desktop-file-utils xdg-desktop-portal xdg-desktop-portal-gtk)
log_info "Installing/ensuring packages: ${pkgs[*]}"
if ! require_cmd sudo; then
log_warn "sudo not found; attempting pacman directly (may fail)."
sudo_cmd=""
else
sudo_cmd="sudo"
fi
# Use --needed to avoid reinstalling
set +e
$sudo_cmd pacman -S --needed --noconfirm "${pkgs[@]}"
local rc=$?
set -e
if [[ $rc -ne 0 ]]; then
log_warn "Package install may have failed or been partial. Continuing anyway."
else
log_ok "Dependencies installed/verified."
fi
}
desktop_dir="$HOME/.local/share/applications"
mkdir -p "$desktop_dir"
detect_unityhub() {
# Outputs: INSTALL_TYPE (FLATPAK|NATIVE|APPIMAGE|UNKNOWN) and EXEC_CMD
local install_type="UNKNOWN" exec_cmd=""
# Outputs: INSTALL_TYPE (FLATPAK|NATIVE|APPIMAGE|UNKNOWN) and EXEC_CMD
local install_type="UNKNOWN" exec_cmd=""
# 1) Flatpak
if command -v flatpak > /dev/null 2>&1; then
if flatpak info com.unity.UnityHub > /dev/null 2>&1; then
install_type="FLATPAK"
exec_cmd="flatpak run com.unity.UnityHub %U"
echo "$install_type|$exec_cmd"
return 0
fi
fi
# 1) Flatpak
if command -v flatpak >/dev/null 2>&1; then
if flatpak info com.unity.UnityHub >/dev/null 2>&1; then
install_type="FLATPAK"
exec_cmd="flatpak run com.unity.UnityHub %U"
echo "$install_type|$exec_cmd"
return 0
fi
fi
# 2) Native binary in PATH
if command -v unityhub > /dev/null 2>&1; then
local path
path="$(command -v unityhub)"
install_type="NATIVE"
exec_cmd="$path %U"
echo "$install_type|$exec_cmd"
return 0
fi
# 2) Native binary in PATH
if command -v unityhub >/dev/null 2>&1; then
local path
path="$(command -v unityhub)"
install_type="NATIVE"
exec_cmd="$path %U"
echo "$install_type|$exec_cmd"
return 0
fi
# 3) Search desktop files for Unity Hub Exec
local search_dirs=(
"$HOME/.local/share/applications"
"/usr/share/applications"
"/var/lib/flatpak/exports/share/applications"
"$HOME/.local/share/flatpak/exports/share/applications"
)
local found_exec=""
for d in "${search_dirs[@]}"; do
[[ -d $d ]] || continue
# prefer official naming when present
local f
for f in "$d"/*.desktop; do
[[ -e $f ]] || continue
if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2> /dev/null ||
grep -qiE 'Exec=.*unityhub' "$f" 2> /dev/null; then
local exec_line
exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')"
if [[ -n $exec_line ]]; then
found_exec="$exec_line"
break 2
fi
fi
done
done
# 3) Search desktop files for Unity Hub Exec
local search_dirs=(
"$HOME/.local/share/applications"
"/usr/share/applications"
"/var/lib/flatpak/exports/share/applications"
"$HOME/.local/share/flatpak/exports/share/applications"
)
local found_exec=""
for d in "${search_dirs[@]}"; do
[[ -d $d ]] || continue
# prefer official naming when present
local f
for f in "$d"/*.desktop; do
[[ -e $f ]] || continue
if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2>/dev/null ||
grep -qiE 'Exec=.*unityhub' "$f" 2>/dev/null; then
local exec_line
exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')"
if [[ -n $exec_line ]]; then
found_exec="$exec_line"
break 2
fi
fi
done
done
if [[ -n $found_exec ]]; then
# Normalize: ensure %U present
if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then
found_exec+=" %U"
fi
if [[ $found_exec == flatpak* ]]; then
install_type="FLATPAK"
elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then
install_type="APPIMAGE"
else
install_type="NATIVE"
fi
echo "$install_type|$found_exec"
return 0
fi
if [[ -n $found_exec ]]; then
# Normalize: ensure %U present
if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then
found_exec+=" %U"
fi
if [[ $found_exec == flatpak* ]]; then
install_type="FLATPAK"
elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then
install_type="APPIMAGE"
else
install_type="NATIVE"
fi
echo "$install_type|$found_exec"
return 0
fi
# 4) Try common AppImage locations
local ai_candidates=(
"$HOME/Applications/UnityHub*.AppImage"
"$HOME/.local/bin/UnityHub*.AppImage"
"/opt/UnityHub*/UnityHub*.AppImage"
)
local ai
for ai in "${ai_candidates[@]}"; do
for p in $ai; do
if [[ -f $p && -x $p ]]; then
install_type="APPIMAGE"
exec_cmd="$p %U"
echo "$install_type|$exec_cmd"
return 0
fi
done
done
# 4) Try common AppImage locations
local ai_candidates=(
"$HOME/Applications/UnityHub*.AppImage"
"$HOME/.local/bin/UnityHub*.AppImage"
"/opt/UnityHub*/UnityHub*.AppImage"
)
local ai
for ai in "${ai_candidates[@]}"; do
for p in $ai; do
if [[ -f $p && -x $p ]]; then
install_type="APPIMAGE"
exec_cmd="$p %U"
echo "$install_type|$exec_cmd"
return 0
fi
done
done
echo "$install_type|$exec_cmd"
echo "$install_type|$exec_cmd"
}
create_handler_desktop() {
local exec_cmd="$1"
local dest="$desktop_dir/unityhub-url-handler.desktop"
log_info "Writing handler desktop entry: $dest"
cat > "$dest" << DESK
local exec_cmd="$1"
local dest="$desktop_dir/unityhub-url-handler.desktop"
log_info "Writing handler desktop entry: $dest"
cat >"$dest" <<DESK
[Desktop Entry]
Name=Unity Hub URL Handler
Comment=Handle unityhub:// links for Unity Hub sign-in
@ -216,82 +211,82 @@ StartupWMClass=Unity Hub
MimeType=x-scheme-handler/unityhub;x-scheme-handler/unity;
NoDisplay=true
DESK
log_ok "Desktop entry created/updated."
echo "$dest"
log_ok "Desktop entry created/updated."
echo "$dest"
}
register_mime_handler() {
local desktop_file="$1"
# Update desktop database if available
if command -v update-desktop-database > /dev/null 2>&1; then
update-desktop-database "$desktop_dir" || true
else
log_warn "update-desktop-database not found (install desktop-file-utils)."
fi
local desktop_file="$1"
# Update desktop database if available
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$desktop_dir" || true
else
log_warn "update-desktop-database not found (install desktop-file-utils)."
fi
# Register as default handler for both schemes
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/unity || true
else
log_error "xdg-mime not found (install xdg-utils)."
return 1
fi
log_ok "MIME handler registered for unityhub:// (and unity://)."
# Register as default handler for both schemes
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/unity || true
else
log_error "xdg-mime not found (install xdg-utils)."
return 1
fi
log_ok "MIME handler registered for unityhub:// (and unity://)."
}
verify_registration() {
local expected cur1 cur2
expected="$(basename "$1")"
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)"
log_info "Current handler (unityhub): ${cur1:-<none>}"
log_info "Current handler (unity): ${cur2:-<none>}"
if [[ $cur1 == "$expected" ]]; then
log_ok "unityhub scheme correctly set to $expected"
else
log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})."
fi
local expected cur1 cur2
expected="$(basename "$1")"
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)"
log_info "Current handler (unityhub): ${cur1:-<none>}"
log_info "Current handler (unity): ${cur2:-<none>}"
if [[ $cur1 == "$expected" ]]; then
log_ok "unityhub scheme correctly set to $expected"
else
log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})."
fi
}
maybe_test_open() {
if [[ $RUN_TEST == true ]]; then
log_info "Opening test link: unityhub://v1/editor-signin"
if command -v xdg-open > /dev/null 2>&1; then
xdg-open 'unityhub://v1/editor-signin' > /dev/null 2>&1 || true
log_ok "Test link invoked. Check if Unity Hub launches or focuses."
else
log_warn "xdg-open not found; cannot run test automatically."
fi
else
log_info "You can test manually with: xdg-open 'unityhub://v1/editor-signin'"
fi
if [[ $RUN_TEST == true ]]; then
log_info "Opening test link: unityhub://v1/editor-signin"
if command -v xdg-open >/dev/null 2>&1; then
xdg-open 'unityhub://v1/editor-signin' >/dev/null 2>&1 || true
log_ok "Test link invoked. Check if Unity Hub launches or focuses."
else
log_warn "xdg-open not found; cannot run test automatically."
fi
else
log_info "You can test manually with: xdg-open 'unityhub://v1/editor-signin'"
fi
}
main() {
log_info "Ensuring required tools (optional)."
ensure_deps_arch
log_info "Ensuring required tools (optional)."
ensure_deps_arch
log_info "Detecting Unity Hub installation..."
IFS='|' read -r install_type exec_cmd < <(detect_unityhub)
log_info "Detected type: $install_type"
if [[ -z ${exec_cmd:-} ]]; then
log_warn "Could not find Unity Hub executable automatically."
log_warn "- If using Flatpak: install with 'flatpak install flathub com.unity.UnityHub'"
log_warn "- If native (AUR): ensure 'unityhub' is in PATH"
log_warn "- If AppImage: place it in ~/Applications and make it executable"
log_error "Aborting—no Exec command available to create handler."
exit 2
fi
log_info "Using Exec: $exec_cmd"
log_info "Detecting Unity Hub installation..."
IFS='|' read -r install_type exec_cmd < <(detect_unityhub)
log_info "Detected type: $install_type"
if [[ -z ${exec_cmd:-} ]]; then
log_warn "Could not find Unity Hub executable automatically."
log_warn "- If using Flatpak: install with 'flatpak install flathub com.unity.UnityHub'"
log_warn "- If native (AUR): ensure 'unityhub' is in PATH"
log_warn "- If AppImage: place it in ~/Applications and make it executable"
log_error "Aborting—no Exec command available to create handler."
exit 2
fi
log_info "Using Exec: $exec_cmd"
local desktop_file
desktop_file="$(create_handler_desktop "$exec_cmd")"
local desktop_file
desktop_file="$(create_handler_desktop "$exec_cmd")"
register_mime_handler "$desktop_file"
verify_registration "$desktop_file"
register_mime_handler "$desktop_file"
verify_registration "$desktop_file"
cat << 'NOTE'
cat <<'NOTE'
---
Next steps:
- Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub.
@ -299,9 +294,9 @@ Next steps:
---
NOTE
maybe_test_open
maybe_test_open
log_ok "Done. If login still fails, check the Hub's logs and share the outputs of:\n which unityhub || true\n flatpak info com.unity.UnityHub 2>/dev/null | sed -n '1,5p' || true\n xdg-mime query default x-scheme-handler/unityhub\n grep -R \"x-scheme-handler/unityhub\" ~/.local/share/applications /usr/share/applications 2>/dev/null | head -n 10"
log_ok "Done. If login still fails, check the Hub's logs and share the outputs of:\n which unityhub || true\n flatpak info com.unity.UnityHub 2>/dev/null | sed -n '1,5p' || true\n xdg-mime query default x-scheme-handler/unityhub\n grep -R \"x-scheme-handler/unityhub\" ~/.local/share/applications /usr/share/applications 2>/dev/null | head -n 10"
}
main "$@"

View File

@ -11,28 +11,23 @@ set -euo pipefail
# Bash/get_rnnoise_model.sh # interactive download
# RN_TARGET_DIR=./models Bash/get_rnnoise_model.sh --yes
ask_yes_no() {
read -r -p "$1 [y/N]: " ans || true
case "${ans:-}" in
y | Y | yes | YES) return 0 ;;
*) return 1 ;;
esac
}
has_cmd() { command -v "$1" > /dev/null 2>&1; }
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
YES=false
while [[ $# -gt 0 ]]; do
case "$1" in
-y | --yes)
YES=true
shift
;;
*)
echo "Unknown option: $1" >&2
exit 2
;;
esac
case "$1" in
-y | --yes)
YES=true
shift
;;
*)
echo "Unknown option: $1" >&2
exit 2
;;
esac
done
RN_TARGET_DIR=${RN_TARGET_DIR:-"$(dirname "$0")/models"}
@ -42,153 +37,125 @@ mkdir -p "$RN_TARGET_DIR"
dest="$RN_TARGET_DIR/$RN_TARGET_NAME"
if [[ -f $dest ]]; then
echo "Model already exists at: $dest"
exit 0
echo "Model already exists at: $dest"
exit 0
fi
if ! $YES; then
if ! ask_yes_no "Download RNNoise model to $dest?"; then
echo "Aborted."
exit 1
fi
if ! ask_yes_no "Download RNNoise model to $dest?"; then
echo "Aborted."
exit 1
fi
fi
if ! has_cmd curl && ! has_cmd wget; then
echo "Error: Need curl or wget to download RNNoise model." >&2
exit 3
echo "Error: Need curl or wget to download RNNoise model." >&2
exit 3
fi
# Helper: try to download a URL to destination, exit 0 on success
# Usage: try_download_model URL DEST
try_download_model() {
local url="$1"
local dest="$2"
local tmp
tmp=$(mktemp)
echo "Attempting to download RNNoise model from: $url" >&2
if has_cmd curl; then
curl -fsSL "$url" -o "$tmp" 2>/dev/null || true
else
wget -qO "$tmp" "$url" 2>/dev/null || true
fi
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
rm -f "$tmp" || true
}
# Priority 1: explicit URL
if [[ -n ${RN_URL:-} ]]; then
echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2
tmp=$(mktemp)
if has_cmd curl; then
curl -fsSL "$RN_URL" -o "$tmp"
else
wget -qO "$tmp" "$RN_URL"
fi
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest"
exit 0
fi
rm -f "$tmp" || true
echo "Warning: RN_URL download failed; continuing to fallback sources." >&2
echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2
try_download_model "$RN_URL" "$dest"
echo "Warning: RN_URL download failed; continuing to fallback sources." >&2
fi
# Priority 2: rnnoise-nu known models (GregorR)
NU_URLS=(
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/sh.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/lq.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/mp.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/bd.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/cb.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/sh.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/lq.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/mp.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/bd.rnnn"
"https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/cb.rnnn"
)
for u in "${NU_URLS[@]}"; do
echo "Attempting to download RNNoise model from: $u" >&2
tmp=$(mktemp)
if has_cmd curl; then
if curl -fsSL "$u" -o "$tmp"; then
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
fi
else
if wget -qO "$tmp" "$u"; then
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
fi
fi
rm -f "$tmp" || true
try_download_model "$u" "$dest"
done
# Priority 2b: arnndn-models fallback (richardpl)
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
echo "Attempting to download RNNoise model from: $u" >&2
tmp=$(mktemp)
if has_cmd curl; then
if curl -fsSL "$u" -o "$tmp"; then
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
fi
else
if wget -qO "$tmp" "$u"; then
if [[ -s $tmp ]]; then
mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2
exit 0
fi
fi
fi
rm -f "$tmp" || true
try_download_model "$u" "$dest"
done
# Priority 3: repo archives (rnnoise-nu and arnndn-models)
ARCHIVES=(
"https://github.com/GregorR/rnnoise-nu/archive/refs/heads/master.zip"
"https://github.com/richardpl/arnndn-models/archive/refs/heads/master.zip"
"https://github.com/GregorR/rnnoise-nu/archive/refs/heads/master.zip"
"https://github.com/richardpl/arnndn-models/archive/refs/heads/master.zip"
)
for aurl in "${ARCHIVES[@]}"; do
echo "Attempting to download archive: $aurl" >&2
tmpdir=$(mktemp -d)
archive="$tmpdir/models.zip"
set +e
if has_cmd curl; then
curl -fL "$aurl" -o "$archive"
else
wget -O "$archive" "$aurl"
fi
status=$?
set -e
if [[ $status -ne 0 ]]; then
rm -rf "$tmpdir" || true
continue
fi
if has_cmd bsdtar; then
bsdtar -xf "$archive" -C "$tmpdir"
elif has_cmd unzip; then
unzip -q "$archive" -d "$tmpdir"
else
echo "Warning: Need bsdtar or unzip to extract archive; skipping archive method." >&2
rm -rf "$tmpdir" || true
continue
fi
mapfile -t nnfiles < <(bash -lc 'shopt -s globstar nullglob; for f in '"$tmpdir"'/**/*.rnnn '"$tmpdir"'/**/*.nn; do [[ -f "$f" ]] && echo "$f"; done')
if [[ ${#nnfiles[@]} -gt 0 ]]; then
cp -f "${nnfiles[0]}" "$dest"
echo "Saved RNNoise model to: $dest (from archive)" >&2
rm -rf "$tmpdir" || true
exit 0
fi
rm -rf "$tmpdir" || true
echo "Attempting to download archive: $aurl" >&2
tmpdir=$(mktemp -d)
archive="$tmpdir/models.zip"
set +e
if has_cmd curl; then
curl -fL "$aurl" -o "$archive"
else
wget -O "$archive" "$aurl"
fi
status=$?
set -e
if [[ $status -ne 0 ]]; then
rm -rf "$tmpdir" || true
continue
fi
if has_cmd bsdtar; then
bsdtar -xf "$archive" -C "$tmpdir"
elif has_cmd unzip; then
unzip -q "$archive" -d "$tmpdir"
else
echo "Warning: Need bsdtar or unzip to extract archive; skipping archive method." >&2
rm -rf "$tmpdir" || true
continue
fi
mapfile -t nnfiles < <(bash -lc 'shopt -s globstar nullglob; for f in '"$tmpdir"'/**/*.rnnn '"$tmpdir"'/**/*.nn; do [[ -f "$f" ]] && echo "$f"; done')
if [[ ${#nnfiles[@]} -gt 0 ]]; then
cp -f "${nnfiles[0]}" "$dest"
echo "Saved RNNoise model to: $dest (from archive)" >&2
rm -rf "$tmpdir" || true
exit 0
fi
rm -rf "$tmpdir" || true
done
# Priority 4: Arch-based AUR packages and search only .nn/.rnnn
if has_cmd yay; then
echo "Attempting to install AUR packages that may include RNNoise models..." >&2
set +e
yay -S --noconfirm denoiseit-git 2> /dev/null
yay -S --noconfirm speech-denoiser-git 2> /dev/null
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)
if [[ ${#found[@]} -gt 0 ]]; then
echo "Found candidate models:" >&2
printf ' %s\n' "${found[@]}" >&2
cp -f "${found[0]}" "$dest"
echo "Copied model to: $dest" >&2
exit 0
fi
echo "Attempting to install AUR packages that may include RNNoise models..." >&2
set +e
yay -S --noconfirm denoiseit-git 2>/dev/null
yay -S --noconfirm speech-denoiser-git 2>/dev/null
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)
if [[ ${#found[@]} -gt 0 ]]; then
echo "Found candidate models:" >&2
printf ' %s\n' "${found[@]}" >&2
cp -f "${found[0]}" "$dest"
echo "Copied model to: $dest" >&2
exit 0
fi
fi
echo "Error: Could not obtain an RNNoise model automatically." >&2

View File

@ -7,124 +7,119 @@ set -euo pipefail
# Tries distro packages first; if not suitable, offers to build from source.
# This script prints commands and asks for confirmation before building.
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
print_info() {
echo "[info] $*"
echo "[info] $*"
}
ask_yes_no() {
read -r -p "$1 [y/N]: " ans || true
case "${ans:-}" in
y | Y | yes | YES) return 0 ;;
*) return 1 ;;
esac
}
has_cmd() { command -v "$1" > /dev/null 2>&1; }
detect_distro() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
echo "${ID:-unknown}"
else
echo "unknown"
fi
if [[ -f /etc/os-release ]]; then
. /etc/os-release
echo "${ID:-unknown}"
else
echo "unknown"
fi
}
main() {
local distro
distro=$(detect_distro)
print_info "Detected distro: $distro"
local distro
distro=$(detect_distro)
print_info "Detected distro: $distro"
if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then
print_info "Your ffmpeg already supports arnndn."
else
case "$distro" in
ubuntu | debian)
print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source."
echo "Options:"
echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg"
echo " - source build (recommended for latest): run this script to build from source"
;;
arch | manjaro | endeavouros)
print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg"
;;
fedora)
print_info "On Fedora, try: sudo dnf install ffmpeg"
;;
*)
print_info "Distro not recognized; will offer source build."
;;
esac
fi
if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then
print_info "Your ffmpeg already supports arnndn."
else
case "$distro" in
ubuntu | debian)
print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source."
echo "Options:"
echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg"
echo " - source build (recommended for latest): run this script to build from source"
;;
arch | manjaro | endeavouros)
print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg"
;;
fedora)
print_info "On Fedora, try: sudo dnf install ffmpeg"
;;
*)
print_info "Distro not recognized; will offer source build."
;;
esac
fi
if ask_yes_no "Build FFmpeg from source with rnnoise/arnndn support now?"; then
echo "This will clone FFmpeg and build locally under ./ffmpeg-build. Continue?"
if ! ask_yes_no "Proceed"; then
exit 0
fi
set -x
mkdir -p ffmpeg-build && cd ffmpeg-build
# Prepare repository
if [[ -d FFmpeg ]]; then
if [[ -d FFmpeg/.git ]]; then
if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then
set +e
git -C FFmpeg fetch --all --tags --prune
git -C FFmpeg pull --rebase --ff-only || true
set -e
else
if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then
rm -rf FFmpeg
else
echo "Keeping existing FFmpeg directory as-is."
fi
fi
else
if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then
rm -rf FFmpeg
else
echo "Cannot proceed with a non-git FFmpeg directory present. Aborting."
exit 4
fi
fi
fi
# Dependencies
if [[ $distro == "ubuntu" || $distro == "debian" ]]; then
sudo apt update
sudo apt install -y git build-essential yasm nasm pkg-config libx264-dev libx265-dev libvpx-dev libopus-dev libfdk-aac-dev libmp3lame-dev libvorbis-dev libass-dev libfreetype6-dev libgnutls28-dev libaom-dev libdav1d-dev libxvidcore-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxcb-shape0-dev libdrm-dev libvulkan-dev libva-dev libvdpau-dev librtmp-dev libunistring-dev libgnutls28-dev libchromaprint-dev libbluray-dev librubberband-dev libspeex-dev libsoxr-dev libvmaf-dev libzimg-dev libsvtav1-dev libtheora-dev libwebp-dev libopenal-dev libjack-jackd2-dev libpulse-dev librnnoise-dev
elif [[ $distro == "arch" || $distro == "manjaro" || $distro == "endeavouros" ]]; then
sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise
elif [[ $distro == "fedora" ]]; then
sudo dnf install -y git make gcc yasm nasm pkgconf-pkg-config rnnoise-devel libX11-devel libXext-devel libXfixes-devel libXv-devel libXrandr-devel libXi-devel libXtst-devel libXinerama-devel freetype-devel fontconfig-devel libass-devel libvpx-devel libaom-devel libdav1d-devel zimg-devel rubberband-devel soxr-devel libvorbis-devel opus-devel lame-devel
else
echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2
fi
if [[ ! -d FFmpeg/.git ]]; then
git clone https://github.com/FFmpeg/FFmpeg.git --depth=1
fi
cd FFmpeg
RN_FLAG=""
# Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported
if ./configure --help | grep -q "librnnoise"; then
RN_FLAG="--enable-librnnoise"
else
echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2
fi
if ask_yes_no "Build FFmpeg from source with rnnoise/arnndn support now?"; then
echo "This will clone FFmpeg and build locally under ./ffmpeg-build. Continue?"
if ! ask_yes_no "Proceed"; then
exit 0
fi
set -x
mkdir -p ffmpeg-build && cd ffmpeg-build
# Prepare repository
if [[ -d FFmpeg ]]; then
if [[ -d FFmpeg/.git ]]; then
if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then
set +e
git -C FFmpeg fetch --all --tags --prune
git -C FFmpeg pull --rebase --ff-only || true
set -e
else
if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then
rm -rf FFmpeg
else
echo "Keeping existing FFmpeg directory as-is."
fi
fi
else
if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then
rm -rf FFmpeg
else
echo "Cannot proceed with a non-git FFmpeg directory present. Aborting."
exit 4
fi
fi
fi
# Dependencies
if [[ $distro == "ubuntu" || $distro == "debian" ]]; then
sudo apt update
sudo apt install -y git build-essential yasm nasm pkg-config libx264-dev libx265-dev libvpx-dev libopus-dev libfdk-aac-dev libmp3lame-dev libvorbis-dev libass-dev libfreetype6-dev libgnutls28-dev libaom-dev libdav1d-dev libxvidcore-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxcb-shape0-dev libdrm-dev libvulkan-dev libva-dev libvdpau-dev librtmp-dev libunistring-dev libgnutls28-dev libchromaprint-dev libbluray-dev librubberband-dev libspeex-dev libsoxr-dev libvmaf-dev libzimg-dev libsvtav1-dev libtheora-dev libwebp-dev libopenal-dev libjack-jackd2-dev libpulse-dev librnnoise-dev
elif [[ $distro == "arch" || $distro == "manjaro" || $distro == "endeavouros" ]]; then
sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise
elif [[ $distro == "fedora" ]]; then
sudo dnf install -y git make gcc yasm nasm pkgconf-pkg-config rnnoise-devel libX11-devel libXext-devel libXfixes-devel libXv-devel libXrandr-devel libXi-devel libXtst-devel libXinerama-devel freetype-devel fontconfig-devel libass-devel libvpx-devel libaom-devel libdav1d-devel zimg-devel rubberband-devel soxr-devel libvorbis-devel opus-devel lame-devel
else
echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2
fi
if [[ ! -d FFmpeg/.git ]]; then
git clone https://github.com/FFmpeg/FFmpeg.git --depth=1
fi
cd FFmpeg
RN_FLAG=""
# Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported
if ./configure --help | grep -q "librnnoise"; then
RN_FLAG="--enable-librnnoise"
else
echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2
fi
./configure \
--enable-gpl --enable-nonfree \
--enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \
--enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \
--enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \
--enable-libdav1d --enable-libaom --enable-libsvtav1 \
${RN_FLAG} \
--enable-ffplay --enable-ffprobe
make -j"$(nproc)"
echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide."
set +x
else
echo "Skipped building from source."
fi
./configure \
--enable-gpl --enable-nonfree \
--enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \
--enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \
--enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \
--enable-libdav1d --enable-libaom --enable-libsvtav1 \
${RN_FLAG} \
--enable-ffplay --enable-ffprobe
make -j"$(nproc)"
echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide."
set +x
else
echo "Skipped building from source."
fi
}
main "$@"

View File

@ -2,6 +2,11 @@
set -euo pipefail
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
SCRIPT_NAME="$(basename "$0")"
RED="\033[31m"
@ -10,185 +15,158 @@ BLUE="\033[34m"
RESET="\033[0m"
info() {
printf "%b[%s]%b %s\n" "$BLUE" "$SCRIPT_NAME" "$RESET" "$*"
printf "%b[%s]%b %s\n" "$BLUE" "$SCRIPT_NAME" "$RESET" "$*"
}
warn() {
printf "%b[%s]%b %s\n" "$YELLOW" "$SCRIPT_NAME" "$RESET" "$*" >&2
printf "%b[%s]%b %s\n" "$YELLOW" "$SCRIPT_NAME" "$RESET" "$*" >&2
}
error() {
printf "%b[%s]%b %s\n" "$RED" "$SCRIPT_NAME" "$RESET" "$*" >&2
}
require_command() {
local cmd="$1"
local package_hint="${2:-}"
if ! command -v "$cmd" > /dev/null 2>&1; then
if [[ -n $package_hint ]]; then
error "Missing command '$cmd'. Try installing the package: $package_hint"
else
error "Missing command '$cmd'."
fi
exit 1
fi
printf "%b[%s]%b %s\n" "$RED" "$SCRIPT_NAME" "$RESET" "$*" >&2
}
ensure_pacman_packages() {
local packages=("python" "git" "curl" "jq" "code")
local missing=()
for pkg in "${packages[@]}"; do
if ! pacman -Qi "$pkg" > /dev/null 2>&1; then
missing+=("$pkg")
fi
done
if ((${#missing[@]} > 0)); then
info "Installing required packages with pacman: ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}"
else
info "All required pacman packages are already installed."
fi
install_missing_pacman_packages python git curl jq code
}
install_uv() {
if command -v uv > /dev/null 2>&1; then
info "uv is already installed."
return
fi
if command -v uv >/dev/null 2>&1; then
info "uv is already installed."
return
fi
info "Installing uv toolchain manager via official installer."
curl -LsSf https://astral.sh/uv/install.sh | sh
info "Installing uv toolchain manager via official installer."
curl -LsSf https://astral.sh/uv/install.sh | sh
local local_bin="$HOME/.local/bin"
if [[ :$PATH: != *":$local_bin:"* ]]; then
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/.zshrc"
fi
local local_bin="$HOME/.local/bin"
if [[ :$PATH: != *":$local_bin:"* ]]; then
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/.zshrc"
fi
}
ensure_unity_hub() {
if command -v unityhub > /dev/null 2>&1; then
info "Unity Hub already installed."
return
fi
if command -v unityhub >/dev/null 2>&1; then
info "Unity Hub already installed."
return
fi
if command -v yay > /dev/null 2>&1; then
info "Installing Unity Hub from AUR using yay."
yay -S --needed --noconfirm unityhub
elif command -v flatpak > /dev/null 2>&1; then
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"
else
warn "Unity Hub not found and neither yay nor flatpak is available. Install Unity Hub manually from https://unity.com/download."
fi
if command -v yay >/dev/null 2>&1; then
info "Installing Unity Hub from AUR using yay."
yay -S --needed --noconfirm unityhub
elif command -v flatpak >/dev/null 2>&1; then
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"
else
warn "Unity Hub not found and neither yay nor flatpak is available. Install Unity Hub manually from https://unity.com/download."
fi
}
sync_unity_mcp_repo() {
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
local unity_mcp_root="$data_home/UnityMCP"
local repo_dir="$unity_mcp_root/unity-mcp-repo"
local server_link="$unity_mcp_root/UnityMcpServer"
local candidates=(
"UnityMcpServer"
"UnityMcpBridge/UnityMcpServer"
"UnityMcpBridge/UnityMcpServer~"
)
local server_subdir=""
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
local unity_mcp_root="$data_home/UnityMCP"
local repo_dir="$unity_mcp_root/unity-mcp-repo"
local server_link="$unity_mcp_root/UnityMcpServer"
local candidates=(
"UnityMcpServer"
"UnityMcpBridge/UnityMcpServer"
"UnityMcpBridge/UnityMcpServer~"
)
local server_subdir=""
mkdir -p "$unity_mcp_root"
mkdir -p "$unity_mcp_root"
if [[ -d "$repo_dir/.git" ]]; then
info "Updating existing unity-mcp repository."
git -C "$repo_dir" pull --ff-only
else
info "Cloning unity-mcp repository."
rm -rf "$repo_dir"
git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "$repo_dir"
fi
if [[ -d "$repo_dir/.git" ]]; then
info "Updating existing unity-mcp repository."
git -C "$repo_dir" pull --ff-only
else
info "Cloning unity-mcp repository."
rm -rf "$repo_dir"
git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "$repo_dir"
fi
for candidate in "${candidates[@]}"; do
if [[ -d "$repo_dir/$candidate/src" ]]; then
server_subdir="$candidate"
break
fi
done
for candidate in "${candidates[@]}"; do
if [[ -d "$repo_dir/$candidate/src" ]]; then
server_subdir="$candidate"
break
fi
done
if [[ -z $server_subdir ]]; then
error "UnityMcpServer src directory not found. Checked candidates: ${candidates[*]}"
error "Repository layout may have changed. Inspect $repo_dir for the new server location."
exit 1
fi
if [[ -z $server_subdir ]]; then
error "UnityMcpServer src directory not found. Checked candidates: ${candidates[*]}"
error "Repository layout may have changed. Inspect $repo_dir for the new server location."
exit 1
fi
ln -sfn "$repo_dir/$server_subdir" "$server_link"
info "UnityMcpServer synchronized at $server_link (source: $server_subdir)"
ln -sfn "$repo_dir/$server_subdir" "$server_link"
info "UnityMcpServer synchronized at $server_link (source: $server_subdir)"
}
configure_vscode_mcp() {
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
local server_src="$data_home/UnityMCP/UnityMcpServer/src"
local mcp_config_dir="$HOME/.config/Code/User"
local mcp_config="$mcp_config_dir/mcp.json"
local tmp
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
local server_src="$data_home/UnityMCP/UnityMcpServer/src"
local mcp_config_dir="$HOME/.config/Code/User"
local mcp_config="$mcp_config_dir/mcp.json"
local tmp
if [[ ! -d $server_src ]]; then
error "Server source directory $server_src is missing."
exit 1
fi
if [[ ! -d $server_src ]]; then
error "Server source directory $server_src is missing."
exit 1
fi
mkdir -p "$mcp_config_dir"
mkdir -p "$mcp_config_dir"
if [[ ! -f $mcp_config ]]; then
info "Creating new VS Code MCP configuration at $mcp_config"
echo '{}' > "$mcp_config"
else
info "Updating existing VS Code MCP configuration at $mcp_config"
fi
if [[ ! -f $mcp_config ]]; then
info "Creating new VS Code MCP configuration at $mcp_config"
echo '{}' >"$mcp_config"
else
info "Updating existing VS Code MCP configuration at $mcp_config"
fi
tmp="$(mktemp)"
tmp="$(mktemp)"
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."
exit 1
fi
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."
exit 1
fi
jq \
--arg path "$server_src" \
'(.servers //= {}) |
jq \
--arg path "$server_src" \
'(.servers //= {}) |
.servers.unityMCP = {
command: "uv",
args: ["--directory", $path, "run", "server.py"],
type: "stdio"
}' \
"$mcp_config" > "$tmp"
"$mcp_config" >"$tmp"
mv "$tmp" "$mcp_config"
info "VS Code MCP server configuration updated for UnityMCP."
mv "$tmp" "$mcp_config"
info "VS Code MCP server configuration updated for UnityMCP."
}
verify_python_version() {
require_command python "python"
local version
version="$(
python - << 'PY'
require_command python "python"
local version
version="$(
python - <<'PY'
import sys
print("%d.%d.%d" % sys.version_info[:3])
PY
)"
local major minor
IFS='.' read -r major minor _ <<< "$version"
if ((major < 3 || (major == 3 && minor < 12))); then
error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing."
exit 1
fi
info "Python version $version satisfies requirement (>= 3.12)."
)"
local major minor
IFS='.' read -r major minor _ <<<"$version"
if ((major < 3 || (major == 3 && minor < 12))); then
error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing."
exit 1
fi
info "Python version $version satisfies requirement (>= 3.12)."
}
print_next_steps() {
cat << 'EOT'
cat <<'EOT'
Next steps:
1. Launch Unity Hub and install a Unity Editor version 2021.3 LTS or newer.
@ -212,22 +190,22 @@ EOT
}
main() {
if [[ ! -f /etc/arch-release ]]; then
error "This script is intended for Arch Linux."
exit 1
fi
if [[ ! -f /etc/arch-release ]]; then
error "This script is intended for Arch Linux."
exit 1
fi
info "Ensuring base dependencies are installed."
require_command sudo "sudo"
require_command pacman "pacman"
ensure_pacman_packages
verify_python_version
install_uv
ensure_unity_hub
sync_unity_mcp_repo
configure_vscode_mcp
print_next_steps
info "Setup complete. Follow the next steps above to finish configuration inside Unity."
info "Ensuring base dependencies are installed."
require_command sudo "sudo"
require_command pacman "pacman"
ensure_pacman_packages
verify_python_version
install_uv
ensure_unity_hub
sync_unity_mcp_repo
configure_vscode_mcp
print_next_steps
info "Setup complete. Follow the next steps above to finish configuration inside Unity."
}
main "$@"

View File

@ -17,16 +17,7 @@ shift "$COMMON_ARGS_SHIFT"
# Check for sudo privileges
require_root "$@"
echo "Periodic System Setup - Pacman Wrapper & Hosts File"
echo "==================================================="
echo "Current Date: $(date)"
echo "User: $USER"
echo "Original user: $(get_actual_user)"
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Mode: Interactive (prompts enabled)"
else
echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
fi
print_setup_header "Periodic System Setup - Pacman Wrapper & Hosts File"
# Get the directory where this script is located
CONFIG_DIR="$(dirname "$SCRIPT_DIR")"

View File

@ -16,16 +16,7 @@ shift "$COMMON_ARGS_SHIFT"
# Check for sudo privileges
require_root "$@"
echo "Thorium Browser Auto-Startup Setup"
echo "=================================="
echo "Current Date: $(date)"
echo "User: $USER"
echo "Original user: $(get_actual_user)"
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Mode: Interactive (prompts enabled)"
else
echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
fi
print_setup_header "Thorium Browser Auto-Startup Setup"
# Target URL
TARGET_URL="https://www.fitatu.com/app/planner"

View File

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

View File

@ -5,12 +5,17 @@
set -euo pipefail
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Default resolution
DEFAULT_RESOLUTION="320x240"
# Function to display usage
usage() {
cat << EOF
cat <<EOF
Usage: $0 <input_image> [resolution] [output_image]
Arguments:
@ -26,22 +31,16 @@ Examples:
Note: Requires ImageMagick (convert command)
EOF
exit 1
exit 1
}
# Check if ImageMagick is installed
if ! command -v convert &> /dev/null; then
echo "Error: ImageMagick (convert) is not installed."
echo "Install it with:"
echo " Arch Linux: sudo pacman -S imagemagick"
echo " Ubuntu/Debian: sudo apt install imagemagick"
exit 1
fi
require_imagemagick "convert" || exit 1
# Parse arguments
if [[ $# -lt 1 ]]; then
echo "Error: Missing required argument <input_image>"
usage
echo "Error: Missing required argument <input_image>"
usage
fi
INPUT_IMAGE="$1"
@ -50,32 +49,20 @@ OUTPUT_IMAGE="${3:-}"
# Validate input image exists
if [[ ! -f ${INPUT_IMAGE} ]]; then
echo "Error: Input image '${INPUT_IMAGE}' does not exist."
exit 1
echo "Error: Input image '${INPUT_IMAGE}' does not exist."
exit 1
fi
# Validate resolution format (WIDTHxHEIGHT)
if [[ ! ${RESOLUTION} =~ ^[0-9]+x[0-9]+$ ]]; then
echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1
if ! validate_resolution "$RESOLUTION"; then
echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1
fi
# Generate output filename if not provided
if [[ -z ${OUTPUT_IMAGE} ]]; then
# Extract filename without extension and extension
BASENAME=$(basename "${INPUT_IMAGE}")
FILENAME="${BASENAME%.*}"
EXTENSION="${BASENAME##*.}"
# If no extension (single name file), default to jpg
if [[ ${FILENAME} == "${EXTENSION}" ]]; then
EXTENSION="jpg"
fi
# Create output filename with resolution suffix
DIRNAME=$(dirname "${INPUT_IMAGE}")
OUTPUT_IMAGE="${DIRNAME}/${FILENAME}_${RESOLUTION}.${EXTENSION}"
OUTPUT_IMAGE=$(generate_output_filename "${INPUT_IMAGE}" "_${RESOLUTION}")
fi
# Perform the conversion
@ -83,15 +70,15 @@ echo "Converting '${INPUT_IMAGE}' to ${RESOLUTION}..."
echo "Output will be saved to: ${OUTPUT_IMAGE}"
if convert "${INPUT_IMAGE}" -resize "${RESOLUTION}!" "${OUTPUT_IMAGE}"; then
echo "✓ Successfully converted image to ${RESOLUTION}"
echo "Output: ${OUTPUT_IMAGE}"
echo "✓ Successfully converted image to ${RESOLUTION}"
echo "Output: ${OUTPUT_IMAGE}"
# Show file sizes
INPUT_SIZE=$(du -h "${INPUT_IMAGE}" | cut -f1)
OUTPUT_SIZE=$(du -h "${OUTPUT_IMAGE}" | cut -f1)
echo "Input size: ${INPUT_SIZE}"
echo "Output size: ${OUTPUT_SIZE}"
# Show file sizes
INPUT_SIZE=$(du -h "${INPUT_IMAGE}" | cut -f1)
OUTPUT_SIZE=$(du -h "${OUTPUT_IMAGE}" | cut -f1)
echo "Input size: ${INPUT_SIZE}"
echo "Output size: ${OUTPUT_SIZE}"
else
echo "✗ Error: Conversion failed"
exit 1
echo "✗ Error: Conversion failed"
exit 1
fi

View File

@ -7,16 +7,17 @@ set -euo pipefail
# Convert one or more PDF files to image files using ImageMagick v7 `magick`.
# Default output format is jpg, but can be changed with -f.
# Source common library
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
OUTPUT_DIR=""
OUTPUT_FORMAT="jpg"
PDF_FILES=()
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
usage() {
cat << EOF
cat <<EOF
Usage:
$(basename "$0") [OPTIONS] PDF_FILE [PDF_FILE...]
@ -35,83 +36,80 @@ EOF
}
ensure_magick() {
if ! command -v magick > /dev/null 2>&1; then
echo "Error: 'magick' (ImageMagick v7) is not installed or not in PATH." >&2
exit 1
fi
require_imagemagick "magick" || exit 1
}
parse_args() {
local opt
OUTPUT_DIR=""
OUTPUT_FORMAT="jpg"
PDF_FILES=()
local opt
OUTPUT_DIR=""
OUTPUT_FORMAT="jpg"
PDF_FILES=()
while getopts ":o:f:h" opt; do
case "$opt" in
o)
OUTPUT_DIR="$OPTARG"
;;
f)
OUTPUT_FORMAT="$OPTARG"
;;
h)
usage
exit 0
;;
*)
usage
exit 1
;;
esac
done
while getopts ":o:f:h" opt; do
case "$opt" in
o)
OUTPUT_DIR="$OPTARG"
;;
f)
OUTPUT_FORMAT="$OPTARG"
;;
h)
usage
exit 0
;;
*)
usage
exit 1
;;
esac
done
shift $((OPTIND - 1))
shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then
echo "Error: at least one PDF file must be specified." >&2
usage
exit 1
fi
if [[ $# -lt 1 ]]; then
echo "Error: at least one PDF file must be specified." >&2
usage
exit 1
fi
PDF_FILES=("$@")
PDF_FILES=("$@")
if [[ -z ${OUTPUT_DIR:-} ]]; then
OUTPUT_DIR="${PWD}"
fi
if [[ -z ${OUTPUT_DIR:-} ]]; then
OUTPUT_DIR="${PWD}"
fi
if [[ ! -d $OUTPUT_DIR ]]; then
mkdir -p "$OUTPUT_DIR"
fi
if [[ ! -d $OUTPUT_DIR ]]; then
mkdir -p "$OUTPUT_DIR"
fi
}
convert_pdf() {
local pdf_file="$1"
local base name out_pattern
local pdf_file="$1"
local base name out_pattern
name="$(basename "$pdf_file")"
base="${name%.*}"
out_pattern="${OUTPUT_DIR%/}/${base}_page-"
name="$(basename "$pdf_file")"
base="${name%.*}"
out_pattern="${OUTPUT_DIR%/}/${base}_page-"
log "Converting '$pdf_file' to $OUTPUT_FORMAT using magick -> ${out_pattern}*.${OUTPUT_FORMAT}"
magick -density 300 "$pdf_file" -quality 90 "${out_pattern}%d.${OUTPUT_FORMAT}"
log "Converting '$pdf_file' to $OUTPUT_FORMAT using magick -> ${out_pattern}*.${OUTPUT_FORMAT}"
magick -density 300 "$pdf_file" -quality 90 "${out_pattern}%d.${OUTPUT_FORMAT}"
}
main() {
ensure_magick
parse_args "$@"
ensure_magick
parse_args "$@"
local pdf
for pdf in "${PDF_FILES[@]}"; do
if [[ ! -f $pdf ]]; then
echo "Warning: '$pdf' is not a regular file, skipping." >&2
continue
fi
local pdf
for pdf in "${PDF_FILES[@]}"; do
if [[ ! -f $pdf ]]; then
echo "Warning: '$pdf' is not a regular file, skipping." >&2
continue
fi
convert_pdf "$pdf"
done
convert_pdf "$pdf"
done
log "Done converting PDFs to ${OUTPUT_FORMAT}. Output directory: $OUTPUT_DIR"
log "Done converting PDFs to ${OUTPUT_FORMAT}. Output directory: $OUTPUT_DIR"
}
main "$@"

View File

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

View File

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

View File

@ -2,64 +2,42 @@
set -euo pipefail
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Configuration -----------------------------------------------------------------
TARGET_SESSION_NAME="Xfce Session"
TARGET_PACKAGES=(
xfwm4 # Compositing window manager with XFCE integration
xfce4-session # Provides the Xfce session entry for display managers
xfce4-panel # Panel with system tray support
xfce4-settings # Settings daemon (enables compositing toggle, theming, etc.)
xfce4-terminal # Handy default terminal for the new environment
xfwm4 # Compositing window manager with XFCE integration
xfce4-session # Provides the Xfce session entry for display managers
xfce4-panel # Panel with system tray support
xfce4-settings # Settings daemon (enables compositing toggle, theming, etc.)
xfce4-terminal # Handy default terminal for the new environment
)
# Utility functions --------------------------------------------------------------
info() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
error() {
echo "[ERROR] $*" >&2
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
echo "[ERROR] $*" >&2
exit 1
}
ensure_pacman() {
require_command pacman "pacman"
if ! grep -qi "arch" /etc/os-release 2> /dev/null; then
warn "This script was designed for Arch Linux; continuing anyway."
fi
require_command pacman "pacman" || error "Required command 'pacman' not found."
if ! grep -qi "arch" /etc/os-release 2>/dev/null; then
warn "This script was designed for Arch Linux; continuing anyway."
fi
}
install_packages() {
local missing=()
for pkg in "${TARGET_PACKAGES[@]}"; do
if ! pacman -Qi "$pkg" > /dev/null 2>&1; then
missing+=("$pkg")
fi
done
if [[ ${#missing[@]} -eq 0 ]]; then
info "All target packages are already installed."
return
fi
if ! command -v sudo > /dev/null 2>&1; then
error "sudo is required to install packages. Install sudo or run this script as root."
fi
info "Installing missing packages: ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}"
install_missing_pacman_packages "${TARGET_PACKAGES[@]}"
}
print_post_install_tips() {
cat << EOF
cat <<EOF
------------------------------------------------------------------------
XFCE session installed.
@ -76,31 +54,31 @@ EOF
}
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
info "Terminating current session (ID: $session_id) via loginctl."
loginctl terminate-session "$session_id"
return
fi
if [[ -n $session_id ]] && loginctl show-session "$session_id" >/dev/null 2>&1; then
info "Terminating current session (ID: $session_id) via loginctl."
loginctl terminate-session "$session_id"
return
fi
if loginctl list-sessions 2> /dev/null | awk '{print $1" "$3}' | grep -q " $USER$"; then
info "Terminating all sessions for user '$USER' via loginctl."
loginctl terminate-user "$USER"
return
fi
if loginctl list-sessions 2>/dev/null | awk '{print $1" "$3}' | grep -q " $USER$"; then
info "Terminating all sessions for user '$USER' via loginctl."
loginctl terminate-user "$USER"
return
fi
warn "loginctl could not terminate the session; attempting fallback logout."
pkill -KILL -u "$USER" || error "Failed to terminate user sessions. Please log out manually."
warn "loginctl could not terminate the session; attempting fallback logout."
pkill -KILL -u "$USER" || error "Failed to terminate user sessions. Please log out manually."
}
main() {
ensure_pacman
install_packages
print_post_install_tips
ensure_pacman
install_packages
print_post_install_tips
# Give the user a moment to read the instructions before logging out.
logout_user
# Give the user a moment to read the instructions before logging out.
logout_user
}
main "$@"

View File

@ -6,12 +6,17 @@
set -euo pipefail
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# Default resolution
DEFAULT_RESOLUTION="320x240"
# Function to display usage
usage() {
cat << EOF
cat <<EOF
Usage: $0 <input_text_file> [resolution] [output_prefix]
Arguments:
@ -26,26 +31,16 @@ Examples:
Note: Requires ImageMagick (magick or convert command)
EOF
exit 1
exit 1
}
# Check if ImageMagick is installed and determine which command to use
if command -v magick &> /dev/null; then
MAGICK_CMD="magick"
elif command -v convert &> /dev/null; then
MAGICK_CMD="convert"
else
echo "Error: ImageMagick is not installed."
echo "Install it with:"
echo " Arch Linux: sudo pacman -S imagemagick"
echo " Ubuntu/Debian: sudo apt install imagemagick"
exit 1
fi
require_imagemagick || exit 1
# Parse arguments
if [[ $# -lt 1 ]]; then
echo "Error: Missing required argument <input_text_file>"
usage
echo "Error: Missing required argument <input_text_file>"
usage
fi
INPUT_FILE="$1"
@ -54,15 +49,15 @@ OUTPUT_PREFIX="${3:-}"
# Validate input file exists
if [[ ! -f ${INPUT_FILE} ]]; then
echo "Error: Input file '${INPUT_FILE}' does not exist."
exit 1
echo "Error: Input file '${INPUT_FILE}' does not exist."
exit 1
fi
# Validate resolution format (WIDTHxHEIGHT)
if [[ ! ${RESOLUTION} =~ ^[0-9]+x[0-9]+$ ]]; then
echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1
if ! validate_resolution "$RESOLUTION"; then
echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1
fi
# Extract width and height
@ -72,22 +67,22 @@ HEIGHT=$(echo "${RESOLUTION}" | cut -d'x' -f2)
# Calculate font size based on resolution
FONT_SIZE=$((WIDTH / 30))
if [[ ${FONT_SIZE} -lt 8 ]]; then
FONT_SIZE=8
FONT_SIZE=8
fi
# Generate output prefix if not provided
if [[ -z ${OUTPUT_PREFIX} ]]; then
BASENAME=$(basename "${INPUT_FILE}")
FILENAME="${BASENAME%.*}"
DIRNAME=$(dirname "${INPUT_FILE}")
OUTPUT_PREFIX="${DIRNAME}/${FILENAME}"
BASENAME=$(basename "${INPUT_FILE}")
FILENAME="${BASENAME%.*}"
DIRNAME=$(dirname "${INPUT_FILE}")
OUTPUT_PREFIX="${DIRNAME}/${FILENAME}"
fi
# Calculate lines per image based on resolution and font size
# Rough estimate: height / (font_size * 1.5) for line spacing
LINES_PER_IMAGE=$((HEIGHT / (FONT_SIZE * 3 / 2)))
if [[ ${LINES_PER_IMAGE} -lt 5 ]]; then
LINES_PER_IMAGE=5
LINES_PER_IMAGE=5
fi
echo "Converting text file to image(s)..."
@ -96,7 +91,7 @@ echo "Font size: ${FONT_SIZE}"
echo "Estimated lines per image: ${LINES_PER_IMAGE}"
# Read the file and count total lines
mapfile -t LINES < "${INPUT_FILE}"
mapfile -t LINES <"${INPUT_FILE}"
TOTAL_LINES=${#LINES[@]}
echo "Total lines in file: ${TOTAL_LINES}"
@ -113,53 +108,53 @@ trap 'rm -rf ${TEMP_DIR}' EXIT
# Split text into chunks and create images
IMAGE_COUNT=0
for ((i = 0; i < TOTAL_LINES; i += LINES_PER_IMAGE)); do
IMAGE_COUNT=$((IMAGE_COUNT + 1))
IMAGE_COUNT=$((IMAGE_COUNT + 1))
# Calculate end line for this chunk
END_LINE=$((i + LINES_PER_IMAGE))
if [[ ${END_LINE} -gt ${TOTAL_LINES} ]]; then
END_LINE=${TOTAL_LINES}
fi
# Calculate end line for this chunk
END_LINE=$((i + LINES_PER_IMAGE))
if [[ ${END_LINE} -gt ${TOTAL_LINES} ]]; then
END_LINE=${TOTAL_LINES}
fi
# Create chunk file
CHUNK_FILE="${TEMP_DIR}/chunk_${IMAGE_COUNT}.txt"
for ((j = i; j < END_LINE; j++)); do
echo "${LINES[$j]}" >> "${CHUNK_FILE}"
done
# Create chunk file
CHUNK_FILE="${TEMP_DIR}/chunk_${IMAGE_COUNT}.txt"
for ((j = i; j < END_LINE; j++)); do
echo "${LINES[$j]}" >>"${CHUNK_FILE}"
done
# Determine output filename
if [[ ${NUM_IMAGES} -eq 1 ]]; then
OUTPUT_FILE="${OUTPUT_PREFIX}.png"
else
OUTPUT_FILE="${OUTPUT_PREFIX}_$(printf "%03d" ${IMAGE_COUNT}).png"
fi
# Determine output filename
if [[ ${NUM_IMAGES} -eq 1 ]]; then
OUTPUT_FILE="${OUTPUT_PREFIX}.png"
else
OUTPUT_FILE="${OUTPUT_PREFIX}_$(printf "%03d" ${IMAGE_COUNT}).png"
fi
echo " Creating image ${IMAGE_COUNT}/${NUM_IMAGES}: ${OUTPUT_FILE}"
echo " Creating image ${IMAGE_COUNT}/${NUM_IMAGES}: ${OUTPUT_FILE}"
# Create image from text
# Using label: instead of caption: for better control
if ${MAGICK_CMD} -size "${WIDTH}x${HEIGHT}" \
-background white \
-fill black \
-font "DejaVu-Sans-Mono" \
-pointsize "${FONT_SIZE}" \
-gravity northwest \
label:@"${CHUNK_FILE}" \
-extent "${WIDTH}x${HEIGHT}" \
"${OUTPUT_FILE}"; then
OUTPUT_SIZE=$(du -h "${OUTPUT_FILE}" | cut -f1)
echo " ✓ Created: ${OUTPUT_FILE} (${OUTPUT_SIZE})"
else
echo " ✗ Failed to create: ${OUTPUT_FILE}"
exit 1
fi
# Create image from text
# Using label: instead of caption: for better control
if ${MAGICK_CMD} -size "${WIDTH}x${HEIGHT}" \
-background white \
-fill black \
-font "DejaVu-Sans-Mono" \
-pointsize "${FONT_SIZE}" \
-gravity northwest \
label:@"${CHUNK_FILE}" \
-extent "${WIDTH}x${HEIGHT}" \
"${OUTPUT_FILE}"; then
OUTPUT_SIZE=$(du -h "${OUTPUT_FILE}" | cut -f1)
echo " ✓ Created: ${OUTPUT_FILE} (${OUTPUT_SIZE})"
else
echo " ✗ Failed to create: ${OUTPUT_FILE}"
exit 1
fi
done
echo ""
echo "✓ Successfully created ${IMAGE_COUNT} image(s)"
echo "Output files:"
if [[ ${NUM_IMAGES} -eq 1 ]]; then
echo " ${OUTPUT_PREFIX}.png"
echo " ${OUTPUT_PREFIX}.png"
else
echo " ${OUTPUT_PREFIX}_001.png to ${OUTPUT_PREFIX}_$(printf "%03d" ${IMAGE_COUNT}).png"
echo " ${OUTPUT_PREFIX}_001.png to ${OUTPUT_PREFIX}_$(printf "%03d" ${IMAGE_COUNT}).png"
fi

View File

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