Fix shell script formatting and add PR workflow validation (#3)

* Initial plan

* fix: format shell scripts with shfmt (convert tabs to 2 spaces)

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

* feat: enhance shell-check workflow for PR pre-merge validation

- Add pull_request_target trigger to check PRs from forks
- Add explicit failure message with instructions
- Create BRANCH_PROTECTION.md with setup guide
- Ensure workflow runs on all PRs targeting main/master

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

* refactor: improve workflow security and remove redundant exit code

- Remove pull_request_target to avoid executing untrusted fork code
- Remove redundant exit 1 from failure step
- Update documentation to reflect changes
- Standard pull_request trigger handles forks securely

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>
This commit is contained in:
Copilot 2026-01-07 22:52:20 +01:00 committed by GitHub
parent c72ddb6ddb
commit 18b9f020bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 13558 additions and 13476 deletions

71
.github/BRANCH_PROTECTION.md vendored Normal file
View File

@ -0,0 +1,71 @@
# Branch Protection and Pre-Merge Checks
This repository uses GitHub Actions to ensure code quality before merging to `main` or `master` branches.
## Required Checks
### Shell Script Linting
The `Shell Script Linting` workflow automatically runs on:
- Pull requests targeting `main` or `master` branches (including from forks)
- Direct pushes to `main` or `master` branches
This workflow checks:
- Shell script syntax with `shellcheck`
- Code formatting with `shfmt` (2-space indentation, no tabs)
- Optional checks: `checkbashisms`, syntax validation
## Enabling Branch Protection
To make the shell linting check **required** before merging PRs, follow these steps:
1. Go to repository **Settings** → **Branches**
2. Click **Add rule** or edit existing rule for `main`/`master`
3. Configure the following settings:
- ✅ **Require a pull request before merging**
- ✅ **Require status checks to pass before merging**
- Search for and select: `Lint Shell Scripts`
- ✅ **Require branches to be up to date before merging** (recommended)
- ✅ **Do not allow bypassing the above settings** (recommended)
4. Click **Create** or **Save changes**
## Running Checks Locally
Before pushing changes, run the linting script locally to catch issues early:
```bash
bash scripts/meta/shell_check.sh
```
This will:
- Install required linters on Arch Linux (if needed)
- Check all shell scripts in the repository
- Report any formatting or syntax issues
To auto-fix formatting issues:
```bash
# Install shfmt if not already installed
# On Arch: sudo pacman -S shfmt
# Or download from: https://github.com/mvdan/sh/releases
# Fix formatting in-place
find . -name "*.sh" -type f | xargs shfmt -w -i 2 -ci -sr -s
```
## What Gets Checked
The workflow validates shell scripts with these extensions or shebangs:
- `*.sh`, `*.bash`, `*.zsh` files
- Executable files with shell shebangs (`#!/bin/bash`, `#!/bin/sh`, etc.)
## Troubleshooting
If the check fails on your PR:
1. Review the workflow logs to see which files failed
2. Run `bash scripts/meta/shell_check.sh` locally to reproduce
3. Fix the issues (usually formatting with `shfmt -w -i 2 -ci -sr -s`)
4. Commit and push the fixes
The workflow will automatically re-run on new commits to the PR.

View File

@ -48,3 +48,10 @@ jobs:
- name: Report status - name: Report status
if: success() if: success()
run: echo "✅ All shell scripts passed linting checks!" run: echo "✅ All shell scripts passed linting checks!"
- name: Provide help on failure
if: failure()
run: |
echo "❌ Shell script linting failed!"
echo "This check is required to merge PRs into main/master."
echo "Please run 'bash scripts/meta/shell_check.sh' locally and fix any issues."

View File

@ -13,8 +13,8 @@
set -e set -e
[ "${GPU_VENDOR}" = "amd" ] || { [ "${GPU_VENDOR}" = "amd" ] || {
echo "AMD installer invoked but GPU_VENDOR=${GPU_VENDOR}" echo "AMD installer invoked but GPU_VENDOR=${GPU_VENDOR}"
exit 0 exit 0
} }
AMD_INSTALL_XF86=${AMD_INSTALL_XF86:-0} AMD_INSTALL_XF86=${AMD_INSTALL_XF86:-0}
@ -32,9 +32,9 @@ warn() { echo "[amd][warn] $*" >&2; }
# Detect multilib enabled # Detect multilib enabled
if grep -q '^\[multilib\]' /etc/pacman.conf; then if grep -q '^\[multilib\]' /etc/pacman.conf; then
MULTILIB_ENABLED=1 MULTILIB_ENABLED=1
else else
MULTILIB_ENABLED=0 MULTILIB_ENABLED=0
fi fi
# Basic packages # Basic packages
@ -58,49 +58,49 @@ LIB32_AMDVLK_PKG="lib32-amdvlk"
# Simple AUR builder (reused from NVIDIA script style) # Simple AUR builder (reused from NVIDIA script style)
_build_aur_pkg() { _build_aur_pkg() {
local pkg="$1" local pkg="$1"
local url="https://aur.archlinux.org/${pkg}.git" local url="https://aur.archlinux.org/${pkg}.git"
mkdir -p "$HOME/aur" mkdir -p "$HOME/aur"
cd "$HOME/aur" cd "$HOME/aur"
if [ ! -d "$pkg" ]; then git clone "$url"; else (cd "$pkg" && git fetch -q --all && git reset -q --hard origin/HEAD || git pull --ff-only || true); fi if [ ! -d "$pkg" ]; then git clone "$url"; else (cd "$pkg" && git fetch -q --all && git reset -q --hard origin/HEAD || git pull --ff-only || true); fi
cd "$pkg" cd "$pkg"
rm -f -- *.pkg.tar.* 2>/dev/null || true rm -f -- *.pkg.tar.* 2> /dev/null || true
yes | makepkg -s -c -C --noconfirm --needed yes | makepkg -s -c -C --noconfirm --needed
local built=(*.pkg.tar.zst) local built=(*.pkg.tar.zst)
yes | sudo pacman -U --noconfirm "${built[@]}" yes | sudo pacman -U --noconfirm "${built[@]}"
} }
_install_repo_or_aur() { _install_repo_or_aur() {
local pkg="$1" local pkg="$1"
if pacman -Si "$pkg" >/dev/null 2>&1; then if pacman -Si "$pkg" > /dev/null 2>&1; then
if pacman -Qi "$pkg" >/dev/null 2>&1; then if pacman -Qi "$pkg" > /dev/null 2>&1; then
vlog "$pkg already installed" vlog "$pkg already installed"
else else
yes | sudo pacman -Sy --noconfirm "$pkg" yes | sudo pacman -Sy --noconfirm "$pkg"
fi fi
else else
info "Building AUR package: $pkg" info "Building AUR package: $pkg"
_build_aur_pkg "$pkg" _build_aur_pkg "$pkg"
fi fi
} }
info "Installing AMD GPU stack" info "Installing AMD GPU stack"
for p in "${BASE_PKGS[@]}" "$VULKAN_PKG"; do _install_repo_or_aur "$p"; done for p in "${BASE_PKGS[@]}" "$VULKAN_PKG"; do _install_repo_or_aur "$p"; done
if [ "$AMD_INSTALL_XF86" = 1 ]; then if [ "$AMD_INSTALL_XF86" = 1 ]; then
_install_repo_or_aur "$XF86_PKG" _install_repo_or_aur "$XF86_PKG"
fi fi
# AMDVLK optional (install after vulkan-radeon if requested) # AMDVLK optional (install after vulkan-radeon if requested)
if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then
_install_repo_or_aur "$AMDVLK_PKG" _install_repo_or_aur "$AMDVLK_PKG"
fi fi
if [ $MULTILIB_ENABLED = 1 ] || [ "$AMD_INSTALL_LIB32" = 1 ]; then if [ $MULTILIB_ENABLED = 1 ] || [ "$AMD_INSTALL_LIB32" = 1 ]; then
for p in "${LIB32_BASE[@]}" "$LIB32_VULKAN_PKG"; do _install_repo_or_aur "$p"; done for p in "${LIB32_BASE[@]}" "$LIB32_VULKAN_PKG"; do _install_repo_or_aur "$p"; done
if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then _install_repo_or_aur "$LIB32_AMDVLK_PKG"; fi if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then _install_repo_or_aur "$LIB32_AMDVLK_PKG"; fi
else else
vlog "Skipping 32-bit packages (multilib disabled)" vlog "Skipping 32-bit packages (multilib disabled)"
fi fi
# Detect SI / CIK codename presence for optional amdgpu enablement # Detect SI / CIK codename presence for optional amdgpu enablement
@ -113,41 +113,41 @@ for n in "${SI_NAMES[@]}"; do echo "$GPU_LINES" | grep -q "$n" && IS_SI=1 && bre
for n in "${CIK_NAMES[@]}"; do echo "$GPU_LINES" | grep -q "$n" && IS_CIK=1 && break; done for n in "${CIK_NAMES[@]}"; do echo "$GPU_LINES" | grep -q "$n" && IS_CIK=1 && break; done
if [ "$AMD_ENABLE_SI_CIK" = "1" ] || { [ "$AMD_ENABLE_SI_CIK" = "auto" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; }; then if [ "$AMD_ENABLE_SI_CIK" = "1" ] || { [ "$AMD_ENABLE_SI_CIK" = "auto" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; }; then
info "Configuring amdgpu for SI/CIK (IS_SI=$IS_SI IS_CIK=$IS_CIK)" info "Configuring amdgpu for SI/CIK (IS_SI=$IS_SI IS_CIK=$IS_CIK)"
TMP_CONF=$(mktemp) TMP_CONF=$(mktemp)
printf 'options amdgpu si_support=1\noptions amdgpu cik_support=1\n' >"$TMP_CONF" printf 'options amdgpu si_support=1\noptions amdgpu cik_support=1\n' > "$TMP_CONF"
printf 'options radeon si_support=0\noptions radeon cik_support=0\n' >>"$TMP_CONF" printf 'options radeon si_support=0\noptions radeon cik_support=0\n' >> "$TMP_CONF"
sudo mkdir -p /etc/modprobe.d sudo mkdir -p /etc/modprobe.d
sudo cp "$TMP_CONF" /etc/modprobe.d/10-amdgpu-si-cik.conf sudo cp "$TMP_CONF" /etc/modprobe.d/10-amdgpu-si-cik.conf
rm -f "$TMP_CONF" rm -f "$TMP_CONF"
# Ensure amdgpu early in MODULES # Ensure amdgpu early in MODULES
if [ -f /etc/mkinitcpio.conf ]; then if [ -f /etc/mkinitcpio.conf ]; then
if ! grep -q '^MODULES=.*amdgpu' /etc/mkinitcpio.conf; then if ! grep -q '^MODULES=.*amdgpu' /etc/mkinitcpio.conf; then
sudo sed -i 's/^MODULES=\(.*\)/MODULES=(amdgpu radeon)/' /etc/mkinitcpio.conf || true sudo sed -i 's/^MODULES=\(.*\)/MODULES=(amdgpu radeon)/' /etc/mkinitcpio.conf || true
fi fi
if ! grep -q 'modconf' /etc/mkinitcpio.conf; then if ! grep -q 'modconf' /etc/mkinitcpio.conf; then
warn "modconf hook not found in mkinitcpio.conf (needed for module options)" warn "modconf hook not found in mkinitcpio.conf (needed for module options)"
fi fi
if [ "$AMD_SKIP_INITRAMFS" != 1 ]; then if [ "$AMD_SKIP_INITRAMFS" != 1 ]; then
info "Regenerating initramfs (mkinitcpio -P)" info "Regenerating initramfs (mkinitcpio -P)"
sudo mkinitcpio -P || warn "mkinitcpio failed; review manually" sudo mkinitcpio -P || warn "mkinitcpio failed; review manually"
else else
info "Skipping initramfs regeneration per AMD_SKIP_INITRAMFS=1" info "Skipping initramfs regeneration per AMD_SKIP_INITRAMFS=1"
fi fi
else else
warn "/etc/mkinitcpio.conf not found; skipping MODULES update" warn "/etc/mkinitcpio.conf not found; skipping MODULES update"
fi fi
else else
vlog "SI/CIK enablement not required (AMD_ENABLE_SI_CIK=$AMD_ENABLE_SI_CIK IS_SI=$IS_SI IS_CIK=$IS_CIK)" vlog "SI/CIK enablement not required (AMD_ENABLE_SI_CIK=$AMD_ENABLE_SI_CIK IS_SI=$IS_SI IS_CIK=$IS_CIK)"
fi fi
# Check active kernel driver # Check active kernel driver
KDRV=$(lspci -k -d ::0300 2>/dev/null | awk '/Kernel driver in use:/ {print $5; exit}') KDRV=$(lspci -k -d ::0300 2> /dev/null | awk '/Kernel driver in use:/ {print $5; exit}')
[ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'amdgpu|radeon' | head -n1 | awk '{print $1}') [ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'amdgpu|radeon' | head -n1 | awk '{print $1}')
info "Kernel driver in use: ${KDRV:-unknown}" info "Kernel driver in use: ${KDRV:-unknown}"
if [ "$KDRV" = "radeon" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; then if [ "$KDRV" = "radeon" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; then
warn "radeon driver still active for SI/CIK; reboot may be required to switch to amdgpu" warn "radeon driver still active for SI/CIK; reboot may be required to switch to amdgpu"
fi fi
export AMD_STACK_DONE=1 export AMD_STACK_DONE=1

View File

@ -13,8 +13,8 @@
set -e set -e
[ "$GPU_VENDOR" = "intel" ] || { [ "$GPU_VENDOR" = "intel" ] || {
echo "Intel installer invoked but GPU_VENDOR=$GPU_VENDOR" echo "Intel installer invoked but GPU_VENDOR=$GPU_VENDOR"
exit 0 exit 0
} }
INTEL_USE_AMBER=${INTEL_USE_AMBER:-0} INTEL_USE_AMBER=${INTEL_USE_AMBER:-0}
@ -35,24 +35,24 @@ if grep -q '^\[multilib\]' /etc/pacman.conf; then MULTILIB=1; else MULTILIB=0; f
# Base mesa package # Base mesa package
if [ "$INTEL_USE_AMBER" = 1 ]; then if [ "$INTEL_USE_AMBER" = 1 ]; then
BASE_MESA=mesa-amber BASE_MESA=mesa-amber
LIB32_BASE=lib32-mesa-amber LIB32_BASE=lib32-mesa-amber
else else
BASE_MESA=mesa BASE_MESA=mesa
LIB32_BASE=lib32-mesa LIB32_BASE=lib32-mesa
fi fi
install_pkg() { install_pkg() {
local pkg="$1" local pkg="$1"
if pacman -Qi "$pkg" >/dev/null 2>&1; then if pacman -Qi "$pkg" > /dev/null 2>&1; then
vlog "$pkg already installed" vlog "$pkg already installed"
else else
if pacman -Si "$pkg" >/dev/null 2>&1; then if pacman -Si "$pkg" > /dev/null 2>&1; then
yes | sudo pacman -Sy --noconfirm "$pkg" yes | sudo pacman -Sy --noconfirm "$pkg"
else else
warn "Package $pkg not found in repos (not handling AUR here)" warn "Package $pkg not found in repos (not handling AUR here)"
fi fi
fi fi
} }
info "Installing Intel GPU stack" info "Installing Intel GPU stack"
@ -60,47 +60,47 @@ install_pkg "$BASE_MESA"
# 32-bit mesa # 32-bit mesa
if { [ "$INTEL_INSTALL_LIB32" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32" = 1 ]; then if { [ "$INTEL_INSTALL_LIB32" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32" = 1 ]; then
install_pkg "$LIB32_BASE" install_pkg "$LIB32_BASE"
else else
vlog "Skipping 32-bit mesa (INTEL_INSTALL_LIB32=$INTEL_INSTALL_LIB32 MULTILIB=$MULTILIB)" vlog "Skipping 32-bit mesa (INTEL_INSTALL_LIB32=$INTEL_INSTALL_LIB32 MULTILIB=$MULTILIB)"
fi fi
# Vulkan # Vulkan
if [ "$INTEL_INSTALL_VULKAN" = 1 ]; then if [ "$INTEL_INSTALL_VULKAN" = 1 ]; then
install_pkg vulkan-intel install_pkg vulkan-intel
if { [ "$INTEL_INSTALL_LIB32_VK" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32_VK" = 1 ]; then if { [ "$INTEL_INSTALL_LIB32_VK" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32_VK" = 1 ]; then
install_pkg lib32-vulkan-intel install_pkg lib32-vulkan-intel
else else
vlog "Skipping 32-bit vulkan (INTEL_INSTALL_LIB32_VK=$INTEL_INSTALL_LIB32_VK MULTILIB=$MULTILIB)" vlog "Skipping 32-bit vulkan (INTEL_INSTALL_LIB32_VK=$INTEL_INSTALL_LIB32_VK MULTILIB=$MULTILIB)"
fi fi
fi fi
# Legacy xf86-video-intel (not recommended) # Legacy xf86-video-intel (not recommended)
if [ "$INTEL_INSTALL_XF86" = 1 ]; then if [ "$INTEL_INSTALL_XF86" = 1 ]; then
install_pkg xf86-video-intel install_pkg xf86-video-intel
else else
vlog "Not installing xf86-video-intel (INTEL_INSTALL_XF86=$INTEL_INSTALL_XF86)" vlog "Not installing xf86-video-intel (INTEL_INSTALL_XF86=$INTEL_INSTALL_XF86)"
fi fi
# GuC / HuC enablement # GuC / HuC enablement
if [ -n "$INTEL_ENABLE_GUC" ]; then if [ -n "$INTEL_ENABLE_GUC" ]; then
if ! echo "$INTEL_ENABLE_GUC" | grep -Eq '^[0-3]$'; then if ! echo "$INTEL_ENABLE_GUC" | grep -Eq '^[0-3]$'; then
warn "INTEL_ENABLE_GUC must be 0..3; ignoring" warn "INTEL_ENABLE_GUC must be 0..3; ignoring"
else else
info "Configuring enable_guc=$INTEL_ENABLE_GUC" info "Configuring enable_guc=$INTEL_ENABLE_GUC"
sudo mkdir -p /etc/modprobe.d sudo mkdir -p /etc/modprobe.d
echo "options i915 enable_guc=$INTEL_ENABLE_GUC" | sudo tee /etc/modprobe.d/i915-guc.conf >/dev/null echo "options i915 enable_guc=$INTEL_ENABLE_GUC" | sudo tee /etc/modprobe.d/i915-guc.conf > /dev/null
if [ "$INTEL_SKIP_INITRAMFS" != 1 ] && [ -f /etc/mkinitcpio.conf ]; then if [ "$INTEL_SKIP_INITRAMFS" != 1 ] && [ -f /etc/mkinitcpio.conf ]; then
info "Regenerating initramfs (mkinitcpio -P) for GuC/HuC change" info "Regenerating initramfs (mkinitcpio -P) for GuC/HuC change"
sudo mkinitcpio -P || warn "mkinitcpio failed; continue manually" sudo mkinitcpio -P || warn "mkinitcpio failed; continue manually"
else else
info "Skipping initramfs regeneration (INTEL_SKIP_INITRAMFS=$INTEL_SKIP_INITRAMFS)" info "Skipping initramfs regeneration (INTEL_SKIP_INITRAMFS=$INTEL_SKIP_INITRAMFS)"
fi fi
fi fi
fi fi
# Report kernel driver # Report kernel driver
KDRV=$(lspci -k -d ::0300 2>/dev/null | awk '/Kernel driver in use:/ {print $5; exit}') KDRV=$(lspci -k -d ::0300 2> /dev/null | awk '/Kernel driver in use:/ {print $5; exit}')
[ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'i915|xe' | head -n1 | awk '{print $1}') [ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'i915|xe' | head -n1 | awk '{print $1}')
info "Kernel driver in use: ${KDRV:-unknown}" info "Kernel driver in use: ${KDRV:-unknown}"

View File

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

View File

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

View File

@ -16,7 +16,7 @@ collapse_mounts
# Run enforcement script if available # Run enforcement script if available
if [[ -x $ENFORCE ]]; then if [[ -x $ENFORCE ]]; then
"$ENFORCE" >/dev/null 2>&1 || true "$ENFORCE" > /dev/null 2>&1 || true
fi fi
# Apply protections # Apply protections

View File

@ -20,7 +20,7 @@ collapse_mounts
# Ensure writable by remounting if still read-only # Ensure writable by remounting if still read-only
if is_ro_mount; then if is_ro_mount; then
mount -o remount,rw "$TARGET" >/dev/null 2>&1 || collapse_mounts mount -o remount,rw "$TARGET" > /dev/null 2>&1 || collapse_mounts
fi fi
log_hook "pre" "unlocking(done)" log_hook "pre" "unlocking(done)"

View File

@ -2,7 +2,7 @@
# Re-run with sudo if not root # Re-run with sudo if not root
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
exec sudo -E bash "$0" "$@" exec sudo -E bash "$0" "$@"
fi fi
# Options # Options
@ -11,18 +11,18 @@ FLUSH_DNS=0
# Parse CLI flags # Parse CLI flags
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--flush-dns) --flush-dns)
FLUSH_DNS=1 FLUSH_DNS=1
;; ;;
--no-flush-dns) --no-flush-dns)
FLUSH_DNS=0 FLUSH_DNS=0
;; ;;
-h | --help) -h | --help)
echo "Usage: $0 [--flush-dns|--no-flush-dns]" echo "Usage: $0 [--flush-dns|--no-flush-dns]"
exit 0 exit 0
;; ;;
esac esac
done done
# ============================================================================ # ============================================================================
@ -39,136 +39,136 @@ CUSTOM_ENTRIES_STATE_FILE="/etc/hosts.custom-entries.state"
# Extract custom blocked entries from a hosts file or heredoc section # Extract custom blocked entries from a hosts file or heredoc section
# Returns only the "0.0.0.0 domain.com" lines (normalized, sorted, unique) # Returns only the "0.0.0.0 domain.com" lines (normalized, sorted, unique)
extract_custom_entries_from_script() { extract_custom_entries_from_script() {
# Extract entries from the heredoc in this script (between EOF markers after "Custom blocking entries") # Extract entries from the heredoc in this script (between EOF markers after "Custom blocking entries")
local script_path="$1" local script_path="$1"
sed -n '/^# Custom blocking entries$/,/^EOF$/p' "$script_path" | sed -n '/^# Custom blocking entries$/,/^EOF$/p' "$script_path" |
grep -E '^0\.0\.0\.0[[:space:]]+' | grep -E '^0\.0\.0\.0[[:space:]]+' |
awk '{print $2}' | awk '{print $2}' |
sort -u sort -u
} }
# Extract custom entries from the current /etc/hosts (entries after "# Custom blocking entries" marker) # Extract custom entries from the current /etc/hosts (entries after "# Custom blocking entries" marker)
extract_custom_entries_from_hosts() { extract_custom_entries_from_hosts() {
local hosts_file="$1" local hosts_file="$1"
if [[ ! -f $hosts_file ]]; then if [[ ! -f $hosts_file ]]; then
return return
fi fi
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" | sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
grep -E '^0\.0\.0\.0[[:space:]]+' | grep -E '^0\.0\.0\.0[[:space:]]+' |
awk '{print $2}' | awk '{print $2}' |
sort -u sort -u
} }
# Load previously saved custom entries state # Load previously saved custom entries state
load_saved_custom_entries() { load_saved_custom_entries() {
if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
sort -u "$CUSTOM_ENTRIES_STATE_FILE" sort -u "$CUSTOM_ENTRIES_STATE_FILE"
fi fi
} }
# Save current custom entries to state file # Save current custom entries to state file
save_custom_entries_state() { save_custom_entries_state() {
local entries="$1" local entries="$1"
echo "$entries" | sort -u >"$CUSTOM_ENTRIES_STATE_FILE" echo "$entries" | sort -u > "$CUSTOM_ENTRIES_STATE_FILE"
chmod 644 "$CUSTOM_ENTRIES_STATE_FILE" chmod 644 "$CUSTOM_ENTRIES_STATE_FILE"
chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2> /dev/null || true
} }
# Helper function to count non-empty lines # Helper function to count non-empty lines
count_lines() { count_lines() {
local input="$1" local input="$1"
if [[ -z $input ]]; then if [[ -z $input ]]; then
echo 0 echo 0
else else
echo "$input" | grep -c . 2>/dev/null || echo 0 echo "$input" | grep -c . 2> /dev/null || echo 0
fi fi
} }
# Main protection check # Main protection check
check_custom_entries_protection() { check_custom_entries_protection() {
local script_path local script_path
script_path="$(readlink -f "$0")" script_path="$(readlink -f "$0")"
# Get new entries from the script's heredoc # Get new entries from the script's heredoc
local new_entries local new_entries
new_entries=$(extract_custom_entries_from_script "$script_path") new_entries=$(extract_custom_entries_from_script "$script_path")
local new_count local new_count
new_count=$(count_lines "$new_entries") new_count=$(count_lines "$new_entries")
# Get saved/existing entries (prefer state file, fall back to current /etc/hosts) # Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
local saved_entries local saved_entries
saved_entries=$(load_saved_custom_entries) saved_entries=$(load_saved_custom_entries)
if [[ -z $saved_entries ]]; then if [[ -z $saved_entries ]]; then
# First run or state file missing - extract from current /etc/hosts if it has our marker # First run or state file missing - extract from current /etc/hosts if it has our marker
saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts") saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts")
fi fi
local saved_count local saved_count
saved_count=$(count_lines "$saved_entries") saved_count=$(count_lines "$saved_entries")
# If no saved state exists, this is first installation - allow it # If no saved state exists, this is first installation - allow it
if [[ $saved_count -eq 0 ]]; then if [[ $saved_count -eq 0 ]]; then
echo " First installation detected - no protection check needed." echo " First installation detected - no protection check needed."
return 0 return 0
fi fi
# Find entries that were removed # Find entries that were removed
local removed_entries local removed_entries
removed_entries=$(comm -23 <(echo "$saved_entries") <(echo "$new_entries")) removed_entries=$(comm -23 <(echo "$saved_entries") <(echo "$new_entries"))
local removed_count local removed_count
removed_count=$(count_lines "$removed_entries") removed_count=$(count_lines "$removed_entries")
# Find entries that are new # Find entries that are new
local added_entries local added_entries
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries")) added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
local added_count local added_count
added_count=$(count_lines "$added_entries") added_count=$(count_lines "$added_entries")
echo "" echo ""
echo "📊 Custom Entries Protection Check:" echo "📊 Custom Entries Protection Check:"
echo " Previously blocked: $saved_count entries" echo " Previously blocked: $saved_count entries"
echo " Currently in script: $new_count entries" echo " Currently in script: $new_count entries"
echo " Removed: $removed_count | Added: $added_count" echo " Removed: $removed_count | Added: $added_count"
# RULE 1: No entries removed - always OK # RULE 1: No entries removed - always OK
if [[ $removed_count -eq 0 ]]; then if [[ $removed_count -eq 0 ]]; then
echo " ✅ No entries removed - protection check passed." echo " ✅ No entries removed - protection check passed."
return 0 return 0
fi fi
# RULE 2: Entries were removed - BLOCK INSTALLATION # RULE 2: Entries were removed - BLOCK INSTALLATION
echo "" echo ""
echo "============================================================" echo "============================================================"
echo " ❌ INSTALLATION BLOCKED - CUSTOM ENTRIES REMOVED" echo " ❌ INSTALLATION BLOCKED - CUSTOM ENTRIES REMOVED"
echo "============================================================" echo "============================================================"
echo "" echo ""
echo "You are attempting to REMOVE the following blocked entries:" echo "You are attempting to REMOVE the following blocked entries:"
while IFS= read -r entry; do while IFS= read -r entry; do
echo " - $entry" echo " - $entry"
done <<<"$removed_entries" done <<< "$removed_entries"
echo "" echo ""
echo "This is NOT allowed. The only way to unblock sites is to:" echo "This is NOT allowed. The only way to unblock sites is to:"
echo "" echo ""
echo " 1. Manually edit /etc/hosts (requires removing chattr protection)" echo " 1. Manually edit /etc/hosts (requires removing chattr protection)"
echo " 2. Delete the state file /etc/hosts.custom-entries.state" echo " 2. Delete the state file /etc/hosts.custom-entries.state"
echo " (also protected with chattr)" echo " (also protected with chattr)"
echo "" echo ""
echo "These manual steps are intentionally difficult to prevent" echo "These manual steps are intentionally difficult to prevent"
echo "impulsive unblocking. If you really need to unblock something," echo "impulsive unblocking. If you really need to unblock something,"
echo "you'll have to work for it." echo "you'll have to work for it."
echo "" echo ""
return 1 return 1
} }
# Run the protection check # Run the protection check
if ! check_custom_entries_protection; then if ! check_custom_entries_protection; then
exit 1 exit 1
fi fi
# Enable systemd-resolved # Enable systemd-resolved
sudo systemctl enable systemd-resolved sudo systemctl enable systemd-resolved
# Remove all attributes from /etc/hosts to allow modifications # Remove all attributes from /etc/hosts to allow modifications
sudo chattr -i -a /etc/hosts 2>/dev/null || true sudo chattr -i -a /etc/hosts 2> /dev/null || true
# Source and local cache configuration # Source and local cache configuration
URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts" URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts"
@ -177,33 +177,33 @@ LOCAL_CACHE="/etc/hosts.stevenblack"
# Helpers # Helpers
extract_date_epoch_from_file() { extract_date_epoch_from_file() {
# Grep "# Date:" line and convert to epoch seconds (UTC) # Grep "# Date:" line and convert to epoch seconds (UTC)
local f="$1" local f="$1"
local line local line
line=$(grep -m1 '^# Date:' "$f" 2>/dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/') line=$(grep -m1 '^# Date:' "$f" 2> /dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
if [[ -n $line ]]; then if [[ -n $line ]]; then
date -u -d "$line" +%s 2>/dev/null || echo "" date -u -d "$line" +%s 2> /dev/null || echo ""
else else
echo "" echo ""
fi fi
} }
fetch_remote_header() { fetch_remote_header() {
# Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head # Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head
local out="$1" local out="$1"
if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then
return 0 return 0
fi fi
# Fallback may download more, but we only keep first lines # Fallback may download more, but we only keep first lines
if curl -LfsS --max-time 10 "$URL" | head -n 20 >"$out"; then if curl -LfsS --max-time 10 "$URL" | head -n 20 > "$out"; then
return 0 return 0
fi fi
return 1 return 1
} }
download_remote_full_to() { download_remote_full_to() {
local out="$1" local out="$1"
curl -LfsS "$URL" -o "$out" curl -LfsS "$URL" -o "$out"
} }
# Decide whether to use cache or update # Decide whether to use cache or update
@ -212,47 +212,47 @@ trap 'rm -f "$TMP_REMOTE_HEAD"' EXIT
REMOTE_AVAILABLE=0 REMOTE_AVAILABLE=0
if fetch_remote_header "$TMP_REMOTE_HEAD"; then if fetch_remote_header "$TMP_REMOTE_HEAD"; then
REMOTE_AVAILABLE=1 REMOTE_AVAILABLE=1
fi fi
NEED_UPDATE=0 NEED_UPDATE=0
if [[ -f $LOCAL_CACHE ]]; then if [[ -f $LOCAL_CACHE ]]; then
local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE") local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE")
else else
local_epoch="" local_epoch=""
fi fi
if [[ $REMOTE_AVAILABLE -eq 1 ]]; then if [[ $REMOTE_AVAILABLE -eq 1 ]]; then
remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD") remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD")
if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then
echo "Using cached StevenBlack hosts (up-to-date)." echo "Using cached StevenBlack hosts (up-to-date)."
else else
echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..." echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..."
NEED_UPDATE=1 NEED_UPDATE=1
fi fi
else else
if [[ -f $LOCAL_CACHE ]]; then if [[ -f $LOCAL_CACHE ]]; then
echo "No internet; using cached StevenBlack hosts." echo "No internet; using cached StevenBlack hosts."
else else
echo "Error: No internet and no cached StevenBlack hosts found." >&2 echo "Error: No internet and no cached StevenBlack hosts found." >&2
exit 1 exit 1
fi fi
fi fi
# Ensure we have a fresh cache if needed # Ensure we have a fresh cache if needed
if [[ $NEED_UPDATE -eq 1 ]]; then if [[ $NEED_UPDATE -eq 1 ]]; then
TMP_DL=$(mktemp) TMP_DL=$(mktemp)
if download_remote_full_to "$TMP_DL"; then if download_remote_full_to "$TMP_DL"; then
# Save raw upstream to cache # Save raw upstream to cache
sudo mv "$TMP_DL" "$LOCAL_CACHE" sudo mv "$TMP_DL" "$LOCAL_CACHE"
sudo chmod 644 "$LOCAL_CACHE" sudo chmod 644 "$LOCAL_CACHE"
echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE" echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE"
else else
rm -f "$TMP_DL" rm -f "$TMP_DL"
echo "Error: Failed to download latest StevenBlack hosts." >&2 echo "Error: Failed to download latest StevenBlack hosts." >&2
exit 1 exit 1
fi fi
fi fi
# Install the base hosts from cache into /etc/hosts # Install the base hosts from cache into /etc/hosts
@ -272,7 +272,7 @@ sudo sed -i 's/^0\.0\.0\.0 messenger\.com/#0.0.0.0 messenger.com/' /etc/hosts
# Add custom entries for YouTube and Discord # Add custom entries for YouTube and Discord
echo "Adding custom entries for YouTube and Discord..." echo "Adding custom entries for YouTube and Discord..."
tee -a /etc/hosts >/dev/null <<'EOF' tee -a /etc/hosts > /dev/null << 'EOF'
# Custom blocking entries # Custom blocking entries
# YouTube # YouTube
@ -407,17 +407,17 @@ echo "Saving custom entries state for protection mechanism..."
script_path="$(readlink -f "$0")" script_path="$(readlink -f "$0")"
current_custom_entries=$(extract_custom_entries_from_script "$script_path") current_custom_entries=$(extract_custom_entries_from_script "$script_path")
# Remove immutable from state file if it exists # Remove immutable from state file if it exists
chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2> /dev/null || true
save_custom_entries_state "$current_custom_entries" save_custom_entries_state "$current_custom_entries"
echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE" echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE"
# Optionally flush DNS caches # Optionally flush DNS caches
if [[ $FLUSH_DNS -eq 1 ]]; then if [[ $FLUSH_DNS -eq 1 ]]; then
echo "Flushing DNS caches..." echo "Flushing DNS caches..."
sudo systemd-resolve --flush-caches sudo systemd-resolve --flush-caches
sudo systemctl restart NetworkManager.service sudo systemctl restart NetworkManager.service
else else
echo "DNS cache flush skipped (use --flush-dns to enable)." echo "DNS cache flush skipped (use --flush-dns to enable)."
fi fi
echo "" echo ""

View File

@ -7,32 +7,32 @@ SHUTDOWN_CONFIG="/etc/shutdown-schedule.conf"
# Function to show error state in i3blocks and exit # Function to show error state in i3blocks and exit
show_error() { show_error() {
local message="$1" local message="$1"
echo "$message" echo "$message"
echo "⏻" echo "⏻"
echo "#FF79C6" # Pink/magenta for config errors echo "#FF79C6" # Pink/magenta for config errors
exit 0 exit 0
} }
# Validate and load config file # Validate and load config file
if [[ ! -f "$SHUTDOWN_CONFIG" ]]; then if [[ ! -f $SHUTDOWN_CONFIG ]]; then
show_error "NO CONFIG" show_error "NO CONFIG"
fi fi
# Source the config file to get MON_WED_HOUR and THU_SUN_HOUR # Source the config file to get MON_WED_HOUR and THU_SUN_HOUR
# shellcheck source=/dev/null # shellcheck source=/dev/null
if ! source "$SHUTDOWN_CONFIG" 2>/dev/null; then if ! source "$SHUTDOWN_CONFIG" 2> /dev/null; then
show_error "BAD CONFIG" show_error "BAD CONFIG"
fi fi
# Validate that required variables are set # Validate that required variables are set
if [[ -z "${MON_WED_HOUR:-}" ]] || [[ -z "${THU_SUN_HOUR:-}" ]]; then if [[ -z ${MON_WED_HOUR:-} ]] || [[ -z ${THU_SUN_HOUR:-} ]]; then
show_error "MISSING VARS" show_error "MISSING VARS"
fi fi
# Validate that values are numbers # Validate that values are numbers
if ! [[ "$MON_WED_HOUR" =~ ^[0-9]+$ ]] || ! [[ "$THU_SUN_HOUR" =~ ^[0-9]+$ ]]; then if ! [[ $MON_WED_HOUR =~ ^[0-9]+$ ]] || ! [[ $THU_SUN_HOUR =~ ^[0-9]+$ ]]; then
show_error "INVALID HOURS" show_error "INVALID HOURS"
fi fi
# Get current time info # Get current time info
@ -43,22 +43,22 @@ day_of_week=$(date +%u) # 1=Monday, 7=Sunday
# Determine shutdown hour based on day of week # Determine shutdown hour based on day of week
if [[ $day_of_week -ge 1 ]] && [[ $day_of_week -le 3 ]]; then if [[ $day_of_week -ge 1 ]] && [[ $day_of_week -le 3 ]]; then
# Monday-Wednesday # Monday-Wednesday
shutdown_hour=$MON_WED_HOUR shutdown_hour=$MON_WED_HOUR
else else
# Thursday-Sunday # Thursday-Sunday
shutdown_hour=$THU_SUN_HOUR shutdown_hour=$THU_SUN_HOUR
fi fi
shutdown_time_minutes=$((shutdown_hour * 60)) shutdown_time_minutes=$((shutdown_hour * 60))
# Check if we're currently in the shutdown window (after shutdown time or before 05:00) # Check if we're currently in the shutdown window (after shutdown time or before 05:00)
if [[ $current_time_minutes -ge $shutdown_time_minutes ]] || [[ $current_time_minutes -le 300 ]]; then if [[ $current_time_minutes -ge $shutdown_time_minutes ]] || [[ $current_time_minutes -le 300 ]]; then
# We're in shutdown window - show warning # We're in shutdown window - show warning
echo "⏻ SHUTDOWN" echo "⏻ SHUTDOWN"
echo "⏻" echo "⏻"
echo "#FF5555" echo "#FF5555"
exit 0 exit 0
fi fi
# Calculate minutes until shutdown # Calculate minutes until shutdown
@ -70,28 +70,28 @@ minutes=$((minutes_until_shutdown % 60))
# Format output # Format output
if [[ $hours -gt 0 ]]; then if [[ $hours -gt 0 ]]; then
time_str="${hours}h ${minutes}m" time_str="${hours}h ${minutes}m"
else else
time_str="${minutes}m" time_str="${minutes}m"
fi fi
# Color based on time remaining # Color based on time remaining
if [[ $minutes_until_shutdown -le 30 ]]; then if [[ $minutes_until_shutdown -le 30 ]]; then
# Less than 30 min - red warning # Less than 30 min - red warning
color="#FF5555" color="#FF5555"
icon="⏻" icon="⏻"
elif [[ $minutes_until_shutdown -le 60 ]]; then elif [[ $minutes_until_shutdown -le 60 ]]; then
# Less than 1 hour - orange warning # Less than 1 hour - orange warning
color="#FFB86C" color="#FFB86C"
icon="⏻" icon="⏻"
elif [[ $minutes_until_shutdown -le 120 ]]; then elif [[ $minutes_until_shutdown -le 120 ]]; then
# Less than 2 hours - yellow # Less than 2 hours - yellow
color="#F1FA8C" color="#F1FA8C"
icon="⏻" icon="⏻"
else else
# More than 2 hours - normal # More than 2 hours - normal
color="#6272A4" color="#6272A4"
icon="⏻" icon="⏻"
fi fi
# Output for i3blocks (full_text, short_text, color) # Output for i3blocks (full_text, short_text, color)

View File

@ -48,24 +48,24 @@ err() { printf "${RED}[✗]${NC} %s\n" "$*"; }
header() { printf "\n${CYAN}=== %s ===${NC}\n" "$*"; } header() { printf "\n${CYAN}=== %s ===${NC}\n" "$*"; }
run() { run() {
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo -e "${YELLOW}DRY-RUN:${NC} $*" echo -e "${YELLOW}DRY-RUN:${NC} $*"
return 0 return 0
else else
"$@" "$@"
fi fi
} }
require_root() { require_root() {
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "This script requires root privileges." echo "This script requires root privileges."
echo "Re-executing with sudo..." echo "Re-executing with sudo..."
exec sudo -E bash "$0" "$@" exec sudo -E bash "$0" "$@"
fi fi
} }
usage() { usage() {
cat <<'EOF' cat << 'EOF'
Check and Enable Digital Wellbeing Services Check and Enable Digital Wellbeing Services
============================================ ============================================
@ -90,25 +90,25 @@ EOF
###################################################################### ######################################################################
ORIGINAL_ARGS=("$@") ORIGINAL_ARGS=("$@")
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--dry-run) --dry-run)
DRY_RUN=1 DRY_RUN=1
shift shift
;; ;;
--status) --status)
STATUS_ONLY=1 STATUS_ONLY=1
shift shift
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
*) *)
err "Unknown option: $1" err "Unknown option: $1"
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
require_root "${ORIGINAL_ARGS[@]}" require_root "${ORIGINAL_ARGS[@]}"
@ -125,41 +125,41 @@ FIXES_APPLIED=0
# Usage: report_and_fix issues_array status_var status_key fix_note setup_script verify_service [args...] # Usage: report_and_fix issues_array status_var status_key fix_note setup_script verify_service [args...]
###################################################################### ######################################################################
report_and_fix() { report_and_fix() {
local -n _issues=$1 local -n _issues=$1
local -n _status=$2 local -n _status=$2
local status_key="$3" local status_key="$3"
local fix_note="$4" local fix_note="$4"
local setup_script="$5" local setup_script="$5"
local verify_service="${6:-}" local verify_service="${6:-}"
shift 6 shift 6
local script_args=("$@") local script_args=("$@")
if [[ $_status != "ok" ]]; then if [[ $_status != "ok" ]]; then
for issue in "${_issues[@]}"; do for issue in "${_issues[@]}"; do
if [[ $_status == "error" ]]; then if [[ $_status == "error" ]]; then
err "$issue" err "$issue"
else else
warn "$issue" warn "$issue"
fi fi
done done
((ISSUES_FOUND++)) || true ((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 && $_status == "error" ]]; then if [[ $STATUS_ONLY -eq 0 && $_status == "error" ]]; then
note "$fix_note" note "$fix_note"
if [[ -f $setup_script ]]; then if [[ -f $setup_script ]]; then
run bash "$setup_script" "${script_args[@]}" run bash "$setup_script" "${script_args[@]}"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
# Re-verify after fix # Re-verify after fix
if [[ $DRY_RUN -eq 0 && -n $verify_service ]] && systemctl is-enabled "$verify_service" &>/dev/null; then if [[ $DRY_RUN -eq 0 && -n $verify_service ]] && systemctl is-enabled "$verify_service" &> /dev/null; then
_status="ok" _status="ok"
fi fi
else else
err "Setup script not found: $setup_script" err "Setup script not found: $setup_script"
fi fi
fi fi
fi fi
SERVICE_STATUS["$status_key"]=$_status SERVICE_STATUS["$status_key"]=$_status
} }
###################################################################### ######################################################################
@ -167,443 +167,443 @@ report_and_fix() {
###################################################################### ######################################################################
check_pacman_wrapper() { check_pacman_wrapper() {
header "Pacman Wrapper" header "Pacman Wrapper"
local status="ok" local status="ok"
local issues=() local issues=()
# Check if wrapper is installed # Check if wrapper is installed
if [[ -L /usr/bin/pacman ]]; then if [[ -L /usr/bin/pacman ]]; then
local target local target
target=$(readlink -f /usr/bin/pacman) target=$(readlink -f /usr/bin/pacman)
if [[ $target == "/usr/local/bin/pacman_wrapper" ]]; then if [[ $target == "/usr/local/bin/pacman_wrapper" ]]; then
msg "Pacman symlink points to wrapper" msg "Pacman symlink points to wrapper"
else else
issues+=("Pacman symlink points to: $target (expected /usr/local/bin/pacman_wrapper)") issues+=("Pacman symlink points to: $target (expected /usr/local/bin/pacman_wrapper)")
status="error" status="error"
fi fi
else else
issues+=("Pacman is not a symlink (wrapper not installed)") issues+=("Pacman is not a symlink (wrapper not installed)")
status="error" status="error"
fi fi
# Check if original pacman is backed up # Check if original pacman is backed up
if [[ -f /usr/bin/pacman.orig ]]; then if [[ -f /usr/bin/pacman.orig ]]; then
msg "Original pacman backed up at /usr/bin/pacman.orig" msg "Original pacman backed up at /usr/bin/pacman.orig"
else else
issues+=("Original pacman backup not found at /usr/bin/pacman.orig") issues+=("Original pacman backup not found at /usr/bin/pacman.orig")
status="error" status="error"
fi fi
# Check if wrapper script exists # Check if wrapper script exists
if [[ -f /usr/local/bin/pacman_wrapper ]]; then if [[ -f /usr/local/bin/pacman_wrapper ]]; then
msg "Wrapper script exists at /usr/local/bin/pacman_wrapper" msg "Wrapper script exists at /usr/local/bin/pacman_wrapper"
else else
issues+=("Wrapper script not found at /usr/local/bin/pacman_wrapper") issues+=("Wrapper script not found at /usr/local/bin/pacman_wrapper")
status="error" status="error"
fi fi
# Check supporting files # Check supporting files
for file in words.txt pacman_blocked_keywords.txt pacman_whitelist.txt; do for file in words.txt pacman_blocked_keywords.txt pacman_whitelist.txt; do
if [[ -f "/usr/local/bin/$file" ]]; then if [[ -f "/usr/local/bin/$file" ]]; then
msg "Supporting file exists: /usr/local/bin/$file" msg "Supporting file exists: /usr/local/bin/$file"
else else
warn "Supporting file missing: /usr/local/bin/$file" warn "Supporting file missing: /usr/local/bin/$file"
fi fi
done done
# Report and fix # Report and fix
if [[ $status == "error" ]]; then if [[ $status == "error" ]]; then
for issue in "${issues[@]}"; do for issue in "${issues[@]}"; do
err "$issue" err "$issue"
done done
((ISSUES_FOUND++)) || true ((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 ]]; then if [[ $STATUS_ONLY -eq 0 ]]; then
note "Installing pacman wrapper..." note "Installing pacman wrapper..."
if [[ -f $PACMAN_WRAPPER_INSTALL ]]; then if [[ -f $PACMAN_WRAPPER_INSTALL ]]; then
run bash "$PACMAN_WRAPPER_INSTALL" run bash "$PACMAN_WRAPPER_INSTALL"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
# Re-verify after fix # Re-verify after fix
if [[ $DRY_RUN -eq 0 ]] && [[ -L /usr/bin/pacman ]] && [[ -f /usr/bin/pacman.orig ]] && [[ -f /usr/local/bin/pacman_wrapper ]]; then if [[ $DRY_RUN -eq 0 ]] && [[ -L /usr/bin/pacman ]] && [[ -f /usr/bin/pacman.orig ]] && [[ -f /usr/local/bin/pacman_wrapper ]]; then
status="ok" status="ok"
fi fi
else else
err "Installer script not found: $PACMAN_WRAPPER_INSTALL" err "Installer script not found: $PACMAN_WRAPPER_INSTALL"
fi fi
fi fi
fi fi
SERVICE_STATUS["pacman_wrapper"]=$status SERVICE_STATUS["pacman_wrapper"]=$status
} }
check_midnight_shutdown() { check_midnight_shutdown() {
header "Midnight Shutdown (Day-Specific Auto-Shutdown)" header "Midnight Shutdown (Day-Specific Auto-Shutdown)"
local status="ok" local status="ok"
local issues=() local issues=()
# Check timer # Check timer
if systemctl is-enabled day-specific-shutdown.timer &>/dev/null; then if systemctl is-enabled day-specific-shutdown.timer &> /dev/null; then
msg "day-specific-shutdown.timer is enabled" msg "day-specific-shutdown.timer is enabled"
else else
issues+=("day-specific-shutdown.timer is not enabled") issues+=("day-specific-shutdown.timer is not enabled")
status="error" status="error"
fi fi
if systemctl is-active day-specific-shutdown.timer &>/dev/null; then if systemctl is-active day-specific-shutdown.timer &> /dev/null; then
msg "day-specific-shutdown.timer is active" msg "day-specific-shutdown.timer is active"
else else
issues+=("day-specific-shutdown.timer is not active") issues+=("day-specific-shutdown.timer is not active")
status="warning" status="warning"
fi fi
# Check service file exists # Check service file exists
if [[ -f /etc/systemd/system/day-specific-shutdown.service ]]; then if [[ -f /etc/systemd/system/day-specific-shutdown.service ]]; then
msg "day-specific-shutdown.service file exists" msg "day-specific-shutdown.service file exists"
else else
issues+=("day-specific-shutdown.service file missing") issues+=("day-specific-shutdown.service file missing")
status="error" status="error"
fi fi
# Check management script # Check management script
if [[ -f /usr/local/bin/day-specific-shutdown-manager.sh ]]; then if [[ -f /usr/local/bin/day-specific-shutdown-manager.sh ]]; then
msg "Shutdown manager script exists" msg "Shutdown manager script exists"
else else
issues+=("day-specific-shutdown-manager.sh not found") issues+=("day-specific-shutdown-manager.sh not found")
status="error" status="error"
fi fi
report_and_fix issues status "midnight_shutdown" \ report_and_fix issues status "midnight_shutdown" \
"Setting up midnight shutdown..." \ "Setting up midnight shutdown..." \
"$MIDNIGHT_SHUTDOWN_SCRIPT" \ "$MIDNIGHT_SHUTDOWN_SCRIPT" \
"day-specific-shutdown.timer" \ "day-specific-shutdown.timer" \
enable enable
} }
check_startup_monitor() { check_startup_monitor() {
header "PC Startup Monitor" header "PC Startup Monitor"
local status="ok" local status="ok"
local issues=() local issues=()
# Check timer (the timer triggers the service, so we check the timer) # Check timer (the timer triggers the service, so we check the timer)
if systemctl is-enabled pc-startup-monitor.timer &>/dev/null; then if systemctl is-enabled pc-startup-monitor.timer &> /dev/null; then
msg "pc-startup-monitor.timer is enabled" msg "pc-startup-monitor.timer is enabled"
else else
issues+=("pc-startup-monitor.timer is not enabled") issues+=("pc-startup-monitor.timer is not enabled")
status="error" status="error"
fi fi
if systemctl is-active pc-startup-monitor.timer &>/dev/null; then if systemctl is-active pc-startup-monitor.timer &> /dev/null; then
msg "pc-startup-monitor.timer is active" msg "pc-startup-monitor.timer is active"
else else
issues+=("pc-startup-monitor.timer is not active") issues+=("pc-startup-monitor.timer is not active")
status="warning" status="warning"
fi fi
# Check service file exists # Check service file exists
if [[ -f /etc/systemd/system/pc-startup-monitor.service ]]; then if [[ -f /etc/systemd/system/pc-startup-monitor.service ]]; then
msg "pc-startup-monitor.service file exists" msg "pc-startup-monitor.service file exists"
else else
issues+=("pc-startup-monitor.service file missing") issues+=("pc-startup-monitor.service file missing")
status="error" status="error"
fi fi
# Check monitor script # Check monitor script
if [[ -f /usr/local/bin/pc-startup-check.sh ]]; then if [[ -f /usr/local/bin/pc-startup-check.sh ]]; then
msg "Startup check script exists" msg "Startup check script exists"
else else
issues+=("pc-startup-check.sh not found") issues+=("pc-startup-check.sh not found")
status="error" status="error"
fi fi
report_and_fix issues status "startup_monitor" \ report_and_fix issues status "startup_monitor" \
"Setting up startup monitor..." \ "Setting up startup monitor..." \
"$STARTUP_MONITOR_SCRIPT" \ "$STARTUP_MONITOR_SCRIPT" \
"pc-startup-monitor.timer" "pc-startup-monitor.timer"
} }
check_periodic_systems() { check_periodic_systems() {
header "Periodic System Maintenance" header "Periodic System Maintenance"
local status="ok" local status="ok"
local issues=() local issues=()
# Check timer # Check timer
if systemctl is-enabled periodic-system-maintenance.timer &>/dev/null; then if systemctl is-enabled periodic-system-maintenance.timer &> /dev/null; then
msg "periodic-system-maintenance.timer is enabled" msg "periodic-system-maintenance.timer is enabled"
else else
issues+=("periodic-system-maintenance.timer is not enabled") issues+=("periodic-system-maintenance.timer is not enabled")
status="error" status="error"
fi fi
if systemctl is-active periodic-system-maintenance.timer &>/dev/null; then if systemctl is-active periodic-system-maintenance.timer &> /dev/null; then
msg "periodic-system-maintenance.timer is active" msg "periodic-system-maintenance.timer is active"
else else
issues+=("periodic-system-maintenance.timer is not active") issues+=("periodic-system-maintenance.timer is not active")
status="warning" status="warning"
fi fi
# Check startup service # Check startup service
if systemctl is-enabled periodic-system-startup.service &>/dev/null; then if systemctl is-enabled periodic-system-startup.service &> /dev/null; then
msg "periodic-system-startup.service is enabled" msg "periodic-system-startup.service is enabled"
else else
issues+=("periodic-system-startup.service is not enabled") issues+=("periodic-system-startup.service is not enabled")
status="error" status="error"
fi fi
# Check hosts file monitor # Check hosts file monitor
if systemctl is-enabled hosts-file-monitor.service &>/dev/null; then if systemctl is-enabled hosts-file-monitor.service &> /dev/null; then
msg "hosts-file-monitor.service is enabled" msg "hosts-file-monitor.service is enabled"
else else
issues+=("hosts-file-monitor.service is not enabled") issues+=("hosts-file-monitor.service is not enabled")
status="error" status="error"
fi fi
if systemctl is-active hosts-file-monitor.service &>/dev/null; then if systemctl is-active hosts-file-monitor.service &> /dev/null; then
msg "hosts-file-monitor.service is active" msg "hosts-file-monitor.service is active"
else else
issues+=("hosts-file-monitor.service is not active") issues+=("hosts-file-monitor.service is not active")
status="warning" status="warning"
fi fi
# Check maintenance script # Check maintenance script
if [[ -f /usr/local/bin/periodic-system-maintenance.sh ]]; then if [[ -f /usr/local/bin/periodic-system-maintenance.sh ]]; then
msg "Maintenance script exists" msg "Maintenance script exists"
else else
issues+=("periodic-system-maintenance.sh not found") issues+=("periodic-system-maintenance.sh not found")
status="error" status="error"
fi fi
report_and_fix issues status "periodic_systems" \ report_and_fix issues status "periodic_systems" \
"Setting up periodic systems..." \ "Setting up periodic systems..." \
"$PERIODIC_SYSTEM_SCRIPT" \ "$PERIODIC_SYSTEM_SCRIPT" \
"periodic-system-maintenance.timer" "periodic-system-maintenance.timer"
} }
check_hosts() { check_hosts() {
header "Hosts File and Guards" header "Hosts File and Guards"
local status="ok" local status="ok"
local issues=() local issues=()
# Check /etc/hosts exists and has content # Check /etc/hosts exists and has content
if [[ -f /etc/hosts ]]; then if [[ -f /etc/hosts ]]; then
local line_count local line_count
line_count=$(wc -l </etc/hosts) line_count=$(wc -l < /etc/hosts)
if [[ $line_count -gt 100 ]]; then if [[ $line_count -gt 100 ]]; then
msg "/etc/hosts exists with $line_count lines (StevenBlack list likely installed)" msg "/etc/hosts exists with $line_count lines (StevenBlack list likely installed)"
else else
issues+=("/etc/hosts has only $line_count lines (StevenBlack list may not be installed)") issues+=("/etc/hosts has only $line_count lines (StevenBlack list may not be installed)")
status="warning" status="warning"
fi fi
else else
issues+=("/etc/hosts does not exist") issues+=("/etc/hosts does not exist")
status="error" status="error"
fi fi
# Check if hosts file is immutable # Check if hosts file is immutable
local attrs local attrs
attrs=$(lsattr /etc/hosts 2>/dev/null | cut -d' ' -f1 || echo "") attrs=$(lsattr /etc/hosts 2> /dev/null | cut -d' ' -f1 || echo "")
if [[ $attrs == *"i"* ]]; then if [[ $attrs == *"i"* ]]; then
msg "/etc/hosts has immutable attribute set" msg "/etc/hosts has immutable attribute set"
else else
issues+=("/etc/hosts is not immutable") issues+=("/etc/hosts is not immutable")
status="warning" status="warning"
fi fi
# Check cached hosts file # Check cached hosts file
if [[ -f /etc/hosts.stevenblack ]]; then if [[ -f /etc/hosts.stevenblack ]]; then
msg "StevenBlack cache exists at /etc/hosts.stevenblack" msg "StevenBlack cache exists at /etc/hosts.stevenblack"
else else
issues+=("StevenBlack cache not found") issues+=("StevenBlack cache not found")
status="warning" status="warning"
fi fi
# Check hosts guard path watcher # Check hosts guard path watcher
if systemctl is-enabled hosts-guard.path &>/dev/null; then if systemctl is-enabled hosts-guard.path &> /dev/null; then
msg "hosts-guard.path is enabled" msg "hosts-guard.path is enabled"
else else
issues+=("hosts-guard.path is not enabled") issues+=("hosts-guard.path is not enabled")
status="error" status="error"
fi fi
if systemctl is-active hosts-guard.path &>/dev/null; then if systemctl is-active hosts-guard.path &> /dev/null; then
msg "hosts-guard.path is active" msg "hosts-guard.path is active"
else else
issues+=("hosts-guard.path is not active") issues+=("hosts-guard.path is not active")
status="warning" status="warning"
fi fi
# Check hosts bind mount service # Check hosts bind mount service
if systemctl is-enabled hosts-bind-mount.service &>/dev/null; then if systemctl is-enabled hosts-bind-mount.service &> /dev/null; then
msg "hosts-bind-mount.service is enabled" msg "hosts-bind-mount.service is enabled"
else else
issues+=("hosts-bind-mount.service is not enabled") issues+=("hosts-bind-mount.service is not enabled")
status="warning" status="warning"
fi fi
# Check enforcement script # Check enforcement script
if [[ -f /usr/local/sbin/enforce-hosts.sh ]]; then if [[ -f /usr/local/sbin/enforce-hosts.sh ]]; then
msg "Enforcement script exists at /usr/local/sbin/enforce-hosts.sh" msg "Enforcement script exists at /usr/local/sbin/enforce-hosts.sh"
else else
issues+=("enforce-hosts.sh not found") issues+=("enforce-hosts.sh not found")
status="error" status="error"
fi fi
# Check unlock script # Check unlock script
if [[ -f /usr/local/sbin/unlock-hosts ]]; then if [[ -f /usr/local/sbin/unlock-hosts ]]; then
msg "Unlock script exists at /usr/local/sbin/unlock-hosts" msg "Unlock script exists at /usr/local/sbin/unlock-hosts"
else else
issues+=("unlock-hosts not found") issues+=("unlock-hosts not found")
status="warning" status="warning"
fi fi
# Check locked hosts snapshot # Check locked hosts snapshot
if [[ -f /usr/local/share/locked-hosts ]]; then if [[ -f /usr/local/share/locked-hosts ]]; then
msg "Canonical hosts snapshot exists at /usr/local/share/locked-hosts" msg "Canonical hosts snapshot exists at /usr/local/share/locked-hosts"
else else
issues+=("Canonical hosts snapshot not found") issues+=("Canonical hosts snapshot not found")
status="error" status="error"
fi fi
# Check pacman hooks # Check pacman hooks
if [[ -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]] && [[ -f /etc/pacman.d/hooks/90-relock-etc-hosts.hook ]]; then if [[ -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]] && [[ -f /etc/pacman.d/hooks/90-relock-etc-hosts.hook ]]; then
msg "Pacman hooks installed" msg "Pacman hooks installed"
else else
issues+=("Pacman hooks not installed") issues+=("Pacman hooks not installed")
status="warning" status="warning"
fi fi
# Report issues # Report issues
if [[ $status != "ok" ]]; then if [[ $status != "ok" ]]; then
for issue in "${issues[@]}"; do for issue in "${issues[@]}"; do
if [[ $status == "error" ]]; then if [[ $status == "error" ]]; then
err "$issue" err "$issue"
else else
warn "$issue" warn "$issue"
fi fi
done done
((ISSUES_FOUND++)) || true ((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 ]]; then if [[ $STATUS_ONLY -eq 0 ]]; then
# Run hosts install first # Run hosts install first
if [[ ! -f /etc/hosts ]] || [[ $(wc -l </etc/hosts) -lt 100 ]]; then if [[ ! -f /etc/hosts ]] || [[ $(wc -l < /etc/hosts) -lt 100 ]]; then
note "Installing hosts file..." note "Installing hosts file..."
if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then
run bash "$HOSTS_INSTALL_SCRIPT" run bash "$HOSTS_INSTALL_SCRIPT"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
else else
err "Hosts install script not found: $HOSTS_INSTALL_SCRIPT" err "Hosts install script not found: $HOSTS_INSTALL_SCRIPT"
fi fi
fi fi
# Run hosts guard setup # Run hosts guard setup
if ! systemctl is-enabled hosts-guard.path &>/dev/null || [[ ! -f /usr/local/sbin/enforce-hosts.sh ]]; then if ! systemctl is-enabled hosts-guard.path &> /dev/null || [[ ! -f /usr/local/sbin/enforce-hosts.sh ]]; then
note "Setting up hosts guard..." note "Setting up hosts guard..."
if [[ -f $HOSTS_GUARD_SCRIPT ]]; then if [[ -f $HOSTS_GUARD_SCRIPT ]]; then
run bash "$HOSTS_GUARD_SCRIPT" run bash "$HOSTS_GUARD_SCRIPT"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
else else
err "Hosts guard script not found: $HOSTS_GUARD_SCRIPT" err "Hosts guard script not found: $HOSTS_GUARD_SCRIPT"
fi fi
fi fi
# Install pacman hooks if missing # Install pacman hooks if missing
if [[ ! -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then if [[ ! -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then
note "Installing pacman hooks..." note "Installing pacman hooks..."
if [[ -f $HOSTS_PACMAN_HOOKS_SCRIPT ]]; then if [[ -f $HOSTS_PACMAN_HOOKS_SCRIPT ]]; then
run bash "$HOSTS_PACMAN_HOOKS_SCRIPT" run bash "$HOSTS_PACMAN_HOOKS_SCRIPT"
((FIXES_APPLIED++)) || true ((FIXES_APPLIED++)) || true
else else
err "Pacman hooks script not found: $HOSTS_PACMAN_HOOKS_SCRIPT" err "Pacman hooks script not found: $HOSTS_PACMAN_HOOKS_SCRIPT"
fi fi
fi fi
# Re-verify after fixes # Re-verify after fixes
if [[ $DRY_RUN -eq 0 ]]; then if [[ $DRY_RUN -eq 0 ]]; then
if systemctl is-enabled hosts-guard.path &>/dev/null && if systemctl is-enabled hosts-guard.path &> /dev/null &&
[[ -f /usr/local/sbin/enforce-hosts.sh ]] && [[ -f /usr/local/sbin/enforce-hosts.sh ]] &&
[[ -f /usr/local/share/locked-hosts ]] && [[ -f /usr/local/share/locked-hosts ]] &&
[[ -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then [[ -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then
# Downgrade to warning if only minor issues remain (immutable attr, etc.) # Downgrade to warning if only minor issues remain (immutable attr, etc.)
status="ok" status="ok"
fi fi
fi fi
fi fi
fi fi
SERVICE_STATUS["hosts"]=$status SERVICE_STATUS["hosts"]=$status
} }
###################################################################### ######################################################################
# Summary # Summary
###################################################################### ######################################################################
print_summary() { print_summary() {
header "Summary" header "Summary"
echo "" echo ""
printf "%-25s %s\n" "Service" "Status" printf "%-25s %s\n" "Service" "Status"
printf "%-25s %s\n" "-------" "------" printf "%-25s %s\n" "-------" "------"
for service in pacman_wrapper midnight_shutdown startup_monitor periodic_systems hosts; do for service in pacman_wrapper midnight_shutdown startup_monitor periodic_systems hosts; do
local status="${SERVICE_STATUS[$service]:-unknown}" local status="${SERVICE_STATUS[$service]:-unknown}"
local color local color
case "$status" in case "$status" in
ok) color=$GREEN ;; ok) color=$GREEN ;;
warning) color=$YELLOW ;; warning) color=$YELLOW ;;
error) color=$RED ;; error) color=$RED ;;
*) color=$NC ;; *) color=$NC ;;
esac esac
printf "%-25s ${color}%s${NC}\n" "$service" "$status" printf "%-25s ${color}%s${NC}\n" "$service" "$status"
done done
echo "" echo ""
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
note "DRY RUN - No changes were made" note "DRY RUN - No changes were made"
fi fi
if [[ $ISSUES_FOUND -eq 0 ]]; then if [[ $ISSUES_FOUND -eq 0 ]]; then
msg "All services are properly configured!" msg "All services are properly configured!"
else else
if [[ $STATUS_ONLY -eq 1 ]]; then if [[ $STATUS_ONLY -eq 1 ]]; then
warn "Found $ISSUES_FOUND service(s) with issues" warn "Found $ISSUES_FOUND service(s) with issues"
note "Run without --status to fix issues" note "Run without --status to fix issues"
else else
if [[ $FIXES_APPLIED -gt 0 ]]; then if [[ $FIXES_APPLIED -gt 0 ]]; then
msg "Applied $FIXES_APPLIED fix(es)" msg "Applied $FIXES_APPLIED fix(es)"
else else
warn "Found $ISSUES_FOUND issue(s) but no fixes were applied" warn "Found $ISSUES_FOUND issue(s) but no fixes were applied"
fi fi
fi fi
fi fi
} }
###################################################################### ######################################################################
# Main # Main
###################################################################### ######################################################################
main() { main() {
echo "" echo ""
echo "Digital Wellbeing Services Status Check" echo "Digital Wellbeing Services Status Check"
echo "========================================" echo "========================================"
echo "Date: $(date)" echo "Date: $(date)"
echo "User: ${SUDO_USER:-$USER}" echo "User: ${SUDO_USER:-$USER}"
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo "Mode: DRY RUN (no changes will be made)" echo "Mode: DRY RUN (no changes will be made)"
elif [[ $STATUS_ONLY -eq 1 ]]; then elif [[ $STATUS_ONLY -eq 1 ]]; then
echo "Mode: STATUS ONLY (no changes will be made)" echo "Mode: STATUS ONLY (no changes will be made)"
else else
echo "Mode: CHECK AND FIX" echo "Mode: CHECK AND FIX"
fi fi
check_pacman_wrapper check_pacman_wrapper
check_midnight_shutdown check_midnight_shutdown
check_startup_monitor check_startup_monitor
check_periodic_systems check_periodic_systems
check_hosts check_hosts
print_summary print_summary
} }
main main

View File

@ -12,14 +12,14 @@ set -euo pipefail
# Send desktop notification (inlined from common.sh to avoid dependency issues # Send desktop notification (inlined from common.sh to avoid dependency issues
# when script is installed to /usr/local/bin) # when script is installed to /usr/local/bin)
notify() { notify() {
local title="$1" local title="$1"
local message="$2" local message="$2"
local urgency="${3:-normal}" local urgency="${3:-normal}"
local timeout="${4:-5000}" local timeout="${4:-5000}"
if command -v notify-send &>/dev/null; then if command -v notify-send &> /dev/null; then
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2>/dev/null || true notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2> /dev/null || true
fi fi
} }
# Configuration # Configuration
@ -29,165 +29,165 @@ LOG_FILE="$STATE_DIR/compulsive-block.log"
# Apps to limit (name -> binary path) # Apps to limit (name -> binary path)
# These are the primary wrapper locations (what the user calls) # These are the primary wrapper locations (what the user calls)
declare -A APPS=( declare -A APPS=(
["beeper"]="/usr/bin/beeper" ["beeper"]="/usr/bin/beeper"
["signal-desktop"]="/usr/bin/signal-desktop" ["signal-desktop"]="/usr/bin/signal-desktop"
["discord"]="/usr/bin/discord" ["discord"]="/usr/bin/discord"
) )
# Actual executable paths (the real binaries to exec after wrapper check) # Actual executable paths (the real binaries to exec after wrapper check)
# These are where the real code lives # These are where the real code lives
declare -A REAL_BINARIES=( declare -A REAL_BINARIES=(
["beeper"]="/opt/beeper/beepertexts" ["beeper"]="/opt/beeper/beepertexts"
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop" ["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
["discord"]="/opt/discord/Discord" ["discord"]="/opt/discord/Discord"
) )
# Ensure state directory exists # Ensure state directory exists
ensure_state_dir() { ensure_state_dir() {
mkdir -p "$STATE_DIR" 2>/dev/null || true mkdir -p "$STATE_DIR" 2> /dev/null || true
} }
# Log message with timestamp # Log message with timestamp
log_message() { log_message() {
local msg local msg
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1" msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
echo "$msg" >&2 echo "$msg" >&2
echo "$msg" >>"$LOG_FILE" 2>/dev/null || true echo "$msg" >> "$LOG_FILE" 2> /dev/null || true
} }
# Get current hour key (YYYY-MM-DD-HH format) # Get current hour key (YYYY-MM-DD-HH format)
get_hour_key() { get_hour_key() {
date '+%Y-%m-%d-%H' date '+%Y-%m-%d-%H'
} }
# Get state file path for an app # Get state file path for an app
get_state_file() { get_state_file() {
local app="$1" local app="$1"
echo "$STATE_DIR/${app}.lastopen" echo "$STATE_DIR/${app}.lastopen"
} }
# Check if app was already opened this hour # Check if app was already opened this hour
was_opened_this_hour() { was_opened_this_hour() {
local app="$1" local app="$1"
local state_file local state_file
state_file=$(get_state_file "$app") state_file=$(get_state_file "$app")
local current_hour local current_hour
current_hour=$(get_hour_key) current_hour=$(get_hour_key)
if [[ -f $state_file ]]; then if [[ -f $state_file ]]; then
local last_hour local last_hour
last_hour=$(cat "$state_file" 2>/dev/null || echo "") last_hour=$(cat "$state_file" 2> /dev/null || echo "")
if [[ $last_hour == "$current_hour" ]]; then if [[ $last_hour == "$current_hour" ]]; then
return 0 # Was opened this hour return 0 # Was opened this hour
fi fi
fi fi
return 1 # Not opened this hour return 1 # Not opened this hour
} }
# Record app opening # Record app opening
record_opening() { record_opening() {
local app="$1" local app="$1"
local state_file local state_file
state_file=$(get_state_file "$app") state_file=$(get_state_file "$app")
local current_hour local current_hour
current_hour=$(get_hour_key) current_hour=$(get_hour_key)
echo "$current_hour" >"$state_file" echo "$current_hour" > "$state_file"
log_message "ALLOWED: $app opened (first time this hour: $current_hour)" log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
} }
# Block app and notify # Block app and notify
block_app() { block_app() {
local app="$1" local app="$1"
local current_hour local current_hour
current_hour=$(get_hour_key) current_hour=$(get_hour_key)
log_message "BLOCKED: $app launch prevented (already opened this hour: $current_hour)" log_message "BLOCKED: $app launch prevented (already opened this hour: $current_hour)"
# Send notification using common library # Send notification using common library
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000 notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
} }
# Get real binary path for an app # Get real binary path for an app
get_real_binary() { get_real_binary() {
local app="$1" local app="$1"
local wrapper_path="${APPS[$app]}" local wrapper_path="${APPS[$app]}"
local real_binary="${REAL_BINARIES[$app]}" local real_binary="${REAL_BINARIES[$app]}"
# Check if wrapper is installed (original moved to .orig) # Check if wrapper is installed (original moved to .orig)
if [[ -f "${wrapper_path}.orig" ]]; then if [[ -f "${wrapper_path}.orig" ]]; then
# Wrapper installed, return the actual executable # Wrapper installed, return the actual executable
echo "$real_binary" echo "$real_binary"
return 0 return 0
fi fi
return 1 return 1
} }
# Main wrapper function - called when wrapping app launches # Main wrapper function - called when wrapping app launches
wrapper_main() { wrapper_main() {
local app="$1" local app="$1"
shift shift
ensure_state_dir ensure_state_dir
local real_binary local real_binary
if ! real_binary=$(get_real_binary "$app"); then if ! real_binary=$(get_real_binary "$app"); then
log_message "ERROR: Real binary not found for $app" log_message "ERROR: Real binary not found for $app"
echo "Error: Real binary for $app not found. Was the installer run?" >&2 echo "Error: Real binary for $app not found. Was the installer run?" >&2
exit 1 exit 1
fi fi
if was_opened_this_hour "$app"; then if was_opened_this_hour "$app"; then
block_app "$app" block_app "$app"
exit 1 exit 1
fi fi
record_opening "$app" record_opening "$app"
exec "$real_binary" "$@" exec "$real_binary" "$@"
} }
# Install wrapper for a specific app # Install wrapper for a specific app
install_wrapper() { install_wrapper() {
local app="$1" local app="$1"
local wrapper_path="${APPS[$app]}" local wrapper_path="${APPS[$app]}"
local real_binary="${REAL_BINARIES[$app]}" local real_binary="${REAL_BINARIES[$app]}"
# Check if already wrapped # Check if already wrapped
if [[ -f "${wrapper_path}.orig" ]]; then if [[ -f "${wrapper_path}.orig" ]]; then
echo "$app already wrapped" echo "$app already wrapped"
return 0 return 0
fi fi
# Check if wrapper location exists (file or symlink) # Check if wrapper location exists (file or symlink)
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
echo "$app not installed ($wrapper_path not found)" echo "$app not installed ($wrapper_path not found)"
return 1 return 1
fi fi
# Check if real binary exists # Check if real binary exists
if [[ ! -x $real_binary ]]; then if [[ ! -x $real_binary ]]; then
echo "$app real binary not found ($real_binary)" echo "$app real binary not found ($real_binary)"
return 1 return 1
fi fi
echo " Installing wrapper for $app..." echo " Installing wrapper for $app..."
# Handle symlinks: save the symlink itself, not the target # Handle symlinks: save the symlink itself, not the target
if [[ -L $wrapper_path ]]; then if [[ -L $wrapper_path ]]; then
local link_target local link_target
link_target=$(readlink "$wrapper_path") link_target=$(readlink "$wrapper_path")
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig" echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
# Remove symlink and create .orig that stores the link target info # Remove symlink and create .orig that stores the link target info
echo "SYMLINK:$link_target" >"${wrapper_path}.orig" echo "SYMLINK:$link_target" > "${wrapper_path}.orig"
rm "$wrapper_path" rm "$wrapper_path"
else else
echo " Backing up $wrapper_path -> ${wrapper_path}.orig" echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
mv "$wrapper_path" "${wrapper_path}.orig" mv "$wrapper_path" "${wrapper_path}.orig"
fi fi
echo " Creating wrapper at $wrapper_path" echo " Creating wrapper at $wrapper_path"
cat >"$wrapper_path" <<WRAPPER_EOF cat > "$wrapper_path" << WRAPPER_EOF
#!/bin/bash #!/bin/bash
# Auto-generated wrapper for $app - blocks compulsive opening # Auto-generated wrapper for $app - blocks compulsive opening
# Real binary: $real_binary # Real binary: $real_binary
@ -195,88 +195,88 @@ install_wrapper() {
exec /usr/local/bin/block-compulsive-opening.sh wrapper "$app" "\$@" exec /usr/local/bin/block-compulsive-opening.sh wrapper "$app" "\$@"
WRAPPER_EOF WRAPPER_EOF
chmod +x "$wrapper_path" chmod +x "$wrapper_path"
echo "$app wrapper installed" echo "$app wrapper installed"
} }
# Uninstall wrapper for a specific app # Uninstall wrapper for a specific app
uninstall_wrapper() { uninstall_wrapper() {
local app="$1" local app="$1"
local wrapper_path="${APPS[$app]}" local wrapper_path="${APPS[$app]}"
if [[ ! -f "${wrapper_path}.orig" ]]; then if [[ ! -f "${wrapper_path}.orig" ]]; then
echo "$app wrapper not found" echo "$app wrapper not found"
return 1 return 1
fi fi
echo " Removing wrapper for $app..." echo " Removing wrapper for $app..."
rm -f "$wrapper_path" rm -f "$wrapper_path"
# Check if it was a symlink (stored as SYMLINK:target in .orig) # Check if it was a symlink (stored as SYMLINK:target in .orig)
local orig_content local orig_content
orig_content=$(cat "${wrapper_path}.orig" 2>/dev/null || echo "") orig_content=$(cat "${wrapper_path}.orig" 2> /dev/null || echo "")
if [[ $orig_content == SYMLINK:* ]]; then if [[ $orig_content == SYMLINK:* ]]; then
local link_target="${orig_content#SYMLINK:}" local link_target="${orig_content#SYMLINK:}"
echo " Restoring symlink $wrapper_path -> $link_target" echo " Restoring symlink $wrapper_path -> $link_target"
ln -s "$link_target" "$wrapper_path" ln -s "$link_target" "$wrapper_path"
rm "${wrapper_path}.orig" rm "${wrapper_path}.orig"
else else
echo " Restoring original file" echo " Restoring original file"
mv "${wrapper_path}.orig" "$wrapper_path" mv "${wrapper_path}.orig" "$wrapper_path"
fi fi
echo "$app restored" echo "$app restored"
} }
# Install all wrappers # Install all wrappers
install_all() { install_all() {
echo "Installing compulsive opening blockers..." echo "Installing compulsive opening blockers..."
echo "" echo ""
# Install main script to /usr/local/bin # Install main script to /usr/local/bin
local script_path local script_path
script_path="$(readlink -f "$0")" script_path="$(readlink -f "$0")"
local install_path="/usr/local/bin/block-compulsive-opening.sh" local install_path="/usr/local/bin/block-compulsive-opening.sh"
if [[ $script_path != "$install_path" ]]; then if [[ $script_path != "$install_path" ]]; then
echo "Installing main script to $install_path..." echo "Installing main script to $install_path..."
cp "$script_path" "$install_path" cp "$script_path" "$install_path"
chmod +x "$install_path" chmod +x "$install_path"
echo "✓ Main script installed" echo "✓ Main script installed"
else else
echo "Main script already at $install_path" echo "Main script already at $install_path"
fi fi
echo "" echo ""
# Install wrappers for each app # Install wrappers for each app
local installed=0 local installed=0
for app in "${!APPS[@]}"; do for app in "${!APPS[@]}"; do
if install_wrapper "$app"; then if install_wrapper "$app"; then
((installed++)) || true ((installed++)) || true
fi fi
done done
echo "" echo ""
echo "Installation complete. $installed app(s) wrapped." echo "Installation complete. $installed app(s) wrapped."
echo "" echo ""
echo "Each app can now only be opened once per hour." echo "Each app can now only be opened once per hour."
echo "State files stored in: $STATE_DIR" echo "State files stored in: $STATE_DIR"
echo "Logs stored in: $LOG_FILE" echo "Logs stored in: $LOG_FILE"
# Install pacman hook to re-wrap after package updates # Install pacman hook to re-wrap after package updates
install_pacman_hook install_pacman_hook
} }
# Install pacman hook to re-install wrappers after package updates # Install pacman hook to re-install wrappers after package updates
install_pacman_hook() { install_pacman_hook() {
local hook_dir="/etc/pacman.d/hooks" local hook_dir="/etc/pacman.d/hooks"
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook" local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
echo "" echo ""
echo "Installing pacman hook..." echo "Installing pacman hook..."
mkdir -p "$hook_dir" mkdir -p "$hook_dir"
cat >"$hook_file" <<'HOOK_EOF' cat > "$hook_file" << 'HOOK_EOF'
[Trigger] [Trigger]
Operation = Upgrade Operation = Upgrade
Operation = Install Operation = Install
@ -291,131 +291,131 @@ When = PostTransaction
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
HOOK_EOF HOOK_EOF
chmod 644 "$hook_file" chmod 644 "$hook_file"
echo "✓ Pacman hook installed: $hook_file" echo "✓ Pacman hook installed: $hook_file"
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates" echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
} }
# Uninstall pacman hook # Uninstall pacman hook
uninstall_pacman_hook() { uninstall_pacman_hook() {
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook" local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
if [[ -f "$hook_file" ]]; then if [[ -f $hook_file ]]; then
rm -f "$hook_file" rm -f "$hook_file"
echo "✓ Pacman hook removed" echo "✓ Pacman hook removed"
fi fi
} }
# Quietly re-wrap apps (for pacman hook - no interactive output) # Quietly re-wrap apps (for pacman hook - no interactive output)
rewrap_quiet() { rewrap_quiet() {
log_message "REWRAP: Pacman hook triggered, re-installing wrappers" log_message "REWRAP: Pacman hook triggered, re-installing wrappers"
for app in "${!APPS[@]}"; do for app in "${!APPS[@]}"; do
local wrapper_path="${APPS[$app]}" local wrapper_path="${APPS[$app]}"
# Check if wrapper was overwritten (no longer our wrapper script) # Check if wrapper was overwritten (no longer our wrapper script)
if [[ -f "$wrapper_path" ]] && ! grep -q "block-compulsive-opening" "$wrapper_path" 2>/dev/null; then if [[ -f $wrapper_path ]] && ! grep -q "block-compulsive-opening" "$wrapper_path" 2> /dev/null; then
# Wrapper was overwritten by package update # Wrapper was overwritten by package update
log_message "REWRAP: $app wrapper was overwritten, re-installing" log_message "REWRAP: $app wrapper was overwritten, re-installing"
# Remove old .orig if exists (it's now stale) # Remove old .orig if exists (it's now stale)
rm -f "${wrapper_path}.orig" rm -f "${wrapper_path}.orig"
# Re-install wrapper # Re-install wrapper
install_wrapper "$app" >>"$LOG_FILE" 2>&1 || true install_wrapper "$app" >> "$LOG_FILE" 2>&1 || true
fi fi
done done
log_message "REWRAP: Complete" log_message "REWRAP: Complete"
} }
# Uninstall all wrappers # Uninstall all wrappers
uninstall_all() { uninstall_all() {
echo "Removing compulsive opening blockers..." echo "Removing compulsive opening blockers..."
echo "" echo ""
for app in "${!APPS[@]}"; do for app in "${!APPS[@]}"; do
uninstall_wrapper "$app" || true uninstall_wrapper "$app" || true
done done
rm -f "/usr/local/bin/block-compulsive-opening.sh" rm -f "/usr/local/bin/block-compulsive-opening.sh"
# Remove pacman hook # Remove pacman hook
uninstall_pacman_hook uninstall_pacman_hook
echo "" echo ""
echo "Uninstallation complete." echo "Uninstallation complete."
} }
# Show status of all apps # Show status of all apps
show_status() { show_status() {
ensure_state_dir ensure_state_dir
local current_hour local current_hour
current_hour=$(get_hour_key) current_hour=$(get_hour_key)
echo "Compulsive Opening Blocker Status" echo "Compulsive Opening Blocker Status"
echo "==================================" echo "=================================="
echo "Current hour: $current_hour" echo "Current hour: $current_hour"
echo "" echo ""
for app in "${!APPS[@]}"; do for app in "${!APPS[@]}"; do
local state_file local state_file
state_file=$(get_state_file "$app") state_file=$(get_state_file "$app")
local status="not opened this hour" local status="not opened this hour"
local icon="○" local icon="○"
if [[ -f $state_file ]]; then if [[ -f $state_file ]]; then
local last_hour local last_hour
last_hour=$(cat "$state_file" 2>/dev/null || echo "") last_hour=$(cat "$state_file" 2> /dev/null || echo "")
if [[ $last_hour == "$current_hour" ]]; then if [[ $last_hour == "$current_hour" ]]; then
status="already opened (blocked until next hour)" status="already opened (blocked until next hour)"
icon="●" icon="●"
else else
status="last opened: $last_hour" status="last opened: $last_hour"
fi fi
fi fi
# Check if wrapped # Check if wrapped
local wrapped="not installed" local wrapped="not installed"
local wrapper_path="${APPS[$app]}" local wrapper_path="${APPS[$app]}"
if [[ -f "${wrapper_path}.orig" ]]; then if [[ -f "${wrapper_path}.orig" ]]; then
wrapped="wrapped" wrapped="wrapped"
elif [[ -f $wrapper_path ]]; then elif [[ -f $wrapper_path ]]; then
wrapped="installed (not wrapped)" wrapped="installed (not wrapped)"
fi fi
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status" printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
done done
echo "" echo ""
echo "State directory: $STATE_DIR" echo "State directory: $STATE_DIR"
} }
# Reset state for an app (allow opening again) # Reset state for an app (allow opening again)
reset_app() { reset_app() {
local app="$1" local app="$1"
local state_file local state_file
state_file=$(get_state_file "$app") state_file=$(get_state_file "$app")
if [[ -f $state_file ]]; then if [[ -f $state_file ]]; then
rm -f "$state_file" rm -f "$state_file"
echo "Reset $app - can be opened again this hour" echo "Reset $app - can be opened again this hour"
log_message "RESET: $app state cleared by user" log_message "RESET: $app state cleared by user"
else else
echo "$app was not marked as opened" echo "$app was not marked as opened"
fi fi
} }
# Clear all state # Clear all state
reset_all() { reset_all() {
ensure_state_dir ensure_state_dir
rm -f "$STATE_DIR"/*.lastopen rm -f "$STATE_DIR"/*.lastopen
echo "All apps reset - can be opened again this hour" echo "All apps reset - can be opened again this hour"
log_message "RESET: All app states cleared by user" log_message "RESET: All app states cleared by user"
} }
# Show usage # Show usage
show_usage() { show_usage() {
cat <<EOF cat << EOF
Block Compulsive Opening Script Block Compulsive Opening Script
================================ ================================
@ -447,60 +447,60 @@ EOF
# Main entry point # Main entry point
main() { main() {
case "${1:-help}" in case "${1:-help}" in
install) install)
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "Error: install requires root privileges" echo "Error: install requires root privileges"
echo "Run: sudo $0 install" echo "Run: sudo $0 install"
exit 1 exit 1
fi fi
install_all install_all
;; ;;
uninstall) uninstall)
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "Error: uninstall requires root privileges" echo "Error: uninstall requires root privileges"
echo "Run: sudo $0 uninstall" echo "Run: sudo $0 uninstall"
exit 1 exit 1
fi fi
uninstall_all uninstall_all
;; ;;
status) status)
show_status show_status
;; ;;
reset) reset)
if [[ -z ${2:-} ]]; then if [[ -z ${2:-} ]]; then
echo "Error: specify app to reset" echo "Error: specify app to reset"
echo "Apps: ${!APPS[*]}" echo "Apps: ${!APPS[*]}"
exit 1 exit 1
fi fi
reset_app "$2" reset_app "$2"
;; ;;
reset-all) reset-all)
reset_all reset_all
;; ;;
rewrap-quiet) rewrap-quiet)
# Called by pacman hook - quietly re-wrap apps after package updates # Called by pacman hook - quietly re-wrap apps after package updates
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
exit 1 exit 1
fi fi
rewrap_quiet rewrap_quiet
;; ;;
wrapper) wrapper)
if [[ -z ${2:-} ]]; then if [[ -z ${2:-} ]]; then
echo "Error: wrapper requires app name" echo "Error: wrapper requires app name"
exit 1 exit 1
fi fi
wrapper_main "${@:2}" wrapper_main "${@:2}"
;; ;;
help | -h | --help) help | -h | --help)
show_usage show_usage
;; ;;
*) *)
echo "Unknown command: $1" echo "Unknown command: $1"
show_usage show_usage
exit 1 exit 1
;; ;;
esac esac
} }
main "$@" main "$@"

View File

@ -15,14 +15,14 @@ warn() { printf "\033[1;33m[WARN]\033[0m %s\n" "$*"; }
err() { printf "\033[1;31m[ERR ]\033[0m %s\n" "$*"; } err() { printf "\033[1;31m[ERR ]\033[0m %s\n" "$*"; }
require_cmd() { require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then if ! command -v "$1" > /dev/null 2>&1; then
err "Missing dependency: $1" err "Missing dependency: $1"
MISSING=1 MISSING=1
fi fi
} }
usage() { usage() {
cat <<EOF cat << EOF
${SCRIPT_NAME} — Download and wire up LeechBlockNG from GitHub ${SCRIPT_NAME} — Download and wire up LeechBlockNG from GitHub
Usage: ${SCRIPT_NAME} [--version vX.Y[.Z]] [--force] [--install-firefox] Usage: ${SCRIPT_NAME} [--version vX.Y[.Z]] [--force] [--install-firefox]
@ -44,29 +44,29 @@ VERSION=""
FORCE=0 FORCE=0
AUTO_FIREFOX=0 AUTO_FIREFOX=0
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--version) --version)
VERSION="$2" VERSION="$2"
shift 2 shift 2
;; ;;
--force) --force)
FORCE=1 FORCE=1
shift shift
;; ;;
--install-firefox) --install-firefox)
AUTO_FIREFOX=1 AUTO_FIREFOX=1
shift shift
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
*) *)
err "Unrecognized option: $1" err "Unrecognized option: $1"
usage usage
exit 2 exit 2
;; ;;
esac esac
done done
# Dependencies # Dependencies
@ -76,45 +76,45 @@ require_cmd tar
require_cmd find require_cmd find
require_cmd sed require_cmd sed
require_cmd awk require_cmd awk
if ! command -v jq >/dev/null 2>&1; then if ! command -v jq > /dev/null 2>&1; then
warn "jq not found — will fall back to a simpler tag detection method." warn "jq not found — will fall back to a simpler tag detection method."
fi fi
[[ $MISSING -eq 1 ]] && { [[ $MISSING -eq 1 ]] && {
err "Please install missing tools and re-run." err "Please install missing tools and re-run."
exit 1 exit 1
} }
REPO_OWNER="proginosko" REPO_OWNER="proginosko"
REPO_NAME="LeechBlockNG" REPO_NAME="LeechBlockNG"
get_latest_tag() { get_latest_tag() {
local tag local tag
if command -v jq >/dev/null 2>&1; then if command -v jq > /dev/null 2>&1; then
tag=$(curl -fsSL "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | jq -r '.tag_name // empty' || true) tag=$(curl -fsSL "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | jq -r '.tag_name // empty' || true)
if [[ -n $tag && $tag != "null" ]]; then if [[ -n $tag && $tag != "null" ]]; then
echo "$tag" echo "$tag"
return 0 return 0
fi fi
fi fi
# Fallback: follow redirect for /releases/latest to extract tag # Fallback: follow redirect for /releases/latest to extract tag
tag=$(curl -fsSLI "https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/latest" | awk -F'/tag/' '/^location:/I {print $2}' | tr -d '\r\n' || true) tag=$(curl -fsSLI "https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/latest" | awk -F'/tag/' '/^location:/I {print $2}' | tr -d '\r\n' || true)
if [[ -n $tag ]]; then if [[ -n $tag ]]; then
echo "$tag" echo "$tag"
return 0 return 0
fi fi
return 1 return 1
} }
if [[ -z $VERSION ]]; then if [[ -z $VERSION ]]; then
info "Resolving latest release tag from GitHub…" info "Resolving latest release tag from GitHub…"
if ! VERSION=$(get_latest_tag); then if ! VERSION=$(get_latest_tag); then
err "Failed to determine latest version tag" err "Failed to determine latest version tag"
exit 1 exit 1
fi fi
fi fi
if [[ ! $VERSION =~ ^v?[0-9]+(\.[0-9]+)*$ ]]; then if [[ ! $VERSION =~ ^v?[0-9]+(\.[0-9]+)*$ ]]; then
warn "Version tag '$VERSION' doesn't look like vX[.Y[.Z]] — continuing anyway." warn "Version tag '$VERSION' doesn't look like vX[.Y[.Z]] — continuing anyway."
fi fi
VERSION=${VERSION#v} # strip leading v for folder names VERSION=${VERSION#v} # strip leading v for folder names
@ -126,40 +126,40 @@ VERSION_DIR="$INSTALL_ROOT/$VERSION"
CURRENT_LINK="$INSTALL_ROOT/current" CURRENT_LINK="$INSTALL_ROOT/current"
if [[ -d $VERSION_DIR && $FORCE -ne 1 ]]; then if [[ -d $VERSION_DIR && $FORCE -ne 1 ]]; then
info "LeechBlockNG $VERSION already present at $VERSION_DIR (use --force to reinstall)." info "LeechBlockNG $VERSION already present at $VERSION_DIR (use --force to reinstall)."
else else
info "Downloading LeechBlockNG $TAG source from GitHub…" info "Downloading LeechBlockNG $TAG source from GitHub…"
tmpdir=$(mktemp -d) tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT trap 'rm -rf "$tmpdir"' EXIT
ARCHIVE_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${TAG}.tar.gz" ARCHIVE_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${TAG}.tar.gz"
ARCHIVE_FILE="$tmpdir/${REPO_NAME}-${TAG}.tar.gz" ARCHIVE_FILE="$tmpdir/${REPO_NAME}-${TAG}.tar.gz"
curl -fL --retry 3 -o "$ARCHIVE_FILE" "$ARCHIVE_URL" curl -fL --retry 3 -o "$ARCHIVE_FILE" "$ARCHIVE_URL"
info "Extracting…" info "Extracting…"
mkdir -p "$tmpdir/extract" mkdir -p "$tmpdir/extract"
tar -xzf "$ARCHIVE_FILE" -C "$tmpdir/extract" tar -xzf "$ARCHIVE_FILE" -C "$tmpdir/extract"
# The archive usually extracts to REPO_NAME-TAG/ … # The archive usually extracts to REPO_NAME-TAG/ …
src_root=$(find "$tmpdir/extract" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n1 || true) src_root=$(find "$tmpdir/extract" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n1 || true)
[[ -z $src_root ]] && { [[ -z $src_root ]] && {
err "Could not locate extracted source root" err "Could not locate extracted source root"
exit 1 exit 1
} }
# Find the extension manifest (support a couple of common layouts) # Find the extension manifest (support a couple of common layouts)
manifest_path=$(find "$src_root" -maxdepth 5 -type f -name manifest.json | head -n1 || true) manifest_path=$(find "$src_root" -maxdepth 5 -type f -name manifest.json | head -n1 || true)
if [[ -z $manifest_path ]]; then if [[ -z $manifest_path ]]; then
err "manifest.json not found in the extracted archive. The project layout may have changed." err "manifest.json not found in the extracted archive. The project layout may have changed."
exit 1 exit 1
fi fi
ext_dir=$(dirname "$manifest_path") ext_dir=$(dirname "$manifest_path")
mkdir -p "$INSTALL_ROOT" mkdir -p "$INSTALL_ROOT"
rm -rf "$VERSION_DIR" rm -rf "$VERSION_DIR"
info "Installing to $VERSION_DIR" info "Installing to $VERSION_DIR"
mkdir -p "$VERSION_DIR" mkdir -p "$VERSION_DIR"
# Copy the extension directory as-is (avoid bringing tests or build scripts) # Copy the extension directory as-is (avoid bringing tests or build scripts)
rsync -a --delete "$ext_dir/" "$VERSION_DIR/" 2>/dev/null || cp -a "$ext_dir/." "$VERSION_DIR/" rsync -a --delete "$ext_dir/" "$VERSION_DIR/" 2> /dev/null || cp -a "$ext_dir/." "$VERSION_DIR/"
ln -sfn "$VERSION_DIR" "$CURRENT_LINK" ln -sfn "$VERSION_DIR" "$CURRENT_LINK"
fi fi
EXT_PATH="$CURRENT_LINK" # stable path used by wrappers EXT_PATH="$CURRENT_LINK" # stable path used by wrappers
@ -167,21 +167,21 @@ EXT_PATH="$CURRENT_LINK" # stable path used by wrappers
# Detect browsers # Detect browsers
declare -A BROWSERS declare -A BROWSERS
BROWSERS=( BROWSERS=(
[chromium]="Chromium" [chromium]="Chromium"
[google - chrome - stable]="Google Chrome" [google - chrome - stable]="Google Chrome"
[google - chrome]="Google Chrome" [google - chrome]="Google Chrome"
[brave - browser]="Brave" [brave - browser]="Brave"
[vivaldi - stable]="Vivaldi" [vivaldi - stable]="Vivaldi"
[vivaldi]="Vivaldi" [vivaldi]="Vivaldi"
[opera]="Opera" [opera]="Opera"
[thorium - browser]="Thorium" [thorium - browser]="Thorium"
) )
declare -A FIREFOXES declare -A FIREFOXES
FIREFOXES=( FIREFOXES=(
[firefox]="Firefox" [firefox]="Firefox"
[firefox - developer - edition]="Firefox Developer Edition" [firefox - developer - edition]="Firefox Developer Edition"
[librewolf]="LibreWolf" [librewolf]="LibreWolf"
) )
found_any=0 found_any=0
@ -193,36 +193,36 @@ user_apps_dir="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
mkdir -p "$user_apps_dir" mkdir -p "$user_apps_dir"
create_wrapper_and_desktop() { create_wrapper_and_desktop() {
local bin="$1" local bin="$1"
shift shift
local pretty="$1" local pretty="$1"
shift shift
local wrapper="$wrap_bin_dir/${bin}-with-leechblock" local wrapper="$wrap_bin_dir/${bin}-with-leechblock"
local real_bin local real_bin
real_bin=$(command -v "$bin" || true) real_bin=$(command -v "$bin" || true)
[[ -z $real_bin ]] && return [[ -z $real_bin ]] && return
cat >"$wrapper" <<WRAP cat > "$wrapper" << WRAP
#!/usr/bin/env bash #!/usr/bin/env bash
exec "$real_bin" --load-extension="$EXT_PATH" "$@" exec "$real_bin" --load-extension="$EXT_PATH" "$@"
WRAP WRAP
chmod +x "$wrapper" chmod +x "$wrapper"
# Try to reuse icon from an existing desktop file if available # Try to reuse icon from an existing desktop file if available
local sys_desktop existing_icon existing_name categories local sys_desktop existing_icon existing_name categories
sys_desktop=$(grep -RIl "^Exec=.*${bin}" /usr/share/applications 2>/dev/null | head -n1 || true) sys_desktop=$(grep -RIl "^Exec=.*${bin}" /usr/share/applications 2> /dev/null | head -n1 || true)
if [[ -n $sys_desktop ]]; then if [[ -n $sys_desktop ]]; then
existing_icon=$(awk -F= '/^Icon=/{print $2; exit}' "$sys_desktop" || true) existing_icon=$(awk -F= '/^Icon=/{print $2; exit}' "$sys_desktop" || true)
existing_name=$(awk -F= '/^Name=/{print $2; exit}' "$sys_desktop" || true) existing_name=$(awk -F= '/^Name=/{print $2; exit}' "$sys_desktop" || true)
categories=$(awk -F= '/^Categories=/{print $2; exit}' "$sys_desktop" || true) categories=$(awk -F= '/^Categories=/{print $2; exit}' "$sys_desktop" || true)
fi fi
[[ -z $existing_icon ]] && existing_icon="$bin" [[ -z $existing_icon ]] && existing_icon="$bin"
[[ -z $existing_name ]] && existing_name="$pretty" [[ -z $existing_name ]] && existing_name="$pretty"
[[ -z $categories ]] && categories="Network;WebBrowser;" [[ -z $categories ]] && categories="Network;WebBrowser;"
local desktop_file="$user_apps_dir/${bin}-with-leechblock.desktop" local desktop_file="$user_apps_dir/${bin}-with-leechblock.desktop"
cat >"$desktop_file" <<DESK cat > "$desktop_file" << DESK
[Desktop Entry] [Desktop Entry]
Name=${existing_name} (LeechBlock) Name=${existing_name} (LeechBlock)
Exec=${wrapper} %U Exec=${wrapper} %U
@ -233,35 +233,35 @@ Categories=${categories}
StartupNotify=true StartupNotify=true
DESK DESK
info "Created wrapper: $wrapper" info "Created wrapper: $wrapper"
info "Created launcher: $desktop_file" info "Created launcher: $desktop_file"
found_any=1 found_any=1
} }
info "Detecting installed browsers…" info "Detecting installed browsers…"
for bin in "${!BROWSERS[@]}"; do for bin in "${!BROWSERS[@]}"; do
if command -v "$bin" >/dev/null 2>&1; then if command -v "$bin" > /dev/null 2>&1; then
create_wrapper_and_desktop "$bin" "${BROWSERS[$bin]}" create_wrapper_and_desktop "$bin" "${BROWSERS[$bin]}"
fi fi
done done
ff_found=0 ff_found=0
for bin in "${!FIREFOXES[@]}"; do for bin in "${!FIREFOXES[@]}"; do
if command -v "$bin" >/dev/null 2>&1; then if command -v "$bin" > /dev/null 2>&1; then
ff_found=1 ff_found=1
fi fi
done done
echo echo
if [[ $found_any -eq 1 ]]; then if [[ $found_any -eq 1 ]]; then
info "Chromium-based integration complete. Launch the browser via its '(LeechBlock)' launcher." info "Chromium-based integration complete. Launch the browser via its '(LeechBlock)' launcher."
warn "Chromium will mark it as a developer extension; this is expected for unpacked installs." warn "Chromium will mark it as a developer extension; this is expected for unpacked installs."
fi fi
if [[ $ff_found -eq 1 ]]; then if [[ $ff_found -eq 1 ]]; then
echo echo
warn "Detected Firefox-based browser(s). Permanent install from GitHub source isn't possible on stable builds due to required signing." warn "Detected Firefox-based browser(s). Permanent install from GitHub source isn't possible on stable builds due to required signing."
cat <<FF cat << FF
Options: Options:
1) Install from Mozilla Add-ons (recommended): 1) Install from Mozilla Add-ons (recommended):
https://addons.mozilla.org/firefox/addon/leechblock-ng/ https://addons.mozilla.org/firefox/addon/leechblock-ng/
@ -276,8 +276,8 @@ FF
fi fi
if [[ $found_any -eq 0 && $ff_found -eq 0 ]]; then if [[ $found_any -eq 0 && $ff_found -eq 0 ]]; then
warn "No supported browsers detected. We placed the extension at: $VERSION_DIR" warn "No supported browsers detected. We placed the extension at: $VERSION_DIR"
echo "Supported (auto-wired): ${!BROWSERS[*]}. Detected Firefox variants will show guidance only." echo "Supported (auto-wired): ${!BROWSERS[*]}. Detected Firefox variants will show guidance only."
fi fi
echo echo
@ -285,36 +285,36 @@ info "Done. Version: $VERSION (tag $TAG) installed under $VERSION_DIR"
# If requested, attempt automatic install on Firefox via enterprise policies # If requested, attempt automatic install on Firefox via enterprise policies
if [[ $AUTO_FIREFOX -eq 1 && $ff_found -eq 1 ]]; then if [[ $AUTO_FIREFOX -eq 1 && $ff_found -eq 1 ]]; then
echo echo
info "Attempting Firefox auto-install via Enterprise Policies (requires sudo)." info "Attempting Firefox auto-install via Enterprise Policies (requires sudo)."
# AMO info # AMO info
ADDON_ID="leechblockng@proginosko.com" ADDON_ID="leechblockng@proginosko.com"
ADDON_AMO_URL="https://addons.mozilla.org/firefox/downloads/latest/leechblock-ng/latest.xpi" ADDON_AMO_URL="https://addons.mozilla.org/firefox/downloads/latest/leechblock-ng/latest.xpi"
# Determine policy directories for detected Firefox-like browsers # Determine policy directories for detected Firefox-like browsers
declare -a POLICY_DIRS declare -a POLICY_DIRS
POLICY_DIRS=() POLICY_DIRS=()
if command -v firefox >/dev/null 2>&1; then if command -v firefox > /dev/null 2>&1; then
POLICY_DIRS+=("/etc/firefox/policies" "/usr/lib/firefox/distribution") POLICY_DIRS+=("/etc/firefox/policies" "/usr/lib/firefox/distribution")
fi fi
if command -v firefox-developer-edition >/dev/null 2>&1; then if command -v firefox-developer-edition > /dev/null 2>&1; then
POLICY_DIRS+=("/etc/firefox-developer-edition/policies" "/usr/lib/firefox-developer-edition/distribution") POLICY_DIRS+=("/etc/firefox-developer-edition/policies" "/usr/lib/firefox-developer-edition/distribution")
fi fi
if command -v librewolf >/dev/null 2>&1; then if command -v librewolf > /dev/null 2>&1; then
POLICY_DIRS+=("/etc/librewolf/policies" "/usr/lib/librewolf/distribution") POLICY_DIRS+=("/etc/librewolf/policies" "/usr/lib/librewolf/distribution")
fi fi
# Generic mozilla path as fallback # Generic mozilla path as fallback
POLICY_DIRS+=("/usr/lib/mozilla/distribution") POLICY_DIRS+=("/usr/lib/mozilla/distribution")
updated_any=0 updated_any=0
for pol_target in "${POLICY_DIRS[@]}"; do for pol_target in "${POLICY_DIRS[@]}"; do
tmp_pol=$(mktemp) tmp_pol=$(mktemp)
existing="${pol_target}/policies.json" existing="${pol_target}/policies.json"
if sudo test -f "$existing"; then if sudo test -f "$existing"; then
info "Merging into existing policies.json at $existing" info "Merging into existing policies.json at $existing"
sudo cp "$existing" "$tmp_pol" sudo cp "$existing" "$tmp_pol"
if command -v jq >/dev/null 2>&1; then if command -v jq > /dev/null 2>&1; then
merged=$(jq --arg id "$ADDON_ID" --arg url "$ADDON_AMO_URL" ' merged=$(jq --arg id "$ADDON_ID" --arg url "$ADDON_AMO_URL" '
.policies |= (. // {}) | .policies |= (. // {}) |
.policies.ExtensionSettings |= (. // {}) | .policies.ExtensionSettings |= (. // {}) |
.policies.ExtensionSettings."*" |= (. // {"installation_mode":"allowed"}) | .policies.ExtensionSettings."*" |= (. // {"installation_mode":"allowed"}) |
@ -322,17 +322,17 @@ if [[ $AUTO_FIREFOX -eq 1 && $ff_found -eq 1 ]]; then
.policies.ExtensionSettings[$id].installation_mode = "force_installed" | .policies.ExtensionSettings[$id].installation_mode = "force_installed" |
.policies.ExtensionSettings[$id].install_url = $url .policies.ExtensionSettings[$id].install_url = $url
' "$tmp_pol") || merged="" ' "$tmp_pol") || merged=""
if [[ -n $merged ]]; then if [[ -n $merged ]]; then
printf '%s\n' "$merged" >"$tmp_pol" printf '%s\n' "$merged" > "$tmp_pol"
else else
warn "jq merge failed; skipping $pol_target" warn "jq merge failed; skipping $pol_target"
rm -f "$tmp_pol" rm -f "$tmp_pol"
continue continue
fi fi
else else
warn "jq not available; creating minimal policies.json (existing file will be backed up)." warn "jq not available; creating minimal policies.json (existing file will be backed up)."
sudo cp "$existing" "${existing}.bak.$(date +%s)" sudo cp "$existing" "${existing}.bak.$(date +%s)"
cat >"$tmp_pol" <<JSON cat > "$tmp_pol" << JSON
{ {
"policies": { "policies": {
"ExtensionSettings": { "ExtensionSettings": {
@ -345,10 +345,10 @@ if [[ $AUTO_FIREFOX -eq 1 && $ff_found -eq 1 ]]; then
} }
} }
JSON JSON
fi fi
else else
info "Creating new policies.json at $pol_target" info "Creating new policies.json at $pol_target"
cat >"$tmp_pol" <<JSON cat > "$tmp_pol" << JSON
{ {
"policies": { "policies": {
"ExtensionSettings": { "ExtensionSettings": {
@ -361,18 +361,18 @@ JSON
} }
} }
JSON JSON
fi fi
sudo mkdir -p "$pol_target" sudo mkdir -p "$pol_target"
sudo cp "$tmp_pol" "$pol_target/policies.json" sudo cp "$tmp_pol" "$pol_target/policies.json"
rm -f "$tmp_pol" rm -f "$tmp_pol"
updated_any=1 updated_any=1
done done
if [[ $updated_any -eq 1 ]]; then if [[ $updated_any -eq 1 ]]; then
info "Firefox policies updated. Restart Firefox/LibreWolf to complete installation of LeechBlock NG." info "Firefox policies updated. Restart Firefox/LibreWolf to complete installation of LeechBlock NG."
else else
warn "No Firefox policy locations updated. You may not have a supported Firefox installed." warn "No Firefox policy locations updated. You may not have a supported Firefox installed."
fi fi
info "Firefox policy updated. Restart Firefox to complete installation of LeechBlock NG." info "Firefox policy updated. Restart Firefox to complete installation of LeechBlock NG."
fi fi

View File

@ -16,333 +16,333 @@ source "$SCRIPT_DIR/../lib/common.sh"
# Configuration # Configuration
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/music-parallelism" LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/music-parallelism"
mkdir -p "$LOG_DIR" 2>/dev/null || true mkdir -p "$LOG_DIR" 2> /dev/null || true
export LOG_FILE="$LOG_DIR/music-parallelism.log" export LOG_FILE="$LOG_DIR/music-parallelism.log"
CHECK_INTERVAL=3 CHECK_INTERVAL=3
# Override focus apps with extended list for this script # Override focus apps with extended list for this script
FOCUS_APPS_WINDOWS=( FOCUS_APPS_WINDOWS=(
# IDEs and code editors - match window titles # IDEs and code editors - match window titles
"Visual Studio Code" "Visual Studio Code"
"VSCodium" "VSCodium"
"Cursor" "Cursor"
"IntelliJ IDEA" "IntelliJ IDEA"
"PyCharm" "PyCharm"
"WebStorm" "WebStorm"
"CLion" "CLion"
"Rider" "Rider"
"Sublime Text" "Sublime Text"
"Atom" "Atom"
"Neovide" "Neovide"
# Gaming # Gaming
"Steam" "Steam"
# Creative apps # Creative apps
"Blender" "Blender"
"Godot" "Godot"
"Unity" "Unity"
"Unreal Editor" "Unreal Editor"
) )
# Music streaming services - browser tabs or electron apps # Music streaming services - browser tabs or electron apps
# These will be killed when focus apps are detected # These will be killed when focus apps are detected
MUSIC_SERVICES=( MUSIC_SERVICES=(
# YouTube Music specific patterns (NOT regular YouTube) # YouTube Music specific patterns (NOT regular YouTube)
"music.youtube.com" "music.youtube.com"
"youtube-music" # Electron app "youtube-music" # Electron app
"YouTube Music" # Window title "YouTube Music" # Window title
# Spotify # Spotify
"spotify" "spotify"
"Spotify" "Spotify"
# Tidal # Tidal
"tidal" "tidal"
"TIDAL" "TIDAL"
# Deezer # Deezer
"deezer" "deezer"
# Amazon Music # Amazon Music
"Amazon Music" "Amazon Music"
"amazon music" "amazon music"
# Apple Music (web) # Apple Music (web)
"music.apple.com" "music.apple.com"
# SoundCloud # SoundCloud
"soundcloud.com" "soundcloud.com"
# Pandora # Pandora
"pandora.com" "pandora.com"
) )
# Check if any music service is running and return its details # Check if any music service is running and return its details
find_music_services() { find_music_services() {
local found_services=() local found_services=()
for service in "${MUSIC_SERVICES[@]}"; do for service in "${MUSIC_SERVICES[@]}"; do
# Check for browser tabs with music services # Check for browser tabs with music services
# This checks window titles which usually contain the URL or tab title # This checks window titles which usually contain the URL or tab title
if command -v xdotool &>/dev/null; then if command -v xdotool &> /dev/null; then
if xdotool search --name "$service" &>/dev/null 2>&1; then if xdotool search --name "$service" &> /dev/null 2>&1; then
found_services+=("$service (window)") found_services+=("$service (window)")
fi fi
fi fi
# Check for dedicated desktop apps # Check for dedicated desktop apps
if pgrep -i -f "$service" &>/dev/null; then if pgrep -i -f "$service" &> /dev/null; then
found_services+=("$service (process)") found_services+=("$service (process)")
fi fi
done done
if [[ ${#found_services[@]} -gt 0 ]]; then if [[ ${#found_services[@]} -gt 0 ]]; then
printf '%s\n' "${found_services[@]}" printf '%s\n' "${found_services[@]}"
return 0 return 0
fi fi
return 1 return 1
} }
# Kill music services # Kill music services
kill_music_services() { kill_music_services() {
local killed=false local killed=false
# Kill YouTube Music browser tabs # Kill YouTube Music browser tabs
# YouTube Music runs in browser, so we need to close specific tabs # YouTube Music runs in browser, so we need to close specific tabs
# We use xdotool to find and close windows with "YouTube Music" or "music.youtube.com" # We use xdotool to find and close windows with "YouTube Music" or "music.youtube.com"
if command -v xdotool &>/dev/null; then if command -v xdotool &> /dev/null; then
# Find windows with YouTube Music in title # Find windows with YouTube Music in title
local yt_music_windows local yt_music_windows
yt_music_windows=$(xdotool search --name "YouTube Music" 2>/dev/null || true) yt_music_windows=$(xdotool search --name "YouTube Music" 2> /dev/null || true)
for wid in $yt_music_windows; do for wid in $yt_music_windows; do
if [[ -n $wid ]]; then if [[ -n $wid ]]; then
# Get window name for logging # Get window name for logging
local wname local wname
wname=$(xdotool getwindowname "$wid" 2>/dev/null || echo "unknown") wname=$(xdotool getwindowname "$wid" 2> /dev/null || echo "unknown")
# Only close if it's YouTube Music, not regular YouTube # Only close if it's YouTube Music, not regular YouTube
if [[ $wname == *"YouTube Music"* ]] || [[ $wname == *"music.youtube.com"* ]]; then if [[ $wname == *"YouTube Music"* ]] || [[ $wname == *"music.youtube.com"* ]]; then
log_message "Closing YouTube Music window: $wname (ID: $wid)" log_message "Closing YouTube Music window: $wname (ID: $wid)"
xdotool windowclose "$wid" 2>/dev/null || true xdotool windowclose "$wid" 2> /dev/null || true
killed=true killed=true
fi fi
fi fi
done done
fi fi
# Kill YouTube Music Electron app # Kill YouTube Music Electron app
if pgrep -f "youtube-music" &>/dev/null; then if pgrep -f "youtube-music" &> /dev/null; then
log_message "Killing YouTube Music app" log_message "Killing YouTube Music app"
pkill -9 -f "youtube-music" 2>/dev/null || true pkill -9 -f "youtube-music" 2> /dev/null || true
killed=true killed=true
fi fi
# Kill Spotify # Kill Spotify
if pgrep -x "spotify" &>/dev/null; then if pgrep -x "spotify" &> /dev/null; then
log_message "Killing Spotify" log_message "Killing Spotify"
pkill -9 -x "spotify" 2>/dev/null || true pkill -9 -x "spotify" 2> /dev/null || true
killed=true killed=true
fi fi
# Kill other music streaming app processes # Kill other music streaming app processes
local music_processes=("tidal" "deezer" "Amazon Music") local music_processes=("tidal" "deezer" "Amazon Music")
for proc in "${music_processes[@]}"; do for proc in "${music_processes[@]}"; do
if pgrep -i -f "$proc" &>/dev/null; then if pgrep -i -f "$proc" &> /dev/null; then
log_message "Killing $proc" log_message "Killing $proc"
pkill -9 -i -f "$proc" 2>/dev/null || true pkill -9 -i -f "$proc" 2> /dev/null || true
killed=true killed=true
fi fi
done done
# Close browser tabs for web-based music services # Close browser tabs for web-based music services
if command -v xdotool &>/dev/null; then if command -v xdotool &> /dev/null; then
local web_music_patterns=("music.apple.com" "soundcloud.com" "pandora.com" "deezer.com" "tidal.com") local web_music_patterns=("music.apple.com" "soundcloud.com" "pandora.com" "deezer.com" "tidal.com")
for pattern in "${web_music_patterns[@]}"; do for pattern in "${web_music_patterns[@]}"; do
local windows local windows
windows=$(xdotool search --name "$pattern" 2>/dev/null || true) windows=$(xdotool search --name "$pattern" 2> /dev/null || true)
for wid in $windows; do for wid in $windows; do
if [[ -n $wid ]]; then if [[ -n $wid ]]; then
local wname local wname
wname=$(xdotool getwindowname "$wid" 2>/dev/null || echo "unknown") wname=$(xdotool getwindowname "$wid" 2> /dev/null || echo "unknown")
log_message "Closing music service window: $wname (ID: $wid)" log_message "Closing music service window: $wname (ID: $wid)"
xdotool windowclose "$wid" 2>/dev/null || true xdotool windowclose "$wid" 2> /dev/null || true
killed=true killed=true
fi fi
done done
done done
fi fi
if $killed; then if $killed; then
return 0 return 0
fi fi
return 1 return 1
} }
# Send notification to user # Send notification to user
notify_user() { notify_user() {
local focus_app="$1" local focus_app="$1"
local message="Music stopped - focus mode active ($focus_app detected)" local message="Music stopped - focus mode active ($focus_app detected)"
# Try to send desktop notification # Try to send desktop notification
if command -v notify-send &>/dev/null; then if command -v notify-send &> /dev/null; then
notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2>/dev/null || true notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2> /dev/null || true
fi fi
log_message "$message" log_message "$message"
} }
# Instant monitoring loop - uses polling at high frequency # Instant monitoring loop - uses polling at high frequency
# This runs every 0.5 seconds for near-instant detection # This runs every 0.5 seconds for near-instant detection
instant_monitor_loop() { instant_monitor_loop() {
log_message "=== Music Parallelism INSTANT Monitor Started ===" log_message "=== Music Parallelism INSTANT Monitor Started ==="
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
log_message "Polling every 0.5 seconds for instant kill" log_message "Polling every 0.5 seconds for instant kill"
while true; do while true; do
# Only check if focus app is running # Only check if focus app is running
if is_focus_app_running &>/dev/null; then if is_focus_app_running &> /dev/null; then
# Instant kill youtube-music if detected # Instant kill youtube-music if detected
if pgrep -f "youtube-music" &>/dev/null; then if pgrep -f "youtube-music" &> /dev/null; then
pkill -9 -f "youtube-music" 2>/dev/null || true pkill -9 -f "youtube-music" 2> /dev/null || true
log_message "INSTANT KILL: YouTube Music terminated" log_message "INSTANT KILL: YouTube Music terminated"
notify-send -u normal -t 2000 "🎵 YouTube Music killed" "Focus mode active" 2>/dev/null || true notify-send -u normal -t 2000 "🎵 YouTube Music killed" "Focus mode active" 2> /dev/null || true
fi fi
# Also check other music services # Also check other music services
if pgrep -x "spotify" &>/dev/null; then if pgrep -x "spotify" &> /dev/null; then
pkill -9 -x "spotify" 2>/dev/null || true pkill -9 -x "spotify" 2> /dev/null || true
log_message "INSTANT KILL: Spotify terminated" log_message "INSTANT KILL: Spotify terminated"
fi fi
fi fi
sleep 0.5 sleep 0.5
done done
} }
# Main monitoring loop # Main monitoring loop
monitor_loop() { monitor_loop() {
log_message "=== Music Parallelism Monitor Started ===" log_message "=== Music Parallelism Monitor Started ==="
log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}"
log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}"
log_message "Music services monitored: ${MUSIC_SERVICES[*]}" log_message "Music services monitored: ${MUSIC_SERVICES[*]}"
log_message "Check interval: ${CHECK_INTERVAL}s" log_message "Check interval: ${CHECK_INTERVAL}s"
while true; do while true; do
# Check if a focus app is running # Check if a focus app is running
local focus_app local focus_app
if focus_app=$(is_focus_app_running); then if focus_app=$(is_focus_app_running); then
# Focus app detected, check for music services # Focus app detected, check for music services
local music_services local music_services
if music_services=$(find_music_services); then if music_services=$(find_music_services); then
log_message "Conflict detected: Focus app '$focus_app' running with music services" log_message "Conflict detected: Focus app '$focus_app' running with music services"
log_message "Active music services: $music_services" log_message "Active music services: $music_services"
# Kill the music services # Kill the music services
if kill_music_services; then if kill_music_services; then
notify_user "$focus_app" notify_user "$focus_app"
fi fi
fi fi
fi fi
sleep "$CHECK_INTERVAL" sleep "$CHECK_INTERVAL"
done done
} }
# Show status # Show status
show_status() { show_status() {
echo "Music Parallelism Monitor Status" echo "Music Parallelism Monitor Status"
echo "=================================" echo "================================="
echo "" echo ""
echo "Focus Applications (window-based detection):" echo "Focus Applications (window-based detection):"
local focus_running=false local focus_running=false
# Check windows # Check windows
if command -v xdotool &>/dev/null; then if command -v xdotool &> /dev/null; then
for app in "${FOCUS_APPS_WINDOWS[@]}"; do for app in "${FOCUS_APPS_WINDOWS[@]}"; do
if xdotool search --name "$app" &>/dev/null 2>&1; then if xdotool search --name "$app" &> /dev/null 2>&1; then
echo "$app (WINDOW OPEN)" echo "$app (WINDOW OPEN)"
focus_running=true focus_running=true
fi fi
done done
fi fi
# Check processes # Check processes
for app in "${FOCUS_APPS_PROCESSES[@]}"; do for app in "${FOCUS_APPS_PROCESSES[@]}"; do
if pgrep -f "$app" &>/dev/null; then if pgrep -f "$app" &> /dev/null; then
echo "$app (PROCESS RUNNING)" echo "$app (PROCESS RUNNING)"
focus_running=true focus_running=true
fi fi
done done
if ! $focus_running; then if ! $focus_running; then
echo " (none detected)" echo " (none detected)"
fi fi
echo "" echo ""
echo "Music Services:" echo "Music Services:"
local music_running=false local music_running=false
if music_services=$(find_music_services 2>/dev/null); then if music_services=$(find_music_services 2> /dev/null); then
echo "$music_services" | while read -r svc; do echo "$music_services" | while read -r svc; do
echo "$svc (RUNNING)" echo "$svc (RUNNING)"
done done
music_running=true music_running=true
fi fi
if ! $music_running; then if ! $music_running; then
echo " (none detected)" echo " (none detected)"
fi fi
echo "" echo ""
if $focus_running && $music_running; then if $focus_running && $music_running; then
echo "⚠️ CONFLICT: Focus app and music running together!" echo "⚠️ CONFLICT: Focus app and music running together!"
echo " Music would be killed in monitoring mode." echo " Music would be killed in monitoring mode."
elif $focus_running; then elif $focus_running; then
echo "✓ Focus mode active (no music playing)" echo "✓ Focus mode active (no music playing)"
elif $music_running; then elif $music_running; then
echo "✓ Music playing (no focus app detected - this is fine)" echo "✓ Music playing (no focus app detected - this is fine)"
else else
echo "✓ Idle (nothing detected)" echo "✓ Idle (nothing detected)"
fi fi
} }
# Show usage # Show usage
show_usage() { show_usage() {
echo "Music Parallelism Prevention Script" echo "Music Parallelism Prevention Script"
echo "====================================" echo "===================================="
echo "" echo ""
echo "Usage: $0 [command]" echo "Usage: $0 [command]"
echo "" echo ""
echo "Commands:" echo "Commands:"
echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)" echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)"
echo " instant - Instant monitoring (checks every 0.5s for immediate kill)" echo " instant - Instant monitoring (checks every 0.5s for immediate kill)"
echo " status - Show current status of focus apps and music services" echo " status - Show current status of focus apps and music services"
echo " kill - Immediately kill all music services" echo " kill - Immediately kill all music services"
echo " help - Show this help message" echo " help - Show this help message"
echo "" echo ""
echo "Description:" echo "Description:"
echo " This script prevents multitasking between focus work and music." echo " This script prevents multitasking between focus work and music."
echo " When a focus application (VS Code, Steam, etc.) is detected" echo " When a focus application (VS Code, Steam, etc.) is detected"
echo " alongside a music streaming service, the music is stopped." echo " alongside a music streaming service, the music is stopped."
echo "" echo ""
echo " Music is allowed when no focus apps are running." echo " Music is allowed when no focus apps are running."
echo "" echo ""
} }
# Main # Main
case "${1:-instant}" in case "${1:-instant}" in
monitor | start | run) monitor | start | run)
monitor_loop monitor_loop
;; ;;
instant | fast) instant | fast)
instant_monitor_loop instant_monitor_loop
;; ;;
status) status)
show_status show_status
;; ;;
kill) kill)
log_message "Manual kill requested" log_message "Manual kill requested"
if kill_music_services; then if kill_music_services; then
echo "Music services killed" echo "Music services killed"
else else
echo "No music services found to kill" echo "No music services found to kill"
fi fi
;; ;;
help | -h | --help) help | -h | --help)
show_usage show_usage
;; ;;
*) *)
echo "Unknown command: $1" echo "Unknown command: $1"
show_usage show_usage
exit 1 exit 1
;; ;;
esac esac

View File

@ -3,9 +3,9 @@
# Auto-sudo functionality # Auto-sudo functionality
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
echo "Executing with sudo..." echo "Executing with sudo..."
sudo "$0" "$@" sudo "$0" "$@"
exit $? exit $?
fi fi
# Colors # Colors
@ -30,14 +30,14 @@ WHITELIST_DEST="${INSTALL_DIR}/pacman_whitelist.txt"
GREYLIST_DEST="${INSTALL_DIR}/pacman_greylist.txt" GREYLIST_DEST="${INSTALL_DIR}/pacman_greylist.txt"
# Check if script is run as root # Check if script is run as root
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root${NC}" echo -e "${RED}Please run as root${NC}"
exit 1 exit 1
fi fi
# Check if the wrapper script exists # Check if the wrapper script exists
if [ ! -f "$WRAPPER_SOURCE" ]; then if [ ! -f "$WRAPPER_SOURCE" ]; then
echo -e "${RED}Error: Wrapper script not found at ${WRAPPER_SOURCE}${NC}" echo -e "${RED}Error: Wrapper script not found at ${WRAPPER_SOURCE}${NC}"
exit 1 exit 1
fi fi
echo -e "${CYAN}Installing pacman wrapper...${NC}" echo -e "${CYAN}Installing pacman wrapper...${NC}"
@ -47,32 +47,32 @@ echo -e "${BLUE}Copying wrapper script to ${WRAPPER_DEST}...${NC}"
cp "$WRAPPER_SOURCE" "$WRAPPER_DEST" cp "$WRAPPER_SOURCE" "$WRAPPER_DEST"
cp "$WORDS_SOURCE" "$WORDS_DEST" cp "$WORDS_SOURCE" "$WORDS_DEST"
if [ -f "$BLOCKED_SOURCE" ]; then if [ -f "$BLOCKED_SOURCE" ]; then
cp "$BLOCKED_SOURCE" "$BLOCKED_DEST" cp "$BLOCKED_SOURCE" "$BLOCKED_DEST"
else else
echo -e "${YELLOW}Warning:${NC} Missing blocked keywords source at ${BLOCKED_SOURCE}${NC}" echo -e "${YELLOW}Warning:${NC} Missing blocked keywords source at ${BLOCKED_SOURCE}${NC}"
fi fi
if [ -f "$WHITELIST_SOURCE" ]; then if [ -f "$WHITELIST_SOURCE" ]; then
cp "$WHITELIST_SOURCE" "$WHITELIST_DEST" cp "$WHITELIST_SOURCE" "$WHITELIST_DEST"
else else
echo -e "${YELLOW}Warning:${NC} Missing whitelist source at ${WHITELIST_SOURCE}${NC}" echo -e "${YELLOW}Warning:${NC} Missing whitelist source at ${WHITELIST_SOURCE}${NC}"
fi fi
if [ -f "$GREYLIST_SOURCE" ]; then if [ -f "$GREYLIST_SOURCE" ]; then
cp "$GREYLIST_SOURCE" "$GREYLIST_DEST" cp "$GREYLIST_SOURCE" "$GREYLIST_DEST"
else else
echo -e "${YELLOW}Warning:${NC} Missing greylist source at ${GREYLIST_SOURCE}${NC}" echo -e "${YELLOW}Warning:${NC} Missing greylist source at ${GREYLIST_SOURCE}${NC}"
fi fi
chmod +x "$WRAPPER_DEST" chmod +x "$WRAPPER_DEST"
chmod 644 "$WORDS_DEST" "$BLOCKED_DEST" "$WHITELIST_DEST" "$GREYLIST_DEST" 2>/dev/null || true chmod 644 "$WORDS_DEST" "$BLOCKED_DEST" "$WHITELIST_DEST" "$GREYLIST_DEST" 2> /dev/null || true
# Automatically use symbolic link installation method # Automatically use symbolic link installation method
echo -e "${YELLOW}Installing using symbolic link method...${NC}" echo -e "${YELLOW}Installing using symbolic link method...${NC}"
# Backup original pacman # Backup original pacman
if [ ! -f "/usr/bin/pacman.orig" ]; then if [ ! -f "/usr/bin/pacman.orig" ]; then
echo -e "${BLUE}Backing up original pacman to /usr/bin/pacman.orig...${NC}" echo -e "${BLUE}Backing up original pacman to /usr/bin/pacman.orig...${NC}"
cp /usr/bin/pacman /usr/bin/pacman.orig cp /usr/bin/pacman /usr/bin/pacman.orig
fi fi
# Update the PACMAN_BIN variable in the wrapper to point to the original # Update the PACMAN_BIN variable in the wrapper to point to the original

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -19,9 +19,9 @@ echo "======================================"
echo "Current Date: $(date)" echo "Current Date: $(date)"
echo "User: $(get_actual_user)" echo "User: $(get_actual_user)"
if [[ $INTERACTIVE_MODE == "true" ]]; then if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Mode: Interactive (prompts enabled)" echo "Mode: Interactive (prompts enabled)"
else else
echo "Mode: Automatic (auto-yes, use --interactive for prompts)" echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
fi fi
# Get the actual user (even when running with sudo) # Get the actual user (even when running with sudo)
@ -33,147 +33,147 @@ echo "User home: $USER_HOME"
# Function to check if today is a monitored day # Function to check if today is a monitored day
is_monitored_day() { is_monitored_day() {
local day_of_week local day_of_week
day_of_week=$(date +%u) # 1=Monday, 7=Sunday day_of_week=$(date +%u) # 1=Monday, 7=Sunday
# Check if today is Monday (1), Friday (5), Saturday (6), or Sunday (7) # Check if today is Monday (1), Friday (5), Saturday (6), or Sunday (7)
if [[ $day_of_week == "1" ]] || [[ $day_of_week == "5" ]] || [[ $day_of_week == "6" ]] || [[ $day_of_week == "7" ]]; then if [[ $day_of_week == "1" ]] || [[ $day_of_week == "5" ]] || [[ $day_of_week == "6" ]] || [[ $day_of_week == "7" ]]; then
return 0 # Yes, it's a monitored day return 0 # Yes, it's a monitored day
else else
return 1 # No, it's not a monitored day return 1 # No, it's not a monitored day
fi fi
} }
# Function to check if current time is between 5AM and 8AM # Function to check if current time is between 5AM and 8AM
is_current_time_in_window() { is_current_time_in_window() {
local current_hour current_hour_num local current_hour current_hour_num
current_hour=$(date +%H) current_hour=$(date +%H)
current_hour_num=$((10#$current_hour)) # Convert to decimal to avoid octal issues current_hour_num=$((10#$current_hour)) # Convert to decimal to avoid octal issues
if [[ $current_hour_num -ge 5 ]] && [[ $current_hour_num -lt 8 ]]; then if [[ $current_hour_num -ge 5 ]] && [[ $current_hour_num -lt 8 ]]; then
return 0 # Yes, current time is in the 5AM-8AM window return 0 # Yes, current time is in the 5AM-8AM window
else else
return 1 # No, current time is outside the window return 1 # No, current time is outside the window
fi fi
} }
# Function to check if PC was booted between 5AM-8AM today # Function to check if PC was booted between 5AM-8AM today
was_booted_in_window_today() { was_booted_in_window_today() {
local today boot_time local today boot_time
today=$(date +%Y-%m-%d) today=$(date +%Y-%m-%d)
boot_time="" boot_time=""
# Get the last boot time using multiple methods for reliability # Get the last boot time using multiple methods for reliability
if command -v uptime &>/dev/null; then if command -v uptime &> /dev/null; then
# Method 1: Calculate boot time from uptime # Method 1: Calculate boot time from uptime
local uptime_seconds local uptime_seconds
uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo "0") uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2> /dev/null || echo "0")
if [[ $uptime_seconds -gt 0 ]]; then if [[ $uptime_seconds -gt 0 ]]; then
boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S") boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S")
fi fi
fi fi
# Method 2: Use systemd if available (fallback) # Method 2: Use systemd if available (fallback)
if [[ -z $boot_time ]] && command -v systemctl &>/dev/null; then if [[ -z $boot_time ]] && command -v systemctl &> /dev/null; then
boot_time=$(systemd-analyze | grep "Startup finished" | sed -n 's/.*finished in .* = \(.*\)$/\1/p' 2>/dev/null || echo "") boot_time=$(systemd-analyze | grep "Startup finished" | sed -n 's/.*finished in .* = \(.*\)$/\1/p' 2> /dev/null || echo "")
if [[ -n $boot_time ]]; then if [[ -n $boot_time ]]; then
# This gives us relative time, need to calculate absolute time # This gives us relative time, need to calculate absolute time
local current_time uptime_sec local current_time uptime_sec
current_time=$(date +%s) current_time=$(date +%s)
uptime_sec=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo "0") uptime_sec=$(awk '{print int($1)}' /proc/uptime 2> /dev/null || echo "0")
boot_time=$(date -d "@$((current_time - uptime_sec))" +"%Y-%m-%d %H:%M:%S") boot_time=$(date -d "@$((current_time - uptime_sec))" +"%Y-%m-%d %H:%M:%S")
fi fi
fi fi
# Method 3: Use who -b (fallback) # Method 3: Use who -b (fallback)
if [[ -z $boot_time ]] && command -v who &>/dev/null; then if [[ -z $boot_time ]] && command -v who &> /dev/null; then
boot_time=$(who -b | awk '{print $3, $4}' 2>/dev/null || echo "") boot_time=$(who -b | awk '{print $3, $4}' 2> /dev/null || echo "")
if [[ -n $boot_time ]]; then if [[ -n $boot_time ]]; then
boot_time="$today $boot_time" boot_time="$today $boot_time"
fi fi
fi fi
# Method 4: Use /proc/uptime as final fallback # Method 4: Use /proc/uptime as final fallback
if [[ -z $boot_time ]]; then if [[ -z $boot_time ]]; then
local uptime_seconds local uptime_seconds
uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo "0") uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2> /dev/null || echo "0")
boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S") boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S")
fi fi
echo "Boot time detected: $boot_time" echo "Boot time detected: $boot_time"
# Check if boot time is from today # Check if boot time is from today
local boot_date local boot_date
boot_date=$(echo "$boot_time" | cut -d' ' -f1) boot_date=$(echo "$boot_time" | cut -d' ' -f1)
if [[ $boot_date != "$today" ]]; then if [[ $boot_date != "$today" ]]; then
echo "PC was not booted today (boot date: $boot_date, today: $today)" echo "PC was not booted today (boot date: $boot_date, today: $today)"
return 1 # Not booted today return 1 # Not booted today
fi fi
# Extract hour from boot time # Extract hour from boot time
local boot_hour boot_hour_num local boot_hour boot_hour_num
boot_hour=$(echo "$boot_time" | cut -d' ' -f2 | cut -d':' -f1) boot_hour=$(echo "$boot_time" | cut -d' ' -f2 | cut -d':' -f1)
boot_hour_num=$((10#$boot_hour)) # Convert to decimal boot_hour_num=$((10#$boot_hour)) # Convert to decimal
echo "Boot hour: $boot_hour_num" echo "Boot hour: $boot_hour_num"
# Check if boot time was between 5AM (5) and 8AM (7, since we want before 8AM) # Check if boot time was between 5AM (5) and 8AM (7, since we want before 8AM)
if [[ $boot_hour_num -ge 5 ]] && [[ $boot_hour_num -lt 8 ]]; then if [[ $boot_hour_num -ge 5 ]] && [[ $boot_hour_num -lt 8 ]]; then
echo "PC was booted in the expected window (5AM-8AM)" echo "PC was booted in the expected window (5AM-8AM)"
return 0 # Yes, booted in window return 0 # Yes, booted in window
else else
echo "PC was NOT booted in the expected window (5AM-8AM)" echo "PC was NOT booted in the expected window (5AM-8AM)"
return 1 # No, not booted in window return 1 # No, not booted in window
fi fi
} }
# Function to show notification/warning # Function to show notification/warning
show_startup_warning() { show_startup_warning() {
local day_name current_time today local day_name current_time today
day_name=$(date +%A) day_name=$(date +%A)
current_time=$(date +"%H:%M") current_time=$(date +"%H:%M")
today=$(date +%Y-%m-%d) today=$(date +%Y-%m-%d)
echo "" echo ""
echo "⚠️ PC STARTUP TIME WARNING" echo "⚠️ PC STARTUP TIME WARNING"
echo "==========================" echo "=========================="
echo "Date: $today ($day_name)" echo "Date: $today ($day_name)"
echo "Current time: $current_time" echo "Current time: $current_time"
echo "" echo ""
echo "This PC was expected to be turned on between 5:00 AM and 8:00 AM today," echo "This PC was expected to be turned on between 5:00 AM and 8:00 AM today,"
echo "but it was not turned on during that time window." echo "but it was not turned on during that time window."
echo "" echo ""
echo "Expected: Monday, Friday, Saturday, Sunday between 5:00-8:00 AM" echo "Expected: Monday, Friday, Saturday, Sunday between 5:00-8:00 AM"
echo "Actual: PC was turned on outside the expected window" echo "Actual: PC was turned on outside the expected window"
echo "" echo ""
# Log the warning # Log the warning
logger -t pc-startup-monitor "WARNING: PC was not turned on during expected window (5AM-8AM) on $day_name $today" logger -t pc-startup-monitor "WARNING: PC was not turned on during expected window (5AM-8AM) on $day_name $today"
# Try to show desktop notification if possible # Try to show desktop notification if possible
if command -v notify-send &>/dev/null && [[ -n $DISPLAY ]]; then if command -v notify-send &> /dev/null && [[ -n $DISPLAY ]]; then
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
# Running as root, send notification as user # Running as root, send notification as user
sudo -u "$ACTUAL_USER" DISPLAY="$DISPLAY" notify-send "PC Startup Warning" "PC was not turned on between 5AM-8AM as expected on $day_name" --urgency=normal --expire-time=10000 2>/dev/null || true sudo -u "$ACTUAL_USER" DISPLAY="$DISPLAY" notify-send "PC Startup Warning" "PC was not turned on between 5AM-8AM as expected on $day_name" --urgency=normal --expire-time=10000 2> /dev/null || true
else else
notify-send "PC Startup Warning" "PC was not turned on between 5AM-8AM as expected on $day_name" --urgency=normal --expire-time=10000 2>/dev/null || true notify-send "PC Startup Warning" "PC was not turned on between 5AM-8AM as expected on $day_name" --urgency=normal --expire-time=10000 2> /dev/null || true
fi fi
fi fi
echo "This warning has been logged to the system journal." echo "This warning has been logged to the system journal."
echo "You can view startup logs with: journalctl -t pc-startup-monitor" echo "You can view startup logs with: journalctl -t pc-startup-monitor"
echo "" echo ""
} }
# Function to create the monitoring service # Function to create the monitoring service
create_monitoring_service() { create_monitoring_service() {
echo "" echo ""
echo "1. Creating PC Startup Monitor Service..." echo "1. Creating PC Startup Monitor Service..."
echo "=======================================" echo "======================================="
local service_file="/etc/systemd/system/pc-startup-monitor.service" local service_file="/etc/systemd/system/pc-startup-monitor.service"
cat >"$service_file" <<'EOF' cat > "$service_file" << 'EOF'
[Unit] [Unit]
Description=PC Startup Time Monitor Description=PC Startup Time Monitor
After=multi-user.target After=multi-user.target
@ -190,18 +190,18 @@ RemainAfterExit=true
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
echo "✓ Created monitoring service: $service_file" echo "✓ Created monitoring service: $service_file"
} }
# Function to create the monitoring timer # Function to create the monitoring timer
create_monitoring_timer() { create_monitoring_timer() {
echo "" echo ""
echo "2. Creating PC Startup Monitor Timer..." echo "2. Creating PC Startup Monitor Timer..."
echo "=====================================" echo "====================================="
local timer_file="/etc/systemd/system/pc-startup-monitor.timer" local timer_file="/etc/systemd/system/pc-startup-monitor.timer"
cat >"$timer_file" <<'EOF' cat > "$timer_file" << 'EOF'
[Unit] [Unit]
Description=Timer for PC startup monitoring Description=Timer for PC startup monitoring
Requires=pc-startup-monitor.service Requires=pc-startup-monitor.service
@ -215,18 +215,18 @@ AccuracySec=1m
WantedBy=timers.target WantedBy=timers.target
EOF EOF
echo "✓ Created monitoring timer: $timer_file" echo "✓ Created monitoring timer: $timer_file"
} }
# Function to create the main monitoring script # Function to create the main monitoring script
create_monitoring_script() { create_monitoring_script() {
echo "" echo ""
echo "3. Creating PC Startup Monitor Script..." echo "3. Creating PC Startup Monitor Script..."
echo "======================================" echo "======================================"
local script_file="/usr/local/bin/pc-startup-check.sh" local script_file="/usr/local/bin/pc-startup-check.sh"
cat >"$script_file" <<'EOF' cat > "$script_file" << 'EOF'
#!/bin/bash #!/bin/bash
# PC Startup Time Monitor Check Script # PC Startup Time Monitor Check Script
# Monitors if PC was turned on during expected hours on specific days # Monitors if PC was turned on during expected hours on specific days
@ -332,19 +332,19 @@ else
fi fi
EOF EOF
chmod +x "$script_file" chmod +x "$script_file"
echo "✓ Created monitoring script: $script_file" echo "✓ Created monitoring script: $script_file"
} }
# Function to create management script # Function to create management script
create_management_script() { create_management_script() {
echo "" echo ""
echo "4. Creating Management Script..." echo "4. Creating Management Script..."
echo "==============================" echo "=============================="
local script_file="/usr/local/bin/pc-startup-monitor-manager.sh" local script_file="/usr/local/bin/pc-startup-monitor-manager.sh"
cat >"$script_file" <<'EOF' cat > "$script_file" << 'EOF'
#!/bin/bash #!/bin/bash
# PC Startup Monitor Manager # PC Startup Monitor Manager
# Provides easy management of the PC startup monitoring feature # Provides easy management of the PC startup monitoring feature
@ -407,150 +407,150 @@ case "$1" in
esac esac
EOF EOF
chmod +x "$script_file" chmod +x "$script_file"
echo "✓ Created management script: $script_file" echo "✓ Created management script: $script_file"
} }
# Function to enable the services # Function to enable the services
enable_services() { enable_services() {
echo "" echo ""
echo "5. Enabling PC Startup Monitor..." echo "5. Enabling PC Startup Monitor..."
echo "===============================" echo "==============================="
# Reload systemd daemon # Reload systemd daemon
systemctl daemon-reload systemctl daemon-reload
echo "✓ Reloaded systemd daemon" echo "✓ Reloaded systemd daemon"
# Enable and start the timer # Enable and start the timer
systemctl enable pc-startup-monitor.timer systemctl enable pc-startup-monitor.timer
echo "✓ Enabled pc-startup-monitor timer" echo "✓ Enabled pc-startup-monitor timer"
systemctl start pc-startup-monitor.timer systemctl start pc-startup-monitor.timer
echo "✓ Started pc-startup-monitor timer" echo "✓ Started pc-startup-monitor timer"
} }
# Function to test the setup # Function to test the setup
test_setup() { test_setup() {
echo "" echo ""
echo "6. Testing Setup..." echo "6. Testing Setup..."
echo "==================" echo "=================="
echo "Service files:" echo "Service files:"
if [[ -f "/etc/systemd/system/pc-startup-monitor.service" ]]; then if [[ -f "/etc/systemd/system/pc-startup-monitor.service" ]]; then
echo "✓ Service file exists" echo "✓ Service file exists"
else else
echo "✗ Service file missing" echo "✗ Service file missing"
fi fi
if [[ -f "/etc/systemd/system/pc-startup-monitor.timer" ]]; then if [[ -f "/etc/systemd/system/pc-startup-monitor.timer" ]]; then
echo "✓ Timer file exists" echo "✓ Timer file exists"
else else
echo "✗ Timer file missing" echo "✗ Timer file missing"
fi fi
echo "" echo ""
echo "Timer status:" echo "Timer status:"
if systemctl is-enabled pc-startup-monitor.timer &>/dev/null; then if systemctl is-enabled pc-startup-monitor.timer &> /dev/null; then
echo "✓ Timer is enabled" echo "✓ Timer is enabled"
else else
echo "✗ Timer is not enabled" echo "✗ Timer is not enabled"
fi fi
if systemctl is-active pc-startup-monitor.timer &>/dev/null; then if systemctl is-active pc-startup-monitor.timer &> /dev/null; then
echo "✓ Timer is active" echo "✓ Timer is active"
else else
echo "✗ Timer is not active" echo "✗ Timer is not active"
fi fi
echo "" echo ""
echo "Testing current logic:" echo "Testing current logic:"
/usr/local/bin/pc-startup-check.sh /usr/local/bin/pc-startup-check.sh
} }
# Function to show final instructions # Function to show final instructions
show_instructions() { show_instructions() {
echo "" echo ""
echo "==========================================" echo "=========================================="
echo "PC Startup Monitor Setup Complete" echo "PC Startup Monitor Setup Complete"
echo "==========================================" echo "=========================================="
echo "Summary:" echo "Summary:"
echo "✓ Monitoring service created (/etc/systemd/system/pc-startup-monitor.service)" echo "✓ Monitoring service created (/etc/systemd/system/pc-startup-monitor.service)"
echo "✓ Monitoring timer created (/etc/systemd/system/pc-startup-monitor.timer)" echo "✓ Monitoring timer created (/etc/systemd/system/pc-startup-monitor.timer)"
echo "✓ Monitor script created (/usr/local/bin/pc-startup-check.sh)" echo "✓ Monitor script created (/usr/local/bin/pc-startup-check.sh)"
echo "✓ Management script created (/usr/local/bin/pc-startup-monitor-manager.sh)" echo "✓ Management script created (/usr/local/bin/pc-startup-monitor-manager.sh)"
echo "✓ Timer enabled and started" echo "✓ Timer enabled and started"
echo "" echo ""
echo "How it works:" echo "How it works:"
echo "• Monitors PC startup times on Monday, Friday, Saturday, Sunday" echo "• Monitors PC startup times on Monday, Friday, Saturday, Sunday"
echo "• Expects PC to be turned on between 5:00 AM - 8:00 AM" echo "• Expects PC to be turned on between 5:00 AM - 8:00 AM"
echo "• Checks daily at 8:30 AM if PC was turned on in expected window" echo "• Checks daily at 8:30 AM if PC was turned on in expected window"
echo "• Shows warning if PC was not turned on during expected time" echo "• Shows warning if PC was not turned on during expected time"
echo "" echo ""
echo "Management commands:" echo "Management commands:"
echo " sudo pc-startup-monitor-manager.sh status - Check status" echo " sudo pc-startup-monitor-manager.sh status - Check status"
echo " sudo pc-startup-monitor-manager.sh logs - View monitor logs" echo " sudo pc-startup-monitor-manager.sh logs - View monitor logs"
echo " sudo pc-startup-monitor-manager.sh test - Test monitor now" echo " sudo pc-startup-monitor-manager.sh test - Test monitor now"
echo "" echo ""
echo "Next check: Tomorrow at 8:30 AM (if it's a monitored day)" echo "Next check: Tomorrow at 8:30 AM (if it's a monitored day)"
echo "" echo ""
} }
# Function to prompt for confirmation # Function to prompt for confirmation
confirm_setup() { confirm_setup() {
echo "" echo ""
echo "PC Startup Monitor Setup" echo "PC Startup Monitor Setup"
echo "=======================" echo "======================="
echo "This will set up monitoring for PC startup times." echo "This will set up monitoring for PC startup times."
echo "" echo ""
echo "Monitoring schedule:" echo "Monitoring schedule:"
echo "- Days: Monday, Friday, Saturday, Sunday" echo "- Days: Monday, Friday, Saturday, Sunday"
echo "- Expected startup time: 5:00 AM - 8:00 AM" echo "- Expected startup time: 5:00 AM - 8:00 AM"
echo "- Check time: 8:30 AM daily" echo "- Check time: 8:30 AM daily"
echo "- Action: Show warning if PC wasn't started in expected window" echo "- Action: Show warning if PC wasn't started in expected window"
echo "" echo ""
if [[ $INTERACTIVE_MODE == "true" ]]; then if [[ $INTERACTIVE_MODE == "true" ]]; then
read -r -p "Do you want to proceed? (y/N): " confirm read -r -p "Do you want to proceed? (y/N): " confirm
case "$confirm" in case "$confirm" in
[yY] | [yY][eE][sS]) [yY] | [yY][eE][sS])
echo "Proceeding with setup..." echo "Proceeding with setup..."
return 0 return 0
;; ;;
*) *)
echo "Setup cancelled." echo "Setup cancelled."
exit 0 exit 0
;; ;;
esac esac
else else
echo "Auto-proceeding with setup (use --interactive to prompt)" echo "Auto-proceeding with setup (use --interactive to prompt)"
echo "Proceeding with setup..." echo "Proceeding with setup..."
return 0 return 0
fi fi
} }
# Main execution flow # Main execution flow
main() { main() {
# Check for sudo privileges # Check for sudo privileges
check_sudo "$@" check_sudo "$@"
# Confirm setup # Confirm setup
confirm_setup confirm_setup
# Create all components # Create all components
create_monitoring_service create_monitoring_service
create_monitoring_timer create_monitoring_timer
create_monitoring_script create_monitoring_script
create_management_script create_management_script
# Enable services # Enable services
enable_services enable_services
# Test setup # Test setup
test_setup test_setup
# Show instructions # Show instructions
show_instructions show_instructions
} }
# Run main function # Run main function

View File

@ -13,9 +13,9 @@ LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/music-parallelism/music-parallel
# Main # Main
if focus_app=$(is_focus_app_running); then if focus_app=$(is_focus_app_running); then
log_message "BLOCKED: YouTube Music launch prevented (focus app: $focus_app)" "$LOG_FILE" log_message "BLOCKED: YouTube Music launch prevented (focus app: $focus_app)" "$LOG_FILE"
notify "🚫 YouTube Music Blocked" "Focus mode active ($focus_app)" normal 3000 notify "🚫 YouTube Music Blocked" "Focus mode active ($focus_app)" normal 3000
exit 1 exit 1
fi fi
# No focus app running, launch normally # No focus app running, launch normally

View File

@ -31,12 +31,12 @@ FORCE_UPDATE=false
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { fail() {
echo "[ERROR] $*" >&2 echo "[ERROR] $*" >&2
exit 1 exit 1
} }
usage() { usage() {
cat <<EOF cat << EOF
Usage: $SCRIPT_NAME [options] Usage: $SCRIPT_NAME [options]
Options: Options:
@ -55,150 +55,150 @@ EOF
} }
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--install-dir) --install-dir)
shift shift
[[ $# -gt 0 ]] || fail "--install-dir requires a value" [[ $# -gt 0 ]] || fail "--install-dir requires a value"
INSTALL_ROOT="$1" INSTALL_ROOT="$1"
;; ;;
--project) --project)
shift shift
[[ $# -gt 0 ]] || fail "--project requires a path to .uproject" [[ $# -gt 0 ]] || fail "--project requires a path to .uproject"
PROJECT_UPROJECT="$1" PROJECT_UPROJECT="$1"
;; ;;
--no-continue) --no-continue)
CONFIGURE_CONTINUE=false CONFIGURE_CONTINUE=false
;; ;;
--no-vscode) --no-vscode)
CONFIGURE_VSCODE_USER=false CONFIGURE_VSCODE_USER=false
;; ;;
--force-update) --force-update)
FORCE_UPDATE=true FORCE_UPDATE=true
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
*) *)
fail "Unknown option: $1" fail "Unknown option: $1"
;; ;;
esac esac
shift shift
done done
REPO_DIR="$INSTALL_ROOT/unreal-mcp" REPO_DIR="$INSTALL_ROOT/unreal-mcp"
# ---------- Dependencies ---------- # ---------- Dependencies ----------
require_cmd() { command -v "$1" >/dev/null 2>&1; } require_cmd() { command -v "$1" > /dev/null 2>&1; }
ensure_packages_arch() { ensure_packages_arch() {
# Install with pacman using sudo when needed; keep idempotent with --needed # Install with pacman using sudo when needed; keep idempotent with --needed
local pkgs=(git jq uv python rsync) local pkgs=(git jq uv python rsync)
local to_install=() local to_install=()
for p in "${pkgs[@]}"; do for p in "${pkgs[@]}"; do
if ! pacman -Qi "$p" >/dev/null 2>&1; then if ! pacman -Qi "$p" > /dev/null 2>&1; then
to_install+=("$p") to_install+=("$p")
fi fi
done done
if [[ ${#to_install[@]} -gt 0 ]]; then if [[ ${#to_install[@]} -gt 0 ]]; then
log "Installing packages: ${to_install[*]}" log "Installing packages: ${to_install[*]}"
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
pacman -S --noconfirm --needed "${to_install[@]}" pacman -S --noconfirm --needed "${to_install[@]}"
else else
sudo pacman -S --noconfirm --needed "${to_install[@]}" sudo pacman -S --noconfirm --needed "${to_install[@]}"
fi fi
else else
log "All required packages already installed" log "All required packages already installed"
fi fi
} }
check_python_version() { check_python_version() {
if require_cmd python; then if require_cmd python; then
local v local v
v=$(python -V 2>&1 | awk '{print $2}') v=$(python -V 2>&1 | awk '{print $2}')
elif require_cmd python3; then elif require_cmd python3; then
local v local v
v=$(python3 -V 2>&1 | awk '{print $2}') v=$(python3 -V 2>&1 | awk '{print $2}')
else else
log "python not found; pacman install will provide it" log "python not found; pacman install will provide it"
return 0 return 0
fi fi
# Require >= 3.12 (Unreal MCP docs) # Require >= 3.12 (Unreal MCP docs)
local major minor local major minor
major=$(echo "$v" | cut -d. -f1) major=$(echo "$v" | cut -d. -f1)
minor=$(echo "$v" | cut -d. -f2) minor=$(echo "$v" | cut -d. -f2)
if ((major < 3 || (major == 3 && minor < 12))); then if ((major < 3 || (major == 3 && minor < 12))); then
log "Python $v detected; installing newer python via pacman" log "Python $v detected; installing newer python via pacman"
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
pacman -S --noconfirm --needed python pacman -S --noconfirm --needed python
else else
sudo pacman -S --noconfirm --needed python sudo pacman -S --noconfirm --needed python
fi fi
fi fi
} }
# ---------- Git clone/update ---------- # ---------- Git clone/update ----------
setup_repo() { setup_repo() {
mkdir -p "$INSTALL_ROOT" mkdir -p "$INSTALL_ROOT"
if [[ ! -d "$REPO_DIR/.git" ]]; then if [[ ! -d "$REPO_DIR/.git" ]]; then
log "Cloning unreal-mcp into $REPO_DIR" log "Cloning unreal-mcp into $REPO_DIR"
if require_cmd git; then if require_cmd git; then
git clone "$REPO_URL" "$REPO_DIR" git clone "$REPO_URL" "$REPO_DIR"
else else
fail "git is required but not found after install" fail "git is required but not found after install"
fi fi
else else
log "Repo exists at $REPO_DIR" log "Repo exists at $REPO_DIR"
if [[ $FORCE_UPDATE == true ]]; then if [[ $FORCE_UPDATE == true ]]; then
log "Updating repo with --force-update" log "Updating repo with --force-update"
git -C "$REPO_DIR" fetch origin git -C "$REPO_DIR" fetch origin
git -C "$REPO_DIR" reset --hard origin/main git -C "$REPO_DIR" reset --hard origin/main
git -C "$REPO_DIR" pull --rebase --autostash git -C "$REPO_DIR" pull --rebase --autostash
else else
log "Pulling latest changes" log "Pulling latest changes"
git -C "$REPO_DIR" pull --rebase --autostash git -C "$REPO_DIR" pull --rebase --autostash
fi fi
fi fi
# Ensure ownership for the real user when script ran via sudo # Ensure ownership for the real user when script ran via sudo
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT" chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT"
fi fi
} }
# ---------- Launcher ---------- # ---------- Launcher ----------
install_launcher() { install_launcher() {
local bin_dir="$USER_HOME/.local/bin" local bin_dir="$USER_HOME/.local/bin"
local python_dir="$REPO_DIR/Python" local python_dir="$REPO_DIR/Python"
local launcher="$bin_dir/unreal-mcp-server" local launcher="$bin_dir/unreal-mcp-server"
mkdir -p "$bin_dir" mkdir -p "$bin_dir"
cat >"$launcher" <<EOF cat > "$launcher" << EOF
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
exec uv --directory "$python_dir" run unreal_mcp_server.py "\${1:-}" < /dev/null exec uv --directory "$python_dir" run unreal_mcp_server.py "\${1:-}" < /dev/null
EOF EOF
chmod +x "$launcher" chmod +x "$launcher"
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$launcher"; fi if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$launcher"; fi
log "Installed launcher: $launcher" log "Installed launcher: $launcher"
} }
# ---------- VS Code: Continue MCP config ---------- # ---------- VS Code: Continue MCP config ----------
configure_continue() { configure_continue() {
if [[ $CONFIGURE_CONTINUE != true ]]; then if [[ $CONFIGURE_CONTINUE != true ]]; then
log "Skipping Continue config (--no-continue)" log "Skipping Continue config (--no-continue)"
return 0 return 0
fi fi
local cont_dir="$USER_HOME/.continue" local cont_dir="$USER_HOME/.continue"
local cont_cfg="$cont_dir/config.json" local cont_cfg="$cont_dir/config.json"
local python_dir="$REPO_DIR/Python" local python_dir="$REPO_DIR/Python"
mkdir -p "$cont_dir" mkdir -p "$cont_dir"
# Base JSON when no config exists # Base JSON when no config exists
local tmp_file local tmp_file
tmp_file="$(mktemp)" tmp_file="$(mktemp)"
if [[ ! -f $cont_cfg ]]; then if [[ ! -f $cont_cfg ]]; then
cat >"$tmp_file" <<JSON cat > "$tmp_file" << JSON
{ {
"mcpServers": { "mcpServers": {
"unrealMCP": { "unrealMCP": {
@ -208,147 +208,147 @@ configure_continue() {
} }
} }
JSON JSON
mv "$tmp_file" "$cont_cfg" mv "$tmp_file" "$cont_cfg"
else else
# Merge using jq: ensure .mcpServers exists, then set/overwrite unrealMCP # Merge using jq: ensure .mcpServers exists, then set/overwrite unrealMCP
if ! require_cmd jq; then if ! require_cmd jq; then
fail "jq is required to merge ~/.continue/config.json" fail "jq is required to merge ~/.continue/config.json"
fi fi
jq --arg dir "$python_dir" ' jq --arg dir "$python_dir" '
.mcpServers = (.mcpServers // {}) | .mcpServers = (.mcpServers // {}) |
.mcpServers.unrealMCP = { .mcpServers.unrealMCP = {
command: "uv", command: "uv",
args: ["--directory", $dir, "run", "unreal_mcp_server.py"] args: ["--directory", $dir, "run", "unreal_mcp_server.py"]
} }
' "$cont_cfg" >"$tmp_file" && mv "$tmp_file" "$cont_cfg" ' "$cont_cfg" > "$tmp_file" && mv "$tmp_file" "$cont_cfg"
fi fi
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi
log "Configured Continue MCP at: $cont_cfg" log "Configured Continue MCP at: $cont_cfg"
} }
# ---------- VS Code user MCP (native) ---------- # ---------- VS Code user MCP (native) ----------
configure_vscode_user_mcp() { configure_vscode_user_mcp() {
if [[ $CONFIGURE_VSCODE_USER != true ]]; then if [[ $CONFIGURE_VSCODE_USER != true ]]; then
log "Skipping VS Code user MCP config (--no-vscode)" log "Skipping VS Code user MCP config (--no-vscode)"
return 0 return 0
fi fi
if ! require_cmd jq; then if ! require_cmd jq; then
fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles" fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles"
fi fi
local python_dir="$REPO_DIR/Python" local python_dir="$REPO_DIR/Python"
local json local json
json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}') json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}')
# Handle multiple VS Code variants if present # Handle multiple VS Code variants if present
local candidates=(code code-insiders codium) local candidates=(code code-insiders codium)
local found_any=false local found_any=false
for cli in "${candidates[@]}"; do for cli in "${candidates[@]}"; do
if ! command -v "$cli" >/dev/null 2>&1; then if ! command -v "$cli" > /dev/null 2>&1; then
continue continue
fi fi
found_any=true found_any=true
log "Registering MCP server in VS Code user profile via: $cli --add-mcp" log "Registering MCP server in VS Code user profile via: $cli --add-mcp"
if "$cli" --add-mcp "$json" >"/tmp/${cli}-add-mcp.log" 2>&1; then if "$cli" --add-mcp "$json" > "/tmp/${cli}-add-mcp.log" 2>&1; then
log "[$cli] user profile: unrealMCP added/updated" log "[$cli] user profile: unrealMCP added/updated"
else else
sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true
fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode." fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode."
fi fi
# Detect profiles with 'unreal' (case-insensitive) and add there too # Detect profiles with 'unreal' (case-insensitive) and add there too
local data_dir="" local data_dir=""
case "$cli" in case "$cli" in
code) code)
data_dir="$USER_HOME/.config/Code" data_dir="$USER_HOME/.config/Code"
;; ;;
code-insiders) code-insiders)
data_dir="$USER_HOME/.config/Code - Insiders" data_dir="$USER_HOME/.config/Code - Insiders"
;; ;;
codium) codium)
data_dir="$USER_HOME/.config/VSCodium" data_dir="$USER_HOME/.config/VSCodium"
;; ;;
esac esac
local profiles_json="$data_dir/User/profiles/profiles.json" local profiles_json="$data_dir/User/profiles/profiles.json"
if [[ -f $profiles_json ]]; then if [[ -f $profiles_json ]]; then
# Extract profile names matching /unreal/i # Extract profile names matching /unreal/i
mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json") mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json")
if [[ ${#unreal_profiles[@]} -gt 0 ]]; then if [[ ${#unreal_profiles[@]} -gt 0 ]]; then
log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}" log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}"
local name local name
for name in "${unreal_profiles[@]}"; do for name in "${unreal_profiles[@]}"; do
log "[$cli] Adding unrealMCP to profile: $name" log "[$cli] Adding unrealMCP to profile: $name"
if "$cli" --profile "$name" --add-mcp "$json" >"/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then if "$cli" --profile "$name" --add-mcp "$json" > "/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then
log "[$cli] profile '$name': unrealMCP added/updated" log "[$cli] profile '$name': unrealMCP added/updated"
else else
sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true
fail "[$cli] --add-mcp failed for profile '$name'." fail "[$cli] --add-mcp failed for profile '$name'."
fi fi
done done
else else
log "[$cli] No VS Code profiles with 'unreal' in name" log "[$cli] No VS Code profiles with 'unreal' in name"
fi fi
else else
log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)" log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)"
fi fi
done done
if [[ $found_any == false ]]; then if [[ $found_any == false ]]; then
fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip." fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip."
fi fi
} }
# ---------- Unreal Plugin copy (optional) ---------- # ---------- Unreal Plugin copy (optional) ----------
install_plugin_into_project() { install_plugin_into_project() {
[[ -n $PROJECT_UPROJECT ]] || return 0 [[ -n $PROJECT_UPROJECT ]] || return 0
local upath="$PROJECT_UPROJECT" local upath="$PROJECT_UPROJECT"
if [[ -d $upath ]]; then if [[ -d $upath ]]; then
# Resolve .uproject in the provided directory # Resolve .uproject in the provided directory
mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2>/dev/null || true) mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2> /dev/null || true)
if [[ ${#_uprojects[@]} -eq 0 ]]; then if [[ ${#_uprojects[@]} -eq 0 ]]; then
fail "--project directory '$upath' contains no .uproject files" fail "--project directory '$upath' contains no .uproject files"
elif [[ ${#_uprojects[@]} -gt 1 ]]; then elif [[ ${#_uprojects[@]} -gt 1 ]]; then
printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2 printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2
printf ' - %s\n' "${_uprojects[@]}" >&2 printf ' - %s\n' "${_uprojects[@]}" >&2
fail "Please pass the specific .uproject path to --project" fail "Please pass the specific .uproject path to --project"
else else
upath="${_uprojects[0]}" upath="${_uprojects[0]}"
log "Resolved .uproject: $upath" log "Resolved .uproject: $upath"
fi fi
elif [[ -f $upath ]]; then elif [[ -f $upath ]]; then
true true
else else
fail "--project path does not exist: $upath" fail "--project path does not exist: $upath"
fi fi
if [[ ${upath##*.} != "uproject" ]]; then if [[ ${upath##*.} != "uproject" ]]; then
fail "--project must point to a .uproject file (got: $upath)" fail "--project must point to a .uproject file (got: $upath)"
fi fi
local proj_dir local proj_dir
proj_dir="$(cd "$(dirname "$upath")" && pwd)" proj_dir="$(cd "$(dirname "$upath")" && pwd)"
RESOLVED_PROJECT_DIR="$proj_dir" RESOLVED_PROJECT_DIR="$proj_dir"
local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP" local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP"
local dst_plugin="$proj_dir/Plugins/UnrealMCP" local dst_plugin="$proj_dir/Plugins/UnrealMCP"
if [[ ! -d $src_plugin ]]; then if [[ ! -d $src_plugin ]]; then
fail "Source plugin not found at $src_plugin (did repo layout change?)" fail "Source plugin not found at $src_plugin (did repo layout change?)"
fi fi
mkdir -p "$proj_dir/Plugins" mkdir -p "$proj_dir/Plugins"
log "Copying UnrealMCP plugin to project: $dst_plugin" log "Copying UnrealMCP plugin to project: $dst_plugin"
rsync -a --delete "$src_plugin/" "$dst_plugin/" rsync -a --delete "$src_plugin/" "$dst_plugin/"
# Set ownership back to actual user if run as root # Set ownership back to actual user if run as root
if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi
log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed." log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed."
} }
# ---------- Summary ---------- # ---------- Summary ----------
print_summary() { print_summary() {
local python_dir="$REPO_DIR/Python" local python_dir="$REPO_DIR/Python"
local plugin_dest="N/A" local plugin_dest="N/A"
if [[ -n $RESOLVED_PROJECT_DIR ]]; then if [[ -n $RESOLVED_PROJECT_DIR ]]; then
plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP" plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP"
fi fi
cat <<EOF cat << EOF
============================================ ============================================
Unreal MCP setup complete Unreal MCP setup complete
============================================ ============================================
@ -381,15 +381,15 @@ EOF
} }
main() { main() {
log "Installing prerequisites (Arch Linux)" log "Installing prerequisites (Arch Linux)"
ensure_packages_arch ensure_packages_arch
check_python_version check_python_version
setup_repo setup_repo
install_launcher install_launcher
configure_continue configure_continue
install_plugin_into_project install_plugin_into_project
configure_vscode_user_mcp configure_vscode_user_mcp
print_summary print_summary
} }
main "$@" main "$@"

View File

@ -13,33 +13,33 @@ echo -e "${BLUE}=== Unreal MCP Installer for Arch Linux ===${NC}"
# Check dependencies # Check dependencies
echo -e "${BLUE}Checking dependencies...${NC}" echo -e "${BLUE}Checking dependencies...${NC}"
for cmd in git python pip; do for cmd in git python pip; do
if ! command -v $cmd &>/dev/null; then if ! command -v $cmd &> /dev/null; then
echo -e "${RED}Error: $cmd is not installed. Please install it (e.g., sudo pacman -S $cmd)${NC}" echo -e "${RED}Error: $cmd is not installed. Please install it (e.g., sudo pacman -S $cmd)${NC}"
exit 1 exit 1
fi fi
done done
# Get Unreal Project Path # Get Unreal Project Path
PROJECT_PATH="$1" PROJECT_PATH="$1"
if [ -z "$PROJECT_PATH" ]; then if [ -z "$PROJECT_PATH" ]; then
echo -e "${YELLOW}Please enter the path to your Unreal Engine Project (the folder containing .uproject file):${NC}" echo -e "${YELLOW}Please enter the path to your Unreal Engine Project (the folder containing .uproject file):${NC}"
read -r -e -p "> " PROJECT_PATH read -r -e -p "> " PROJECT_PATH
fi fi
# Validate path # Validate path
# Expand tilde if present # Expand tilde if present
PROJECT_PATH="${PROJECT_PATH/#\~/$HOME}" PROJECT_PATH="${PROJECT_PATH/#\~/$HOME}"
PROJECT_PATH=$(realpath "$PROJECT_PATH" 2>/dev/null || echo "") PROJECT_PATH=$(realpath "$PROJECT_PATH" 2> /dev/null || echo "")
if [ -z "$PROJECT_PATH" ] || [ ! -d "$PROJECT_PATH" ]; then if [ -z "$PROJECT_PATH" ] || [ ! -d "$PROJECT_PATH" ]; then
echo -e "${RED}Error: Invalid directory: $PROJECT_PATH${NC}" echo -e "${RED}Error: Invalid directory: $PROJECT_PATH${NC}"
exit 1 exit 1
fi fi
UPROJECT_FILES=("$PROJECT_PATH"/*.uproject) UPROJECT_FILES=("$PROJECT_PATH"/*.uproject)
if [ ! -e "${UPROJECT_FILES[0]}" ]; then if [ ! -e "${UPROJECT_FILES[0]}" ]; then
echo -e "${RED}Error: No .uproject file found in $PROJECT_PATH${NC}" echo -e "${RED}Error: No .uproject file found in $PROJECT_PATH${NC}"
exit 1 exit 1
fi fi
echo -e "${GREEN}Target Project: $PROJECT_PATH${NC}" echo -e "${GREEN}Target Project: $PROJECT_PATH${NC}"
@ -51,12 +51,12 @@ mkdir -p "$PLUGINS_DIR"
# Clone UnrealMCP # Clone UnrealMCP
MCP_PLUGIN_DIR="$PLUGINS_DIR/UnrealMCP" MCP_PLUGIN_DIR="$PLUGINS_DIR/UnrealMCP"
if [ -d "$MCP_PLUGIN_DIR" ]; then if [ -d "$MCP_PLUGIN_DIR" ]; then
echo -e "${BLUE}UnrealMCP already exists. Updating...${NC}" echo -e "${BLUE}UnrealMCP already exists. Updating...${NC}"
cd "$MCP_PLUGIN_DIR" cd "$MCP_PLUGIN_DIR"
git pull git pull
else else
echo -e "${BLUE}Cloning UnrealMCP...${NC}" echo -e "${BLUE}Cloning UnrealMCP...${NC}"
git clone https://github.com/kvick-games/UnrealMCP.git "$MCP_PLUGIN_DIR" git clone https://github.com/kvick-games/UnrealMCP.git "$MCP_PLUGIN_DIR"
fi fi
# Setup Python Environment # Setup Python Environment
@ -64,41 +64,41 @@ echo -e "${BLUE}Setting up Python environment...${NC}"
MCP_DIR="$MCP_PLUGIN_DIR/MCP" MCP_DIR="$MCP_PLUGIN_DIR/MCP"
if [ ! -f "$MCP_DIR/unreal_mcp_bridge.py" ]; then if [ ! -f "$MCP_DIR/unreal_mcp_bridge.py" ]; then
echo -e "${RED}Error: unreal_mcp_bridge.py not found in $MCP_DIR. Repository structure might have changed.${NC}" echo -e "${RED}Error: unreal_mcp_bridge.py not found in $MCP_DIR. Repository structure might have changed.${NC}"
exit 1 exit 1
fi fi
VENV_DIR="$MCP_DIR/python_env" VENV_DIR="$MCP_DIR/python_env"
if [ ! -d "$VENV_DIR" ]; then if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..." echo "Creating virtual environment..."
python -m venv "$VENV_DIR" python -m venv "$VENV_DIR"
fi fi
# Install requirements # Install requirements
echo "Installing dependencies in virtual environment..." echo "Installing dependencies in virtual environment..."
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
pip install --upgrade pip >/dev/null pip install --upgrade pip > /dev/null
pip install "mcp>=0.1.0" >/dev/null pip install "mcp>=0.1.0" > /dev/null
# Patch unreal_mcp_bridge.py for newer mcp package compatibility # Patch unreal_mcp_bridge.py for newer mcp package compatibility
# The newer mcp package (1.x) renamed 'description' parameter to 'instructions' # The newer mcp package (1.x) renamed 'description' parameter to 'instructions'
BRIDGE_SCRIPT="$MCP_DIR/unreal_mcp_bridge.py" BRIDGE_SCRIPT="$MCP_DIR/unreal_mcp_bridge.py"
if grep -q 'description="Unreal Engine integration' "$BRIDGE_SCRIPT" 2>/dev/null; then if grep -q 'description="Unreal Engine integration' "$BRIDGE_SCRIPT" 2> /dev/null; then
echo "Patching unreal_mcp_bridge.py for mcp package compatibility..." echo "Patching unreal_mcp_bridge.py for mcp package compatibility..."
sed -i 's/description="Unreal Engine integration through the Model Context Protocol"/instructions="Unreal Engine integration through the Model Context Protocol"/' "$BRIDGE_SCRIPT" sed -i 's/description="Unreal Engine integration through the Model Context Protocol"/instructions="Unreal Engine integration through the Model Context Protocol"/' "$BRIDGE_SCRIPT"
fi fi
# Fix case-sensitive includes for Linux (Windows is case-insensitive, Linux is not) # Fix case-sensitive includes for Linux (Windows is case-insensitive, Linux is not)
echo "Fixing case-sensitive includes for Linux..." echo "Fixing case-sensitive includes for Linux..."
find "$MCP_PLUGIN_DIR/Source/" \( -name "*.cpp" -o -name "*.h" \) -exec sed -i 's/HAL\/PlatformFilemanager\.h/HAL\/PlatformFileManager.h/g' {} + 2>/dev/null || true find "$MCP_PLUGIN_DIR/Source/" \( -name "*.cpp" -o -name "*.h" \) -exec sed -i 's/HAL\/PlatformFilemanager\.h/HAL\/PlatformFileManager.h/g' {} + 2> /dev/null || true
# Create Linux Run Script # Create Linux Run Script
RUN_SCRIPT="$MCP_DIR/run_unreal_mcp.sh" RUN_SCRIPT="$MCP_DIR/run_unreal_mcp.sh"
echo -e "${BLUE}Creating run script at $RUN_SCRIPT...${NC}" echo -e "${BLUE}Creating run script at $RUN_SCRIPT...${NC}"
cat <<EOF >"$RUN_SCRIPT" cat << EOF > "$RUN_SCRIPT"
#!/bin/bash #!/bin/bash
set -e set -e
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
@ -115,7 +115,7 @@ echo -e "${BLUE}=== Configuration Setup ===${NC}"
# Python script to update JSON configs # Python script to update JSON configs
CONFIG_UPDATER_SCRIPT=$(mktemp) CONFIG_UPDATER_SCRIPT=$(mktemp)
cat <<EOF >"$CONFIG_UPDATER_SCRIPT" cat << EOF > "$CONFIG_UPDATER_SCRIPT"
import json import json
import os import os
import sys import sys
@ -164,18 +164,18 @@ CLAUDE_CONFIG="$HOME/.config/Claude/claude_desktop_config.json"
# Function to ask and update # Function to ask and update
update_config() { update_config() {
local path="$1" local path="$1"
local type="$2" local type="$2"
local name="$3" local name="$3"
if [ -f "$path" ] || [ -d "$(dirname "$path")" ]; then if [ -f "$path" ] || [ -d "$(dirname "$path")" ]; then
echo -e "Found $name configuration at: $path" echo -e "Found $name configuration at: $path"
read -p "Do you want to add UnrealMCP to this config? (y/n) " -n 1 -r read -p "Do you want to add UnrealMCP to this config? (y/n) " -n 1 -r
echo echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
python "$CONFIG_UPDATER_SCRIPT" "$path" "$RUN_SCRIPT" "$type" python "$CONFIG_UPDATER_SCRIPT" "$path" "$RUN_SCRIPT" "$type"
fi fi
fi fi
} }
update_config "$ROO_CODE_CONFIG" "roo_code" "Roo Code (VS Code Extension)" update_config "$ROO_CODE_CONFIG" "roo_code" "Roo Code (VS Code Extension)"
@ -189,8 +189,8 @@ mkdir -p "$VSCODE_DIR"
MCP_JSON="$VSCODE_DIR/mcp.json" MCP_JSON="$VSCODE_DIR/mcp.json"
if [ ! -f "$MCP_JSON" ]; then if [ ! -f "$MCP_JSON" ]; then
echo -e "${BLUE}Creating workspace MCP config at $MCP_JSON...${NC}" echo -e "${BLUE}Creating workspace MCP config at $MCP_JSON...${NC}"
cat <<EOF >"$MCP_JSON" cat << EOF > "$MCP_JSON"
{ {
"mcpServers": { "mcpServers": {
"unreal": { "unreal": {
@ -201,23 +201,23 @@ if [ ! -f "$MCP_JSON" ]; then
} }
EOF EOF
else else
echo -e "${YELLOW}Workspace MCP config already exists at $MCP_JSON. Skipping overwrite.${NC}" echo -e "${YELLOW}Workspace MCP config already exists at $MCP_JSON. Skipping overwrite.${NC}"
echo "Ensure it contains the following configuration:" echo "Ensure it contains the following configuration:"
echo "\"unreal\": { \"command\": \"$RUN_SCRIPT\", \"args\": [] }" echo "\"unreal\": { \"command\": \"$RUN_SCRIPT\", \"args\": [] }"
fi fi
echo -e "${BLUE}=== Build Instructions ===${NC}" echo -e "${BLUE}=== Build Instructions ===${NC}"
echo "1. You need to regenerate project files." echo "1. You need to regenerate project files."
if [ -f "$PROJECT_PATH/GenerateProjectFiles.sh" ]; then if [ -f "$PROJECT_PATH/GenerateProjectFiles.sh" ]; then
echo " Found GenerateProjectFiles.sh in project root." echo " Found GenerateProjectFiles.sh in project root."
read -p " Do you want to run it now? (y/n) " -n 1 -r read -p " Do you want to run it now? (y/n) " -n 1 -r
echo echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
cd "$PROJECT_PATH" cd "$PROJECT_PATH"
./GenerateProjectFiles.sh ./GenerateProjectFiles.sh
fi fi
else else
echo " Run your engine's GenerateProjectFiles.sh or right-click .uproject -> Generate Project Files." echo " Run your engine's GenerateProjectFiles.sh or right-click .uproject -> Generate Project Files."
fi fi
echo "2. Build the project (e.g., run 'make' in the project root)." echo "2. Build the project (e.g., run 'make' in the project root)."
@ -232,7 +232,7 @@ echo -e "${YELLOW}$RUN_SCRIPT${NC}"
echo echo
echo "For VS Code (User Settings), add this to your settings.json:" echo "For VS Code (User Settings), add this to your settings.json:"
echo -e "${GREEN}" echo -e "${GREEN}"
cat <<EOF cat << EOF
"mcpServers": { "mcpServers": {
"unreal": { "unreal": {
"command": "$RUN_SCRIPT", "command": "$RUN_SCRIPT",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ NC='\033[0m' # No Color
CHECK_ONLY=false CHECK_ONLY=false
usage() { usage() {
cat <<EOF cat << EOF
fix_anki.sh - Fix Anki startup issues fix_anki.sh - Fix Anki startup issues
Usage: $(basename "$0") [OPTIONS] Usage: $(basename "$0") [OPTIONS]
@ -48,177 +48,177 @@ log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
log_success() { echo -e "${GREEN}[OK]${NC} $*"; } log_success() { echo -e "${GREEN}[OK]${NC} $*"; }
check_anki_installed() { check_anki_installed() {
if pacman -Qi anki-git &>/dev/null; then if pacman -Qi anki-git &> /dev/null; then
echo "anki-git" echo "anki-git"
elif pacman -Qi anki &>/dev/null; then elif pacman -Qi anki &> /dev/null; then
echo "anki" echo "anki"
elif pacman -Qi anki-bin &>/dev/null; then elif pacman -Qi anki-bin &> /dev/null; then
echo "anki-bin" echo "anki-bin"
else else
echo "" echo ""
fi fi
} }
get_system_python_version() { get_system_python_version() {
python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
} }
get_anki_python_version() { get_anki_python_version() {
local anki_pkg="$1" local anki_pkg="$1"
local anki_path local anki_path
anki_path=$(pacman -Ql "$anki_pkg" 2>/dev/null | grep -oP '/usr/lib/python\K[0-9]+\.[0-9]+' | head -1) anki_path=$(pacman -Ql "$anki_pkg" 2> /dev/null | grep -oP '/usr/lib/python\K[0-9]+\.[0-9]+' | head -1)
echo "$anki_path" echo "$anki_path"
} }
check_aqt_conflict() { check_aqt_conflict() {
local sys_python="$1" local sys_python="$1"
local aqt_path="/usr/lib/python${sys_python}/site-packages/aqt/__init__.py" local aqt_path="/usr/lib/python${sys_python}/site-packages/aqt/__init__.py"
if [[ -f "$aqt_path" ]]; then if [[ -f $aqt_path ]]; then
if grep -q "aqtinstall" "$aqt_path" 2>/dev/null; then if grep -q "aqtinstall" "$aqt_path" 2> /dev/null; then
echo "aqtinstall" echo "aqtinstall"
elif grep -q "anki" "$aqt_path" 2>/dev/null; then elif grep -q "anki" "$aqt_path" 2> /dev/null; then
echo "anki" echo "anki"
else else
echo "unknown" echo "unknown"
fi fi
else else
echo "none" echo "none"
fi fi
} }
main() { main() {
# Parse arguments # Parse arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--check) --check)
CHECK_ONLY=true CHECK_ONLY=true
shift shift
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
*) *)
log_error "Unknown option: $1" log_error "Unknown option: $1"
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
log_info "Checking Anki installation..." log_info "Checking Anki installation..."
# Check which Anki package is installed # Check which Anki package is installed
local anki_pkg local anki_pkg
anki_pkg=$(check_anki_installed) anki_pkg=$(check_anki_installed)
if [[ -z "$anki_pkg" ]]; then if [[ -z $anki_pkg ]]; then
log_error "Anki is not installed" log_error "Anki is not installed"
exit 1 exit 1
fi fi
log_info "Found Anki package: $anki_pkg" log_info "Found Anki package: $anki_pkg"
# Get Python versions # Get Python versions
local sys_python anki_python local sys_python anki_python
sys_python=$(get_system_python_version) sys_python=$(get_system_python_version)
anki_python=$(get_anki_python_version "$anki_pkg") anki_python=$(get_anki_python_version "$anki_pkg")
log_info "System Python version: $sys_python" log_info "System Python version: $sys_python"
log_info "Anki built for Python: ${anki_python:-unknown}" log_info "Anki built for Python: ${anki_python:-unknown}"
local issues_found=false local issues_found=false
# Check for Python version mismatch # Check for Python version mismatch
if [[ -n "$anki_python" && "$sys_python" != "$anki_python" ]]; then if [[ -n $anki_python && $sys_python != "$anki_python" ]]; then
log_warn "Python version mismatch detected!" log_warn "Python version mismatch detected!"
log_warn " Anki was built for Python $anki_python but system runs Python $sys_python" log_warn " Anki was built for Python $anki_python but system runs Python $sys_python"
issues_found=true issues_found=true
fi fi
# Check for aqt namespace conflict # Check for aqt namespace conflict
local aqt_owner local aqt_owner
aqt_owner=$(check_aqt_conflict "$sys_python") aqt_owner=$(check_aqt_conflict "$sys_python")
case "$aqt_owner" in case "$aqt_owner" in
aqtinstall) aqtinstall)
log_warn "aqt namespace conflict detected!" log_warn "aqt namespace conflict detected!"
log_warn " python-aqtinstall owns /usr/lib/python${sys_python}/site-packages/aqt/" log_warn " python-aqtinstall owns /usr/lib/python${sys_python}/site-packages/aqt/"
log_warn " This conflicts with Anki's aqt module" log_warn " This conflicts with Anki's aqt module"
issues_found=true issues_found=true
;; ;;
anki) anki)
log_success "aqt module belongs to Anki (correct)" log_success "aqt module belongs to Anki (correct)"
;; ;;
none) none)
if [[ "$sys_python" != "$anki_python" ]]; then if [[ $sys_python != "$anki_python" ]]; then
log_warn "No aqt module found for Python $sys_python" log_warn "No aqt module found for Python $sys_python"
fi fi
;; ;;
*) *)
log_warn "Unknown aqt module owner" log_warn "Unknown aqt module owner"
;; ;;
esac esac
# Test if Anki actually works # Test if Anki actually works
log_info "Testing Anki startup..." log_info "Testing Anki startup..."
if python -c "from aqt import run" 2>/dev/null; then if python -c "from aqt import run" 2> /dev/null; then
log_success "Anki imports work correctly" log_success "Anki imports work correctly"
if [[ "$issues_found" == "false" ]]; then if [[ $issues_found == "false" ]]; then
log_success "No issues found with Anki installation" log_success "No issues found with Anki installation"
exit 0 exit 0
fi fi
else else
log_error "Anki import test failed" log_error "Anki import test failed"
issues_found=true issues_found=true
fi fi
if [[ "$CHECK_ONLY" == "true" ]]; then if [[ $CHECK_ONLY == "true" ]]; then
if [[ "$issues_found" == "true" ]]; then if [[ $issues_found == "true" ]]; then
echo "" echo ""
log_info "Issues detected. Run without --check to fix." log_info "Issues detected. Run without --check to fix."
exit 1 exit 1
fi fi
exit 0 exit 0
fi fi
# Apply fixes # Apply fixes
echo "" echo ""
log_info "Applying fixes..." log_info "Applying fixes..."
# Check if python-aqtinstall is installed and remove it if nothing depends on it # Check if python-aqtinstall is installed and remove it if nothing depends on it
if pacman -Qi python-aqtinstall &>/dev/null; then if pacman -Qi python-aqtinstall &> /dev/null; then
local required_by local required_by
required_by=$(pacman -Qi python-aqtinstall | grep "Required By" | cut -d: -f2 | xargs) required_by=$(pacman -Qi python-aqtinstall | grep "Required By" | cut -d: -f2 | xargs)
if [[ "$required_by" == "None" ]]; then if [[ $required_by == "None" ]]; then
log_info "Removing python-aqtinstall (conflicts with Anki)..." log_info "Removing python-aqtinstall (conflicts with Anki)..."
sudo pacman -R --noconfirm python-aqtinstall sudo pacman -R --noconfirm python-aqtinstall
else else
log_warn "python-aqtinstall is required by: $required_by" log_warn "python-aqtinstall is required by: $required_by"
log_warn "Cannot remove automatically. You may need to resolve this manually." log_warn "Cannot remove automatically. You may need to resolve this manually."
fi fi
fi fi
# Rebuild anki package # Rebuild anki package
if [[ "$anki_pkg" == "anki-git" ]]; then if [[ $anki_pkg == "anki-git" ]]; then
log_info "Rebuilding anki-git for Python $sys_python..." log_info "Rebuilding anki-git for Python $sys_python..."
yay -S anki-git --rebuild --noconfirm yay -S anki-git --rebuild --noconfirm
elif [[ "$anki_pkg" == "anki" ]]; then elif [[ $anki_pkg == "anki" ]]; then
log_info "Reinstalling anki..." log_info "Reinstalling anki..."
sudo pacman -S anki --noconfirm sudo pacman -S anki --noconfirm
else else
log_warn "Package $anki_pkg may need manual rebuild" log_warn "Package $anki_pkg may need manual rebuild"
fi fi
# Verify fix # Verify fix
echo "" echo ""
log_info "Verifying fix..." log_info "Verifying fix..."
if python -c "from aqt import run" 2>/dev/null; then if python -c "from aqt import run" 2> /dev/null; then
log_success "Anki is now working!" log_success "Anki is now working!"
echo "" echo ""
echo "You can start Anki with: anki" echo "You can start Anki with: anki"
else else
log_error "Fix may not have worked. Please check manually." log_error "Fix may not have worked. Please check manually."
exit 1 exit 1
fi fi
} }
main "$@" main "$@"

View File

@ -14,29 +14,29 @@ ORGANIZE_SCRIPT="/home/kuhy/linux-configuration/scripts/utils/organize_downloads
TARGET_USER="kuhy" TARGET_USER="kuhy"
log() { log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
} }
# Check if running as root # Check if running as root
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
log "This script needs to be run as root." log "This script needs to be run as root."
log "Re-executing with sudo..." log "Re-executing with sudo..."
exec sudo "$0" "$@" exec sudo "$0" "$@"
fi fi
log "Fixing media-organizer.service..." log "Fixing media-organizer.service..."
# Verify the organize_downloads.sh script exists # Verify the organize_downloads.sh script exists
if [[ ! -f $ORGANIZE_SCRIPT ]]; then if [[ ! -f $ORGANIZE_SCRIPT ]]; then
log "ERROR: organize_downloads.sh not found at $ORGANIZE_SCRIPT" log "ERROR: organize_downloads.sh not found at $ORGANIZE_SCRIPT"
exit 1 exit 1
fi fi
# Stop the service if running (ignore errors) # Stop the service if running (ignore errors)
systemctl stop "$SERVICE_NAME.service" 2>/dev/null || true systemctl stop "$SERVICE_NAME.service" 2> /dev/null || true
# Recreate the service file with correct configuration # Recreate the service file with correct configuration
cat >"$SERVICE_FILE" <<EOF cat > "$SERVICE_FILE" << EOF
[Unit] [Unit]
Description=Media File Organizer Description=Media File Organizer
After=graphical-session.target After=graphical-session.target
@ -62,7 +62,7 @@ systemctl daemon-reload
log "Reloaded systemd daemon" log "Reloaded systemd daemon"
# Reset the failed state # Reset the failed state
systemctl reset-failed "$SERVICE_NAME.service" 2>/dev/null || true systemctl reset-failed "$SERVICE_NAME.service" 2> /dev/null || true
log "Reset failed state" log "Reset failed state"
# Re-enable the service # Re-enable the service
@ -72,9 +72,9 @@ log "Service enabled"
# Optionally start the service to verify it works # Optionally start the service to verify it works
log "Starting service to verify fix..." log "Starting service to verify fix..."
if systemctl start "$SERVICE_NAME.service"; then if systemctl start "$SERVICE_NAME.service"; then
log "SUCCESS: media-organizer.service started successfully!" log "SUCCESS: media-organizer.service started successfully!"
else else
log "WARNING: Service still has issues. Check: journalctl -u $SERVICE_NAME" log "WARNING: Service still has issues. Check: journalctl -u $SERVICE_NAME"
fi fi
# Show current status # Show current status

View File

@ -32,7 +32,7 @@ YELLOW='\033[1;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
usage() { usage() {
cat <<EOF cat << EOF
fix_thorium.sh - Fix Thorium Browser crashes and startup issues fix_thorium.sh - Fix Thorium Browser crashes and startup issues
Usage: $(basename "$0") [OPTIONS] Usage: $(basename "$0") [OPTIONS]
@ -60,318 +60,318 @@ EOF
DRY_RUN=false DRY_RUN=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--aggressive) --aggressive)
AGGRESSIVE=true AGGRESSIVE=true
shift shift
;; ;;
--test) --test)
TEST_AFTER=true TEST_AFTER=true
shift shift
;; ;;
--dry-run) --dry-run)
DRY_RUN=true DRY_RUN=true
shift shift
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
*) *)
log_error "Unknown option: $1" log_error "Unknown option: $1"
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
# Check if Thorium is installed # Check if Thorium is installed
check_thorium_installed() { check_thorium_installed() {
if ! command -v thorium-browser &>/dev/null; then if ! command -v thorium-browser &> /dev/null; then
log_error "thorium-browser not found in PATH" log_error "thorium-browser not found in PATH"
echo -e "${YELLOW}Install with: yay -S thorium-browser-bin${NC}" echo -e "${YELLOW}Install with: yay -S thorium-browser-bin${NC}"
exit 1 exit 1
fi fi
log_info "Found Thorium: $(thorium-browser --version 2>/dev/null | head -1)" log_info "Found Thorium: $(thorium-browser --version 2> /dev/null | head -1)"
} }
# Check if config directory exists # Check if config directory exists
check_config_exists() { check_config_exists() {
if [[ ! -d "$THORIUM_CONFIG_DIR" ]]; then if [[ ! -d $THORIUM_CONFIG_DIR ]]; then
log_warn "Thorium config directory not found: $THORIUM_CONFIG_DIR" log_warn "Thorium config directory not found: $THORIUM_CONFIG_DIR"
log_info "This may be a fresh install - try running thorium-browser directly" log_info "This may be a fresh install - try running thorium-browser directly"
exit 0 exit 0
fi fi
} }
# Kill any running Thorium processes # Kill any running Thorium processes
kill_thorium() { kill_thorium() {
local count local count
count=$(pgrep -c thorium 2>/dev/null || true) count=$(pgrep -c thorium 2> /dev/null || true)
count=${count:-0} count=${count:-0}
if [[ $count -gt 0 ]]; then if [[ $count -gt 0 ]]; then
log_info "Stopping $count running Thorium process(es)..." log_info "Stopping $count running Thorium process(es)..."
if [[ $DRY_RUN == true ]]; then if [[ $DRY_RUN == true ]]; then
echo " [dry-run] Would kill thorium processes" echo " [dry-run] Would kill thorium processes"
else else
pkill -9 thorium 2>/dev/null || true pkill -9 thorium 2> /dev/null || true
sleep 1 sleep 1
fi fi
fi fi
} }
# Backup a file/directory if it exists # Backup a file/directory if it exists
backup_if_exists() { backup_if_exists() {
local path="$1" local path="$1"
local name local name
name=$(basename "$path") name=$(basename "$path")
if [[ -e "$path" ]]; then if [[ -e $path ]]; then
local backup_path="${path}${BACKUP_SUFFIX}" local backup_path="${path}${BACKUP_SUFFIX}"
if [[ $DRY_RUN == true ]]; then if [[ $DRY_RUN == true ]]; then
echo " [dry-run] Would backup: $name" echo " [dry-run] Would backup: $name"
else else
mv "$path" "$backup_path" mv "$path" "$backup_path"
log_ok "Backed up: $name -> $(basename "$backup_path")" log_ok "Backed up: $name -> $(basename "$backup_path")"
fi fi
return 0 return 0
fi fi
return 1 return 1
} }
# Remove file/directory if it exists # Remove file/directory if it exists
remove_if_exists() { remove_if_exists() {
local path="$1" local path="$1"
local name local name
name=$(basename "$path") name=$(basename "$path")
if [[ -e "$path" ]]; then if [[ -e $path ]]; then
if [[ $DRY_RUN == true ]]; then if [[ $DRY_RUN == true ]]; then
echo " [dry-run] Would remove: $name" echo " [dry-run] Would remove: $name"
else else
rm -rf "$path" rm -rf "$path"
log_ok "Removed: $name" log_ok "Removed: $name"
fi fi
return 0 return 0
fi fi
return 1 return 1
} }
# Fix 1: Handle corrupted Local State file (most common crash cause) # Fix 1: Handle corrupted Local State file (most common crash cause)
fix_local_state() { fix_local_state() {
log_info "Checking Local State file..." log_info "Checking Local State file..."
local local_state="$THORIUM_CONFIG_DIR/Local State" local local_state="$THORIUM_CONFIG_DIR/Local State"
if [[ -f "$local_state" ]]; then if [[ -f $local_state ]]; then
# Check if it's valid JSON # Check if it's valid JSON
if ! python3 -c "import json; json.load(open('$local_state'))" 2>/dev/null; then if ! python3 -c "import json; json.load(open('$local_state'))" 2> /dev/null; then
log_warn "Local State file appears corrupted" log_warn "Local State file appears corrupted"
backup_if_exists "$local_state" backup_if_exists "$local_state"
else else
# Even if valid JSON, back it up as it can still cause crashes # Even if valid JSON, back it up as it can still cause crashes
log_info "Local State exists - backing up (common crash source)" log_info "Local State exists - backing up (common crash source)"
backup_if_exists "$local_state" backup_if_exists "$local_state"
fi fi
else else
log_info "No Local State file found (OK for fresh install)" log_info "No Local State file found (OK for fresh install)"
fi fi
} }
# Fix 2: Clear singleton lock files # Fix 2: Clear singleton lock files
fix_singleton_locks() { fix_singleton_locks() {
log_info "Clearing singleton lock files..." log_info "Clearing singleton lock files..."
local locks=( local locks=(
"$THORIUM_CONFIG_DIR/SingletonLock" "$THORIUM_CONFIG_DIR/SingletonLock"
"$THORIUM_CONFIG_DIR/SingletonSocket" "$THORIUM_CONFIG_DIR/SingletonSocket"
"$THORIUM_CONFIG_DIR/SingletonCookie" "$THORIUM_CONFIG_DIR/SingletonCookie"
) )
local cleared=0 local cleared=0
for lock in "${locks[@]}"; do for lock in "${locks[@]}"; do
if remove_if_exists "$lock"; then if remove_if_exists "$lock"; then
((cleared++)) || true ((cleared++)) || true
fi fi
done done
if [[ $cleared -eq 0 ]]; then if [[ $cleared -eq 0 ]]; then
log_info "No stale lock files found" log_info "No stale lock files found"
fi fi
} }
# Fix 3: Clear GPU cache # Fix 3: Clear GPU cache
fix_gpu_cache() { fix_gpu_cache() {
log_info "Clearing GPU cache..." log_info "Clearing GPU cache..."
local gpu_paths=( local gpu_paths=(
"$THORIUM_CONFIG_DIR/GPUCache" "$THORIUM_CONFIG_DIR/GPUCache"
"$THORIUM_CONFIG_DIR/Default/GPUCache" "$THORIUM_CONFIG_DIR/Default/GPUCache"
"$THORIUM_CONFIG_DIR/ShaderCache" "$THORIUM_CONFIG_DIR/ShaderCache"
"$THORIUM_CONFIG_DIR/Default/ShaderCache" "$THORIUM_CONFIG_DIR/Default/ShaderCache"
) )
local cleared=0 local cleared=0
for cache in "${gpu_paths[@]}"; do for cache in "${gpu_paths[@]}"; do
if remove_if_exists "$cache"; then if remove_if_exists "$cache"; then
((cleared++)) || true ((cleared++)) || true
fi fi
done done
if [[ $cleared -eq 0 ]]; then if [[ $cleared -eq 0 ]]; then
log_info "No GPU cache to clear" log_info "No GPU cache to clear"
fi fi
} }
# Fix 4: Clear crash reports (can accumulate and cause issues) # Fix 4: Clear crash reports (can accumulate and cause issues)
fix_crash_reports() { fix_crash_reports() {
log_info "Clearing old crash reports..." log_info "Clearing old crash reports..."
local crash_dir="$THORIUM_CONFIG_DIR/Crash Reports" local crash_dir="$THORIUM_CONFIG_DIR/Crash Reports"
if [[ -d "$crash_dir" ]]; then if [[ -d $crash_dir ]]; then
local crash_count local crash_count
crash_count=$(find "$crash_dir" -type f 2>/dev/null | wc -l) crash_count=$(find "$crash_dir" -type f 2> /dev/null | wc -l)
if [[ $crash_count -gt 0 ]]; then if [[ $crash_count -gt 0 ]]; then
if [[ $DRY_RUN == true ]]; then if [[ $DRY_RUN == true ]]; then
echo " [dry-run] Would clear $crash_count crash report(s)" echo " [dry-run] Would clear $crash_count crash report(s)"
else else
rm -rf "$crash_dir" rm -rf "$crash_dir"
log_ok "Cleared $crash_count crash report(s)" log_ok "Cleared $crash_count crash report(s)"
fi fi
fi fi
fi fi
} }
# Fix 5: Aggressive cleaning (optional) # Fix 5: Aggressive cleaning (optional)
fix_aggressive() { fix_aggressive() {
if [[ $AGGRESSIVE != true ]]; then if [[ $AGGRESSIVE != true ]]; then
return return
fi fi
log_warn "Applying aggressive fixes (may lose some site data)..." log_warn "Applying aggressive fixes (may lose some site data)..."
local aggressive_paths=( local aggressive_paths=(
"$THORIUM_CONFIG_DIR/Default/Service Worker" "$THORIUM_CONFIG_DIR/Default/Service Worker"
"$THORIUM_CONFIG_DIR/Default/Cache" "$THORIUM_CONFIG_DIR/Default/Cache"
"$THORIUM_CONFIG_DIR/Default/Code Cache" "$THORIUM_CONFIG_DIR/Default/Code Cache"
"$THORIUM_CONFIG_DIR/Default/IndexedDB" "$THORIUM_CONFIG_DIR/Default/IndexedDB"
"$THORIUM_CONFIG_DIR/BrowserMetrics" "$THORIUM_CONFIG_DIR/BrowserMetrics"
"$THORIUM_CONFIG_DIR/component_crx_cache" "$THORIUM_CONFIG_DIR/component_crx_cache"
) )
for path in "${aggressive_paths[@]}"; do for path in "${aggressive_paths[@]}"; do
remove_if_exists "$path" remove_if_exists "$path"
done done
# Backup potentially corrupted databases # Backup potentially corrupted databases
local db_files=( local db_files=(
"$THORIUM_CONFIG_DIR/Default/Web Data" "$THORIUM_CONFIG_DIR/Default/Web Data"
"$THORIUM_CONFIG_DIR/Default/History" "$THORIUM_CONFIG_DIR/Default/History"
) )
for db in "${db_files[@]}"; do for db in "${db_files[@]}"; do
if [[ -f "$db" ]]; then if [[ -f $db ]]; then
log_info "Checking database: $(basename "$db")" log_info "Checking database: $(basename "$db")"
# Simple corruption check - if sqlite3 can't open it, back it up # Simple corruption check - if sqlite3 can't open it, back it up
if command -v sqlite3 &>/dev/null; then if command -v sqlite3 &> /dev/null; then
if ! sqlite3 "$db" "PRAGMA integrity_check;" &>/dev/null; then if ! sqlite3 "$db" "PRAGMA integrity_check;" &> /dev/null; then
log_warn "Database may be corrupted: $(basename "$db")" log_warn "Database may be corrupted: $(basename "$db")"
backup_if_exists "$db" backup_if_exists "$db"
fi fi
fi fi
fi fi
done done
} }
# Test if Thorium starts successfully # Test if Thorium starts successfully
test_thorium() { test_thorium() {
if [[ $TEST_AFTER != true ]]; then if [[ $TEST_AFTER != true ]]; then
return return
fi fi
log_info "Testing Thorium startup..." log_info "Testing Thorium startup..."
if [[ $DRY_RUN == true ]]; then if [[ $DRY_RUN == true ]]; then
echo " [dry-run] Would test thorium-browser startup" echo " [dry-run] Would test thorium-browser startup"
return return
fi fi
# Start Thorium in background # Start Thorium in background
thorium-browser &>/dev/null & thorium-browser &> /dev/null &
local pid=$! local pid=$!
# Wait a few seconds and check if it's still running # Wait a few seconds and check if it's still running
sleep 4 sleep 4
if kill -0 "$pid" 2>/dev/null; then if kill -0 "$pid" 2> /dev/null; then
log_ok "Thorium started successfully! (PID: $pid)" log_ok "Thorium started successfully! (PID: $pid)"
echo -e "${GREEN}Fix successful!${NC} Thorium is now running." echo -e "${GREEN}Fix successful!${NC} Thorium is now running."
# Offer to keep it running or kill it # Offer to keep it running or kill it
read -r -p "Keep browser running? [Y/n] " response read -r -p "Keep browser running? [Y/n] " response
case "$response" in case "$response" in
[nN]*) [nN]*)
kill "$pid" 2>/dev/null || true kill "$pid" 2> /dev/null || true
log_info "Browser closed" log_info "Browser closed"
;; ;;
*) *)
log_info "Browser left running" log_info "Browser left running"
;; ;;
esac esac
else else
log_error "Thorium still crashing after fixes" log_error "Thorium still crashing after fixes"
echo -e "${RED}Standard fixes did not resolve the issue.${NC}" echo -e "${RED}Standard fixes did not resolve the issue.${NC}"
echo "" echo ""
echo "Try these additional steps:" echo "Try these additional steps:"
echo " 1. Run with --aggressive flag for deeper cleaning" echo " 1. Run with --aggressive flag for deeper cleaning"
echo " 2. Test with fresh profile: thorium-browser --user-data-dir=/tmp/thorium-test" echo " 2. Test with fresh profile: thorium-browser --user-data-dir=/tmp/thorium-test"
echo " 3. Reinstall: yay -S thorium-browser-bin" echo " 3. Reinstall: yay -S thorium-browser-bin"
echo " 4. Check NVIDIA drivers: nvidia-smi" echo " 4. Check NVIDIA drivers: nvidia-smi"
exit 1 exit 1
fi fi
} }
# Main execution # Main execution
main() { main() {
echo "========================================" echo "========================================"
echo " Thorium Browser Fix Script" echo " Thorium Browser Fix Script"
echo "========================================" echo "========================================"
echo "" echo ""
if [[ $DRY_RUN == true ]]; then if [[ $DRY_RUN == true ]]; then
echo -e "${YELLOW}[DRY RUN MODE - no changes will be made]${NC}" echo -e "${YELLOW}[DRY RUN MODE - no changes will be made]${NC}"
echo "" echo ""
fi fi
check_thorium_installed check_thorium_installed
check_config_exists check_config_exists
echo "" echo ""
log_info "Applying fixes to: $THORIUM_CONFIG_DIR" log_info "Applying fixes to: $THORIUM_CONFIG_DIR"
echo "" echo ""
kill_thorium kill_thorium
fix_local_state fix_local_state
fix_singleton_locks fix_singleton_locks
fix_gpu_cache fix_gpu_cache
fix_crash_reports fix_crash_reports
fix_aggressive fix_aggressive
echo "" echo ""
echo "========================================" echo "========================================"
log_ok "Fixes applied!" log_ok "Fixes applied!"
echo "========================================" echo "========================================"
if [[ $DRY_RUN != true ]]; then if [[ $DRY_RUN != true ]]; then
echo "" echo ""
echo "Backups created with suffix: $BACKUP_SUFFIX" echo "Backups created with suffix: $BACKUP_SUFFIX"
echo "To restore: mv ~/.config/thorium/Local\\ State${BACKUP_SUFFIX} ~/.config/thorium/Local\\ State" echo "To restore: mv ~/.config/thorium/Local\\ State${BACKUP_SUFFIX} ~/.config/thorium/Local\\ State"
fi fi
test_thorium test_thorium
if [[ $TEST_AFTER != true ]]; then if [[ $TEST_AFTER != true ]]; then
echo "" echo ""
echo "Run 'thorium-browser' to test, or use: $(basename "$0") --test" echo "Run 'thorium-browser' to test, or use: $(basename "$0") --test"
fi fi
} }
main "$@" main "$@"

View File

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

View File

@ -8,57 +8,57 @@ set -euo pipefail
echo "=== Fixing yay AUR database ===" echo "=== Fixing yay AUR database ==="
# Check if using yay-git (development version with potential bugs) # Check if using yay-git (development version with potential bugs)
if pacman -Qi yay-git &>/dev/null; then if pacman -Qi yay-git &> /dev/null; then
echo "" echo ""
echo "Detected yay-git (development version)." echo "Detected yay-git (development version)."
echo "The 'database AUR not found' error is a known bug in some yay-git versions." echo "The 'database AUR not found' error is a known bug in some yay-git versions."
echo "" echo ""
read -rp "Switch to stable yay? [Y/n] " response read -rp "Switch to stable yay? [Y/n] " response
if [[ "${response,,}" != "n" ]]; then if [[ ${response,,} != "n" ]]; then
echo "Switching to stable yay..." echo "Switching to stable yay..."
# Build and install stable yay from AUR # Build and install stable yay from AUR
TEMP_DIR=$(mktemp -d) TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR" cd "$TEMP_DIR"
git clone https://aur.archlinux.org/yay.git git clone https://aur.archlinux.org/yay.git
cd yay cd yay
# Remove yay-git and yay-git-debug (they conflict) # Remove yay-git and yay-git-debug (they conflict)
sudo pacman -Rdd yay-git --noconfirm sudo pacman -Rdd yay-git --noconfirm
sudo pacman -Rdd yay-git-debug --noconfirm 2>/dev/null || true sudo pacman -Rdd yay-git-debug --noconfirm 2> /dev/null || true
# Build and install stable yay # Build and install stable yay
makepkg -si --noconfirm makepkg -si --noconfirm
cd / cd /
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
echo "" echo ""
echo "=== Switched to stable yay ===" echo "=== Switched to stable yay ==="
echo "You can now retry your yay command." echo "You can now retry your yay command."
exit 0 exit 0
fi fi
fi fi
# Remove yay's cache directory # Remove yay's cache directory
YAY_CACHE_DIR="${HOME}/.cache/yay" YAY_CACHE_DIR="${HOME}/.cache/yay"
if [[ -d "$YAY_CACHE_DIR" ]]; then if [[ -d $YAY_CACHE_DIR ]]; then
echo "Removing yay cache directory: $YAY_CACHE_DIR" echo "Removing yay cache directory: $YAY_CACHE_DIR"
rm -rf "$YAY_CACHE_DIR" rm -rf "$YAY_CACHE_DIR"
fi fi
# Remove yay's local database directory (stores AUR package info) # Remove yay's local database directory (stores AUR package info)
YAY_DB_DIR="${HOME}/.local/share/yay" YAY_DB_DIR="${HOME}/.local/share/yay"
if [[ -d "$YAY_DB_DIR" ]]; then if [[ -d $YAY_DB_DIR ]]; then
echo "Removing yay database directory: $YAY_DB_DIR" echo "Removing yay database directory: $YAY_DB_DIR"
rm -rf "$YAY_DB_DIR" rm -rf "$YAY_DB_DIR"
fi fi
# Remove yay state directory # Remove yay state directory
YAY_STATE_DIR="${HOME}/.local/state/yay" YAY_STATE_DIR="${HOME}/.local/state/yay"
if [[ -d "$YAY_STATE_DIR" ]]; then if [[ -d $YAY_STATE_DIR ]]; then
echo "Removing yay state directory: $YAY_STATE_DIR" echo "Removing yay state directory: $YAY_STATE_DIR"
rm -rf "$YAY_STATE_DIR" rm -rf "$YAY_STATE_DIR"
fi fi
# Clear pacman's sync databases and refresh # Clear pacman's sync databases and refresh

View File

@ -21,7 +21,7 @@ print_setup_header "NVIDIA Comprehensive Troubleshooter & GSP Disabler"
# Check if nvidia module is loaded # Check if nvidia module is loaded
if ! lsmod | grep -q nvidia; then if ! lsmod | grep -q nvidia; then
echo "Warning: NVIDIA module not currently loaded" echo "Warning: NVIDIA module not currently loaded"
fi fi
# Create modprobe configuration directory if it doesn't exist # Create modprobe configuration directory if it doesn't exist
@ -34,7 +34,7 @@ echo "======================================"
mkdir -p "$MODPROBE_DIR" mkdir -p "$MODPROBE_DIR"
# Create the configuration file # Create the configuration file
cat >"$CONFIG_FILE" <<EOF cat > "$CONFIG_FILE" << EOF
# Disable NVIDIA GSP firmware to prevent Vulkan failures and crashes # Disable NVIDIA GSP firmware to prevent Vulkan failures and crashes
# Created by nvidia_troubleshoot.sh on $(date) # Created by nvidia_troubleshoot.sh on $(date)
options nvidia NVreg_EnableGpuFirmware=0 options nvidia NVreg_EnableGpuFirmware=0
@ -44,32 +44,32 @@ echo "✓ Configuration written to: $CONFIG_FILE"
# Function to backup file if it exists # Function to backup file if it exists
backup_file() { backup_file() {
local file="$1" local file="$1"
if [[ -f $file ]]; then if [[ -f $file ]]; then
cp "$file" "$file.backup.$(date +%Y%m%d_%H%M%S)" cp "$file" "$file.backup.$(date +%Y%m%d_%H%M%S)"
echo "✓ Backed up $file" echo "✓ Backed up $file"
fi fi
} }
# Function to add or update xorg.conf for RenderAccel # Function to add or update xorg.conf for RenderAccel
configure_xorg() { configure_xorg() {
echo "" echo ""
echo "2. Configuring Xorg Settings..." echo "2. Configuring Xorg Settings..."
echo "===============================" echo "==============================="
XORG_CONF="/etc/X11/xorg.conf" XORG_CONF="/etc/X11/xorg.conf"
XORG_CONF_D="/etc/X11/xorg.conf.d" XORG_CONF_D="/etc/X11/xorg.conf.d"
NVIDIA_CONF="$XORG_CONF_D/20-nvidia.conf" NVIDIA_CONF="$XORG_CONF_D/20-nvidia.conf"
# Create xorg.conf.d directory if it doesn't exist # Create xorg.conf.d directory if it doesn't exist
mkdir -p "$XORG_CONF_D" mkdir -p "$XORG_CONF_D"
# Backup existing xorg.conf if it exists # Backup existing xorg.conf if it exists
backup_file "$XORG_CONF" backup_file "$XORG_CONF"
backup_file "$NVIDIA_CONF" backup_file "$NVIDIA_CONF"
# Create NVIDIA-specific configuration # Create NVIDIA-specific configuration
cat >"$NVIDIA_CONF" <<EOF cat > "$NVIDIA_CONF" << EOF
# NVIDIA configuration with RenderAccel disabled # NVIDIA configuration with RenderAccel disabled
# Created by nvidia_troubleshoot.sh on $(date) # Created by nvidia_troubleshoot.sh on $(date)
Section "Device" Section "Device"
@ -79,106 +79,106 @@ Section "Device"
EndSection EndSection
EOF EOF
echo "✓ Created $NVIDIA_CONF with RenderAccel disabled" echo "✓ Created $NVIDIA_CONF with RenderAccel disabled"
} }
# Function to add GCC mismatch workaround # Function to add GCC mismatch workaround
configure_gcc_workaround() { configure_gcc_workaround() {
echo "" echo ""
echo "3. Configuring GCC Mismatch Workaround..." echo "3. Configuring GCC Mismatch Workaround..."
echo "==========================================" echo "=========================================="
local PROFILE_FILE="/etc/profile" local PROFILE_FILE="/etc/profile"
local timestamp local timestamp
timestamp=$(date) timestamp=$(date)
backup_file "$PROFILE_FILE" backup_file "$PROFILE_FILE"
# Check if IGNORE_CC_MISMATCH is already set # Check if IGNORE_CC_MISMATCH is already set
if ! grep -q "IGNORE_CC_MISMATCH" "$PROFILE_FILE"; then if ! grep -q "IGNORE_CC_MISMATCH" "$PROFILE_FILE"; then
{ {
printf '\n' printf '\n'
printf '# NVIDIA GCC version mismatch workaround\n' printf '# NVIDIA GCC version mismatch workaround\n'
printf '# Added by nvidia_troubleshoot.sh on %s\n' "$timestamp" printf '# Added by nvidia_troubleshoot.sh on %s\n' "$timestamp"
printf 'export IGNORE_CC_MISMATCH=1\n' printf 'export IGNORE_CC_MISMATCH=1\n'
} >>"$PROFILE_FILE" } >> "$PROFILE_FILE"
echo "✓ Added IGNORE_CC_MISMATCH=1 to $PROFILE_FILE" echo "✓ Added IGNORE_CC_MISMATCH=1 to $PROFILE_FILE"
else else
echo "✓ IGNORE_CC_MISMATCH already configured in $PROFILE_FILE" echo "✓ IGNORE_CC_MISMATCH already configured in $PROFILE_FILE"
fi fi
} }
# Function to install pyroveil for mesh shader issues # Function to install pyroveil for mesh shader issues
install_pyroveil() { install_pyroveil() {
echo "" echo ""
echo "4. Pyroveil Setup for Mesh Shader Issues..." echo "4. Pyroveil Setup for Mesh Shader Issues..."
echo "===========================================" echo "==========================================="
local user_home="/home/$SUDO_USER" local user_home="/home/$SUDO_USER"
local pyroveil_dir="$user_home/pyroveil" local pyroveil_dir="$user_home/pyroveil"
echo "Mesh shaders have poor support on NVIDIA drivers, causing issues in games" echo "Mesh shaders have poor support on NVIDIA drivers, causing issues in games"
echo "like Final Fantasy VII Rebirth. Pyroveil can work around these problems." echo "like Final Fantasy VII Rebirth. Pyroveil can work around these problems."
echo "" echo ""
local install_pyroveil=true local install_pyroveil=true
if [[ $INTERACTIVE_MODE == "true" ]]; then if [[ $INTERACTIVE_MODE == "true" ]]; then
read -p "Would you like to install Pyroveil? (y/N): " -n 1 -r read -p "Would you like to install Pyroveil? (y/N): " -n 1 -r
echo echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then if [[ ! $REPLY =~ ^[Yy]$ ]]; then
install_pyroveil=false install_pyroveil=false
fi fi
else else
echo "Auto-installing Pyroveil (use --interactive to prompt)" echo "Auto-installing Pyroveil (use --interactive to prompt)"
fi fi
if [[ $install_pyroveil == "true" ]]; then if [[ $install_pyroveil == "true" ]]; then
# Check for required dependencies # Check for required dependencies
local missing_deps=() local missing_deps=()
for dep in git cmake ninja gcc; do for dep in git cmake ninja gcc; do
if ! command -v "$dep" &>/dev/null; then if ! command -v "$dep" &> /dev/null; then
missing_deps+=("$dep") missing_deps+=("$dep")
fi fi
done done
if [[ ${#missing_deps[@]} -gt 0 ]]; then if [[ ${#missing_deps[@]} -gt 0 ]]; then
echo "Missing dependencies: ${missing_deps[*]}" echo "Missing dependencies: ${missing_deps[*]}"
echo "Please install them first. On Arch Linux:" echo "Please install them first. On Arch Linux:"
echo "pacman -S base-devel git cmake ninja" echo "pacman -S base-devel git cmake ninja"
return 1 return 1
fi fi
# Clone and build pyroveil as the original user # Clone and build pyroveil as the original user
echo "Installing Pyroveil to $pyroveil_dir..." echo "Installing Pyroveil to $pyroveil_dir..."
if [[ -d $pyroveil_dir ]]; then if [[ -d $pyroveil_dir ]]; then
echo "Pyroveil directory already exists. Updating..." echo "Pyroveil directory already exists. Updating..."
sudo -u "$SUDO_USER" bash -c "cd '$pyroveil_dir' && git pull" sudo -u "$SUDO_USER" bash -c "cd '$pyroveil_dir' && git pull"
else else
sudo -u "$SUDO_USER" git clone https://github.com/HansKristian-Work/pyroveil.git "$pyroveil_dir" sudo -u "$SUDO_USER" git clone https://github.com/HansKristian-Work/pyroveil.git "$pyroveil_dir"
fi fi
sudo -u "$SUDO_USER" bash -c " sudo -u "$SUDO_USER" bash -c "
cd '$pyroveil_dir' cd '$pyroveil_dir'
git submodule update --init git submodule update --init
cmake . -Bbuild -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$user_home/.local cmake . -Bbuild -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$user_home/.local
ninja -C build install ninja -C build install
" "
echo "✓ Pyroveil installed successfully" echo "✓ Pyroveil installed successfully"
echo "" echo ""
echo "To use Pyroveil with games that have mesh shader issues:" echo "To use Pyroveil with games that have mesh shader issues:"
echo "1. For Final Fantasy VII Rebirth:" echo "1. For Final Fantasy VII Rebirth:"
echo " PYROVEIL=1 PYROVEIL_CONFIG=$pyroveil_dir/hacks/ffvii-rebirth-nvidia/pyroveil.json %command%" echo " PYROVEIL=1 PYROVEIL_CONFIG=$pyroveil_dir/hacks/ffvii-rebirth-nvidia/pyroveil.json %command%"
echo "" echo ""
echo "2. For Steam games, add to launch options:" echo "2. For Steam games, add to launch options:"
echo " PYROVEIL=1 PYROVEIL_CONFIG=/path/to/config/pyroveil.json %command%" echo " PYROVEIL=1 PYROVEIL_CONFIG=/path/to/config/pyroveil.json %command%"
echo "" echo ""
echo "Available configs in: $pyroveil_dir/hacks/" echo "Available configs in: $pyroveil_dir/hacks/"
# Create a helper script # Create a helper script
cat >"$user_home/run-with-pyroveil.sh" <<EOF cat > "$user_home/run-with-pyroveil.sh" << EOF
#!/bin/bash #!/bin/bash
# Helper script to run games with Pyroveil # Helper script to run games with Pyroveil
# Usage: ./run-with-pyroveil.sh <config-name> <command> # Usage: ./run-with-pyroveil.sh <config-name> <command>
@ -204,88 +204,88 @@ echo "Config file: \$PYROVEIL_CONFIG"
exec "\$@" exec "\$@"
EOF EOF
chown "$SUDO_USER:$SUDO_USER" "$user_home/run-with-pyroveil.sh" chown "$SUDO_USER:$SUDO_USER" "$user_home/run-with-pyroveil.sh"
chmod +x "$user_home/run-with-pyroveil.sh" chmod +x "$user_home/run-with-pyroveil.sh"
echo "✓ Created helper script: $user_home/run-with-pyroveil.sh" echo "✓ Created helper script: $user_home/run-with-pyroveil.sh"
else else
echo "Skipping Pyroveil installation" echo "Skipping Pyroveil installation"
echo "Note: You can manually install it later for mesh shader issues" echo "Note: You can manually install it later for mesh shader issues"
fi fi
} }
# Function to check for kernel parameter modifications # Function to check for kernel parameter modifications
suggest_kernel_params() { suggest_kernel_params() {
echo "" echo ""
echo "5. Kernel Parameter Recommendations..." echo "5. Kernel Parameter Recommendations..."
echo "=====================================" echo "====================================="
echo "NVIDIA Driver Issues and Recommended Kernel Parameters:" echo "NVIDIA Driver Issues and Recommended Kernel Parameters:"
echo "" echo ""
echo "A) For 'conflicting memory type' or 'failed to allocate primary buffer' errors" echo "A) For 'conflicting memory type' or 'failed to allocate primary buffer' errors"
echo " (especially with nvidia-96xx drivers):" echo " (especially with nvidia-96xx drivers):"
echo " → Add 'nopat' to kernel parameters" echo " → Add 'nopat' to kernel parameters"
echo "" echo ""
echo "B) For OpenGL visual glitches, hangs, and errors with modern CPUs:" echo "B) For OpenGL visual glitches, hangs, and errors with modern CPUs:"
echo " → Consider disabling micro-op cache in BIOS settings" echo " → Consider disabling micro-op cache in BIOS settings"
echo " → This affects Intel Sandy Bridge (2011+) and AMD Zen (2017+) CPUs" echo " → This affects Intel Sandy Bridge (2011+) and AMD Zen (2017+) CPUs"
echo " → Helps with severe graphical glitches in Xwayland applications" echo " → Helps with severe graphical glitches in Xwayland applications"
echo " → Note: Disabling micro-op cache reduces CPU performance" echo " → Note: Disabling micro-op cache reduces CPU performance"
echo "" echo ""
echo "To add kernel parameters:" echo "To add kernel parameters:"
echo "1. Edit /etc/default/grub" echo "1. Edit /etc/default/grub"
echo "2. Add parameters to GRUB_CMDLINE_LINUX_DEFAULT" echo "2. Add parameters to GRUB_CMDLINE_LINUX_DEFAULT"
echo "3. Run: grub-mkconfig -o /boot/grub/grub.cfg" echo "3. Run: grub-mkconfig -o /boot/grub/grub.cfg"
echo "4. Reboot" echo "4. Reboot"
echo "" echo ""
echo "Example GRUB_CMDLINE_LINUX_DEFAULT line:" echo "Example GRUB_CMDLINE_LINUX_DEFAULT line:"
echo 'GRUB_CMDLINE_LINUX_DEFAULT="quiet nopat"' echo 'GRUB_CMDLINE_LINUX_DEFAULT="quiet nopat"'
# Check current CPU for micro-op cache relevance # Check current CPU for micro-op cache relevance
echo "" echo ""
echo "CPU Information (for micro-op cache consideration):" echo "CPU Information (for micro-op cache consideration):"
if command -v lscpu &>/dev/null; then if command -v lscpu &> /dev/null; then
local cpu_info local cpu_info
cpu_info=$(lscpu | grep "Model name" | cut -d: -f2 | xargs) cpu_info=$(lscpu | grep "Model name" | cut -d: -f2 | xargs)
echo "Current CPU: $cpu_info" echo "Current CPU: $cpu_info"
if echo "$cpu_info" | grep -qi "intel"; then if echo "$cpu_info" | grep -qi "intel"; then
echo "→ Intel CPU detected. Sandy Bridge (2011) and later have micro-op cache" echo "→ Intel CPU detected. Sandy Bridge (2011) and later have micro-op cache"
elif echo "$cpu_info" | grep -qi "amd"; then elif echo "$cpu_info" | grep -qi "amd"; then
echo "→ AMD CPU detected. Zen (2017) and later have micro-op cache" echo "→ AMD CPU detected. Zen (2017) and later have micro-op cache"
fi fi
fi fi
} }
# Function to suggest desktop environment settings # Function to suggest desktop environment settings
suggest_desktop_settings() { suggest_desktop_settings() {
echo "" echo ""
echo "6. Desktop Environment Recommendations..." echo "6. Desktop Environment Recommendations..."
echo "========================================" echo "========================================"
echo "For fullscreen application freezing/crashing issues:" echo "For fullscreen application freezing/crashing issues:"
echo "" echo ""
echo "Enable Display Compositing and Direct fullscreen rendering:" echo "Enable Display Compositing and Direct fullscreen rendering:"
echo "" echo ""
echo "• KDE Plasma:" echo "• KDE Plasma:"
echo " System Settings → Display and Monitor → Compositor" echo " System Settings → Display and Monitor → Compositor"
echo " → Enable compositor + Enable direct rendering for fullscreen windows" echo " → Enable compositor + Enable direct rendering for fullscreen windows"
echo "" echo ""
echo "• GNOME:" echo "• GNOME:"
echo " Use Extensions or dconf-editor to enable compositing features" echo " Use Extensions or dconf-editor to enable compositing features"
echo "" echo ""
echo "• XFCE:" echo "• XFCE:"
echo " Settings → Window Manager Tweaks → Compositor" echo " Settings → Window Manager Tweaks → Compositor"
echo " → Enable display compositing" echo " → Enable display compositing"
echo "" echo ""
echo "• Cinnamon:" echo "• Cinnamon:"
echo " System Settings → Effects → Enable desktop effects" echo " System Settings → Effects → Enable desktop effects"
# Detect current desktop environment # Detect current desktop environment
if [[ -n $XDG_CURRENT_DESKTOP ]]; then if [[ -n $XDG_CURRENT_DESKTOP ]]; then
echo "" echo ""
echo "Detected desktop environment: $XDG_CURRENT_DESKTOP" echo "Detected desktop environment: $XDG_CURRENT_DESKTOP"
fi fi
} }
# Apply all configurations # Apply all configurations
@ -297,14 +297,14 @@ install_pyroveil
echo "" echo ""
echo "7. Regenerating Initramfs..." echo "7. Regenerating Initramfs..."
echo "============================" echo "============================"
if command -v mkinitcpio &>/dev/null; then if command -v mkinitcpio &> /dev/null; then
mkinitcpio -P mkinitcpio -P
echo "✓ Initramfs regenerated with mkinitcpio" echo "✓ Initramfs regenerated with mkinitcpio"
elif command -v dracut &>/dev/null; then elif command -v dracut &> /dev/null; then
dracut --force dracut --force
echo "✓ Initramfs regenerated with dracut" echo "✓ Initramfs regenerated with dracut"
else else
echo "Warning: Could not find mkinitcpio or dracut. You may need to manually regenerate initramfs." echo "Warning: Could not find mkinitcpio or dracut. You may need to manually regenerate initramfs."
fi fi
# Display all recommendations # Display all recommendations
@ -320,7 +320,7 @@ echo "✓ GSP firmware disabled"
echo "✓ RenderAccel disabled in Xorg configuration" echo "✓ RenderAccel disabled in Xorg configuration"
echo "✓ GCC version mismatch workaround added" echo "✓ GCC version mismatch workaround added"
if [[ -d "/home/$SUDO_USER/pyroveil" ]]; then if [[ -d "/home/$SUDO_USER/pyroveil" ]]; then
echo "✓ Pyroveil installed for mesh shader issues" echo "✓ Pyroveil installed for mesh shader issues"
fi fi
echo "✓ Initramfs regenerated" echo "✓ Initramfs regenerated"
echo "" echo ""

View File

@ -11,49 +11,49 @@ ensure_dir "$ANDROID_WORK_DIR"
# Exit with error message # Exit with error message
die() { die() {
echo "[ERROR] $*" >&2 echo "[ERROR] $*" >&2
exit 1 exit 1
} }
# Print section header # Print section header
print_header() { print_header() {
echo echo
echo "========================================" echo "========================================"
echo " $1" echo " $1"
echo "========================================" echo "========================================"
echo echo
} }
# Initialize an Android script with common setup # Initialize an Android script with common setup
# Usage: init_android_script "$@" # Usage: init_android_script "$@"
# This combines: require_hosts_readable, sets WORK_DIR # This combines: require_hosts_readable, sets WORK_DIR
init_android_script() { init_android_script() {
require_hosts_readable "$@" require_hosts_readable "$@"
WORK_DIR="$ANDROID_WORK_DIR" WORK_DIR="$ANDROID_WORK_DIR"
export WORK_DIR export WORK_DIR
} }
# Check if ADB device is connected # Check if ADB device is connected
check_adb_device() { check_adb_device() {
log "Checking device connection..." log "Checking device connection..."
if ! adb devices | grep -q "device$"; then if ! adb devices | grep -q "device$"; then
die "No device connected. Enable USB debugging and connect your phone." die "No device connected. Enable USB debugging and connect your phone."
fi fi
log "Device connected" log "Device connected"
} }
# Check if device has root access # Check if device has root access
check_adb_root() { check_adb_root() {
log "Checking root access..." log "Checking root access..."
if ! adb shell "su -c 'echo test'" 2>/dev/null | grep -q "test"; then 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." die "Root access not available. Make sure Magisk is installed and grant root to Shell."
fi fi
log "Root access confirmed" log "Root access confirmed"
} }
# Re-exec with sudo if needed to read /etc/hosts # Re-exec with sudo if needed to read /etc/hosts
require_hosts_readable() { require_hosts_readable() {
if [[ $EUID -ne 0 ]] && [[ ! -r /etc/hosts ]]; then if [[ $EUID -ne 0 ]] && [[ ! -r /etc/hosts ]]; then
exec sudo -E bash "$0" "$@" exec sudo -E bash "$0" "$@"
fi fi
} }

View File

@ -16,20 +16,20 @@ _LIB_COMMON_LOADED=1
# Log message with timestamp to stderr and optionally to a file # Log message with timestamp to stderr and optionally to a file
# Usage: log_message "message" [log_file] # Usage: log_message "message" [log_file]
log_message() { log_message() {
local msg="$1" local msg="$1"
local log_file="${2:-}" local log_file="${2:-}"
local formatted local formatted
formatted="$(date '+%Y-%m-%d %H:%M:%S') - $msg" formatted="$(date '+%Y-%m-%d %H:%M:%S') - $msg"
echo "$formatted" >&2 echo "$formatted" >&2
if [[ -n $log_file ]]; then if [[ -n $log_file ]]; then
echo "$formatted" >>"$log_file" 2>/dev/null || true echo "$formatted" >> "$log_file" 2> /dev/null || true
fi fi
} }
# Simple log with timestamp (no file output) # Simple log with timestamp (no file output)
# Usage: log "message" # Usage: log "message"
log() { log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
} }
# ============================================================================= # =============================================================================
@ -39,29 +39,29 @@ log() {
# Check if running as root, if not re-exec with sudo # Check if running as root, if not re-exec with sudo
# Usage: require_root "$@" # Usage: require_root "$@"
require_root() { require_root() {
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "This script requires root privileges." echo "This script requires root privileges."
echo "Requesting sudo access..." echo "Requesting sudo access..."
exec sudo "$0" "$@" exec sudo "$0" "$@"
fi fi
} }
# Get the actual user even when running with sudo # Get the actual user even when running with sudo
# Usage: ACTUAL_USER=$(get_actual_user) # Usage: ACTUAL_USER=$(get_actual_user)
get_actual_user() { get_actual_user() {
echo "${SUDO_USER:-$USER}" echo "${SUDO_USER:-$USER}"
} }
# Get the actual user's home directory # Get the actual user's home directory
# Usage: USER_HOME=$(get_actual_user_home) # Usage: USER_HOME=$(get_actual_user_home)
get_actual_user_home() { get_actual_user_home() {
local user local user
user=$(get_actual_user) user=$(get_actual_user)
if [[ $user == "root" ]]; then if [[ $user == "root" ]]; then
echo "/root" echo "/root"
else else
echo "/home/$user" echo "/home/$user"
fi fi
} }
# Set both ACTUAL_USER and USER_HOME variables (common pattern) # Set both ACTUAL_USER and USER_HOME variables (common pattern)
@ -69,9 +69,9 @@ get_actual_user_home() {
# echo "$ACTUAL_USER" # => the actual user # echo "$ACTUAL_USER" # => the actual user
# echo "$USER_HOME" # => /home/username # echo "$USER_HOME" # => /home/username
set_actual_user_vars() { set_actual_user_vars() {
ACTUAL_USER=$(get_actual_user) ACTUAL_USER=$(get_actual_user)
USER_HOME=$(get_actual_user_home) USER_HOME=$(get_actual_user_home)
export ACTUAL_USER USER_HOME export ACTUAL_USER USER_HOME
} }
# ============================================================================= # =============================================================================
@ -86,30 +86,30 @@ export INTERACTIVE_MODE=false
export COMMON_ARGS_SHIFT=0 export COMMON_ARGS_SHIFT=0
parse_interactive_args() { parse_interactive_args() {
INTERACTIVE_MODE=false INTERACTIVE_MODE=false
COMMON_ARGS_SHIFT=0 COMMON_ARGS_SHIFT=0
local script_name="${0##*/}" local script_name="${0##*/}"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
-i | --interactive) -i | --interactive)
INTERACTIVE_MODE=true INTERACTIVE_MODE=true
((COMMON_ARGS_SHIFT++)) ((COMMON_ARGS_SHIFT++))
shift shift
;; ;;
-h | --help) -h | --help)
echo "Usage: $script_name [OPTIONS]" echo "Usage: $script_name [OPTIONS]"
echo "Options:" echo "Options:"
echo " -i, --interactive Enable interactive prompts (default: auto-yes)" echo " -i, --interactive Enable interactive prompts (default: auto-yes)"
echo " -h, --help Show this help message" echo " -h, --help Show this help message"
exit 0 exit 0
;; ;;
*) *)
# Stop parsing at first unknown argument # Stop parsing at first unknown argument
break break
;; ;;
esac esac
done done
} }
# Handle common argument patterns for scripts with custom usage functions # Handle common argument patterns for scripts with custom usage functions
@ -117,37 +117,37 @@ parse_interactive_args() {
# Returns: 0 if argument was handled (caller should continue), 1 if not our concern # 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) # Exits: on -h/--help (exit 0) or unknown arg starting with - (exit 2)
handle_arg_help_or_unknown() { handle_arg_help_or_unknown() {
local arg="$1" local arg="$1"
local usage_fn="${2:-usage}" local usage_fn="${2:-usage}"
local err_fn="${3:-err}" local err_fn="${3:-err}"
case "$arg" in case "$arg" in
-h | --help) -h | --help)
"$usage_fn" "$usage_fn"
exit 0 exit 0
;; ;;
-*) -*)
"$err_fn" "Unknown argument: $arg" "$err_fn" "Unknown argument: $arg"
"$usage_fn" "$usage_fn"
exit 2 exit 2
;; ;;
*) *)
return 1 # Not a flag, let caller handle it return 1 # Not a flag, let caller handle it
;; ;;
esac esac
return 0 return 0
} }
# Initialize a setup script with common boilerplate # Initialize a setup script with common boilerplate
# Usage: init_setup_script "Script Title" "$@" # Usage: init_setup_script "Script Title" "$@"
# This combines: parse_interactive_args, shift, require_root, print_setup_header # This combines: parse_interactive_args, shift, require_root, print_setup_header
init_setup_script() { init_setup_script() {
local title="$1" local title="$1"
shift shift
parse_interactive_args "$@" parse_interactive_args "$@"
shift "$COMMON_ARGS_SHIFT" shift "$COMMON_ARGS_SHIFT"
require_root "$@" require_root "$@"
print_setup_header "$title" print_setup_header "$title"
} }
# ============================================================================= # =============================================================================
@ -156,51 +156,51 @@ init_setup_script() {
# Default focus apps - can be overridden before calling is_focus_app_running # Default focus apps - can be overridden before calling is_focus_app_running
FOCUS_APPS_WINDOWS=( FOCUS_APPS_WINDOWS=(
"Visual Studio Code" "Visual Studio Code"
"VSCodium" "VSCodium"
"Cursor" "Cursor"
"IntelliJ IDEA" "IntelliJ IDEA"
"PyCharm" "PyCharm"
"WebStorm" "WebStorm"
"CLion" "CLion"
"Rider" "Rider"
"Sublime Text" "Sublime Text"
"Blender" "Blender"
"Godot" "Godot"
"Unity" "Unity"
"Unreal Editor" "Unreal Editor"
) )
FOCUS_APPS_PROCESSES=( FOCUS_APPS_PROCESSES=(
"steam_app_" "steam_app_"
"gamescope" "gamescope"
) )
# Check if any focus app is running (window-based detection) # Check if any focus app is running (window-based detection)
# Returns 0 if focus app found, 1 otherwise # Returns 0 if focus app found, 1 otherwise
# Echoes the name of the found app # Echoes the name of the found app
is_focus_app_running() { is_focus_app_running() {
# Check windows first # Check windows first
if command -v xdotool &>/dev/null; then if command -v xdotool &> /dev/null; then
local app local app
for app in "${FOCUS_APPS_WINDOWS[@]}"; do for app in "${FOCUS_APPS_WINDOWS[@]}"; do
if xdotool search --name "$app" &>/dev/null 2>&1; then if xdotool search --name "$app" &> /dev/null 2>&1; then
echo "$app" echo "$app"
return 0 return 0
fi fi
done done
fi fi
# Check specific processes # Check specific processes
local app local app
for app in "${FOCUS_APPS_PROCESSES[@]}"; do for app in "${FOCUS_APPS_PROCESSES[@]}"; do
if pgrep -f "$app" &>/dev/null; then if pgrep -f "$app" &> /dev/null; then
echo "$app" echo "$app"
return 0 return 0
fi fi
done done
return 1 return 1
} }
# ============================================================================= # =============================================================================
@ -210,69 +210,69 @@ is_focus_app_running() {
# Check if a command exists # Check if a command exists
# Usage: if require_command ffmpeg; then ... # Usage: if require_command ffmpeg; then ...
require_command() { require_command() {
local cmd="$1" local cmd="$1"
local pkg="${2:-$1}" local pkg="${2:-$1}"
if ! command -v "$cmd" >/dev/null 2>&1; then if ! command -v "$cmd" > /dev/null 2>&1; then
echo "Error: '$cmd' is not installed or not in PATH." >&2 echo "Error: '$cmd' is not installed or not in PATH." >&2
echo "Install with: sudo pacman -S $pkg" >&2 echo "Install with: sudo pacman -S $pkg" >&2
return 1 return 1
fi fi
return 0 return 0
} }
# Check for ImageMagick and display helpful installation message # Check for ImageMagick and display helpful installation message
# Usage: require_imagemagick [optional: "magick" or "convert"] # Usage: require_imagemagick [optional: "magick" or "convert"]
# Returns: Sets MAGICK_CMD variable to available command # Returns: Sets MAGICK_CMD variable to available command
require_imagemagick() { require_imagemagick() {
local preferred="${1:-}" local preferred="${1:-}"
if [[ $preferred == "magick" ]] || [[ -z $preferred ]]; then if [[ $preferred == "magick" ]] || [[ -z $preferred ]]; then
if command -v magick &>/dev/null; then if command -v magick &> /dev/null; then
MAGICK_CMD="magick" MAGICK_CMD="magick"
export MAGICK_CMD export MAGICK_CMD
return 0 return 0
fi fi
fi fi
if [[ $preferred == "convert" ]] || [[ -z $preferred ]]; then if [[ $preferred == "convert" ]] || [[ -z $preferred ]]; then
if command -v convert &>/dev/null; then if command -v convert &> /dev/null; then
MAGICK_CMD="convert" MAGICK_CMD="convert"
export MAGICK_CMD export MAGICK_CMD
return 0 return 0
fi fi
fi fi
echo "Error: ImageMagick is not installed." >&2 echo "Error: ImageMagick is not installed." >&2
echo "Install it with:" >&2 echo "Install it with:" >&2
echo " Arch Linux: sudo pacman -S imagemagick" >&2 echo " Arch Linux: sudo pacman -S imagemagick" >&2
echo " Ubuntu/Debian: sudo apt install imagemagick" >&2 echo " Ubuntu/Debian: sudo apt install imagemagick" >&2
return 1 return 1
} }
# Install missing pacman packages # Install missing pacman packages
# Usage: install_missing_pacman_packages pkg1 pkg2 pkg3 ... # Usage: install_missing_pacman_packages pkg1 pkg2 pkg3 ...
# Returns 0 if all packages installed successfully, 1 otherwise # Returns 0 if all packages installed successfully, 1 otherwise
install_missing_pacman_packages() { install_missing_pacman_packages() {
local packages=("$@") local packages=("$@")
local missing=() local missing=()
for pkg in "${packages[@]}"; do for pkg in "${packages[@]}"; do
if ! pacman -Qi "$pkg" >/dev/null 2>&1; then if ! pacman -Qi "$pkg" > /dev/null 2>&1; then
missing+=("$pkg") missing+=("$pkg")
fi fi
done done
if [[ ${#missing[@]} -eq 0 ]]; then if [[ ${#missing[@]} -eq 0 ]]; then
echo "[INFO] All required packages are already installed." echo "[INFO] All required packages are already installed."
return 0 return 0
fi fi
echo "[INFO] Installing missing packages: ${missing[*]}" echo "[INFO] Installing missing packages: ${missing[*]}"
if ! sudo pacman -S --needed --noconfirm "${missing[@]}"; then if ! sudo pacman -S --needed --noconfirm "${missing[@]}"; then
echo "[ERROR] Failed to install packages" >&2 echo "[ERROR] Failed to install packages" >&2
return 1 return 1
fi fi
return 0 return 0
} }
# ============================================================================= # =============================================================================
@ -282,14 +282,14 @@ install_missing_pacman_packages() {
# Send desktop notification (fails silently if notify-send not available) # Send desktop notification (fails silently if notify-send not available)
# Usage: notify "Title" "Message" [urgency: low/normal/critical] [timeout_ms] # Usage: notify "Title" "Message" [urgency: low/normal/critical] [timeout_ms]
notify() { notify() {
local title="$1" local title="$1"
local message="$2" local message="$2"
local urgency="${3:-normal}" local urgency="${3:-normal}"
local timeout="${4:-5000}" local timeout="${4:-5000}"
if command -v notify-send &>/dev/null; then if command -v notify-send &> /dev/null; then
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2>/dev/null || true notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2> /dev/null || true
fi fi
} }
# ============================================================================= # =============================================================================
@ -299,16 +299,16 @@ notify() {
# Get the directory containing the calling script # Get the directory containing the calling script
# Usage: SCRIPT_DIR=$(get_script_dir) # Usage: SCRIPT_DIR=$(get_script_dir)
get_script_dir() { get_script_dir() {
dirname "$(readlink -f "${BASH_SOURCE[1]:-$0}")" dirname "$(readlink -f "${BASH_SOURCE[1]:-$0}")"
} }
# Ensure a directory exists # Ensure a directory exists
# Usage: ensure_dir "/path/to/dir" # Usage: ensure_dir "/path/to/dir"
ensure_dir() { ensure_dir() {
local dir="$1" local dir="$1"
if [[ ! -d $dir ]]; then if [[ ! -d $dir ]]; then
mkdir -p "$dir" mkdir -p "$dir"
fi fi
} }
# ============================================================================= # =============================================================================
@ -317,34 +317,34 @@ ensure_dir() {
# Internal helper for running systemctl with optional --user flag # Internal helper for running systemctl with optional --user flag
_systemctl_cmd() { _systemctl_cmd() {
local user_flag="$1" local user_flag="$1"
shift shift
if [[ $user_flag == "--user" ]]; then if [[ $user_flag == "--user" ]]; then
systemctl --user "$@" systemctl --user "$@"
else else
systemctl "$@" systemctl "$@"
fi fi
} }
# Enable and start a systemd service (user or system) # Enable and start a systemd service (user or system)
# Usage: enable_service "service-name" [--user] # Usage: enable_service "service-name" [--user]
enable_service() { enable_service() {
local service="$1" local service="$1"
local user_flag="${2:-}" local user_flag="${2:-}"
_systemctl_cmd "$user_flag" daemon-reload _systemctl_cmd "$user_flag" daemon-reload
_systemctl_cmd "$user_flag" enable --now "$service" _systemctl_cmd "$user_flag" enable --now "$service"
} }
# Check if a systemd service is active # Check if a systemd service is active
# Usage: if is_service_active "service-name" [--user]; then ... # Usage: if is_service_active "service-name" [--user]; then ...
is_service_active() { is_service_active() {
_systemctl_cmd "${2:-}" is-active --quiet "$1" _systemctl_cmd "${2:-}" is-active --quiet "$1"
} }
# Check if a systemd service is enabled # Check if a systemd service is enabled
# Usage: if is_service_enabled "service-name" [--user]; then ... # Usage: if is_service_enabled "service-name" [--user]; then ...
is_service_enabled() { is_service_enabled() {
_systemctl_cmd "${2:-}" is-enabled --quiet "$1" 2>/dev/null _systemctl_cmd "${2:-}" is-enabled --quiet "$1" 2> /dev/null
} }
# ============================================================================= # =============================================================================
@ -359,19 +359,19 @@ declare -g COLOR_BLUE='\033[1;34m'
declare -g COLOR_NC='\033[0m' declare -g COLOR_NC='\033[0m'
log_info() { log_info() {
printf "${COLOR_BLUE}[INFO]${COLOR_NC} %s\n" "$*" printf "${COLOR_BLUE}[INFO]${COLOR_NC} %s\n" "$*"
} }
log_ok() { log_ok() {
printf "${COLOR_GREEN}[ OK ]${COLOR_NC} %s\n" "$*" printf "${COLOR_GREEN}[ OK ]${COLOR_NC} %s\n" "$*"
} }
log_warn() { log_warn() {
printf "${COLOR_YELLOW}[WARN]${COLOR_NC} %s\n" "$*" >&2 printf "${COLOR_YELLOW}[WARN]${COLOR_NC} %s\n" "$*" >&2
} }
log_error() { log_error() {
printf "${COLOR_RED}[ERROR]${COLOR_NC} %s\n" "$*" >&2 printf "${COLOR_RED}[ERROR]${COLOR_NC} %s\n" "$*" >&2
} }
# Alias for compatibility # Alias for compatibility
@ -385,19 +385,19 @@ err() { log_error "$@"; }
# Ask yes/no question, returns 0 for yes, 1 for no # Ask yes/no question, returns 0 for yes, 1 for no
# Usage: if ask_yes_no "Continue?"; then ... # Usage: if ask_yes_no "Continue?"; then ...
ask_yes_no() { ask_yes_no() {
local prompt="$1" local prompt="$1"
local ans local ans
read -r -p "$prompt [y/N]: " ans || true read -r -p "$prompt [y/N]: " ans || true
case "${ans:-}" in case "${ans:-}" in
y | Y | yes | YES) return 0 ;; y | Y | yes | YES) return 0 ;;
*) return 1 ;; *) return 1 ;;
esac esac
} }
# Check if a command is available # Check if a command is available
# Usage: if has_cmd git; then ... # Usage: if has_cmd git; then ...
has_cmd() { has_cmd() {
command -v "$1" >/dev/null 2>&1 command -v "$1" > /dev/null 2>&1
} }
# ============================================================================= # =============================================================================
@ -407,18 +407,18 @@ has_cmd() {
# Print a standard setup header for scripts # Print a standard setup header for scripts
# Usage: print_setup_header "Script Name" # Usage: print_setup_header "Script Name"
print_setup_header() { print_setup_header() {
local title="$1" local title="$1"
echo "$title" echo "$title"
printf '=%.0s' $(seq 1 ${#title}) printf '=%.0s' $(seq 1 ${#title})
echo "" echo ""
echo "Current Date: $(date)" echo "Current Date: $(date)"
echo "User: $USER" echo "User: $USER"
echo "Original user: $(get_actual_user)" echo "Original user: $(get_actual_user)"
if [[ $INTERACTIVE_MODE == "true" ]]; then if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Mode: Interactive (prompts enabled)" echo "Mode: Interactive (prompts enabled)"
else else
echo "Mode: Automatic (auto-yes, use --interactive for prompts)" echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
fi fi
} }
# ============================================================================= # =============================================================================
@ -428,33 +428,33 @@ print_setup_header() {
# Count mount layers for a path # Count mount layers for a path
# Usage: count=$(mount_layers_count "/etc/hosts") # Usage: count=$(mount_layers_count "/etc/hosts")
mount_layers_count() { mount_layers_count() {
local target="$1" local target="$1"
awk -v t="$target" '$5==t{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0 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 # Collapse all bind mount layers for a path
# Usage: collapse_mounts "/etc/hosts" [max_iterations] # Usage: collapse_mounts "/etc/hosts" [max_iterations]
collapse_mounts() { collapse_mounts() {
local target="$1" local target="$1"
local max_iter="${2:-20}" local max_iter="${2:-20}"
local i=0 local i=0
if has_cmd mountpoint; then if has_cmd mountpoint; then
while mountpoint -q "$target"; do while mountpoint -q "$target"; do
umount -l "$target" >/dev/null 2>&1 || break umount -l "$target" > /dev/null 2>&1 || break
i=$((i + 1)) i=$((i + 1))
((i >= max_iter)) && break ((i >= max_iter)) && break
done done
else else
local cnt local cnt
cnt=$(mount_layers_count "$target") cnt=$(mount_layers_count "$target")
while ((cnt > 1)); do while ((cnt > 1)); do
umount -l "$target" >/dev/null 2>&1 || break umount -l "$target" > /dev/null 2>&1 || break
i=$((i + 1)) i=$((i + 1))
((i >= max_iter)) && break ((i >= max_iter)) && break
cnt=$(mount_layers_count "$target") cnt=$(mount_layers_count "$target")
done done
fi fi
} }
# ============================================================================= # =============================================================================
@ -464,27 +464,27 @@ collapse_mounts() {
# Validate resolution format (WIDTHxHEIGHT) # Validate resolution format (WIDTHxHEIGHT)
# Usage: if validate_resolution "1920x1080"; then ... # Usage: if validate_resolution "1920x1080"; then ...
validate_resolution() { validate_resolution() {
local res="$1" local res="$1"
[[ $res =~ ^[0-9]+x[0-9]+$ ]] [[ $res =~ ^[0-9]+x[0-9]+$ ]]
} }
# Generate output filename with suffix # Generate output filename with suffix
# Usage: output=$(generate_output_filename "input.jpg" "_resized") # Usage: output=$(generate_output_filename "input.jpg" "_resized")
generate_output_filename() { generate_output_filename() {
local input="$1" local input="$1"
local suffix="$2" local suffix="$2"
local ext="${3:-}" local ext="${3:-}"
local basename dirname filename extension local basename dirname filename extension
basename=$(basename "$input") basename=$(basename "$input")
dirname=$(dirname "$input") dirname=$(dirname "$input")
filename="${basename%.*}" filename="${basename%.*}"
extension="${basename##*.}" extension="${basename##*.}"
# Handle files without extension # Handle files without extension
if [[ $filename == "$extension" ]]; then if [[ $filename == "$extension" ]]; then
extension="${ext:-jpg}" extension="${ext:-jpg}"
fi fi
echo "${dirname}/${filename}${suffix}.${extension}" echo "${dirname}/${filename}${suffix}.${extension}"
} }

View File

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

View File

@ -28,7 +28,7 @@ SET_DEFAULT=false
DO_RESTART=false DO_RESTART=false
usage() { usage() {
cat <<EOF cat << EOF
fix_thorium_unity.sh - Auto-allow unityhub:// from Unity origins in Thorium/Chromium fix_thorium_unity.sh - Auto-allow unityhub:// from Unity origins in Thorium/Chromium
Options: Options:
@ -44,52 +44,52 @@ EOF
} }
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--policy) --policy)
DO_POLICY=true DO_POLICY=true
shift shift
;; ;;
--set-default) --set-default)
SET_DEFAULT=true SET_DEFAULT=true
shift shift
;; ;;
--restart) --restart)
DO_RESTART=true DO_RESTART=true
shift shift
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
*) *)
log_error "Unknown argument: $1" log_error "Unknown argument: $1"
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
ensure_sudo() { ensure_sudo() {
if ! command -v sudo >/dev/null 2>&1; then if ! command -v sudo > /dev/null 2>&1; then
log_error "sudo not found; cannot install system policy. Use --set-default or run from root." log_error "sudo not found; cannot install system policy. Use --set-default or run from root."
exit 1 exit 1
fi fi
} }
install_policy() { install_policy() {
ensure_sudo ensure_sudo
# Candidate policy directories (most common for Chromium forks) # Candidate policy directories (most common for Chromium forks)
local candidates=( local candidates=(
"/etc/thorium-browser/policies/managed" # Thorium "/etc/thorium-browser/policies/managed" # Thorium
"/etc/chromium/policies/managed" # Chromium "/etc/chromium/policies/managed" # Chromium
"/etc/opt/chrome/policies/managed" # Google Chrome "/etc/opt/chrome/policies/managed" # Google Chrome
) )
local wrote_any=false local wrote_any=false
for target in "${candidates[@]}"; do for target in "${candidates[@]}"; do
log_info "Installing policy into: $target" log_info "Installing policy into: $target"
sudo mkdir -p "$target" sudo mkdir -p "$target"
local policy_file="$target/unityhub-policy.json" local policy_file="$target/unityhub-policy.json"
sudo tee "$policy_file" >/dev/null <<'JSON' sudo tee "$policy_file" > /dev/null << 'JSON'
{ {
"AutoLaunchProtocolsFromOrigins": [ "AutoLaunchProtocolsFromOrigins": [
{ "protocol": "unityhub", "origin": "https://id.unity.com", "allow": true }, { "protocol": "unityhub", "origin": "https://id.unity.com", "allow": true },
@ -101,53 +101,53 @@ install_policy() {
] ]
} }
JSON JSON
# Some Chromium builds cache policies; no explicit reload on Linux. Restarting browser suffices. # Some Chromium builds cache policies; no explicit reload on Linux. Restarting browser suffices.
log_ok "Policy written: $policy_file" log_ok "Policy written: $policy_file"
wrote_any=true wrote_any=true
done done
if [[ $wrote_any != true ]]; then if [[ $wrote_any != true ]]; then
log_warn "Policy may not have been written. No candidate directories processed." log_warn "Policy may not have been written. No candidate directories processed."
fi fi
} }
set_default_browser() { set_default_browser() {
if command -v xdg-settings >/dev/null 2>&1; then if command -v xdg-settings > /dev/null 2>&1; then
# Prefer the upstream desktop id if it exists # Prefer the upstream desktop id if it exists
local desktop="thorium-browser.desktop" local desktop="thorium-browser.desktop"
if [[ ! -f "/usr/share/applications/$desktop" && -f "$HOME/.local/share/applications/$desktop" ]]; then if [[ ! -f "/usr/share/applications/$desktop" && -f "$HOME/.local/share/applications/$desktop" ]]; then
: # keep desktop as is : # keep desktop as is
elif [[ ! -f "/usr/share/applications/$desktop" && ! -f "$HOME/.local/share/applications/$desktop" ]]; then elif [[ ! -f "/usr/share/applications/$desktop" && ! -f "$HOME/.local/share/applications/$desktop" ]]; then
log_warn "thorium-browser.desktop not found; leaving default browser unchanged." log_warn "thorium-browser.desktop not found; leaving default browser unchanged."
return return
fi fi
log_info "Setting default browser to $desktop" log_info "Setting default browser to $desktop"
xdg-settings set default-web-browser "$desktop" || log_warn "Failed to set default browser via xdg-settings" xdg-settings set default-web-browser "$desktop" || log_warn "Failed to set default browser via xdg-settings"
log_ok "Default browser set to: $(xdg-settings get default-web-browser 2>/dev/null || echo "$desktop")" log_ok "Default browser set to: $(xdg-settings get default-web-browser 2> /dev/null || echo "$desktop")"
else else
log_warn "xdg-settings not found; cannot set default browser automatically." log_warn "xdg-settings not found; cannot set default browser automatically."
fi fi
} }
restart_thorium() { restart_thorium() {
# Kill Thorium processes and start fresh # Kill Thorium processes and start fresh
log_info "Restarting Thorium..." log_info "Restarting Thorium..."
pkill -9 -f 'thorium-browser' 2>/dev/null || true pkill -9 -f 'thorium-browser' 2> /dev/null || true
# Also kill unityhub-bin's embedded Chromium if any leftover (harmless) # Also kill unityhub-bin's embedded Chromium if any leftover (harmless)
pkill -9 -f 'unityhub-bin' 2>/dev/null || true pkill -9 -f 'unityhub-bin' 2> /dev/null || true
# Start Thorium detached if available # Start Thorium detached if available
if command -v thorium-browser >/dev/null 2>&1; then if command -v thorium-browser > /dev/null 2>&1; then
nohup thorium-browser >/dev/null 2>&1 & nohup thorium-browser > /dev/null 2>&1 &
disown || true disown || true
fi fi
log_ok "Thorium restart attempted." log_ok "Thorium restart attempted."
} }
main() { main() {
$DO_POLICY && install_policy $DO_POLICY && install_policy
$SET_DEFAULT && set_default_browser $SET_DEFAULT && set_default_browser
$DO_RESTART && restart_thorium $DO_RESTART && restart_thorium
cat <<'NEXT' cat << 'NEXT'
--- ---
Next steps: Next steps:
- Open Unity Hub, click Sign in, complete in Thorium; when prompted, allow the unityhub link to open the app. - Open Unity Hub, click Sign in, complete in Thorium; when prompted, allow the unityhub link to open the app.

View File

@ -30,7 +30,7 @@ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
source "$SCRIPT_DIR/../../lib/common.sh" source "$SCRIPT_DIR/../../lib/common.sh"
usage() { usage() {
cat <<EOF cat << EOF
${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler ${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler
Options: Options:
@ -47,158 +47,158 @@ AUTO_INSTALL=false
RUN_TEST=false RUN_TEST=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-y | --yes) -y | --yes)
AUTO_INSTALL=true AUTO_INSTALL=true
shift shift
;; ;;
--test) --test)
RUN_TEST=true RUN_TEST=true
shift shift
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
*) *)
log_error "Unknown argument: $1" log_error "Unknown argument: $1"
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
require_cmd() { require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then if ! command -v "$1" > /dev/null 2>&1; then
return 1 return 1
fi fi
} }
ensure_deps_arch() { ensure_deps_arch() {
# Best-effort install for Arch-based systems # Best-effort install for Arch-based systems
if [[ $AUTO_INSTALL != true ]]; then if [[ $AUTO_INSTALL != true ]]; then
log_warn "Skipping package installation (use -y to auto-install)." log_warn "Skipping package installation (use -y to auto-install)."
return 0 return 0
fi fi
if ! require_cmd pacman; then if ! require_cmd pacman; then
log_warn "Not an Arch-based system (pacman not found). Skipping auto-install." log_warn "Not an Arch-based system (pacman not found). Skipping auto-install."
return 0 return 0
fi fi
local pkgs=(xdg-utils desktop-file-utils xdg-desktop-portal xdg-desktop-portal-gtk) local pkgs=(xdg-utils desktop-file-utils xdg-desktop-portal xdg-desktop-portal-gtk)
log_info "Installing/ensuring packages: ${pkgs[*]}" log_info "Installing/ensuring packages: ${pkgs[*]}"
if ! require_cmd sudo; then if ! require_cmd sudo; then
log_warn "sudo not found; attempting pacman directly (may fail)." log_warn "sudo not found; attempting pacman directly (may fail)."
sudo_cmd="" sudo_cmd=""
else else
sudo_cmd="sudo" sudo_cmd="sudo"
fi fi
# Use --needed to avoid reinstalling # Use --needed to avoid reinstalling
set +e set +e
$sudo_cmd pacman -S --needed --noconfirm "${pkgs[@]}" $sudo_cmd pacman -S --needed --noconfirm "${pkgs[@]}"
local rc=$? local rc=$?
set -e set -e
if [[ $rc -ne 0 ]]; then if [[ $rc -ne 0 ]]; then
log_warn "Package install may have failed or been partial. Continuing anyway." log_warn "Package install may have failed or been partial. Continuing anyway."
else else
log_ok "Dependencies installed/verified." log_ok "Dependencies installed/verified."
fi fi
} }
desktop_dir="$HOME/.local/share/applications" desktop_dir="$HOME/.local/share/applications"
mkdir -p "$desktop_dir" mkdir -p "$desktop_dir"
detect_unityhub() { detect_unityhub() {
# Outputs: INSTALL_TYPE (FLATPAK|NATIVE|APPIMAGE|UNKNOWN) and EXEC_CMD # Outputs: INSTALL_TYPE (FLATPAK|NATIVE|APPIMAGE|UNKNOWN) and EXEC_CMD
local install_type="UNKNOWN" exec_cmd="" local install_type="UNKNOWN" exec_cmd=""
# 1) Flatpak # 1) Flatpak
if command -v flatpak >/dev/null 2>&1; then if command -v flatpak > /dev/null 2>&1; then
if flatpak info com.unity.UnityHub >/dev/null 2>&1; then if flatpak info com.unity.UnityHub > /dev/null 2>&1; then
install_type="FLATPAK" install_type="FLATPAK"
exec_cmd="flatpak run com.unity.UnityHub %U" exec_cmd="flatpak run com.unity.UnityHub %U"
echo "$install_type|$exec_cmd" echo "$install_type|$exec_cmd"
return 0 return 0
fi fi
fi fi
# 2) Native binary in PATH # 2) Native binary in PATH
if command -v unityhub >/dev/null 2>&1; then if command -v unityhub > /dev/null 2>&1; then
local path local path
path="$(command -v unityhub)" path="$(command -v unityhub)"
install_type="NATIVE" install_type="NATIVE"
exec_cmd="$path %U" exec_cmd="$path %U"
echo "$install_type|$exec_cmd" echo "$install_type|$exec_cmd"
return 0 return 0
fi fi
# 3) Search desktop files for Unity Hub Exec # 3) Search desktop files for Unity Hub Exec
local search_dirs=( local search_dirs=(
"$HOME/.local/share/applications" "$HOME/.local/share/applications"
"/usr/share/applications" "/usr/share/applications"
"/var/lib/flatpak/exports/share/applications" "/var/lib/flatpak/exports/share/applications"
"$HOME/.local/share/flatpak/exports/share/applications" "$HOME/.local/share/flatpak/exports/share/applications"
) )
local found_exec="" local found_exec=""
for d in "${search_dirs[@]}"; do for d in "${search_dirs[@]}"; do
[[ -d $d ]] || continue [[ -d $d ]] || continue
# prefer official naming when present # prefer official naming when present
local f local f
for f in "$d"/*.desktop; do for f in "$d"/*.desktop; do
[[ -e $f ]] || continue [[ -e $f ]] || continue
if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2>/dev/null || if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2> /dev/null ||
grep -qiE 'Exec=.*unityhub' "$f" 2>/dev/null; then grep -qiE 'Exec=.*unityhub' "$f" 2> /dev/null; then
local exec_line local exec_line
exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')" exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')"
if [[ -n $exec_line ]]; then if [[ -n $exec_line ]]; then
found_exec="$exec_line" found_exec="$exec_line"
break 2 break 2
fi fi
fi fi
done done
done done
if [[ -n $found_exec ]]; then if [[ -n $found_exec ]]; then
# Normalize: ensure %U present # Normalize: ensure %U present
if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then
found_exec+=" %U" found_exec+=" %U"
fi fi
if [[ $found_exec == flatpak* ]]; then if [[ $found_exec == flatpak* ]]; then
install_type="FLATPAK" install_type="FLATPAK"
elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then
install_type="APPIMAGE" install_type="APPIMAGE"
else else
install_type="NATIVE" install_type="NATIVE"
fi fi
echo "$install_type|$found_exec" echo "$install_type|$found_exec"
return 0 return 0
fi fi
# 4) Try common AppImage locations # 4) Try common AppImage locations
local ai_candidates=( local ai_candidates=(
"$HOME/Applications/UnityHub*.AppImage" "$HOME/Applications/UnityHub*.AppImage"
"$HOME/.local/bin/UnityHub*.AppImage" "$HOME/.local/bin/UnityHub*.AppImage"
"/opt/UnityHub*/UnityHub*.AppImage" "/opt/UnityHub*/UnityHub*.AppImage"
) )
local ai local ai
for ai in "${ai_candidates[@]}"; do for ai in "${ai_candidates[@]}"; do
for p in $ai; do for p in $ai; do
if [[ -f $p && -x $p ]]; then if [[ -f $p && -x $p ]]; then
install_type="APPIMAGE" install_type="APPIMAGE"
exec_cmd="$p %U" exec_cmd="$p %U"
echo "$install_type|$exec_cmd" echo "$install_type|$exec_cmd"
return 0 return 0
fi fi
done done
done done
echo "$install_type|$exec_cmd" echo "$install_type|$exec_cmd"
} }
create_handler_desktop() { create_handler_desktop() {
local exec_cmd="$1" local exec_cmd="$1"
local dest="$desktop_dir/unityhub-url-handler.desktop" local dest="$desktop_dir/unityhub-url-handler.desktop"
log_info "Writing handler desktop entry: $dest" log_info "Writing handler desktop entry: $dest"
cat >"$dest" <<DESK cat > "$dest" << DESK
[Desktop Entry] [Desktop Entry]
Name=Unity Hub URL Handler Name=Unity Hub URL Handler
Comment=Handle unityhub:// links for Unity Hub sign-in Comment=Handle unityhub:// links for Unity Hub sign-in
@ -211,82 +211,82 @@ StartupWMClass=Unity Hub
MimeType=x-scheme-handler/unityhub;x-scheme-handler/unity; MimeType=x-scheme-handler/unityhub;x-scheme-handler/unity;
NoDisplay=true NoDisplay=true
DESK DESK
log_ok "Desktop entry created/updated." log_ok "Desktop entry created/updated."
echo "$dest" echo "$dest"
} }
register_mime_handler() { register_mime_handler() {
local desktop_file="$1" local desktop_file="$1"
# Update desktop database if available # Update desktop database if available
if command -v update-desktop-database >/dev/null 2>&1; then if command -v update-desktop-database > /dev/null 2>&1; then
update-desktop-database "$desktop_dir" || true update-desktop-database "$desktop_dir" || true
else else
log_warn "update-desktop-database not found (install desktop-file-utils)." log_warn "update-desktop-database not found (install desktop-file-utils)."
fi fi
# Register as default handler for both schemes # Register as default handler for both schemes
if command -v xdg-mime >/dev/null 2>&1; then if command -v xdg-mime > /dev/null 2>&1; then
xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unityhub || true xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unityhub || true
xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true
else else
log_error "xdg-mime not found (install xdg-utils)." log_error "xdg-mime not found (install xdg-utils)."
return 1 return 1
fi fi
log_ok "MIME handler registered for unityhub:// (and unity://)." log_ok "MIME handler registered for unityhub:// (and unity://)."
} }
verify_registration() { verify_registration() {
local expected cur1 cur2 local expected cur1 cur2
expected="$(basename "$1")" expected="$(basename "$1")"
cur1="$(xdg-mime query default x-scheme-handler/unityhub 2>/dev/null || true)" cur1="$(xdg-mime query default x-scheme-handler/unityhub 2> /dev/null || true)"
cur2="$(xdg-mime query default x-scheme-handler/unity 2>/dev/null || true)" cur2="$(xdg-mime query default x-scheme-handler/unity 2> /dev/null || true)"
log_info "Current handler (unityhub): ${cur1:-<none>}" log_info "Current handler (unityhub): ${cur1:-<none>}"
log_info "Current handler (unity): ${cur2:-<none>}" log_info "Current handler (unity): ${cur2:-<none>}"
if [[ $cur1 == "$expected" ]]; then if [[ $cur1 == "$expected" ]]; then
log_ok "unityhub scheme correctly set to $expected" log_ok "unityhub scheme correctly set to $expected"
else else
log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})." log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})."
fi fi
} }
maybe_test_open() { maybe_test_open() {
if [[ $RUN_TEST == true ]]; then if [[ $RUN_TEST == true ]]; then
log_info "Opening test link: unityhub://v1/editor-signin" log_info "Opening test link: unityhub://v1/editor-signin"
if command -v xdg-open >/dev/null 2>&1; then if command -v xdg-open > /dev/null 2>&1; then
xdg-open 'unityhub://v1/editor-signin' >/dev/null 2>&1 || true xdg-open 'unityhub://v1/editor-signin' > /dev/null 2>&1 || true
log_ok "Test link invoked. Check if Unity Hub launches or focuses." log_ok "Test link invoked. Check if Unity Hub launches or focuses."
else else
log_warn "xdg-open not found; cannot run test automatically." log_warn "xdg-open not found; cannot run test automatically."
fi fi
else else
log_info "You can test manually with: xdg-open 'unityhub://v1/editor-signin'" log_info "You can test manually with: xdg-open 'unityhub://v1/editor-signin'"
fi fi
} }
main() { main() {
log_info "Ensuring required tools (optional)." log_info "Ensuring required tools (optional)."
ensure_deps_arch ensure_deps_arch
log_info "Detecting Unity Hub installation..." log_info "Detecting Unity Hub installation..."
IFS='|' read -r install_type exec_cmd < <(detect_unityhub) IFS='|' read -r install_type exec_cmd < <(detect_unityhub)
log_info "Detected type: $install_type" log_info "Detected type: $install_type"
if [[ -z ${exec_cmd:-} ]]; then if [[ -z ${exec_cmd:-} ]]; then
log_warn "Could not find Unity Hub executable automatically." 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 using Flatpak: install with 'flatpak install flathub com.unity.UnityHub'"
log_warn "- If native (AUR): ensure 'unityhub' is in PATH" log_warn "- If native (AUR): ensure 'unityhub' is in PATH"
log_warn "- If AppImage: place it in ~/Applications and make it executable" log_warn "- If AppImage: place it in ~/Applications and make it executable"
log_error "Aborting—no Exec command available to create handler." log_error "Aborting—no Exec command available to create handler."
exit 2 exit 2
fi fi
log_info "Using Exec: $exec_cmd" log_info "Using Exec: $exec_cmd"
local desktop_file local desktop_file
desktop_file="$(create_handler_desktop "$exec_cmd")" desktop_file="$(create_handler_desktop "$exec_cmd")"
register_mime_handler "$desktop_file" register_mime_handler "$desktop_file"
verify_registration "$desktop_file" verify_registration "$desktop_file"
cat <<'NOTE' cat << 'NOTE'
--- ---
Next steps: Next steps:
- Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub. - Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub.
@ -294,9 +294,9 @@ Next steps:
--- ---
NOTE 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 "$@" main "$@"

View File

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

View File

@ -13,113 +13,113 @@ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
source "$SCRIPT_DIR/../../lib/common.sh" source "$SCRIPT_DIR/../../lib/common.sh"
print_info() { print_info() {
echo "[info] $*" echo "[info] $*"
} }
detect_distro() { detect_distro() {
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
. /etc/os-release . /etc/os-release
echo "${ID:-unknown}" echo "${ID:-unknown}"
else else
echo "unknown" echo "unknown"
fi fi
} }
main() { main() {
local distro local distro
distro=$(detect_distro) distro=$(detect_distro)
print_info "Detected distro: $distro" print_info "Detected distro: $distro"
if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then
print_info "Your ffmpeg already supports arnndn." print_info "Your ffmpeg already supports arnndn."
else else
case "$distro" in case "$distro" in
ubuntu | debian) ubuntu | debian)
print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source." print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source."
echo "Options:" echo "Options:"
echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg" 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" echo " - source build (recommended for latest): run this script to build from source"
;; ;;
arch | manjaro | endeavouros) arch | manjaro | endeavouros)
print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg" print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg"
;; ;;
fedora) fedora)
print_info "On Fedora, try: sudo dnf install ffmpeg" print_info "On Fedora, try: sudo dnf install ffmpeg"
;; ;;
*) *)
print_info "Distro not recognized; will offer source build." print_info "Distro not recognized; will offer source build."
;; ;;
esac esac
fi fi
if ask_yes_no "Build FFmpeg from source with rnnoise/arnndn support now?"; then 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?" echo "This will clone FFmpeg and build locally under ./ffmpeg-build. Continue?"
if ! ask_yes_no "Proceed"; then if ! ask_yes_no "Proceed"; then
exit 0 exit 0
fi fi
set -x set -x
mkdir -p ffmpeg-build && cd ffmpeg-build mkdir -p ffmpeg-build && cd ffmpeg-build
# Prepare repository # Prepare repository
if [[ -d FFmpeg ]]; then if [[ -d FFmpeg ]]; then
if [[ -d FFmpeg/.git ]]; then if [[ -d FFmpeg/.git ]]; then
if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then
set +e set +e
git -C FFmpeg fetch --all --tags --prune git -C FFmpeg fetch --all --tags --prune
git -C FFmpeg pull --rebase --ff-only || true git -C FFmpeg pull --rebase --ff-only || true
set -e set -e
else else
if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then
rm -rf FFmpeg rm -rf FFmpeg
else else
echo "Keeping existing FFmpeg directory as-is." echo "Keeping existing FFmpeg directory as-is."
fi fi
fi fi
else else
if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then
rm -rf FFmpeg rm -rf FFmpeg
else else
echo "Cannot proceed with a non-git FFmpeg directory present. Aborting." echo "Cannot proceed with a non-git FFmpeg directory present. Aborting."
exit 4 exit 4
fi fi
fi fi
fi fi
# Dependencies # Dependencies
if [[ $distro == "ubuntu" || $distro == "debian" ]]; then if [[ $distro == "ubuntu" || $distro == "debian" ]]; then
sudo apt update 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 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 elif [[ $distro == "arch" || $distro == "manjaro" || $distro == "endeavouros" ]]; then
sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise
elif [[ $distro == "fedora" ]]; then 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 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 else
echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2 echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2
fi fi
if [[ ! -d FFmpeg/.git ]]; then if [[ ! -d FFmpeg/.git ]]; then
git clone https://github.com/FFmpeg/FFmpeg.git --depth=1 git clone https://github.com/FFmpeg/FFmpeg.git --depth=1
fi fi
cd FFmpeg cd FFmpeg
RN_FLAG="" RN_FLAG=""
# Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported # Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported
if ./configure --help | grep -q "librnnoise"; then if ./configure --help | grep -q "librnnoise"; then
RN_FLAG="--enable-librnnoise" RN_FLAG="--enable-librnnoise"
else else
echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2 echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2
fi fi
./configure \ ./configure \
--enable-gpl --enable-nonfree \ --enable-gpl --enable-nonfree \
--enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \ --enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \
--enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \ --enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \
--enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \ --enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \
--enable-libdav1d --enable-libaom --enable-libsvtav1 \ --enable-libdav1d --enable-libaom --enable-libsvtav1 \
${RN_FLAG} \ ${RN_FLAG} \
--enable-ffplay --enable-ffprobe --enable-ffplay --enable-ffprobe
make -j"$(nproc)" make -j"$(nproc)"
echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide." echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide."
set +x set +x
else else
echo "Skipped building from source." echo "Skipped building from source."
fi fi
} }
main "$@" main "$@"

View File

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

View File

@ -44,19 +44,19 @@ DEBUG=0
# Colors # Colors
if [[ -t 1 && ${NO_COLOR} -eq 0 ]]; then if [[ -t 1 && ${NO_COLOR} -eq 0 ]]; then
GREEN="\e[32m" GREEN="\e[32m"
YELLOW="\e[33m" YELLOW="\e[33m"
RED="\e[31m" RED="\e[31m"
BLUE="\e[34m" BLUE="\e[34m"
BOLD="\e[1m" BOLD="\e[1m"
RESET="\e[0m" RESET="\e[0m"
else else
GREEN="" GREEN=""
YELLOW="" YELLOW=""
RED="" RED=""
BLUE="" BLUE=""
BOLD="" BOLD=""
RESET="" RESET=""
fi fi
log() { echo -e "${BLUE}[INFO]${RESET} $*"; } log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
@ -65,7 +65,7 @@ err() { echo -e "${RED}[ERR ]${RESET} $*" >&2; }
success() { echo -e "${GREEN}[OK ]${RESET} $*"; } success() { echo -e "${GREEN}[OK ]${RESET} $*"; }
usage() { usage() {
cat <<EOF cat << EOF
${SCRIPT_NAME} v${VERSION} ${SCRIPT_NAME} v${VERSION}
Setup or uninstall a self-hosted LibreTranslate instance via Docker. Setup or uninstall a self-hosted LibreTranslate instance via Docker.
@ -111,378 +111,378 @@ EOF
} }
gen_api_key() { gen_api_key() {
# Avoid SIGPIPE issues under set -o pipefail by capturing output first # Avoid SIGPIPE issues under set -o pipefail by capturing output first
local key local key
key=$(head -c 256 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 40 || true) key=$(head -c 256 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 40 || true)
if [[ -z $key || ${#key} -lt 40 ]]; then if [[ -z $key || ${#key} -lt 40 ]]; then
# Fallback using openssl if available # Fallback using openssl if available
if command -v openssl >/dev/null 2>&1; then if command -v openssl > /dev/null 2>&1; then
key=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 40 || true) key=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 40 || true)
fi fi
fi fi
if [[ -z $key || ${#key} -lt 20 ]]; then if [[ -z $key || ${#key} -lt 20 ]]; then
# Last resort static warning key (should not happen) # Last resort static warning key (should not happen)
key="LT$(date +%s)$$RANDOM" key="LT$(date +%s)$$RANDOM"
fi fi
printf '%s' "$key" printf '%s' "$key"
} }
need_cmd() { need_cmd() {
command -v "$1" >/dev/null 2>&1 || { command -v "$1" > /dev/null 2>&1 || {
err "Required command '$1' not found" err "Required command '$1' not found"
return 1 return 1
} }
} }
parse_args() { parse_args() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--image) --image)
IMAGE="$2" IMAGE="$2"
shift 2 shift 2
;; ;;
--tag) --tag)
TAG="$2" TAG="$2"
shift 2 shift 2
;; ;;
--port) --port)
PORT="$2" PORT="$2"
shift 2 shift 2
;; ;;
--host) --host)
HOST="$2" HOST="$2"
shift 2 shift 2
;; ;;
--data-dir) --data-dir)
DATA_DIR="$2" DATA_DIR="$2"
CACHE_DIR="${DATA_DIR}/cache" CACHE_DIR="${DATA_DIR}/cache"
shift 2 shift 2
;; ;;
--cache-dir) --cache-dir)
CACHE_DIR="$2" CACHE_DIR="$2"
shift 2 shift 2
;; ;;
--no-docker-install) --no-docker-install)
DOCKER_INSTALL=0 DOCKER_INSTALL=0
shift shift
;; ;;
--keep-alive) --keep-alive)
KEEP_ALIVE=1 KEEP_ALIVE=1
shift shift
;; ;;
--) --)
shift shift
RUN_COMMAND=("$@") RUN_COMMAND=("$@")
break break
;; ;;
--api-key) --api-key)
API_KEY="$2" API_KEY="$2"
GENERATE_API_KEY=0 GENERATE_API_KEY=0
shift 2 shift 2
;; ;;
--generate-api-key) --generate-api-key)
GENERATE_API_KEY=1 GENERATE_API_KEY=1
shift shift
;; ;;
--disable-api-key) --disable-api-key)
DISABLE_API_KEY=1 DISABLE_API_KEY=1
shift shift
;; ;;
--preload-langs) --preload-langs)
PRELOAD_LANGS="$2" PRELOAD_LANGS="$2"
shift 2 shift 2
;; ;;
--env) --env)
EXTRA_ENV+=("$2") EXTRA_ENV+=("$2")
shift 2 shift 2
;; ;;
--pull-only) --pull-only)
PULL_ONLY=1 PULL_ONLY=1
shift shift
;; ;;
--uninstall) --uninstall)
UNINSTALL=1 UNINSTALL=1
shift shift
;; ;;
--purge) --purge)
UNINSTALL=1 UNINSTALL=1
KEEP_DATA=0 KEEP_DATA=0
shift shift
;; ;;
--keep-data) --keep-data)
KEEP_DATA=1 KEEP_DATA=1
shift shift
;; ;;
--health-timeout) --health-timeout)
HEALTH_TIMEOUT="$2" HEALTH_TIMEOUT="$2"
shift 2 shift 2
;; ;;
--no-color) --no-color)
NO_COLOR=1 NO_COLOR=1
shift shift
;; ;;
--debug) --debug)
DEBUG=1 DEBUG=1
shift shift
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
-v | --version) -v | --version)
echo "${VERSION}" echo "${VERSION}"
exit 0 exit 0
;; ;;
*) *)
err "Unknown argument: $1" err "Unknown argument: $1"
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
} }
ensure_root() { ensure_root() {
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
err "This script must run as root (or via sudo)." err "This script must run as root (or via sudo)."
exit 1 exit 1
fi fi
} }
install_docker() { install_docker() {
if command -v docker >/dev/null 2>&1; then if command -v docker > /dev/null 2>&1; then
log "Docker already installed" log "Docker already installed"
return 0 return 0
fi fi
if [[ ${DOCKER_INSTALL} -eq 0 ]]; then if [[ ${DOCKER_INSTALL} -eq 0 ]]; then
err "Docker is not installed and --no-docker-install specified." err "Docker is not installed and --no-docker-install specified."
exit 1 exit 1
fi fi
log "Installing Docker..." log "Installing Docker..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get > /dev/null 2>&1; then
apt-get update -y apt-get update -y
apt-get install -y ca-certificates curl gnupg apt-get install -y ca-certificates curl gnupg
install -d -m 0755 /etc/apt/keyrings install -d -m 0755 /etc/apt/keyrings
curl -fsSL "https://download.docker.com/linux/$( curl -fsSL "https://download.docker.com/linux/$(
. /etc/os-release . /etc/os-release
echo "$ID" echo "$ID"
)/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg )/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg
echo \ echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$( "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(
. /etc/os-release . /etc/os-release
echo "$ID" echo "$ID"
) $( ) $(
. /etc/os-release . /etc/os-release
echo "$VERSION_CODENAME" echo "$VERSION_CODENAME"
) stable" \ ) stable" \
>/etc/apt/sources.list.d/docker.list > /etc/apt/sources.list.d/docker.list
apt-get update -y apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
else else
err "Unsupported package manager. Please install Docker manually." err "Unsupported package manager. Please install Docker manually."
exit 1 exit 1
fi fi
# Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user # Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl > /dev/null 2>&1; then
(systemctl enable --now docker 2>/dev/null && success "Docker installed and started") || warn "Docker installed; ensure dockerd is running" (systemctl enable --now docker 2> /dev/null && success "Docker installed and started") || warn "Docker installed; ensure dockerd is running"
else else
warn "Docker installed; please ensure docker daemon is running" warn "Docker installed; please ensure docker daemon is running"
fi fi
} }
pull_image() { pull_image() {
log "Pulling image ${IMAGE}:${TAG}" log "Pulling image ${IMAGE}:${TAG}"
docker pull "${IMAGE}:${TAG}" docker pull "${IMAGE}:${TAG}"
success "Image pulled" success "Image pulled"
} }
detect_container_user() { detect_container_user() {
# Determine uid/gid of configured user inside image so host dirs can be chowned # Determine uid/gid of configured user inside image so host dirs can be chowned
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker > /dev/null 2>&1; then
return 0 return 0
fi fi
local uid gid local uid gid
uid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -u 2>/dev/null || echo "") uid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -u 2> /dev/null || echo "")
gid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -g 2>/dev/null || echo "") gid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -g 2> /dev/null || echo "")
if [[ -n $uid && -n $gid ]]; then if [[ -n $uid && -n $gid ]]; then
CONTAINER_UID=$uid CONTAINER_UID=$uid
CONTAINER_GID=$gid CONTAINER_GID=$gid
fi fi
} }
write_env_file() { write_env_file() {
mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${CACHE_DIR}" mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${CACHE_DIR}"
detect_container_user detect_container_user
if [[ -n ${CONTAINER_UID:-} && -n ${CONTAINER_GID:-} ]]; then if [[ -n ${CONTAINER_UID:-} && -n ${CONTAINER_GID:-} ]]; then
if command -v stat >/dev/null 2>&1; then if command -v stat > /dev/null 2>&1; then
for d in "${DATA_DIR}" "${CACHE_DIR}"; do for d in "${DATA_DIR}" "${CACHE_DIR}"; do
if [[ -d $d ]]; then if [[ -d $d ]]; then
CUR_UID=$(stat -c %u "$d" 2>/dev/null || echo -1) CUR_UID=$(stat -c %u "$d" 2> /dev/null || echo -1)
if [[ ${CUR_UID} -ne ${CONTAINER_UID} ]]; then if [[ ${CUR_UID} -ne ${CONTAINER_UID} ]]; then
chown "${CONTAINER_UID}":"${CONTAINER_GID}" "$d" 2>/dev/null || warn "Unable to chown $d to ${CONTAINER_UID}:${CONTAINER_GID}" chown "${CONTAINER_UID}":"${CONTAINER_GID}" "$d" 2> /dev/null || warn "Unable to chown $d to ${CONTAINER_UID}:${CONTAINER_GID}"
fi fi
fi fi
done done
fi fi
fi fi
if [[ ${DISABLE_API_KEY} -eq 1 ]]; then if [[ ${DISABLE_API_KEY} -eq 1 ]]; then
API_KEY_LINE="LT_NO_API_KEY=true" API_KEY_LINE="LT_NO_API_KEY=true"
else else
if [[ -z ${API_KEY} && ${GENERATE_API_KEY} -eq 1 ]]; then if [[ -z ${API_KEY} && ${GENERATE_API_KEY} -eq 1 ]]; then
API_KEY=$(gen_api_key) API_KEY=$(gen_api_key)
GENERATED=1 GENERATED=1
else else
GENERATED=0 GENERATED=0
fi fi
API_KEY_LINE="LT_API_KEYS=${API_KEY}" API_KEY_LINE="LT_API_KEYS=${API_KEY}"
fi fi
{ {
echo "# LibreTranslate environment file" echo "# LibreTranslate environment file"
echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "${API_KEY_LINE}" echo "${API_KEY_LINE}"
[[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}" [[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}"
for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done
} >"${ENV_FILE}.tmp" } > "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}" mv "${ENV_FILE}.tmp" "${ENV_FILE}"
chmod 600 "${ENV_FILE}" chmod 600 "${ENV_FILE}"
success "Environment file written: ${ENV_FILE}" success "Environment file written: ${ENV_FILE}"
} }
start_container_ephemeral() { start_container_ephemeral() {
docker rm -f "${SERVICE_NAME}" >/dev/null 2>&1 || true docker rm -f "${SERVICE_NAME}" > /dev/null 2>&1 || true
docker run -d --name "${SERVICE_NAME}" \ docker run -d --name "${SERVICE_NAME}" \
--env-file "${ENV_FILE}" \ --env-file "${ENV_FILE}" \
-v "${DATA_DIR}:/home/libretranslate/.local/share/argos-translate" \ -v "${DATA_DIR}:/home/libretranslate/.local/share/argos-translate" \
-v "${CACHE_DIR}:/app/cache" \ -v "${CACHE_DIR}:/app/cache" \
-p "${PORT}:${PORT}" \ -p "${PORT}:${PORT}" \
"${IMAGE}:${TAG}" \ "${IMAGE}:${TAG}" \
--host 0.0.0.0 --port "${PORT}" --host 0.0.0.0 --port "${PORT}"
success "Container started (ephemeral)" success "Container started (ephemeral)"
echo echo
echo "Endpoint (pending readiness): http://$(hostname -I | awk '{print $1}'):${PORT}" echo "Endpoint (pending readiness): http://$(hostname -I | awk '{print $1}'):${PORT}"
echo "Waiting for health..." echo "Waiting for health..."
} }
health_check() { health_check() {
local start local start
start=$(date +%s) start=$(date +%s)
local url="http://127.0.0.1:${PORT}/languages" local url="http://127.0.0.1:${PORT}/languages"
local attempt=0 local attempt=0
while true; do while true; do
attempt=$((attempt + 1)) attempt=$((attempt + 1))
if curl ${DEBUG:+-v} -fsS "$url" >/dev/null 2>&1; then if curl ${DEBUG:+-v} -fsS "$url" > /dev/null 2>&1; then
success "Service healthy (attempt $attempt)" success "Service healthy (attempt $attempt)"
return 0 return 0
else else
[[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed" [[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed"
fi fi
if (($(date +%s) - start > HEALTH_TIMEOUT)); then if (($(date +%s) - start > HEALTH_TIMEOUT)); then
err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)" err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)"
docker logs --tail 200 "${SERVICE_NAME}" || true docker logs --tail 200 "${SERVICE_NAME}" || true
return 1 return 1
fi fi
sleep 0.5 sleep 0.5
done done
} }
sample_request() { sample_request() {
if [[ ${DISABLE_API_KEY} -eq 0 ]]; then if [[ ${DISABLE_API_KEY} -eq 0 ]]; then
local key="${API_KEY}" local key="${API_KEY}"
else else
local key="" local key=""
fi fi
log "Performing sample translation (en->es)..." log "Performing sample translation (en->es)..."
local DATA='{"q":"Hello world","source":"en","target":"es","format":"text"}' local DATA='{"q":"Hello world","source":"en","target":"es","format":"text"}'
if [[ -n $key ]]; then if [[ -n $key ]]; then
curl -fsS -H "Content-Type: application/json" -H "Authorization: ${key}" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" curl -fsS -H "Content-Type: application/json" -H "Authorization: ${key}" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed"
else else
curl -fsS -H "Content-Type: application/json" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" curl -fsS -H "Content-Type: application/json" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed"
fi fi
echo echo
} }
uninstall_all() { uninstall_all() {
log "Uninstalling LibreTranslate (ephemeral mode)..." log "Uninstalling LibreTranslate (ephemeral mode)..."
docker rm -f "${SERVICE_NAME}" 2>/dev/null || true docker rm -f "${SERVICE_NAME}" 2> /dev/null || true
docker rmi "${IMAGE}:${TAG}" 2>/dev/null || true docker rmi "${IMAGE}:${TAG}" 2> /dev/null || true
if [[ ${KEEP_DATA} -eq 0 ]]; then if [[ ${KEEP_DATA} -eq 0 ]]; then
rm -rf "${DATA_DIR}" "${CONFIG_DIR}" || true rm -rf "${DATA_DIR}" "${CONFIG_DIR}" || true
success "Data directories removed" success "Data directories removed"
else else
log "Data kept in ${DATA_DIR} and ${CONFIG_DIR}" log "Data kept in ${DATA_DIR} and ${CONFIG_DIR}"
fi fi
success "Uninstall complete" success "Uninstall complete"
exit 0 exit 0
} }
main() { main() {
parse_args "$@" parse_args "$@"
ensure_root ensure_root
if [[ ${UNINSTALL} -eq 1 ]]; then if [[ ${UNINSTALL} -eq 1 ]]; then
uninstall_all uninstall_all
fi fi
install_docker install_docker
pull_image pull_image
if [[ ${PULL_ONLY} -eq 1 ]]; then if [[ ${PULL_ONLY} -eq 1 ]]; then
log "Pull-only requested, exiting." log "Pull-only requested, exiting."
exit 0 exit 0
fi fi
write_env_file write_env_file
# Always ephemeral now # Always ephemeral now
start_container_ephemeral start_container_ephemeral
health_check health_check
sample_request || true sample_request || true
# If a command is provided, run it and then shutdown container # If a command is provided, run it and then shutdown container
if [[ ${#RUN_COMMAND[@]} -gt 0 ]]; then if [[ ${#RUN_COMMAND[@]} -gt 0 ]]; then
log "Running user command: ${RUN_COMMAND[*]}" log "Running user command: ${RUN_COMMAND[*]}"
set +e set +e
"${RUN_COMMAND[@]}" "${RUN_COMMAND[@]}"
CMD_STATUS=$? CMD_STATUS=$?
set -e set -e
log "Command exited with status ${CMD_STATUS}; stopping container" log "Command exited with status ${CMD_STATUS}; stopping container"
docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true docker stop "${SERVICE_NAME}" > /dev/null 2>&1 || true
exit ${CMD_STATUS} exit ${CMD_STATUS}
fi fi
if [[ ${KEEP_ALIVE} -eq 1 ]]; then if [[ ${KEEP_ALIVE} -eq 1 ]]; then
log "Tailing logs (Ctrl-C to stop and remove container)" log "Tailing logs (Ctrl-C to stop and remove container)"
trap 'log "Stopping container"; docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true; exit 0' INT TERM trap 'log "Stopping container"; docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true; exit 0' INT TERM
docker logs -f "${SERVICE_NAME}" docker logs -f "${SERVICE_NAME}"
log "Logs ended; stopping container" log "Logs ended; stopping container"
docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true docker stop "${SERVICE_NAME}" > /dev/null 2>&1 || true
else else
log "Ephemeral container left running in background (id: $(docker inspect --format '{{.Id}}' ${SERVICE_NAME} 2>/dev/null || echo unknown))" log "Ephemeral container left running in background (id: $(docker inspect --format '{{.Id}}' ${SERVICE_NAME} 2> /dev/null || echo unknown))"
log "Stop manually with: docker stop ${SERVICE_NAME}" log "Stop manually with: docker stop ${SERVICE_NAME}"
fi fi
echo echo
echo "${BOLD}LibreTranslate is ready.${RESET}" echo "${BOLD}LibreTranslate is ready.${RESET}"
echo "Endpoint: http://$(hostname -I | awk '{print $1}'):${PORT}" echo "Endpoint: http://$(hostname -I | awk '{print $1}'):${PORT}"
if [[ ${DISABLE_API_KEY} -eq 0 ]]; then if [[ ${DISABLE_API_KEY} -eq 0 ]]; then
if [[ ${GENERATED:-0} -eq 1 ]]; then if [[ ${GENERATED:-0} -eq 1 ]]; then
echo "Generated API key: ${API_KEY}" echo "Generated API key: ${API_KEY}"
else else
echo "API key: ${API_KEY}" echo "API key: ${API_KEY}"
fi fi
echo "Use header: Authorization: <API_KEY>" echo "Use header: Authorization: <API_KEY>"
else else
echo "API key authentication DISABLED (public instance)." echo "API key authentication DISABLED (public instance)."
fi fi
if [[ -n ${PRELOAD_LANGS} ]]; then if [[ -n ${PRELOAD_LANGS} ]]; then
echo "Preloaded languages requested: ${PRELOAD_LANGS}" echo "Preloaded languages requested: ${PRELOAD_LANGS}"
fi fi
echo "Environment file: ${ENV_FILE}" echo "Environment file: ${ENV_FILE}"
echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}" echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}"
echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall" echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall"
echo echo
} }
main "$@" main "$@"

View File

@ -15,7 +15,7 @@ PY_HELPERS="$TOOLS_DIR/transcribe_helpers.py"
VENV_DIR="$PROJECT_DIR/.venv" VENV_DIR="$PROJECT_DIR/.venv"
usage() { usage() {
cat <<USAGE cat << USAGE
Usage: $(basename "$0") [--online] [--prepare-model NAME --model-dir DIR] [-m model] [-l lang] [-o outdir] [audio_file] Usage: $(basename "$0") [--online] [--prepare-model NAME --model-dir DIR] [-m model] [-l lang] [-o outdir] [audio_file]
Options: Options:
@ -31,443 +31,443 @@ USAGE
} }
log() { log() {
echo "[$(date +'%H:%M:%S')]" "$@" echo "[$(date +'%H:%M:%S')]" "$@"
} }
detect_pkg_mgr() { detect_pkg_mgr() {
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get > /dev/null 2>&1; then
echo apt echo apt
return return
fi fi
if command -v dnf >/dev/null 2>&1; then if command -v dnf > /dev/null 2>&1; then
echo dnf echo dnf
return return
fi fi
if command -v yum >/dev/null 2>&1; then if command -v yum > /dev/null 2>&1; then
echo yum echo yum
return return
fi fi
if command -v pacman >/dev/null 2>&1; then if command -v pacman > /dev/null 2>&1; then
echo pacman echo pacman
return return
fi fi
if command -v zypper >/dev/null 2>&1; then if command -v zypper > /dev/null 2>&1; then
echo zypper echo zypper
return return
fi fi
echo none echo none
} }
has_libcublas12() { has_libcublas12() {
# Common system locations # Common system locations
for d in \ for d in \
/usr/lib \ /usr/lib \
/usr/lib64 \ /usr/lib64 \
/usr/local/cuda/lib64 \ /usr/local/cuda/lib64 \
/usr/local/cuda-12*/lib64 \ /usr/local/cuda-12*/lib64 \
/opt/cuda/lib64 \ /opt/cuda/lib64 \
/opt/cuda/targets/x86_64-linux/lib; do /opt/cuda/targets/x86_64-linux/lib; do
if [[ -e "$d/libcublas.so.12" ]]; then if [[ -e "$d/libcublas.so.12" ]]; then
return 0 return 0
fi fi
done done
# venv-provided NVIDIA CUDA libs # venv-provided NVIDIA CUDA libs
if [[ -x "$VENV_DIR/bin/python" ]]; then if [[ -x "$VENV_DIR/bin/python" ]]; then
local pyver local pyver
pyver="$("$VENV_DIR"/bin/python "$PY_HELPERS" python-version 2>/dev/null || true)" pyver="$("$VENV_DIR"/bin/python "$PY_HELPERS" python-version 2> /dev/null || true)"
if [[ -n $pyver ]]; then if [[ -n $pyver ]]; then
for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \ for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \ "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do
if [[ -e "$d/libcublas.so.12" ]]; then if [[ -e "$d/libcublas.so.12" ]]; then
return 0 return 0
fi fi
done done
fi fi
fi fi
return 1 return 1
} }
ensure_cuda_runtime() { ensure_cuda_runtime() {
local mgr local mgr
mgr="$(detect_pkg_mgr)" mgr="$(detect_pkg_mgr)"
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
if has_libcublas12; then return 0; fi if has_libcublas12; then return 0; fi
echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2 echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2
exit 6 exit 6
fi fi
if has_libcublas12; then if has_libcublas12; then
return 0 return 0
fi fi
if ! command -v sudo >/dev/null 2>&1; then if ! command -v sudo > /dev/null 2>&1; then
log "sudo not found; skipping CUDA runtime install attempt." log "sudo not found; skipping CUDA runtime install attempt."
else else
log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)" log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)"
set +e set +e
case "$mgr" in case "$mgr" in
pacman) pacman)
sudo pacman -Sy --noconfirm cuda cudnn || true sudo pacman -Sy --noconfirm cuda cudnn || true
;; ;;
apt) apt)
sudo apt-get update -y || true sudo apt-get update -y || true
sudo apt-get install -y nvidia-cuda-toolkit || true sudo apt-get install -y nvidia-cuda-toolkit || true
;; ;;
dnf | yum) dnf | yum)
sudo "$mgr" install -y cuda cudnn || true sudo "$mgr" install -y cuda cudnn || true
;; ;;
zypper) zypper)
sudo zypper install -y cuda cudnn || true sudo zypper install -y cuda cudnn || true
;; ;;
*) log "Unknown package manager; cannot install CUDA automatically." ;; *) log "Unknown package manager; cannot install CUDA automatically." ;;
esac esac
set -e set -e
fi fi
# Re-check # Re-check
if ! has_libcublas12; then if ! has_libcublas12; then
echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2 echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2
exit 6 exit 6
fi fi
} }
install_system_deps() { install_system_deps() {
have_cmd() { command -v "$1" >/dev/null 2>&1; } have_cmd() { command -v "$1" > /dev/null 2>&1; }
local need_ffmpeg=0 need_espeak=0 local need_ffmpeg=0 need_espeak=0
have_cmd ffmpeg || need_ffmpeg=1 have_cmd ffmpeg || need_ffmpeg=1
have_cmd espeak-ng || need_espeak=1 have_cmd espeak-ng || need_espeak=1
# If diarization requested and online, we may also try to ensure libsndfile # If diarization requested and online, we may also try to ensure libsndfile
local need_libsndfile=0 local need_libsndfile=0
if [[ ${FW_DIARIZE:-} == "1" ]]; then if [[ ${FW_DIARIZE:-} == "1" ]]; then
# Heuristic: check common library file # Heuristic: check common library file
if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then
need_libsndfile=1 need_libsndfile=1
fi fi
fi fi
if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then
log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}" log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}"
return 0 return 0
fi fi
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2 echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2
exit 5 exit 5
fi fi
local mgr local mgr
mgr="$(detect_pkg_mgr)" mgr="$(detect_pkg_mgr)"
log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))" log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))"
if ! command -v sudo >/dev/null 2>&1; then if ! command -v sudo > /dev/null 2>&1; then
log "sudo not found; skipping system package installation attempt." log "sudo not found; skipping system package installation attempt."
return 0 return 0
fi fi
# Avoid exiting on install errors; continue best-effort # Avoid exiting on install errors; continue best-effort
set +e set +e
case "$mgr" in case "$mgr" in
apt) apt)
sudo apt-get update -y || log "apt-get update failed; continuing" sudo apt-get update -y || log "apt-get update failed; continuing"
pkgs=(python3-venv python3-pip) pkgs=(python3-venv python3-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
if [[ $need_libsndfile -eq 1 ]]; then if [[ $need_libsndfile -eq 1 ]]; then
# Try both names across releases # Try both names across releases
pkgs+=(libsndfile1) pkgs+=(libsndfile1)
sudo apt-get install -y libsndfile1 || true sudo apt-get install -y libsndfile1 || true
# If that failed, try libsndfile2 (newer distros) # If that failed, try libsndfile2 (newer distros)
sudo apt-get install -y libsndfile2 || true sudo apt-get install -y libsndfile2 || true
fi fi
sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing" sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing"
;; ;;
dnf) dnf)
pkgs=(python3-venv python3-pip) pkgs=(python3-venv python3-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing" sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing"
;; ;;
yum) yum)
pkgs=(python3-venv python3-pip) pkgs=(python3-venv python3-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing" sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing"
;; ;;
pacman) pacman)
pkgs=(python-virtualenv python-pip) pkgs=(python-virtualenv python-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing" sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing"
;; ;;
zypper) zypper)
pkgs=(python311-virtualenv python311-pip) pkgs=(python311-virtualenv python311-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1)
sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing" sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing"
;; ;;
*) *)
log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed." log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed."
;; ;;
esac esac
set -e set -e
} }
setup_venv() { setup_venv() {
if [[ ! -d $VENV_DIR ]]; then if [[ ! -d $VENV_DIR ]]; then
log "Creating venv at $VENV_DIR" log "Creating venv at $VENV_DIR"
python3 -m venv "$VENV_DIR" python3 -m venv "$VENV_DIR"
fi fi
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
if [[ $OFFLINE -eq 0 ]]; then if [[ $OFFLINE -eq 0 ]]; then
python -m pip install --upgrade pip wheel setuptools python -m pip install --upgrade pip wheel setuptools
fi fi
} }
install_python_deps() { install_python_deps() {
# Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12) # Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12)
local has_nvidia_flag="${1:-0}" local has_nvidia_flag="${1:-0}"
log "Installing faster-whisper and dependencies" log "Installing faster-whisper and dependencies"
export PIP_DISABLE_PIP_VERSION_CHECK=1 export PIP_DISABLE_PIP_VERSION_CHECK=1
export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20} export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20}
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
# Offline: do not install, just verify modules # Offline: do not install, just verify modules
if ! python "$PY_HELPERS" check-faster-whisper; then if ! python "$PY_HELPERS" check-faster-whisper; then
exit 7 exit 7
fi fi
# If diarization requested offline, check for its deps too (warn-only) # If diarization requested offline, check for its deps too (warn-only)
if [[ ${FW_DIARIZE:-} == "1" ]]; then if [[ ${FW_DIARIZE:-} == "1" ]]; then
python "$PY_HELPERS" check-diarization || true python "$PY_HELPERS" check-diarization || true
fi fi
return 0 return 0
fi fi
if [[ $has_nvidia_flag -eq 1 ]]; then if [[ $has_nvidia_flag -eq 1 ]]; then
# If ctranslate2 is not installed, attempt CUDA-enabled wheel (with fallback) # If ctranslate2 is not installed, attempt CUDA-enabled wheel (with fallback)
if ! "$VENV_DIR/bin/python" "$PY_HELPERS" check-ctranslate2 2>/dev/null; then if ! "$VENV_DIR/bin/python" "$PY_HELPERS" check-ctranslate2 2> /dev/null; then
log "Installing CUDA-enabled CTranslate2 (cu12 wheel)" log "Installing CUDA-enabled CTranslate2 (cu12 wheel)"
python -m pip install --progress-bar on --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 || python -m pip install --progress-bar on --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 ||
log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2" log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2"
fi fi
# Ensure NVIDIA CUDA 12 runtime libs are available inside the venv # Ensure NVIDIA CUDA 12 runtime libs are available inside the venv
python -m pip install --progress-bar on --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 || python -m pip install --progress-bar on --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 ||
log "Warning: failed to install NVIDIA cu12 runtime libs via pip" log "Warning: failed to install NVIDIA cu12 runtime libs via pip"
fi fi
python -m pip install --progress-bar on --retries 1 --upgrade faster-whisper ffmpeg-python python -m pip install --progress-bar on --retries 1 --upgrade faster-whisper ffmpeg-python
# If diarization requested and online, install its Python deps best-effort # If diarization requested and online, install its Python deps best-effort
if [[ ${FW_DIARIZE:-} == "1" ]]; then if [[ ${FW_DIARIZE:-} == "1" ]]; then
python -m pip install --progress-bar on --retries 1 --upgrade soundfile speechbrain || python -m pip install --progress-bar on --retries 1 --upgrade soundfile speechbrain ||
log "Warning: failed to install soundfile/speechbrain" log "Warning: failed to install soundfile/speechbrain"
# Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds) # Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds)
python -m pip install --progress-bar on --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio || python -m pip install --progress-bar on --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio ||
log "Warning: failed to install torch/torchaudio CPU wheels" log "Warning: failed to install torch/torchaudio CPU wheels"
fi fi
python "$PY_HELPERS" deps-installed python "$PY_HELPERS" deps-installed
} }
ensure_runner() { ensure_runner() {
if [[ ! -f $PY_RUNNER ]]; then if [[ ! -f $PY_RUNNER ]]; then
echo "Runner not found: $PY_RUNNER" >&2 echo "Runner not found: $PY_RUNNER" >&2
exit 3 exit 3
fi fi
} }
generate_test_audio() { generate_test_audio() {
local tmpwav local tmpwav
tmpwav="${PROJECT_DIR}/test_fw.wav" tmpwav="${PROJECT_DIR}/test_fw.wav"
if command -v espeak-ng >/dev/null 2>&1; then if command -v espeak-ng > /dev/null 2>&1; then
log "Generating test audio via espeak-ng -> $tmpwav" >&2 log "Generating test audio via espeak-ng -> $tmpwav" >&2
espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." >/dev/null 2>&1 || true espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true
fi fi
# If espeak-ng failed or not present, try espeak # If espeak-ng failed or not present, try espeak
if [[ ! -s $tmpwav ]] && command -v espeak >/dev/null 2>&1; then if [[ ! -s $tmpwav ]] && command -v espeak > /dev/null 2>&1; then
log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2 log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2
espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." >/dev/null 2>&1 || true espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true
fi fi
# Fallback: generate tone via Python stdlib (no external deps) # Fallback: generate tone via Python stdlib (no external deps)
if [[ ! -s $tmpwav ]]; then if [[ ! -s $tmpwav ]]; then
log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2 log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2
python3 "$PY_HELPERS" generate-wav --file "$tmpwav" || true python3 "$PY_HELPERS" generate-wav --file "$tmpwav" || true
fi fi
# Final fallback: tone via ffmpeg # Final fallback: tone via ffmpeg
if [[ ! -s $tmpwav ]]; then if [[ ! -s $tmpwav ]]; then
log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2 log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2
ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" >/dev/null 2>&1 || true ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" > /dev/null 2>&1 || true
fi fi
echo "$tmpwav" echo "$tmpwav"
} }
prepare_model() { prepare_model() {
# Download a model for offline use into MODEL_DIR # Download a model for offline use into MODEL_DIR
local name="$1" local name="$1"
mkdir -p "$MODEL_DIR" mkdir -p "$MODEL_DIR"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
log "Preparing model '$name' into $MODEL_DIR" log "Preparing model '$name' into $MODEL_DIR"
python "$PY_HELPERS" prepare-model --model "$name" --model-dir "$MODEL_DIR" python "$PY_HELPERS" prepare-model --model "$name" --model-dir "$MODEL_DIR"
} }
main() { main() {
# Defaults # Defaults
OFFLINE=1 OFFLINE=1
PREPARE_MODEL="" PREPARE_MODEL=""
MODEL_DIR="$PROJECT_DIR/models" MODEL_DIR="$PROJECT_DIR/models"
MODEL="large-v3" MODEL="large-v3"
LANGUAGE="" LANGUAGE=""
OUTDIR="" OUTDIR=""
INPUT_FILE="" INPUT_FILE=""
# Parse args # Parse args
PARSED=$(getopt -o m:l:o:h -l online,prepare-model:,model-dir: -- "$@") || { PARSED=$(getopt -o m:l:o:h -l online,prepare-model:,model-dir: -- "$@") || {
usage usage
exit 2 exit 2
} }
eval set -- "$PARSED" eval set -- "$PARSED"
while true; do while true; do
case "$1" in case "$1" in
-m) -m)
MODEL="$2" MODEL="$2"
shift 2 shift 2
;; ;;
-l) -l)
LANGUAGE="$2" LANGUAGE="$2"
shift 2 shift 2
;; ;;
-o) -o)
OUTDIR="$2" OUTDIR="$2"
shift 2 shift 2
;; ;;
-h) -h)
usage usage
exit 0 exit 0
;; ;;
--online) --online)
OFFLINE=0 OFFLINE=0
shift shift
;; ;;
--prepare-model) --prepare-model)
PREPARE_MODEL="$2" PREPARE_MODEL="$2"
OFFLINE=0 OFFLINE=0
shift 2 shift 2
;; ;;
--model-dir) --model-dir)
MODEL_DIR="$2" MODEL_DIR="$2"
shift 2 shift 2
;; ;;
--) --)
shift shift
break break
;; ;;
*) break ;; *) break ;;
esac esac
done done
INPUT_FILE="${1:-}" INPUT_FILE="${1:-}"
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
export HF_HUB_OFFLINE=1 export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1 export TRANSFORMERS_OFFLINE=1
fi fi
install_system_deps install_system_deps
setup_venv setup_venv
# If asked to prepare a model, do that and exit # If asked to prepare a model, do that and exit
if [[ -n $PREPARE_MODEL ]]; then if [[ -n $PREPARE_MODEL ]]; then
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
echo "--prepare-model requires network; rerun with --online." >&2 echo "--prepare-model requires network; rerun with --online." >&2
exit 2 exit 2
fi fi
install_python_deps 0 install_python_deps 0
prepare_model "$PREPARE_MODEL" prepare_model "$PREPARE_MODEL"
log "Model '$PREPARE_MODEL' downloaded to $MODEL_DIR" log "Model '$PREPARE_MODEL' downloaded to $MODEL_DIR"
exit 0 exit 0
fi fi
# Detect NVIDIA GPU and enforce CUDA if present # Detect NVIDIA GPU and enforce CUDA if present
has_nvidia=0 has_nvidia=0
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then if command -v nvidia-smi > /dev/null 2>&1 && nvidia-smi -L > /dev/null 2>&1; then
has_nvidia=1 has_nvidia=1
fi fi
install_python_deps "$has_nvidia" install_python_deps "$has_nvidia"
ensure_runner ensure_runner
local input="$INPUT_FILE" local input="$INPUT_FILE"
if [[ -z $input ]]; then if [[ -z $input ]]; then
input="$(generate_test_audio)" input="$(generate_test_audio)"
if [[ ! -s $input ]]; then if [[ ! -s $input ]]; then
echo "Failed to generate test audio. Please provide an audio file." >&2 echo "Failed to generate test audio. Please provide an audio file." >&2
exit 4 exit 4
fi fi
fi fi
if [[ ! -f $input ]]; then if [[ ! -f $input ]]; then
echo "Input file not found: $input" >&2 echo "Input file not found: $input" >&2
exit 2 exit 2
fi fi
local args=("$input" "--model" "$MODEL") local args=("$input" "--model" "$MODEL")
[[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE")
[[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR")
# Pass diarization via env if requested # Pass diarization via env if requested
if [[ ${FW_DIARIZE:-} == "1" ]]; then if [[ ${FW_DIARIZE:-} == "1" ]]; then
args+=("--diarize") args+=("--diarize")
if [[ -n ${FW_NUM_SPEAKERS:-} ]]; then if [[ -n ${FW_NUM_SPEAKERS:-} ]]; then
args+=("--num-speakers" "${FW_NUM_SPEAKERS}") args+=("--num-speakers" "${FW_NUM_SPEAKERS}")
fi fi
fi fi
if [[ $has_nvidia -eq 1 ]]; then if [[ $has_nvidia -eq 1 ]]; then
ensure_cuda_runtime ensure_cuda_runtime
# Export common CUDA paths in case the env lacks them # Export common CUDA paths in case the env lacks them
export CUDA_HOME="${CUDA_HOME:-/usr/local/cuda}" export CUDA_HOME="${CUDA_HOME:-/usr/local/cuda}"
# Include system and possible venv-provided CUDA libs # Include system and possible venv-provided CUDA libs
local pyver venv_cuda_paths="" local pyver venv_cuda_paths=""
if [[ -x "$VENV_DIR/bin/python" ]]; then if [[ -x "$VENV_DIR/bin/python" ]]; then
pyver="$("$VENV_DIR"/bin/python "$PY_HELPERS" python-version 2>/dev/null || true)" pyver="$("$VENV_DIR"/bin/python "$PY_HELPERS" python-version 2> /dev/null || true)"
if [[ -n $pyver ]]; then if [[ -n $pyver ]]; then
venv_cuda_paths="$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib" venv_cuda_paths="$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"
fi fi
fi fi
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${CUDA_HOME}/lib64:/usr/lib/x86_64-linux-gnu:/opt/cuda/lib64:/opt/cuda/targets/x86_64-linux/lib:${venv_cuda_paths}" export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${CUDA_HOME}/lib64:/usr/lib/x86_64-linux-gnu:/opt/cuda/lib64:/opt/cuda/targets/x86_64-linux/lib:${venv_cuda_paths}"
export PATH="${PATH}:${CUDA_HOME}/bin" export PATH="${PATH}:${CUDA_HOME}/bin"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
python "$PY_HELPERS" test-cuda || { python "$PY_HELPERS" test-cuda || {
echo "CUDA environment check failed. Aborting as requested." >&2 echo "CUDA environment check failed. Aborting as requested." >&2
exit 6 exit 6
} }
args+=("--device" "cuda") args+=("--device" "cuda")
fi fi
log "Transcribing: $input" log "Transcribing: $input"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
if [[ $has_nvidia -eq 1 ]]; then if [[ $has_nvidia -eq 1 ]]; then
if ! python "$PY_RUNNER" "${args[@]}"; then if ! python "$PY_RUNNER" "${args[@]}"; then
echo "CUDA execution requested due to detected NVIDIA GPU, but it failed. Aborting as requested (no CPU fallback)." >&2 echo "CUDA execution requested due to detected NVIDIA GPU, but it failed. Aborting as requested (no CPU fallback)." >&2
exit 6 exit 6
fi fi
else else
# Offline: prefer local directory if present; otherwise use cache without network # Offline: prefer local directory if present; otherwise use cache without network
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
local local_model_path="" local local_model_path=""
if [[ -d $MODEL ]]; then if [[ -d $MODEL ]]; then
local_model_path="$MODEL" local_model_path="$MODEL"
elif [[ -d "$MODEL_DIR/$MODEL" ]]; then elif [[ -d "$MODEL_DIR/$MODEL" ]]; then
local_model_path="$MODEL_DIR/$MODEL" local_model_path="$MODEL_DIR/$MODEL"
fi fi
if [[ -n $local_model_path ]]; then if [[ -n $local_model_path ]]; then
args=("$input" "--model" "$local_model_path") args=("$input" "--model" "$local_model_path")
[[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE")
[[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR")
fi fi
fi fi
python "$PY_RUNNER" "${args[@]}" python "$PY_RUNNER" "${args[@]}"
fi fi
} }
main "$@" main "$@"

View File

@ -45,231 +45,231 @@ TEMPLATE_LOGROTATE="$LOGROTATE_TEMPLATES/periodic-system-maintenance"
# Function to verify required files exist # Function to verify required files exist
verify_files() { verify_files() {
echo "" echo ""
echo "1. Verifying Required Files..." echo "1. Verifying Required Files..."
echo "==============================" echo "=============================="
local missing_files=() local missing_files=()
if [[ ! -f $PACMAN_WRAPPER_SCRIPT ]]; then if [[ ! -f $PACMAN_WRAPPER_SCRIPT ]]; then
missing_files+=("$PACMAN_WRAPPER_SCRIPT") missing_files+=("$PACMAN_WRAPPER_SCRIPT")
fi fi
if [[ ! -f $PACMAN_WRAPPER_INSTALL ]]; then if [[ ! -f $PACMAN_WRAPPER_INSTALL ]]; then
missing_files+=("$PACMAN_WRAPPER_INSTALL") missing_files+=("$PACMAN_WRAPPER_INSTALL")
fi fi
if [[ ! -f $HOSTS_INSTALL_SCRIPT ]]; then if [[ ! -f $HOSTS_INSTALL_SCRIPT ]]; then
missing_files+=("$HOSTS_INSTALL_SCRIPT") missing_files+=("$HOSTS_INSTALL_SCRIPT")
fi fi
# Check template files as well # Check template files as well
for tmpl in \ for tmpl in \
"$TEMPLATE_MAINT_SCRIPT" \ "$TEMPLATE_MAINT_SCRIPT" \
"$TEMPLATE_HOSTS_MONITOR" \ "$TEMPLATE_HOSTS_MONITOR" \
"$TEMPLATE_BROWSER_WRAPPER" \ "$TEMPLATE_BROWSER_WRAPPER" \
"$TEMPLATE_SVC_MAINT" \ "$TEMPLATE_SVC_MAINT" \
"$TEMPLATE_TIMER" \ "$TEMPLATE_TIMER" \
"$TEMPLATE_STARTUP" \ "$TEMPLATE_STARTUP" \
"$TEMPLATE_HOSTS_SVC" \ "$TEMPLATE_HOSTS_SVC" \
"$TEMPLATE_LOGROTATE"; do "$TEMPLATE_LOGROTATE"; do
if [[ ! -f $tmpl ]]; then if [[ ! -f $tmpl ]]; then
missing_files+=("$tmpl") missing_files+=("$tmpl")
fi fi
done done
if [[ ${#missing_files[@]} -gt 0 ]]; then if [[ ${#missing_files[@]} -gt 0 ]]; then
echo "Error: The following required files are missing:" echo "Error: The following required files are missing:"
for file in "${missing_files[@]}"; do for file in "${missing_files[@]}"; do
echo " - $file" echo " - $file"
done done
exit 1 exit 1
fi fi
echo "✓ All required files found" echo "✓ All required files found"
} }
# Function to create the combined execution script # Function to create the combined execution script
create_execution_script() { create_execution_script() {
echo "" echo ""
echo "2. Creating Combined Execution Script..." echo "2. Creating Combined Execution Script..."
echo "=======================================" echo "======================================="
local exec_script="/usr/local/bin/periodic-system-maintenance.sh" local exec_script="/usr/local/bin/periodic-system-maintenance.sh"
# Install from template with path substitutions # Install from template with path substitutions
sed \ sed \
-e "s|__PACMAN_WRAPPER_INSTALL__|$PACMAN_WRAPPER_INSTALL|g" \ -e "s|__PACMAN_WRAPPER_INSTALL__|$PACMAN_WRAPPER_INSTALL|g" \
-e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \ -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \
"$TEMPLATE_MAINT_SCRIPT" >"$exec_script" "$TEMPLATE_MAINT_SCRIPT" > "$exec_script"
chmod +x "$exec_script" chmod +x "$exec_script"
echo "✓ Installed execution script from template: $exec_script" echo "✓ Installed execution script from template: $exec_script"
} }
# Function to create systemd service # Function to create systemd service
create_systemd_service() { create_systemd_service() {
echo "" echo ""
echo "3. Creating Systemd Service..." echo "3. Creating Systemd Service..."
echo "=============================" echo "============================="
local service_file="/etc/systemd/system/periodic-system-maintenance.service" local service_file="/etc/systemd/system/periodic-system-maintenance.service"
install -m 0644 "$TEMPLATE_SVC_MAINT" "$service_file" install -m 0644 "$TEMPLATE_SVC_MAINT" "$service_file"
echo "✓ Installed systemd service from template: $service_file" echo "✓ Installed systemd service from template: $service_file"
} }
# Function to create systemd timer for hourly execution # Function to create systemd timer for hourly execution
create_systemd_timer() { create_systemd_timer() {
echo "" echo ""
echo "4. Creating Systemd Timer..." echo "4. Creating Systemd Timer..."
echo "============================" echo "============================"
local timer_file="/etc/systemd/system/periodic-system-maintenance.timer" local timer_file="/etc/systemd/system/periodic-system-maintenance.timer"
install -m 0644 "$TEMPLATE_TIMER" "$timer_file" install -m 0644 "$TEMPLATE_TIMER" "$timer_file"
echo "✓ Installed systemd timer from template: $timer_file" echo "✓ Installed systemd timer from template: $timer_file"
} }
# Function to create startup service (additional to timer) # Function to create startup service (additional to timer)
create_startup_service() { create_startup_service() {
echo "" echo ""
echo "5. Creating Startup Service..." echo "5. Creating Startup Service..."
echo "==============================" echo "=============================="
local startup_service="/etc/systemd/system/periodic-system-startup.service" local startup_service="/etc/systemd/system/periodic-system-startup.service"
install -m 0644 "$TEMPLATE_STARTUP" "$startup_service" install -m 0644 "$TEMPLATE_STARTUP" "$startup_service"
echo "✓ Installed startup service from template: $startup_service" echo "✓ Installed startup service from template: $startup_service"
} }
# Function to create hosts file monitor service # Function to create hosts file monitor service
create_hosts_monitor_service() { create_hosts_monitor_service() {
echo "" echo ""
echo "6. Creating Hosts File Monitor Service..." echo "6. Creating Hosts File Monitor Service..."
echo "========================================" echo "========================================"
local monitor_script="/usr/local/bin/hosts-file-monitor.sh" local monitor_script="/usr/local/bin/hosts-file-monitor.sh"
local monitor_service="/etc/systemd/system/hosts-file-monitor.service" local monitor_service="/etc/systemd/system/hosts-file-monitor.service"
# Install the monitor script from template with substitution # Install the monitor script from template with substitution
sed -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \ sed -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \
"$TEMPLATE_HOSTS_MONITOR" >"$monitor_script" "$TEMPLATE_HOSTS_MONITOR" > "$monitor_script"
chmod +x "$monitor_script" chmod +x "$monitor_script"
echo "✓ Installed hosts monitor script from template: $monitor_script" echo "✓ Installed hosts monitor script from template: $monitor_script"
# Install the systemd service from template # Install the systemd service from template
install -m 0644 "$TEMPLATE_HOSTS_SVC" "$monitor_service" install -m 0644 "$TEMPLATE_HOSTS_SVC" "$monitor_service"
echo "✓ Installed hosts monitor service from template: $monitor_service" echo "✓ Installed hosts monitor service from template: $monitor_service"
} }
# Function to install browser pre-exec wrapper and wire common browser names # Function to install browser pre-exec wrapper and wire common browser names
install_browser_preexec_wrapper() { install_browser_preexec_wrapper() {
echo "" echo ""
echo "6.1 Installing Browser Pre-Exec Wrapper..." echo "6.1 Installing Browser Pre-Exec Wrapper..."
echo "=========================================" echo "========================================="
local wrapper="/usr/local/bin/browser-preexec-wrapper" local wrapper="/usr/local/bin/browser-preexec-wrapper"
sed -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \ sed -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \
"$TEMPLATE_BROWSER_WRAPPER" >"$wrapper" "$TEMPLATE_BROWSER_WRAPPER" > "$wrapper"
chmod +x "$wrapper" chmod +x "$wrapper"
echo "✓ Installed wrapper: $wrapper" echo "✓ Installed wrapper: $wrapper"
# Allow passwordless execution of hosts installer for root-only actions # Allow passwordless execution of hosts installer for root-only actions
local sudoers_file="/etc/sudoers.d/hosts-install-no-passwd" local sudoers_file="/etc/sudoers.d/hosts-install-no-passwd"
if command -v visudo >/dev/null 2>&1; then if command -v visudo > /dev/null 2>&1; then
echo "${SUDO_USER:-$USER} ALL=(ALL) NOPASSWD: $HOSTS_INSTALL_SCRIPT" >"$sudoers_file" echo "${SUDO_USER:-$USER} ALL=(ALL) NOPASSWD: $HOSTS_INSTALL_SCRIPT" > "$sudoers_file"
chmod 440 "$sudoers_file" chmod 440 "$sudoers_file"
# Validate syntax # Validate syntax
visudo -c >/dev/null || echo "Warning: sudoers validation returned non-zero" visudo -c > /dev/null || echo "Warning: sudoers validation returned non-zero"
echo "✓ Sudoers drop-in created: $sudoers_file" echo "✓ Sudoers drop-in created: $sudoers_file"
else else
echo "visudo not found; skipping sudoers drop-in" echo "visudo not found; skipping sudoers drop-in"
fi fi
# Create symlinks for common browser commands to the wrapper in /usr/local/bin # Create symlinks for common browser commands to the wrapper in /usr/local/bin
# This takes precedence over /usr/bin in PATH on most systems. # This takes precedence over /usr/bin in PATH on most systems.
local browsers=("thorium-browser" "google-chrome" "google-chrome-stable" "chromium" "brave" "brave-browser" "vivaldi-stable" "firefox") local browsers=("thorium-browser" "google-chrome" "google-chrome-stable" "chromium" "brave" "brave-browser" "vivaldi-stable" "firefox")
for b in "${browsers[@]}"; do for b in "${browsers[@]}"; do
local link="/usr/local/bin/$b" local link="/usr/local/bin/$b"
ln -sf "$wrapper" "$link" ln -sf "$wrapper" "$link"
done done
echo "✓ Symlinked wrapper for common browsers in /usr/local/bin" echo "✓ Symlinked wrapper for common browsers in /usr/local/bin"
} }
# Function to enable and start services # Function to enable and start services
enable_services() { enable_services() {
echo "" echo ""
echo "7. Enabling Services and Timer..." echo "7. Enabling Services and Timer..."
echo "=================================" echo "================================="
# Reload systemd daemon # Reload systemd daemon
systemctl daemon-reload systemctl daemon-reload
echo "✓ Systemd daemon reloaded" echo "✓ Systemd daemon reloaded"
# Enable and start the timer # Enable and start the timer
systemctl enable periodic-system-maintenance.timer systemctl enable periodic-system-maintenance.timer
systemctl start periodic-system-maintenance.timer systemctl start periodic-system-maintenance.timer
echo "✓ Timer enabled and started" echo "✓ Timer enabled and started"
# Enable startup service (but don't start it now) # Enable startup service (but don't start it now)
systemctl enable periodic-system-startup.service systemctl enable periodic-system-startup.service
echo "✓ Startup service enabled" echo "✓ Startup service enabled"
# Enable hosts file monitor service # Enable hosts file monitor service
systemctl enable hosts-file-monitor.service systemctl enable hosts-file-monitor.service
systemctl start hosts-file-monitor.service systemctl start hosts-file-monitor.service
echo "✓ Hosts file monitor service enabled and started" echo "✓ Hosts file monitor service enabled and started"
# Show timer status # Show timer status
echo "" echo ""
echo "Timer Status:" echo "Timer Status:"
systemctl status periodic-system-maintenance.timer --no-pager -l systemctl status periodic-system-maintenance.timer --no-pager -l
echo "" echo ""
echo "Hosts Monitor Status:" echo "Hosts Monitor Status:"
systemctl status hosts-file-monitor.service --no-pager -l systemctl status hosts-file-monitor.service --no-pager -l
echo "" echo ""
echo "Next scheduled runs:" echo "Next scheduled runs:"
systemctl list-timers periodic-system-maintenance.timer --no-pager systemctl list-timers periodic-system-maintenance.timer --no-pager
} }
# Function to create log rotation configuration # Function to create log rotation configuration
create_log_rotation() { create_log_rotation() {
echo "" echo ""
echo "8. Setting up Log Rotation..." echo "8. Setting up Log Rotation..."
echo "=============================" echo "============================="
local logrotate_conf="/etc/logrotate.d/periodic-system-maintenance" local logrotate_conf="/etc/logrotate.d/periodic-system-maintenance"
install -m 0644 "$TEMPLATE_LOGROTATE" "$logrotate_conf" install -m 0644 "$TEMPLATE_LOGROTATE" "$logrotate_conf"
echo "✓ Installed log rotation configuration from template: $logrotate_conf" echo "✓ Installed log rotation configuration from template: $logrotate_conf"
} }
# Function to run initial execution # Function to run initial execution
run_initial_execution() { run_initial_execution() {
echo "" echo ""
echo "9. Running Initial Execution..." echo "9. Running Initial Execution..."
echo "===============================" echo "==============================="
local run_initial=true local run_initial=true
if [[ $INTERACTIVE_MODE == "true" ]]; then if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Would you like to run the system maintenance now to test the setup?" echo "Would you like to run the system maintenance now to test the setup?"
read -p "Run initial execution? (y/N): " -n 1 -r read -p "Run initial execution? (y/N): " -n 1 -r
echo echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then if [[ ! $REPLY =~ ^[Yy]$ ]]; then
run_initial=false run_initial=false
fi fi
else else
echo "Auto-running initial execution to test the setup (use --interactive to prompt)" echo "Auto-running initial execution to test the setup (use --interactive to prompt)"
fi fi
if [[ $run_initial == "true" ]]; then if [[ $run_initial == "true" ]]; then
echo "Running initial system maintenance..." echo "Running initial system maintenance..."
/usr/local/bin/periodic-system-maintenance.sh /usr/local/bin/periodic-system-maintenance.sh
echo "✓ Initial execution completed" echo "✓ Initial execution completed"
else else
echo "Skipping initial execution" echo "Skipping initial execution"
fi fi
} }
# Main execution # Main execution

View File

@ -24,72 +24,72 @@ echo "User home: $USER_HOME"
# Function to check if Thorium browser is installed # Function to check if Thorium browser is installed
check_thorium_browser() { check_thorium_browser() {
echo "" echo ""
echo "1. Checking Thorium Browser Installation..." echo "1. Checking Thorium Browser Installation..."
echo "==========================================" echo "=========================================="
if ! command -v "$BROWSER_COMMAND" &>/dev/null; then if ! command -v "$BROWSER_COMMAND" &> /dev/null; then
echo "Warning: Thorium browser not found in PATH" echo "Warning: Thorium browser not found in PATH"
echo "Checking alternative locations..." echo "Checking alternative locations..."
# Check common installation paths # Check common installation paths
local alt_paths=( local alt_paths=(
"/opt/thorium/thorium" "/opt/thorium/thorium"
"/usr/bin/thorium" "/usr/bin/thorium"
"/usr/local/bin/thorium" "/usr/local/bin/thorium"
"/opt/thorium-browser/thorium-browser" "/opt/thorium-browser/thorium-browser"
"${USER_HOME}/.local/bin/thorium-browser" "${USER_HOME}/.local/bin/thorium-browser"
) )
local found=false local found=false
for path in "${alt_paths[@]}"; do for path in "${alt_paths[@]}"; do
if [[ -x $path ]]; then if [[ -x $path ]]; then
BROWSER_COMMAND="$path" BROWSER_COMMAND="$path"
echo "✓ Found Thorium browser at: $path" echo "✓ Found Thorium browser at: $path"
found=true found=true
break break
fi fi
done done
if [[ $found != true ]]; then if [[ $found != true ]]; then
echo "Error: Thorium browser not found!" echo "Error: Thorium browser not found!"
echo "Please install Thorium browser first or ensure it's in your PATH." echo "Please install Thorium browser first or ensure it's in your PATH."
echo "" echo ""
echo "You can install Thorium browser from:" echo "You can install Thorium browser from:"
echo "https://thorium.rocks/" echo "https://thorium.rocks/"
echo "" echo ""
local continue_anyway=false local continue_anyway=false
if [[ $INTERACTIVE_MODE == "true" ]]; then if [[ $INTERACTIVE_MODE == "true" ]]; then
read -p "Continue anyway? The service will be created but may fail to start (y/N): " -n 1 -r read -p "Continue anyway? The service will be created but may fail to start (y/N): " -n 1 -r
echo echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
continue_anyway=true continue_anyway=true
fi fi
else else
echo "Auto-continuing anyway - service will be created but may fail to start (use --interactive to prompt)" echo "Auto-continuing anyway - service will be created but may fail to start (use --interactive to prompt)"
continue_anyway=true continue_anyway=true
fi fi
if [[ $continue_anyway != true ]]; then if [[ $continue_anyway != true ]]; then
exit 1 exit 1
fi fi
fi fi
else else
echo "✓ Thorium browser found: $(which $BROWSER_COMMAND)" echo "✓ Thorium browser found: $(which $BROWSER_COMMAND)"
fi fi
} }
# Function to create the browser launcher script # Function to create the browser launcher script
create_launcher_script() { create_launcher_script() {
echo "" echo ""
echo "2. Creating Browser Launcher Script..." echo "2. Creating Browser Launcher Script..."
echo "=====================================" echo "====================================="
local launcher_script="/usr/local/bin/thorium-fitatu-launcher.sh" local launcher_script="/usr/local/bin/thorium-fitatu-launcher.sh"
cat >"$launcher_script" <<EOF cat > "$launcher_script" << EOF
#!/bin/bash #!/bin/bash
# Thorium browser launcher for Fitatu website # Thorium browser launcher for Fitatu website
# Created by setup_thorium_startup.sh on $(date) # Created by setup_thorium_startup.sh on $(date)
@ -163,24 +163,24 @@ else
fi fi
EOF EOF
chmod +x "$launcher_script" chmod +x "$launcher_script"
echo "✓ Created launcher script: $launcher_script" echo "✓ Created launcher script: $launcher_script"
} }
# Function to create systemd service for user session # Function to create systemd service for user session
create_user_systemd_service() { create_user_systemd_service() {
echo "" echo ""
echo "3. Creating User Systemd Service..." echo "3. Creating User Systemd Service..."
echo "==================================" echo "=================================="
local user_systemd_dir="$USER_HOME/.config/systemd/user" local user_systemd_dir="$USER_HOME/.config/systemd/user"
local service_file="$user_systemd_dir/thorium-fitatu-startup.service" local service_file="$user_systemd_dir/thorium-fitatu-startup.service"
# Create user systemd directory # Create user systemd directory
sudo -u "${SUDO_USER}" mkdir -p "$user_systemd_dir" sudo -u "${SUDO_USER}" mkdir -p "$user_systemd_dir"
# Create the service file # Create the service file
sudo -u "${SUDO_USER}" tee "$service_file" >/dev/null <<EOF sudo -u "${SUDO_USER}" tee "$service_file" > /dev/null << EOF
[Unit] [Unit]
Description=Launch Thorium Browser with Fitatu on Startup Description=Launch Thorium Browser with Fitatu on Startup
After=graphical-session.target After=graphical-session.target
@ -205,18 +205,18 @@ TimeoutStartSec=120
WantedBy=default.target WantedBy=default.target
EOF EOF
echo "✓ Created user systemd service: $service_file" echo "✓ Created user systemd service: $service_file"
} }
# Function to create system-wide systemd service (alternative approach) # Function to create system-wide systemd service (alternative approach)
create_system_systemd_service() { create_system_systemd_service() {
echo "" echo ""
echo "4. Creating System Systemd Service..." echo "4. Creating System Systemd Service..."
echo "====================================" echo "===================================="
local service_file="/etc/systemd/system/thorium-fitatu-startup.service" local service_file="/etc/systemd/system/thorium-fitatu-startup.service"
cat >"$service_file" <<EOF cat > "$service_file" << EOF
[Unit] [Unit]
Description=Launch Thorium Browser with Fitatu on Startup Description=Launch Thorium Browser with Fitatu on Startup
After=multi-user.target network-online.target After=multi-user.target network-online.target
@ -243,23 +243,23 @@ TimeoutStartSec=180
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
echo "✓ Created system systemd service: $service_file" echo "✓ Created system systemd service: $service_file"
} }
# Function to create autostart desktop entry (additional method) # Function to create autostart desktop entry (additional method)
create_autostart_entry() { create_autostart_entry() {
echo "" echo ""
echo "5. Creating Autostart Desktop Entry..." echo "5. Creating Autostart Desktop Entry..."
echo "=====================================" echo "====================================="
local autostart_dir="$USER_HOME/.config/autostart" local autostart_dir="$USER_HOME/.config/autostart"
local desktop_file="$autostart_dir/thorium-fitatu.desktop" local desktop_file="$autostart_dir/thorium-fitatu.desktop"
# Create autostart directory # Create autostart directory
sudo -u "${SUDO_USER}" mkdir -p "$autostart_dir" sudo -u "${SUDO_USER}" mkdir -p "$autostart_dir"
# Create desktop entry # Create desktop entry
sudo -u "${SUDO_USER}" tee "$desktop_file" >/dev/null <<EOF sudo -u "${SUDO_USER}" tee "$desktop_file" > /dev/null << EOF
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Thorium Fitatu Startup Name=Thorium Fitatu Startup
@ -274,45 +274,45 @@ Terminal=false
Categories=Network;WebBrowser; Categories=Network;WebBrowser;
EOF EOF
echo "✓ Created autostart desktop entry: $desktop_file" echo "✓ Created autostart desktop entry: $desktop_file"
} }
# Function to create i3 config autostart entry # Function to create i3 config autostart entry
create_i3_autostart() { create_i3_autostart() {
echo "" echo ""
echo "6. Creating i3 Config Autostart Entry..." echo "6. Creating i3 Config Autostart Entry..."
echo "=======================================" echo "======================================="
local i3_config="$USER_HOME/.config/i3/config" local i3_config="$USER_HOME/.config/i3/config"
local i3_config_dir="$USER_HOME/.config/i3" local i3_config_dir="$USER_HOME/.config/i3"
# Create i3 config directory if it doesn't exist # Create i3 config directory if it doesn't exist
sudo -u "${SUDO_USER}" mkdir -p "$i3_config_dir" sudo -u "${SUDO_USER}" mkdir -p "$i3_config_dir"
# Check if i3 config exists # Check if i3 config exists
if [[ -f $i3_config ]]; then if [[ -f $i3_config ]]; then
# Check if autostart entry already exists # Check if autostart entry already exists
if ! sudo -u "${SUDO_USER}" grep -q "thorium-fitatu-launcher" "$i3_config"; then if ! sudo -u "${SUDO_USER}" grep -q "thorium-fitatu-launcher" "$i3_config"; then
# Add autostart entry to i3 config # Add autostart entry to i3 config
sudo -u "${SUDO_USER}" bash -c "echo '' >> '$i3_config'" sudo -u "${SUDO_USER}" bash -c "echo '' >> '$i3_config'"
sudo -u "${SUDO_USER}" bash -c "echo '# Auto-start Thorium browser with Fitatu' >> '$i3_config'" sudo -u "${SUDO_USER}" bash -c "echo '# Auto-start Thorium browser with Fitatu' >> '$i3_config'"
sudo -u "${SUDO_USER}" bash -c "echo 'exec --no-startup-id /usr/local/bin/thorium-fitatu-launcher.sh' >> '$i3_config'" sudo -u "${SUDO_USER}" bash -c "echo 'exec --no-startup-id /usr/local/bin/thorium-fitatu-launcher.sh' >> '$i3_config'"
echo "✓ Added autostart entry to i3 config: $i3_config" echo "✓ Added autostart entry to i3 config: $i3_config"
else else
echo "✓ Autostart entry already exists in i3 config" echo "✓ Autostart entry already exists in i3 config"
fi fi
else else
echo "Warning: i3 config file not found at $i3_config" echo "Warning: i3 config file not found at $i3_config"
echo "You may need to manually add the following line to your i3 config:" echo "You may need to manually add the following line to your i3 config:"
echo "exec --no-startup-id /usr/local/bin/thorium-fitatu-launcher.sh" echo "exec --no-startup-id /usr/local/bin/thorium-fitatu-launcher.sh"
fi fi
} }
# Function to create a script to enable user service after login # Function to create a script to enable user service after login
create_user_enable_script() { create_user_enable_script() {
local enable_script="$USER_HOME/.config/thorium-enable-service.sh" local enable_script="$USER_HOME/.config/thorium-enable-service.sh"
sudo -u "${SUDO_USER}" tee "$enable_script" >/dev/null <<'EOF' sudo -u "${SUDO_USER}" tee "$enable_script" > /dev/null << 'EOF'
#!/bin/bash #!/bin/bash
# Script to enable thorium-fitatu-startup user service # Script to enable thorium-fitatu-startup user service
# This runs once to enable the service, then removes itself # This runs once to enable the service, then removes itself
@ -325,110 +325,110 @@ systemctl --user enable thorium-fitatu-startup.service
rm "$0" rm "$0"
EOF EOF
sudo -u "${SUDO_USER}" chmod +x "$enable_script" sudo -u "${SUDO_USER}" chmod +x "$enable_script"
# Add to user's .bashrc to run on next login # Add to user's .bashrc to run on next login
local bashrc="$USER_HOME/.bashrc" local bashrc="$USER_HOME/.bashrc"
if [[ -f $bashrc ]]; then if [[ -f $bashrc ]]; then
sudo -u "${SUDO_USER}" bash -c "echo '' >> '$bashrc'" sudo -u "${SUDO_USER}" bash -c "echo '' >> '$bashrc'"
sudo -u "${SUDO_USER}" bash -c "echo '# Auto-enable thorium service (temporary)' >> '$bashrc'" sudo -u "${SUDO_USER}" bash -c "echo '# Auto-enable thorium service (temporary)' >> '$bashrc'"
sudo -u "${SUDO_USER}" bash -c "echo '[[ -x ~/.config/thorium-enable-service.sh ]] && ~/.config/thorium-enable-service.sh' >> '$bashrc'" sudo -u "${SUDO_USER}" bash -c "echo '[[ -x ~/.config/thorium-enable-service.sh ]] && ~/.config/thorium-enable-service.sh' >> '$bashrc'"
fi fi
} }
# Function to enable services # Function to enable services
enable_services() { enable_services() {
echo "" echo ""
echo "7. Enabling Services..." echo "7. Enabling Services..."
echo "======================" echo "======================"
# Reload systemd daemon # Reload systemd daemon
systemctl daemon-reload systemctl daemon-reload
echo "✓ System daemon reloaded" echo "✓ System daemon reloaded"
# Enable system service # Enable system service
systemctl enable thorium-fitatu-startup.service systemctl enable thorium-fitatu-startup.service
echo "✓ System service enabled" echo "✓ System service enabled"
# Enable lingering for the user (allows user services to run without login) # Enable lingering for the user (allows user services to run without login)
loginctl enable-linger "${SUDO_USER}" loginctl enable-linger "${SUDO_USER}"
echo "✓ User lingering enabled" echo "✓ User lingering enabled"
# Create a script to enable user service after login # Create a script to enable user service after login
create_user_enable_script create_user_enable_script
echo "✓ User service will be enabled on next login" echo "✓ User service will be enabled on next login"
} }
# Function to test the setup # Function to test the setup
test_setup() { test_setup() {
echo "" echo ""
echo "8. Testing Setup..." echo "8. Testing Setup..."
echo "==================" echo "=================="
local run_test=true local run_test=true
if [[ $INTERACTIVE_MODE == "true" ]]; then if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Would you like to test the browser launcher now?" echo "Would you like to test the browser launcher now?"
read -p "Test launch Thorium browser with Fitatu? (y/N): " -n 1 -r read -p "Test launch Thorium browser with Fitatu? (y/N): " -n 1 -r
echo echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then if [[ ! $REPLY =~ ^[Yy]$ ]]; then
run_test=false run_test=false
fi fi
else else
echo "Auto-testing the browser launcher (use --interactive to prompt)" echo "Auto-testing the browser launcher (use --interactive to prompt)"
fi fi
if [[ $run_test == "true" ]]; then if [[ $run_test == "true" ]]; then
echo "Testing browser launch..." echo "Testing browser launch..."
echo "Note: This will open Thorium browser with Fitatu website" echo "Note: This will open Thorium browser with Fitatu website"
# Test the launcher immediately # Test the launcher immediately
if /usr/local/bin/thorium-fitatu-launcher.sh; then if /usr/local/bin/thorium-fitatu-launcher.sh; then
echo "✓ Test launch completed successfully" echo "✓ Test launch completed successfully"
else else
echo "✗ Test launch failed" echo "✗ Test launch failed"
echo "Check that Thorium browser is properly installed and accessible" echo "Check that Thorium browser is properly installed and accessible"
fi fi
else else
echo "Skipping test launch" echo "Skipping test launch"
fi fi
} }
# Function to show usage instructions # Function to show usage instructions
show_instructions() { show_instructions() {
echo "" echo ""
echo "==========================================" echo "=========================================="
echo "Thorium Browser Auto-Startup Setup Complete" echo "Thorium Browser Auto-Startup Setup Complete"
echo "==========================================" echo "=========================================="
echo "Summary:" echo "Summary:"
echo "✓ Launcher script created: /usr/local/bin/thorium-fitatu-launcher.sh" echo "✓ Launcher script created: /usr/local/bin/thorium-fitatu-launcher.sh"
echo "✓ System service created: thorium-fitatu-startup.service" echo "✓ System service created: thorium-fitatu-startup.service"
echo "✓ User service created: ~/.config/systemd/user/thorium-fitatu-startup.service" echo "✓ User service created: ~/.config/systemd/user/thorium-fitatu-startup.service"
echo "✓ Autostart entry created: ~/.config/autostart/thorium-fitatu.desktop" echo "✓ Autostart entry created: ~/.config/autostart/thorium-fitatu.desktop"
echo "✓ i3 autostart entry added to: ~/.config/i3/config" echo "✓ i3 autostart entry added to: ~/.config/i3/config"
echo "✓ Services enabled for automatic startup" echo "✓ Services enabled for automatic startup"
echo "" echo ""
echo "The system will now:" echo "The system will now:"
echo "• Launch Thorium browser with $TARGET_URL on every startup" echo "• Launch Thorium browser with $TARGET_URL on every startup"
echo "• Use multiple methods to ensure reliable startup" echo "• Use multiple methods to ensure reliable startup"
echo "• Wait for desktop environment to be ready before launching" echo "• Wait for desktop environment to be ready before launching"
echo "• User service will be enabled automatically on next login" echo "• User service will be enabled automatically on next login"
echo "" echo ""
echo "To check status:" echo "To check status:"
echo " systemctl status thorium-fitatu-startup.service" echo " systemctl status thorium-fitatu-startup.service"
echo " systemctl --user status thorium-fitatu-startup.service (after login)" echo " systemctl --user status thorium-fitatu-startup.service (after login)"
echo "" echo ""
echo "To view logs:" echo "To view logs:"
echo " journalctl -u thorium-fitatu-startup.service" echo " journalctl -u thorium-fitatu-startup.service"
echo " journalctl --user -u thorium-fitatu-startup.service" echo " journalctl --user -u thorium-fitatu-startup.service"
echo "" echo ""
echo "To disable (if needed):" echo "To disable (if needed):"
echo " sudo systemctl disable thorium-fitatu-startup.service" echo " sudo systemctl disable thorium-fitatu-startup.service"
echo " systemctl --user disable thorium-fitatu-startup.service" echo " systemctl --user disable thorium-fitatu-startup.service"
echo " rm ~/.config/autostart/thorium-fitatu.desktop" echo " rm ~/.config/autostart/thorium-fitatu.desktop"
echo "" echo ""
echo "IMPORTANT: Browser will launch automatically on next reboot!" echo "IMPORTANT: Browser will launch automatically on next reboot!"
} }
# Main execution # Main execution

View File

@ -11,100 +11,100 @@ HOSTS_INSTALL_SCRIPT="__HOSTS_INSTALL_SCRIPT__"
# Log with timestamp (hosts-file-monitor specific) # Log with timestamp (hosts-file-monitor specific)
log_message() { log_message() {
printf '%s [hosts-monitor] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "$LOG_FILE" >&2 printf '%s [hosts-monitor] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "$LOG_FILE" >&2
} }
# Function to check if hosts file needs restoration # Function to check if hosts file needs restoration
needs_restoration() { needs_restoration() {
# Check if file exists # Check if file exists
if [[ ! -f $HOSTS_FILE ]]; then if [[ ! -f $HOSTS_FILE ]]; then
return 0 # File missing, needs restoration return 0 # File missing, needs restoration
fi fi
# Check if file is empty or too small (less than 1000 lines indicates tampering) # Check if file is empty or too small (less than 1000 lines indicates tampering)
local line_count local line_count
line_count=$(wc -l <"$HOSTS_FILE" 2>/dev/null || echo "0") line_count=$(wc -l < "$HOSTS_FILE" 2> /dev/null || echo "0")
if [[ $line_count -lt 1000 ]]; then if [[ $line_count -lt 1000 ]]; then
return 0 # File too small, likely tampered with return 0 # File too small, likely tampered with
fi fi
# Check if our custom entries are missing # Check if our custom entries are missing
if ! grep -q "Custom blocking entries" "$HOSTS_FILE" 2>/dev/null; then if ! grep -q "Custom blocking entries" "$HOSTS_FILE" 2> /dev/null; then
return 0 # Our custom entries missing, needs restoration return 0 # Our custom entries missing, needs restoration
fi fi
# Check if StevenBlack entries are missing # Check if StevenBlack entries are missing
if ! grep -q "StevenBlack" "$HOSTS_FILE" 2>/dev/null; then if ! grep -q "StevenBlack" "$HOSTS_FILE" 2> /dev/null; then
return 0 # StevenBlack entries missing, needs restoration return 0 # StevenBlack entries missing, needs restoration
fi fi
return 1 # File seems intact return 1 # File seems intact
} }
# Function to restore hosts file # Function to restore hosts file
restore_hosts_file() { restore_hosts_file() {
log_message "Hosts file modification detected - initiating restoration" log_message "Hosts file modification detected - initiating restoration"
if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then
log_message "Running hosts installation script: $HOSTS_INSTALL_SCRIPT" log_message "Running hosts installation script: $HOSTS_INSTALL_SCRIPT"
if bash "$HOSTS_INSTALL_SCRIPT" >>"$LOG_FILE" 2>&1; then if bash "$HOSTS_INSTALL_SCRIPT" >> "$LOG_FILE" 2>&1; then
log_message "Hosts file restoration completed successfully" log_message "Hosts file restoration completed successfully"
else else
log_message "Hosts file restoration failed with exit code $?" log_message "Hosts file restoration failed with exit code $?"
fi fi
else else
log_message "ERROR: Hosts install script not found at $HOSTS_INSTALL_SCRIPT" log_message "ERROR: Hosts install script not found at $HOSTS_INSTALL_SCRIPT"
fi fi
} }
# Function to monitor with inotifywait # Function to monitor with inotifywait
monitor_with_inotify() { monitor_with_inotify() {
log_message "Starting hosts file monitoring with inotify" log_message "Starting hosts file monitoring with inotify"
# Monitor the hosts file and its directory for various events # Monitor the hosts file and its directory for various events
inotifywait -m -e delete,move,modify,attrib,create --format '%w%f %e %T' --timefmt '%Y-%m-%d %H:%M:%S' "$HOSTS_FILE" /etc/ 2>/dev/null | inotifywait -m -e delete,move,modify,attrib,create --format '%w%f %e %T' --timefmt '%Y-%m-%d %H:%M:%S' "$HOSTS_FILE" /etc/ 2> /dev/null |
while read -r file event time; do while read -r file event time; do
# Check if the event is related to our hosts file # Check if the event is related to our hosts file
if [[ $file == "$HOSTS_FILE" ]] || [[ $file == "/etc/hosts" ]]; then if [[ $file == "$HOSTS_FILE" ]] || [[ $file == "/etc/hosts" ]]; then
log_message "Event detected: $event on $file at $time" log_message "Event detected: $event on $file at $time"
# Small delay to avoid rapid-fire events # Small delay to avoid rapid-fire events
sleep 2 sleep 2
# Check if restoration is needed # Check if restoration is needed
if needs_restoration; then if needs_restoration; then
restore_hosts_file restore_hosts_file
else else
log_message "Hosts file check passed - no restoration needed" log_message "Hosts file check passed - no restoration needed"
fi fi
fi fi
done done
} }
# Function to monitor with polling (fallback) # Function to monitor with polling (fallback)
monitor_with_polling() { monitor_with_polling() {
log_message "Starting hosts file monitoring with polling (fallback method)" log_message "Starting hosts file monitoring with polling (fallback method)"
while true; do while true; do
if needs_restoration; then if needs_restoration; then
restore_hosts_file restore_hosts_file
fi fi
# Check every 30 seconds # Check every 30 seconds
sleep 30 sleep 30
done done
} }
# Main execution # Main execution
log_message "=== Hosts File Monitor Started ===" log_message "=== Hosts File Monitor Started ==="
# Check if inotify-tools is available # Check if inotify-tools is available
if command -v inotifywait >/dev/null 2>&1; then if command -v inotifywait > /dev/null 2>&1; then
log_message "Using inotify for file monitoring" log_message "Using inotify for file monitoring"
monitor_with_inotify monitor_with_inotify
else else
log_message "inotify-tools not available, using polling method" log_message "inotify-tools not available, using polling method"
log_message "Consider installing inotify-tools for better performance: pacman -S inotify-tools" log_message "Consider installing inotify-tools for better performance: pacman -S inotify-tools"
monitor_with_polling monitor_with_polling
fi fi

View File

@ -12,106 +12,106 @@ CHECK_INTERVAL=30
# Log with timestamp (shutdown-timer-monitor specific) # Log with timestamp (shutdown-timer-monitor specific)
log_message() { log_message() {
printf '%s [shutdown-monitor] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "$LOG_FILE" >&2 printf '%s [shutdown-monitor] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "$LOG_FILE" >&2
} }
# Function to check if timer needs to be re-enabled # Function to check if timer needs to be re-enabled
timer_needs_restoration() { timer_needs_restoration() {
# Check if timer is enabled # Check if timer is enabled
if ! systemctl is-enabled "$TIMER_NAME" &>/dev/null; then if ! systemctl is-enabled "$TIMER_NAME" &> /dev/null; then
log_message "Timer $TIMER_NAME is not enabled" log_message "Timer $TIMER_NAME is not enabled"
return 0 return 0
fi fi
# Check if timer is active # Check if timer is active
if ! systemctl is-active "$TIMER_NAME" &>/dev/null; then if ! systemctl is-active "$TIMER_NAME" &> /dev/null; then
log_message "Timer $TIMER_NAME is not active" log_message "Timer $TIMER_NAME is not active"
return 0 return 0
fi fi
# Check if timer unit file exists # Check if timer unit file exists
if [[ ! -f "/etc/systemd/system/$TIMER_NAME" ]]; then if [[ ! -f "/etc/systemd/system/$TIMER_NAME" ]]; then
log_message "Timer unit file missing: /etc/systemd/system/$TIMER_NAME" log_message "Timer unit file missing: /etc/systemd/system/$TIMER_NAME"
return 0 return 0
fi fi
# Check if service unit file exists # Check if service unit file exists
if [[ ! -f "/etc/systemd/system/$SERVICE_NAME" ]]; then if [[ ! -f "/etc/systemd/system/$SERVICE_NAME" ]]; then
log_message "Service unit file missing: /etc/systemd/system/$SERVICE_NAME" log_message "Service unit file missing: /etc/systemd/system/$SERVICE_NAME"
return 0 return 0
fi fi
# Check if check script exists # Check if check script exists
if [[ ! -f "/usr/local/bin/day-specific-shutdown-check.sh" ]]; then if [[ ! -f "/usr/local/bin/day-specific-shutdown-check.sh" ]]; then
log_message "Check script missing: /usr/local/bin/day-specific-shutdown-check.sh" log_message "Check script missing: /usr/local/bin/day-specific-shutdown-check.sh"
return 0 return 0
fi fi
return 1 # Timer is properly configured return 1 # Timer is properly configured
} }
# Function to restore timer # Function to restore timer
restore_timer() { restore_timer() {
log_message "Shutdown timer tampering detected - initiating restoration" log_message "Shutdown timer tampering detected - initiating restoration"
# Reload systemd daemon in case unit files were modified # Reload systemd daemon in case unit files were modified
systemctl daemon-reload systemctl daemon-reload
# Re-enable timer if disabled # Re-enable timer if disabled
if ! systemctl is-enabled "$TIMER_NAME" &>/dev/null; then if ! systemctl is-enabled "$TIMER_NAME" &> /dev/null; then
log_message "Re-enabling $TIMER_NAME" log_message "Re-enabling $TIMER_NAME"
systemctl enable "$TIMER_NAME" 2>/dev/null || true systemctl enable "$TIMER_NAME" 2> /dev/null || true
fi fi
# Re-start timer if not active # Re-start timer if not active
if ! systemctl is-active "$TIMER_NAME" &>/dev/null; then if ! systemctl is-active "$TIMER_NAME" &> /dev/null; then
log_message "Re-starting $TIMER_NAME" log_message "Re-starting $TIMER_NAME"
systemctl start "$TIMER_NAME" 2>/dev/null || true systemctl start "$TIMER_NAME" 2> /dev/null || true
fi fi
# Verify restoration # Verify restoration
if systemctl is-active "$TIMER_NAME" &>/dev/null; then if systemctl is-active "$TIMER_NAME" &> /dev/null; then
log_message "Timer restoration completed successfully" log_message "Timer restoration completed successfully"
else else
log_message "WARNING: Timer restoration may have failed" log_message "WARNING: Timer restoration may have failed"
fi fi
} }
# Function to monitor timer with systemd events # Function to monitor timer with systemd events
monitor_with_dbus() { monitor_with_dbus() {
log_message "Starting shutdown timer monitoring with D-Bus events" log_message "Starting shutdown timer monitoring with D-Bus events"
# Use busctl to monitor systemd unit changes # Use busctl to monitor systemd unit changes
# Fall back to polling if this fails # Fall back to polling if this fails
if command -v busctl &>/dev/null; then if command -v busctl &> /dev/null; then
# Monitor for unit state changes # Monitor for unit state changes
busctl monitor --system org.freedesktop.systemd1 2>/dev/null | busctl monitor --system org.freedesktop.systemd1 2> /dev/null |
while read -r line; do while read -r line; do
# Check if the line mentions our timer # Check if the line mentions our timer
if echo "$line" | grep -q "$TIMER_NAME\|$SERVICE_NAME"; then if echo "$line" | grep -q "$TIMER_NAME\|$SERVICE_NAME"; then
log_message "Systemd event detected for shutdown timer" log_message "Systemd event detected for shutdown timer"
sleep 2 sleep 2
if timer_needs_restoration; then if timer_needs_restoration; then
restore_timer restore_timer
fi fi
fi fi
done done
else else
log_message "busctl not available, falling back to polling" log_message "busctl not available, falling back to polling"
monitor_with_polling monitor_with_polling
fi fi
} }
# Function to monitor with polling (primary method for reliability) # Function to monitor with polling (primary method for reliability)
monitor_with_polling() { monitor_with_polling() {
log_message "Starting shutdown timer monitoring with polling (interval: ${CHECK_INTERVAL}s)" log_message "Starting shutdown timer monitoring with polling (interval: ${CHECK_INTERVAL}s)"
while true; do while true; do
if timer_needs_restoration; then if timer_needs_restoration; then
restore_timer restore_timer
fi fi
sleep "$CHECK_INTERVAL" sleep "$CHECK_INTERVAL"
done done
} }
# Main execution # Main execution
@ -121,10 +121,10 @@ log_message "Monitoring service: $SERVICE_NAME"
# Initial check # Initial check
if timer_needs_restoration; then if timer_needs_restoration; then
log_message "Initial check: Timer needs restoration" log_message "Initial check: Timer needs restoration"
restore_timer restore_timer
else else
log_message "Initial check: Timer is properly configured" log_message "Initial check: Timer is properly configured"
fi fi
# Use polling for reliability (D-Bus monitoring can miss events) # Use polling for reliability (D-Bus monitoring can miss events)

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,10 @@ WATCHDOG_SCRIPT="$GUARDIAN_DIR/watchdog.sh"
mkdir -p "$GUARDIAN_DIR" mkdir -p "$GUARDIAN_DIR"
# Log that we're starting # Log that we're starting
echo "[$(date '+%Y-%m-%d %H:%M:%S')] post-fs-data: Guardian module loading" >>"$GUARDIAN_DIR/guardian.log" echo "[$(date '+%Y-%m-%d %H:%M:%S')] post-fs-data: Guardian module loading" >> "$GUARDIAN_DIR/guardian.log"
# Create persistent watchdog script that runs independently of module state # Create persistent watchdog script that runs independently of module state
cat >"$WATCHDOG_SCRIPT" <<'WATCHDOG' cat > "$WATCHDOG_SCRIPT" << 'WATCHDOG'
#!/system/bin/sh #!/system/bin/sh
# Secondary watchdog - runs independently of module state # Secondary watchdog - runs independently of module state
# Even if module is "disabled" in Magisk UI, this keeps running and undoes it # Even if module is "disabled" in Magisk UI, this keeps running and undoes it
@ -59,5 +59,5 @@ WATCHDOG
chmod 755 "$WATCHDOG_SCRIPT" chmod 755 "$WATCHDOG_SCRIPT"
# Start watchdog as a separate background process # Start watchdog as a separate background process
nohup sh "$WATCHDOG_SCRIPT" >/dev/null 2>&1 & nohup sh "$WATCHDOG_SCRIPT" > /dev/null 2>&1 &
echo "[$(date '+%Y-%m-%d %H:%M:%S')] post-fs-data: Watchdog started" >>"$GUARDIAN_DIR/guardian.log" echo "[$(date '+%Y-%m-%d %H:%M:%S')] post-fs-data: Watchdog started" >> "$GUARDIAN_DIR/guardian.log"

View File

@ -20,83 +20,83 @@ REMOVE_FILE="$MODULE_DIR/remove"
mkdir -p "$GUARDIAN_DIR" mkdir -p "$GUARDIAN_DIR"
log() { log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >>"$LOG_FILE" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
} }
# Initialize control file if not exists # Initialize control file if not exists
[ ! -f "$CONTROL_FILE" ] && echo "ENABLED" >"$CONTROL_FILE" [ ! -f "$CONTROL_FILE" ] && echo "ENABLED" > "$CONTROL_FILE"
log "=== Android Guardian starting ===" log "=== Android Guardian starting ==="
# Function to check if guardian is enabled (via ADB control, not Magisk UI) # Function to check if guardian is enabled (via ADB control, not Magisk UI)
is_enabled() { is_enabled() {
[ "$(cat "$CONTROL_FILE" 2>/dev/null)" = "ENABLED" ] [ "$(cat "$CONTROL_FILE" 2> /dev/null)" = "ENABLED" ]
} }
# Function to protect module from being disabled via Magisk UI # Function to protect module from being disabled via Magisk UI
protect_module() { protect_module() {
# Remove disable file if someone tried to disable via Magisk # Remove disable file if someone tried to disable via Magisk
if [ -f "$DISABLE_FILE" ]; then if [ -f "$DISABLE_FILE" ]; then
log "Module disable attempt detected via Magisk UI! Re-enabling..." log "Module disable attempt detected via Magisk UI! Re-enabling..."
rm -f "$DISABLE_FILE" rm -f "$DISABLE_FILE"
log "Module re-enabled" log "Module re-enabled"
fi fi
# Remove remove file if someone tried to uninstall via Magisk # Remove remove file if someone tried to uninstall via Magisk
if [ -f "$REMOVE_FILE" ]; then if [ -f "$REMOVE_FILE" ]; then
log "Module removal attempt detected via Magisk UI! Blocking..." log "Module removal attempt detected via Magisk UI! Blocking..."
rm -f "$REMOVE_FILE" rm -f "$REMOVE_FILE"
log "Module removal blocked" log "Module removal blocked"
fi fi
} }
# Function to restore hosts file if tampered # Function to restore hosts file if tampered
protect_hosts() { protect_hosts() {
if [ -f "$HOSTS_BACKUP" ]; then if [ -f "$HOSTS_BACKUP" ]; then
current_hash=$(md5sum /system/etc/hosts 2>/dev/null | cut -d' ' -f1) current_hash=$(md5sum /system/etc/hosts 2> /dev/null | cut -d' ' -f1)
backup_hash=$(md5sum "$HOSTS_BACKUP" 2>/dev/null | cut -d' ' -f1) backup_hash=$(md5sum "$HOSTS_BACKUP" 2> /dev/null | cut -d' ' -f1)
if [ "$current_hash" != "$backup_hash" ]; then if [ "$current_hash" != "$backup_hash" ]; then
log "Hosts file tampering detected! Restoring..." log "Hosts file tampering detected! Restoring..."
cp "$HOSTS_BACKUP" "$MODDIR/system/etc/hosts" cp "$HOSTS_BACKUP" "$MODDIR/system/etc/hosts"
log "Hosts file restored" log "Hosts file restored"
fi fi
fi fi
} }
# Function to uninstall blocked apps # Function to uninstall blocked apps
check_blocked_apps() { check_blocked_apps() {
if [ ! -f "$BLOCKED_APPS_FILE" ]; then if [ ! -f "$BLOCKED_APPS_FILE" ]; then
return return
fi fi
while IFS= read -r package || [ -n "$package" ]; do while IFS= read -r package || [ -n "$package" ]; do
# Skip comments and empty lines # Skip comments and empty lines
case "$package" in case "$package" in
\#* | "") continue ;; \#* | "") continue ;;
esac esac
# Check if package is installed # Check if package is installed
if pm list packages 2>/dev/null | grep -q "package:$package"; then if pm list packages 2> /dev/null | grep -q "package:$package"; then
log "Blocked app detected: $package - Uninstalling..." log "Blocked app detected: $package - Uninstalling..."
pm uninstall "$package" 2>/dev/null && log "Uninstalled: $package" || log "Failed to uninstall: $package" pm uninstall "$package" 2> /dev/null && log "Uninstalled: $package" || log "Failed to uninstall: $package"
fi fi
done <"$BLOCKED_APPS_FILE" done < "$BLOCKED_APPS_FILE"
} }
# Main monitoring loop - runs every 5 seconds for faster protection # Main monitoring loop - runs every 5 seconds for faster protection
while true; do while true; do
# ALWAYS protect module from UI disabling (even if guardian is "disabled" via ADB) # ALWAYS protect module from UI disabling (even if guardian is "disabled" via ADB)
# This ensures only ADB can control the guardian # This ensures only ADB can control the guardian
protect_module protect_module
if is_enabled; then if is_enabled; then
protect_hosts protect_hosts
check_blocked_apps check_blocked_apps
fi fi
# Check every 5 seconds (faster response to disable attempts) # Check every 5 seconds (faster response to disable attempts)
sleep 5 sleep 5
done & done &
log "Guardian service started (PID: $!)" log "Guardian service started (PID: $!)"

View File

@ -4,12 +4,12 @@ GUARDIAN_DIR="/data/adb/android_guardian"
# Only allow uninstall if control file says DISABLED # Only allow uninstall if control file says DISABLED
if [ -f "$GUARDIAN_DIR/control" ]; then if [ -f "$GUARDIAN_DIR/control" ]; then
status=$(cat "$GUARDIAN_DIR/control") status=$(cat "$GUARDIAN_DIR/control")
if [ "$status" != "DISABLED" ]; then if [ "$status" != "DISABLED" ]; then
echo "Guardian is still enabled! Use ADB to disable first:" echo "Guardian is still enabled! Use ADB to disable first:"
echo " adb shell 'echo DISABLED > /data/adb/android_guardian/control'" echo " adb shell 'echo DISABLED > /data/adb/android_guardian/control'"
exit 1 exit 1
fi fi
fi fi
# Clean up guardian data # Clean up guardian data

View File

@ -23,7 +23,7 @@ TARGET_PATH=""
ALL_VIDEO_EXTENSIONS=("mp4" "webm" "mkv" "avi" "mov" "wmv" "flv" "m4v" "mpg" "mpeg" "3gp" "ogv" "ts" "mts" "m2ts" "vob" "asf" "rm" "rmvb" "divx" "f4v") ALL_VIDEO_EXTENSIONS=("mp4" "webm" "mkv" "avi" "mov" "wmv" "flv" "m4v" "mpg" "mpeg" "3gp" "ogv" "ts" "mts" "m2ts" "vob" "asf" "rm" "rmvb" "divx" "f4v")
usage() { usage() {
cat <<EOF cat << EOF
Usage: Usage:
$(basename "$0") [OPTIONS] PATH $(basename "$0") [OPTIONS] PATH
@ -47,193 +47,193 @@ EOF
} }
ensure_ffmpeg() { ensure_ffmpeg() {
if ! command -v ffmpeg >/dev/null 2>&1; then if ! command -v ffmpeg > /dev/null 2>&1; then
echo "Error: 'ffmpeg' is not installed or not in PATH." >&2 echo "Error: 'ffmpeg' is not installed or not in PATH." >&2
exit 1 exit 1
fi fi
} }
get_video_extensions_except() { get_video_extensions_except() {
local exclude="$1" local exclude="$1"
local exts=() local exts=()
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ ${ext,,} != "${exclude,,}" ]]; then if [[ ${ext,,} != "${exclude,,}" ]]; then
exts+=("$ext") exts+=("$ext")
fi fi
done done
echo "${exts[@]}" echo "${exts[@]}"
} }
is_video_file() { is_video_file() {
local file="$1" local file="$1"
local ext="${file##*.}" local ext="${file##*.}"
ext="${ext,,}" # lowercase ext="${ext,,}" # lowercase
for video_ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do for video_ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ $ext == "${video_ext,,}" ]]; then if [[ $ext == "${video_ext,,}" ]]; then
return 0 return 0
fi fi
done done
return 1 return 1
} }
convert_video() { convert_video() {
local input_file="$1" local input_file="$1"
local output_file="${input_file%.*}.${TARGET_FORMAT}" local output_file="${input_file%.*}.${TARGET_FORMAT}"
# Skip if output already exists # Skip if output already exists
if [[ -f $output_file ]]; then if [[ -f $output_file ]]; then
log "Skipping '$input_file': output '$output_file' already exists" log "Skipping '$input_file': output '$output_file' already exists"
return 0 return 0
fi fi
log "Converting '$input_file' -> '$output_file'" log "Converting '$input_file' -> '$output_file'"
local ffmpeg_args=() local ffmpeg_args=()
ffmpeg_args+=(-hide_banner -loglevel warning -i "$input_file") ffmpeg_args+=(-hide_banner -loglevel warning -i "$input_file")
if [[ $TARGET_FORMAT == "mp4" ]]; then if [[ $TARGET_FORMAT == "mp4" ]]; then
# H.264 codec for video and AAC for audio (maximum compatibility) # H.264 codec for video and AAC for audio (maximum compatibility)
ffmpeg_args+=(-c:v libx264 -crf "$CRF" -preset "$PRESET") ffmpeg_args+=(-c:v libx264 -crf "$CRF" -preset "$PRESET")
ffmpeg_args+=(-c:a aac -b:a 192k) ffmpeg_args+=(-c:a aac -b:a 192k)
ffmpeg_args+=(-movflags +faststart) ffmpeg_args+=(-movflags +faststart)
elif [[ $TARGET_FORMAT == "webm" ]]; then elif [[ $TARGET_FORMAT == "webm" ]]; then
# VP9 codec for video and Opus for audio # VP9 codec for video and Opus for audio
ffmpeg_args+=(-c:v libvpx-vp9 -crf "$CRF" -b:v 0) ffmpeg_args+=(-c:v libvpx-vp9 -crf "$CRF" -b:v 0)
ffmpeg_args+=(-c:a libopus -b:a 128k) ffmpeg_args+=(-c:a libopus -b:a 128k)
fi fi
ffmpeg_args+=("$output_file") ffmpeg_args+=("$output_file")
if ffmpeg "${ffmpeg_args[@]}"; then if ffmpeg "${ffmpeg_args[@]}"; then
log "Successfully converted '$input_file'" log "Successfully converted '$input_file'"
if [[ $DELETE_ORIGINAL == true ]]; then if [[ $DELETE_ORIGINAL == true ]]; then
log "Deleting original: '$input_file'" log "Deleting original: '$input_file'"
rm "$input_file" rm "$input_file"
fi fi
else else
log "Error converting '$input_file'" log "Error converting '$input_file'"
[[ -f $output_file ]] && rm "$output_file" [[ -f $output_file ]] && rm "$output_file"
return 1 return 1
fi fi
} }
process_directory() { process_directory() {
local dir="$1" local dir="$1"
local count=0 local count=0
local failed=0 local failed=0
log "Searching for video files in '$dir'..." log "Searching for video files in '$dir'..."
# Build find command dynamically # Build find command dynamically
local find_args=(-type f \() local find_args=(-type f \()
local first=true local first=true
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ ${ext,,} != "${TARGET_FORMAT,,}" ]]; then if [[ ${ext,,} != "${TARGET_FORMAT,,}" ]]; then
if [[ $first == true ]]; then if [[ $first == true ]]; then
first=false first=false
else else
find_args+=(-o) find_args+=(-o)
fi fi
find_args+=(-iname "*.$ext") find_args+=(-iname "*.$ext")
fi fi
done done
find_args+=(\) -print0) find_args+=(\) -print0)
while IFS= read -r -d '' file; do while IFS= read -r -d '' file; do
((count++)) || true ((count++)) || true
if ! convert_video "$file"; then if ! convert_video "$file"; then
((failed++)) || true ((failed++)) || true
fi fi
done < <(find "$dir" "${find_args[@]}" 2>/dev/null) done < <(find "$dir" "${find_args[@]}" 2> /dev/null)
log "Processed $count video file(s), $failed failed" log "Processed $count video file(s), $failed failed"
if [[ $count -eq 0 ]]; then if [[ $count -eq 0 ]]; then
log "No video files found in '$dir'" log "No video files found in '$dir'"
fi fi
} }
parse_args() { parse_args() {
while getopts ":f:c:p:dh" opt; do while getopts ":f:c:p:dh" opt; do
case "$opt" in case "$opt" in
f) f)
TARGET_FORMAT="${OPTARG,,}" TARGET_FORMAT="${OPTARG,,}"
if [[ $TARGET_FORMAT != "mp4" && $TARGET_FORMAT != "webm" ]]; then if [[ $TARGET_FORMAT != "mp4" && $TARGET_FORMAT != "webm" ]]; then
echo "Error: Format must be 'mp4' or 'webm'" >&2 echo "Error: Format must be 'mp4' or 'webm'" >&2
exit 1 exit 1
fi fi
;; ;;
c) CRF="$OPTARG" ;; c) CRF="$OPTARG" ;;
p) PRESET="$OPTARG" ;; p) PRESET="$OPTARG" ;;
d) DELETE_ORIGINAL=true ;; d) DELETE_ORIGINAL=true ;;
h) h)
usage usage
exit 0 exit 0
;; ;;
:) :)
echo "Error: Option -$OPTARG requires an argument." >&2 echo "Error: Option -$OPTARG requires an argument." >&2
usage usage
exit 1 exit 1
;; ;;
\?) \?)
echo "Error: Invalid option -$OPTARG" >&2 echo "Error: Invalid option -$OPTARG" >&2
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
shift $((OPTIND - 1)) shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then if [[ $# -lt 1 ]]; then
echo "Error: No path specified." >&2 echo "Error: No path specified." >&2
usage usage
exit 1 exit 1
fi fi
TARGET_PATH="$1" TARGET_PATH="$1"
# Set default CRF based on format if not specified # Set default CRF based on format if not specified
if [[ -z $CRF ]]; then if [[ -z $CRF ]]; then
if [[ $TARGET_FORMAT == "mp4" ]]; then if [[ $TARGET_FORMAT == "mp4" ]]; then
CRF=23 CRF=23
else else
CRF=30 CRF=30
fi fi
fi fi
} }
main() { main() {
ensure_ffmpeg ensure_ffmpeg
parse_args "$@" parse_args "$@"
if [[ ! -e $TARGET_PATH ]]; then if [[ ! -e $TARGET_PATH ]]; then
echo "Error: Path '$TARGET_PATH' does not exist." >&2 echo "Error: Path '$TARGET_PATH' does not exist." >&2
exit 1 exit 1
fi fi
if [[ -f $TARGET_PATH ]]; then if [[ -f $TARGET_PATH ]]; then
# Single file # Single file
if [[ ${TARGET_PATH,,} == *."$TARGET_FORMAT" ]]; then if [[ ${TARGET_PATH,,} == *."$TARGET_FORMAT" ]]; then
log "File '$TARGET_PATH' is already in $TARGET_FORMAT format, skipping." log "File '$TARGET_PATH' is already in $TARGET_FORMAT format, skipping."
exit 0 exit 0
fi fi
if is_video_file "$TARGET_PATH"; then if is_video_file "$TARGET_PATH"; then
convert_video "$TARGET_PATH" convert_video "$TARGET_PATH"
else else
echo "Error: '$TARGET_PATH' is not a recognized video file." >&2 echo "Error: '$TARGET_PATH' is not a recognized video file." >&2
exit 1 exit 1
fi fi
elif [[ -d $TARGET_PATH ]]; then elif [[ -d $TARGET_PATH ]]; then
process_directory "$TARGET_PATH" process_directory "$TARGET_PATH"
else else
echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2 echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2
exit 1 exit 1
fi fi
log "Done!" log "Done!"
} }
main "$@" main "$@"

View File

@ -32,14 +32,14 @@ cd "$TRACKS_DIR"
# Tracks to download (add/remove as needed) # Tracks to download (add/remove as needed)
declare -A TRACKS=( declare -A TRACKS=(
["python"]="https://github.com/exercism/python.git" ["python"]="https://github.com/exercism/python.git"
["c"]="https://github.com/exercism/c.git" ["c"]="https://github.com/exercism/c.git"
["cpp"]="https://github.com/exercism/cpp.git" ["cpp"]="https://github.com/exercism/cpp.git"
["javascript"]="https://github.com/exercism/javascript.git" ["javascript"]="https://github.com/exercism/javascript.git"
["typescript"]="https://github.com/exercism/typescript.git" ["typescript"]="https://github.com/exercism/typescript.git"
["rust"]="https://github.com/exercism/rust.git" ["rust"]="https://github.com/exercism/rust.git"
["go"]="https://github.com/exercism/go.git" ["go"]="https://github.com/exercism/go.git"
["bash"]="https://github.com/exercism/bash.git" ["bash"]="https://github.com/exercism/bash.git"
) )
# Optional tracks (uncomment to include) # Optional tracks (uncomment to include)
@ -52,22 +52,22 @@ echo "Downloading ${#TRACKS[@]} tracks to: $TRACKS_DIR"
echo "" echo ""
for track in "${!TRACKS[@]}"; do for track in "${!TRACKS[@]}"; do
url="${TRACKS[$track]}" url="${TRACKS[$track]}"
if [[ -d "$track" ]]; then if [[ -d $track ]]; then
info "Updating $track..." info "Updating $track..."
(cd "$track" && git pull --quiet) && success "$track updated" (cd "$track" && git pull --quiet) && success "$track updated"
else else
info "Cloning $track..." info "Cloning $track..."
git clone --depth 1 "$url" && success "$track cloned" git clone --depth 1 "$url" && success "$track cloned"
fi fi
# Show exercise count # Show exercise count
if [[ -d "$track/exercises/practice" ]]; then if [[ -d "$track/exercises/practice" ]]; then
count=$(ls "$track/exercises/practice" | wc -l) count=$(ls "$track/exercises/practice" | wc -l)
echo "$count practice exercises available" echo "$count practice exercises available"
fi fi
echo "" echo ""
done done
echo "==============================================" echo "=============================================="
@ -99,8 +99,8 @@ echo "=============================================="
echo "" echo ""
echo "Track summary:" echo "Track summary:"
for track in "${!TRACKS[@]}"; do for track in "${!TRACKS[@]}"; do
if [[ -d "$track/exercises/practice" ]]; then if [[ -d "$track/exercises/practice" ]]; then
count=$(ls "$track/exercises/practice" 2>/dev/null | wc -l) count=$(ls "$track/exercises/practice" 2> /dev/null | wc -l)
printf " %-15s %3d exercises\n" "$track" "$count" printf " %-15s %3d exercises\n" "$track" "$count"
fi fi
done | sort done | sort

View File

@ -10,10 +10,10 @@ set -euo pipefail
# Source common library if available # Source common library if available
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
if [[ -f "$SCRIPT_DIR/../lib/common.sh" ]]; then if [[ -f "$SCRIPT_DIR/../lib/common.sh" ]]; then
# shellcheck source=../lib/common.sh # shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh" source "$SCRIPT_DIR/../lib/common.sh"
else else
log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; } log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
fi fi
# Configuration # Configuration
@ -22,9 +22,9 @@ SEARCH_ROOT="/"
TIMEOUT_SECONDS=30 TIMEOUT_SECONDS=30
# Ensure fd is installed # Ensure fd is installed
if ! command -v fd &>/dev/null; then if ! command -v fd &> /dev/null; then
log "ERROR: 'fd' is not installed. Install with: sudo pacman -S fd" log "ERROR: 'fd' is not installed. Install with: sudo pacman -S fd"
exit 1 exit 1
fi fi
# Create destination directory if it doesn't exist # Create destination directory if it doesn't exist
@ -41,31 +41,31 @@ log "Searching for .kdbx files across the system (timeout: ${TIMEOUT_SECONDS}s).
# Use timeout to ensure the search doesn't take too long # Use timeout to ensure the search doesn't take too long
# Exclude /proc, /sys, /dev, /run, /tmp, /var/cache, /var/tmp for speed # Exclude /proc, /sys, /dev, /run, /tmp, /var/cache, /var/tmp for speed
FOUND_FILES=$(timeout "$TIMEOUT_SECONDS" fd \ FOUND_FILES=$(timeout "$TIMEOUT_SECONDS" fd \
-e kdbx \ -e kdbx \
-u \ -u \
-a \ -a \
--exclude '/proc' \ --exclude '/proc' \
--exclude '/sys' \ --exclude '/sys' \
--exclude '/dev' \ --exclude '/dev' \
--exclude '/run' \ --exclude '/run' \
--exclude '/tmp' \ --exclude '/tmp' \
--exclude '/var/cache' \ --exclude '/var/cache' \
--exclude '/var/tmp' \ --exclude '/var/tmp' \
--exclude '/snap' \ --exclude '/snap' \
--exclude '/.snapshots' \ --exclude '/.snapshots' \
--exclude '/lost+found' \ --exclude '/lost+found' \
. "$SEARCH_ROOT" 2>/dev/null || true) . "$SEARCH_ROOT" 2> /dev/null || true)
if [[ -z "$FOUND_FILES" ]]; then if [[ -z $FOUND_FILES ]]; then
log "No .kdbx files found." log "No .kdbx files found."
exit 0 exit 0
fi fi
# Count and display found files # Count and display found files
FILE_COUNT=$(echo "$FOUND_FILES" | wc -l) FILE_COUNT=$(echo "$FOUND_FILES" | wc -l)
log "Found $FILE_COUNT .kdbx file(s):" log "Found $FILE_COUNT .kdbx file(s):"
echo "$FOUND_FILES" | while read -r file; do echo "$FOUND_FILES" | while read -r file; do
echo " - $file" echo " - $file"
done done
# Move files to destination # Move files to destination
@ -74,51 +74,51 @@ MOVED_COUNT=0
SKIPPED_COUNT=0 SKIPPED_COUNT=0
while IFS= read -r src_file; do while IFS= read -r src_file; do
[[ -z "$src_file" ]] && continue [[ -z $src_file ]] && continue
# Skip if file is already in destination # Skip if file is already in destination
if [[ "$(dirname "$src_file")" == "$DEST_DIR" ]]; then if [[ "$(dirname "$src_file")" == "$DEST_DIR" ]]; then
log "Skipping (already in destination): $src_file" log "Skipping (already in destination): $src_file"
((SKIPPED_COUNT++)) || true ((SKIPPED_COUNT++)) || true
continue continue
fi fi
# Get the base filename # Get the base filename
base_name=$(basename "$src_file") base_name=$(basename "$src_file")
dest_file="$DEST_DIR/$base_name" dest_file="$DEST_DIR/$base_name"
# Handle filename conflicts by adding a number suffix # Handle filename conflicts by adding a number suffix
if [[ -f "$dest_file" ]]; then if [[ -f $dest_file ]]; then
# Check if it's the exact same file (by content) # Check if it's the exact same file (by content)
if cmp -s "$src_file" "$dest_file"; then if cmp -s "$src_file" "$dest_file"; then
log "Skipping (identical file exists): $src_file" log "Skipping (identical file exists): $src_file"
# Remove the duplicate source file # Remove the duplicate source file
rm -v "$src_file" rm -v "$src_file"
((SKIPPED_COUNT++)) || true ((SKIPPED_COUNT++)) || true
continue continue
fi fi
# Different file with same name - add suffix # Different file with same name - add suffix
counter=1 counter=1
name_without_ext="${base_name%.kdbx}" name_without_ext="${base_name%.kdbx}"
while [[ -f "$dest_file" ]]; do while [[ -f $dest_file ]]; do
dest_file="$DEST_DIR/${name_without_ext} ($counter).kdbx" dest_file="$DEST_DIR/${name_without_ext} ($counter).kdbx"
((counter++)) ((counter++))
done done
log "Renaming to avoid conflict: $base_name -> $(basename "$dest_file")" log "Renaming to avoid conflict: $base_name -> $(basename "$dest_file")"
fi fi
# Move the file # Move the file
if mv -v "$src_file" "$dest_file"; then if mv -v "$src_file" "$dest_file"; then
((MOVED_COUNT++)) || true ((MOVED_COUNT++)) || true
else else
log "ERROR: Failed to move $src_file" log "ERROR: Failed to move $src_file"
fi fi
done <<<"$FOUND_FILES" done <<< "$FOUND_FILES"
log "Done! Moved $MOVED_COUNT file(s), skipped $SKIPPED_COUNT file(s)." log "Done! Moved $MOVED_COUNT file(s), skipped $SKIPPED_COUNT file(s)."
log "All KeePassXC databases are now in: $DEST_DIR" log "All KeePassXC databases are now in: $DEST_DIR"
# List final contents # List final contents
log "Contents of $DEST_DIR:" log "Contents of $DEST_DIR:"
ls -la "$DEST_DIR"/*.kdbx 2>/dev/null || log "No .kdbx files in destination" ls -la "$DEST_DIR"/*.kdbx 2> /dev/null || log "No .kdbx files in destination"

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ DEFAULT_RESOLUTION="320x240"
# Function to display usage # Function to display usage
usage() { usage() {
cat <<EOF cat << EOF
Usage: $0 <input_image> [resolution] [output_image] Usage: $0 <input_image> [resolution] [output_image]
Arguments: Arguments:
@ -31,7 +31,7 @@ Examples:
Note: Requires ImageMagick (convert command) Note: Requires ImageMagick (convert command)
EOF EOF
exit 1 exit 1
} }
# Check if ImageMagick is installed # Check if ImageMagick is installed
@ -39,8 +39,8 @@ require_imagemagick "convert" || exit 1
# Parse arguments # Parse arguments
if [[ $# -lt 1 ]]; then if [[ $# -lt 1 ]]; then
echo "Error: Missing required argument <input_image>" echo "Error: Missing required argument <input_image>"
usage usage
fi fi
INPUT_IMAGE="$1" INPUT_IMAGE="$1"
@ -49,20 +49,20 @@ OUTPUT_IMAGE="${3:-}"
# Validate input image exists # Validate input image exists
if [[ ! -f ${INPUT_IMAGE} ]]; then if [[ ! -f ${INPUT_IMAGE} ]]; then
echo "Error: Input image '${INPUT_IMAGE}' does not exist." echo "Error: Input image '${INPUT_IMAGE}' does not exist."
exit 1 exit 1
fi fi
# Validate resolution format (WIDTHxHEIGHT) # Validate resolution format (WIDTHxHEIGHT)
if ! validate_resolution "$RESOLUTION"; then if ! validate_resolution "$RESOLUTION"; then
echo "Error: Invalid resolution format '${RESOLUTION}'" echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)" echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1 exit 1
fi fi
# Generate output filename if not provided # Generate output filename if not provided
if [[ -z ${OUTPUT_IMAGE} ]]; then if [[ -z ${OUTPUT_IMAGE} ]]; then
OUTPUT_IMAGE=$(generate_output_filename "${INPUT_IMAGE}" "_${RESOLUTION}") OUTPUT_IMAGE=$(generate_output_filename "${INPUT_IMAGE}" "_${RESOLUTION}")
fi fi
# Perform the conversion # Perform the conversion
@ -70,15 +70,15 @@ echo "Converting '${INPUT_IMAGE}' to ${RESOLUTION}..."
echo "Output will be saved to: ${OUTPUT_IMAGE}" echo "Output will be saved to: ${OUTPUT_IMAGE}"
if convert "${INPUT_IMAGE}" -resize "${RESOLUTION}!" "${OUTPUT_IMAGE}"; then if convert "${INPUT_IMAGE}" -resize "${RESOLUTION}!" "${OUTPUT_IMAGE}"; then
echo "✓ Successfully converted image to ${RESOLUTION}" echo "✓ Successfully converted image to ${RESOLUTION}"
echo "Output: ${OUTPUT_IMAGE}" echo "Output: ${OUTPUT_IMAGE}"
# Show file sizes # Show file sizes
INPUT_SIZE=$(du -h "${INPUT_IMAGE}" | cut -f1) INPUT_SIZE=$(du -h "${INPUT_IMAGE}" | cut -f1)
OUTPUT_SIZE=$(du -h "${OUTPUT_IMAGE}" | cut -f1) OUTPUT_SIZE=$(du -h "${OUTPUT_IMAGE}" | cut -f1)
echo "Input size: ${INPUT_SIZE}" echo "Input size: ${INPUT_SIZE}"
echo "Output size: ${OUTPUT_SIZE}" echo "Output size: ${OUTPUT_SIZE}"
else else
echo "✗ Error: Conversion failed" echo "✗ Error: Conversion failed"
exit 1 exit 1
fi fi

View File

@ -34,286 +34,286 @@ echo ""
# Install Exercism CLI # Install Exercism CLI
install_exercism_cli() { install_exercism_cli() {
if command -v exercism &>/dev/null; then if command -v exercism &> /dev/null; then
local version local version
version=$(exercism version 2>/dev/null | head -1) version=$(exercism version 2> /dev/null | head -1)
success "Exercism CLI already installed: $version" success "Exercism CLI already installed: $version"
return 0 return 0
fi fi
echo "Installing Exercism CLI..." echo "Installing Exercism CLI..."
# Try package managers first # Try package managers first
if command -v pacman &>/dev/null; then if command -v pacman &> /dev/null; then
# Check AUR # Check AUR
if command -v yay &>/dev/null; then if command -v yay &> /dev/null; then
yay -S --noconfirm exercism-bin yay -S --noconfirm exercism-bin
success "Exercism CLI installed via AUR" success "Exercism CLI installed via AUR"
return 0 return 0
elif command -v paru &>/dev/null; then elif command -v paru &> /dev/null; then
paru -S --noconfirm exercism-bin paru -S --noconfirm exercism-bin
success "Exercism CLI installed via AUR" success "Exercism CLI installed via AUR"
return 0 return 0
fi fi
elif command -v brew &>/dev/null; then elif command -v brew &> /dev/null; then
brew install exercism brew install exercism
success "Exercism CLI installed via Homebrew" success "Exercism CLI installed via Homebrew"
return 0 return 0
fi fi
# Manual installation from GitHub releases # Manual installation from GitHub releases
info "Installing from GitHub releases..." info "Installing from GitHub releases..."
local arch local arch
case "$(uname -m)" in case "$(uname -m)" in
x86_64) arch="x86_64" ;; x86_64) arch="x86_64" ;;
aarch64 | arm64) arch="arm64" ;; aarch64 | arm64) arch="arm64" ;;
armv7l) arch="armv7" ;; armv7l) arch="armv7" ;;
i686) arch="i386" ;; i686) arch="i386" ;;
*) *)
error "Unsupported architecture: $(uname -m)" error "Unsupported architecture: $(uname -m)"
return 1 return 1
;; ;;
esac esac
local os="linux" local os="linux"
[[ "$(uname -s)" == "Darwin" ]] && os="darwin" [[ "$(uname -s)" == "Darwin" ]] && os="darwin"
# Get latest release # Get latest release
local latest_url="https://api.github.com/repos/exercism/cli/releases/latest" local latest_url="https://api.github.com/repos/exercism/cli/releases/latest"
local download_url local download_url
download_url=$(curl -fsSL "$latest_url" | grep "browser_download_url.*${os}-${arch}" | head -1 | cut -d '"' -f 4) download_url=$(curl -fsSL "$latest_url" | grep "browser_download_url.*${os}-${arch}" | head -1 | cut -d '"' -f 4)
if [[ -z "$download_url" ]]; then if [[ -z $download_url ]]; then
error "Could not find download URL for your system" error "Could not find download URL for your system"
echo "Please install manually from: https://exercism.org/docs/using/solving-exercises/working-locally" echo "Please install manually from: https://exercism.org/docs/using/solving-exercises/working-locally"
return 1 return 1
fi fi
echo "Downloading from: $download_url" echo "Downloading from: $download_url"
local temp_dir local temp_dir
temp_dir=$(mktemp -d) temp_dir=$(mktemp -d)
curl -fL --progress-bar "$download_url" -o "$temp_dir/exercism.tar.gz" curl -fL --progress-bar "$download_url" -o "$temp_dir/exercism.tar.gz"
tar -xzf "$temp_dir/exercism.tar.gz" -C "$temp_dir" tar -xzf "$temp_dir/exercism.tar.gz" -C "$temp_dir"
# Install to ~/.local/bin # Install to ~/.local/bin
mkdir -p "$HOME/.local/bin" mkdir -p "$HOME/.local/bin"
mv "$temp_dir/exercism" "$HOME/.local/bin/" mv "$temp_dir/exercism" "$HOME/.local/bin/"
chmod +x "$HOME/.local/bin/exercism" chmod +x "$HOME/.local/bin/exercism"
rm -rf "$temp_dir" rm -rf "$temp_dir"
success "Exercism CLI installed to ~/.local/bin/exercism" success "Exercism CLI installed to ~/.local/bin/exercism"
# Check PATH # Check PATH
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
warn "Add ~/.local/bin to your PATH:" warn "Add ~/.local/bin to your PATH:"
echo ' export PATH="$HOME/.local/bin:$PATH"' echo ' export PATH="$HOME/.local/bin:$PATH"'
fi fi
} }
# Configure exercism workspace # Configure exercism workspace
configure_exercism() { configure_exercism() {
echo "" echo ""
echo "=== Configuring Exercism ===" echo "=== Configuring Exercism ==="
mkdir -p "$EXERCISM_DIR" mkdir -p "$EXERCISM_DIR"
# Check if already configured # Check if already configured
if exercism configure 2>&1 | grep -q "workspace"; then if exercism configure 2>&1 | grep -q "workspace"; then
success "Exercism already configured" success "Exercism already configured"
else else
# Set workspace directory # Set workspace directory
exercism configure --workspace="$EXERCISM_DIR" exercism configure --workspace="$EXERCISM_DIR"
success "Workspace set to: $EXERCISM_DIR" success "Workspace set to: $EXERCISM_DIR"
fi fi
echo "" echo ""
info "To fully configure Exercism with your account:" info "To fully configure Exercism with your account:"
echo " 1. Create free account at https://exercism.org" echo " 1. Create free account at https://exercism.org"
echo " 2. Go to https://exercism.org/settings/api_cli" echo " 2. Go to https://exercism.org/settings/api_cli"
echo " 3. Copy your API token" echo " 3. Copy your API token"
echo " 4. Run: exercism configure --token=YOUR_TOKEN" echo " 4. Run: exercism configure --token=YOUR_TOKEN"
echo "" echo ""
} }
# Install test runners for languages # Install test runners for languages
install_test_runners() { install_test_runners() {
echo "" echo ""
echo "=== Installing Test Runners ===" echo "=== Installing Test Runners ==="
echo "" echo ""
# Python - pytest # Python - pytest
if command -v python3 &>/dev/null; then if command -v python3 &> /dev/null; then
if python3 -c "import pytest" 2>/dev/null; then if python3 -c "import pytest" 2> /dev/null; then
success "Python: pytest already installed" success "Python: pytest already installed"
else else
info "Installing pytest for Python exercises..." info "Installing pytest for Python exercises..."
pip3 install --user pytest 2>/dev/null && success "Python: pytest installed" || warn "Python: install pytest manually" pip3 install --user pytest 2> /dev/null && success "Python: pytest installed" || warn "Python: install pytest manually"
fi fi
fi fi
# JavaScript/TypeScript - Node.js + npm # JavaScript/TypeScript - Node.js + npm
if command -v node &>/dev/null; then if command -v node &> /dev/null; then
success "JavaScript/TypeScript: Node.js available ($(node --version))" success "JavaScript/TypeScript: Node.js available ($(node --version))"
info " Tests run with: npm test (or jest)" info " Tests run with: npm test (or jest)"
else else
warn "JavaScript/TypeScript: Install Node.js for JS/TS exercises" warn "JavaScript/TypeScript: Install Node.js for JS/TS exercises"
fi fi
# C - gcc + criterion/cmocka # C - gcc + criterion/cmocka
if command -v gcc &>/dev/null; then if command -v gcc &> /dev/null; then
success "C: gcc available" success "C: gcc available"
info " Some C exercises use Unity test framework (included in exercise)" info " Some C exercises use Unity test framework (included in exercise)"
else else
warn "C: Install gcc for C exercises" warn "C: Install gcc for C exercises"
fi fi
# C++ - g++ + Catch2/doctest # C++ - g++ + Catch2/doctest
if command -v g++ &>/dev/null; then if command -v g++ &> /dev/null; then
success "C++: g++ available" success "C++: g++ available"
info " C++ exercises use Catch2 (header-only, included in exercise)" info " C++ exercises use Catch2 (header-only, included in exercise)"
else else
warn "C++: Install g++ for C++ exercises" warn "C++: Install g++ for C++ exercises"
fi fi
# Rust # Rust
if command -v cargo &>/dev/null; then if command -v cargo &> /dev/null; then
success "Rust: cargo available (tests with: cargo test)" success "Rust: cargo available (tests with: cargo test)"
fi fi
# Go # Go
if command -v go &>/dev/null; then if command -v go &> /dev/null; then
success "Go: go available (tests with: go test)" success "Go: go available (tests with: go test)"
fi fi
} }
# Download exercises for a track (language) # Download exercises for a track (language)
download_track() { download_track() {
local track="$1" local track="$1"
local count="${2:-10}" local count="${2:-10}"
echo "" echo ""
info "Downloading $count exercises for $track track..." info "Downloading $count exercises for $track track..."
# Get list of exercises # Get list of exercises
local exercises local exercises
exercises=$(curl -fsSL "https://exercism.org/api/v2/tracks/${track}/exercises" 2>/dev/null | exercises=$(curl -fsSL "https://exercism.org/api/v2/tracks/${track}/exercises" 2> /dev/null |
grep -oP '"slug":"\K[^"]+' | head -n "$count") grep -oP '"slug":"\K[^"]+' | head -n "$count")
if [[ -z "$exercises" ]]; then if [[ -z $exercises ]]; then
warn "Could not fetch exercise list for $track" warn "Could not fetch exercise list for $track"
return 1 return 1
fi fi
local downloaded=0 local downloaded=0
for exercise in $exercises; do for exercise in $exercises; do
local exercise_dir="$EXERCISM_DIR/$track/$exercise" local exercise_dir="$EXERCISM_DIR/$track/$exercise"
if [[ -d "$exercise_dir" ]]; then if [[ -d $exercise_dir ]]; then
echo " [exists] $exercise" echo " [exists] $exercise"
else else
if exercism download --track="$track" --exercise="$exercise" 2>/dev/null; then if exercism download --track="$track" --exercise="$exercise" 2> /dev/null; then
echo " [downloaded] $exercise" echo " [downloaded] $exercise"
((downloaded++)) ((downloaded++))
else else
echo " [failed] $exercise (may require auth)" echo " [failed] $exercise (may require auth)"
fi fi
fi fi
done done
success "Downloaded $downloaded new exercises for $track" success "Downloaded $downloaded new exercises for $track"
} }
# Show available tracks and usage # Show available tracks and usage
show_usage() { show_usage() {
echo "" echo ""
echo "==============================================" echo "=============================================="
echo " Exercism Usage Guide" echo " Exercism Usage Guide"
echo "==============================================" echo "=============================================="
echo "" echo ""
echo -e "${CYAN}Download exercises:${NC}" echo -e "${CYAN}Download exercises:${NC}"
echo " exercism download --track=python --exercise=hello-world" echo " exercism download --track=python --exercise=hello-world"
echo " exercism download --track=javascript --exercise=two-fer" echo " exercism download --track=javascript --exercise=two-fer"
echo " exercism download --track=c --exercise=isogram" echo " exercism download --track=c --exercise=isogram"
echo "" echo ""
echo -e "${CYAN}Run tests locally:${NC}" echo -e "${CYAN}Run tests locally:${NC}"
echo " Python: cd ~/exercism/python/hello-world && pytest" echo " Python: cd ~/exercism/python/hello-world && pytest"
echo " JavaScript: cd ~/exercism/javascript/hello-world && npm test" echo " JavaScript: cd ~/exercism/javascript/hello-world && npm test"
echo " TypeScript: cd ~/exercism/typescript/hello-world && npm test" echo " TypeScript: cd ~/exercism/typescript/hello-world && npm test"
echo " C: cd ~/exercism/c/hello-world && make test" echo " C: cd ~/exercism/c/hello-world && make test"
echo " C++: cd ~/exercism/cpp/hello-world && make" echo " C++: cd ~/exercism/cpp/hello-world && make"
echo " Rust: cd ~/exercism/rust/hello-world && cargo test" echo " Rust: cd ~/exercism/rust/hello-world && cargo test"
echo " Go: cd ~/exercism/go/hello-world && go test" echo " Go: cd ~/exercism/go/hello-world && go test"
echo "" echo ""
echo -e "${CYAN}Submit solution (when online):${NC}" echo -e "${CYAN}Submit solution (when online):${NC}"
echo " exercism submit solution.py" echo " exercism submit solution.py"
echo "" echo ""
echo -e "${CYAN}Popular tracks:${NC}" echo -e "${CYAN}Popular tracks:${NC}"
echo " python, javascript, typescript, c, cpp, rust, go, java, ruby" echo " python, javascript, typescript, c, cpp, rust, go, java, ruby"
echo " bash, elixir, haskell, kotlin, swift, csharp, php, sql" echo " bash, elixir, haskell, kotlin, swift, csharp, php, sql"
echo "" echo ""
echo -e "${CYAN}Batch download (requires API token):${NC}" echo -e "${CYAN}Batch download (requires API token):${NC}"
echo " # Download first 20 Python exercises:" echo " # Download first 20 Python exercises:"
echo " for ex in \$(exercism download --track=python 2>&1 | head -20); do" echo ' for ex in $(exercism download --track=python 2>&1 | head -20); do'
echo " exercism download --track=python --exercise=\$ex" echo ' exercism download --track=python --exercise=$ex'
echo " done" echo " done"
echo "" echo ""
echo "Exercises are in: $EXERCISM_DIR" echo "Exercises are in: $EXERCISM_DIR"
echo "" echo ""
echo "==============================================" echo "=============================================="
} }
# Main # Main
main() { main() {
# Step 1: Install CLI # Step 1: Install CLI
echo "" echo ""
echo "=== Step 1: Installing Exercism CLI ===" echo "=== Step 1: Installing Exercism CLI ==="
install_exercism_cli install_exercism_cli
# Step 2: Configure # Step 2: Configure
configure_exercism configure_exercism
# Step 3: Install test runners # Step 3: Install test runners
install_test_runners install_test_runners
# Step 4: Download sample exercises # Step 4: Download sample exercises
echo "" echo ""
echo "=== Step 4: Downloading Sample Exercises ===" echo "=== Step 4: Downloading Sample Exercises ==="
echo "" echo ""
echo "Downloading a few starter exercises for common languages..." echo "Downloading a few starter exercises for common languages..."
echo "(Full download requires API token from exercism.org)" echo "(Full download requires API token from exercism.org)"
echo "" echo ""
# Try to download hello-world for each track # Try to download hello-world for each track
local tracks=("python" "javascript" "typescript" "c" "cpp") local tracks=("python" "javascript" "typescript" "c" "cpp")
for track in "${tracks[@]}"; do for track in "${tracks[@]}"; do
local exercise_dir="$EXERCISM_DIR/$track/hello-world" local exercise_dir="$EXERCISM_DIR/$track/hello-world"
if [[ -d "$exercise_dir" ]]; then if [[ -d $exercise_dir ]]; then
echo " [$track] hello-world already exists" echo " [$track] hello-world already exists"
else else
if exercism download --track="$track" --exercise="hello-world" 2>/dev/null; then if exercism download --track="$track" --exercise="hello-world" 2> /dev/null; then
success "[$track] hello-world downloaded" success "[$track] hello-world downloaded"
else else
warn "[$track] hello-world requires authentication" warn "[$track] hello-world requires authentication"
fi fi
fi fi
done done
# Show usage # Show usage
show_usage show_usage
echo "" echo ""
success "Installation complete!" success "Installation complete!"
echo "" echo ""
echo "Next steps:" echo "Next steps:"
echo " 1. Sign up at https://exercism.org (free)" echo " 1. Sign up at https://exercism.org (free)"
echo " 2. Get your token from https://exercism.org/settings/api_cli" echo " 2. Get your token from https://exercism.org/settings/api_cli"
echo " 3. Run: exercism configure --token=YOUR_TOKEN" echo " 3. Run: exercism configure --token=YOUR_TOKEN"
echo " 4. Download exercises and code offline!" echo " 4. Download exercises and code offline!"
echo "" echo ""
} }
main "$@" main "$@"

View File

@ -27,202 +27,202 @@ echo ""
# Detect package manager and install Zeal # Detect package manager and install Zeal
install_zeal() { install_zeal() {
if command -v zeal &>/dev/null; then if command -v zeal &> /dev/null; then
success "Zeal is already installed" success "Zeal is already installed"
return 0 return 0
fi fi
echo "Installing Zeal offline documentation browser..." echo "Installing Zeal offline documentation browser..."
if command -v pacman &>/dev/null; then if command -v pacman &> /dev/null; then
# Arch Linux # Arch Linux
sudo pacman -S --noconfirm zeal sudo pacman -S --noconfirm zeal
elif command -v apt &>/dev/null; then elif command -v apt &> /dev/null; then
# Debian/Ubuntu # Debian/Ubuntu
sudo apt update sudo apt update
sudo apt install -y zeal sudo apt install -y zeal
elif command -v dnf &>/dev/null; then elif command -v dnf &> /dev/null; then
# Fedora # Fedora
sudo dnf install -y zeal sudo dnf install -y zeal
elif command -v zypper &>/dev/null; then elif command -v zypper &> /dev/null; then
# openSUSE # openSUSE
sudo zypper install -y zeal sudo zypper install -y zeal
elif command -v flatpak &>/dev/null; then elif command -v flatpak &> /dev/null; then
# Flatpak fallback # Flatpak fallback
flatpak install -y flathub org.zealdocs.Zeal flatpak install -y flathub org.zealdocs.Zeal
else else
error "Could not detect package manager. Please install Zeal manually:" error "Could not detect package manager. Please install Zeal manually:"
echo " https://zealdocs.org/download.html" echo " https://zealdocs.org/download.html"
return 1 return 1
fi fi
success "Zeal installed successfully" success "Zeal installed successfully"
} }
# Get Zeal docsets directory # Get Zeal docsets directory
get_docsets_dir() { get_docsets_dir() {
local docsets_dir local docsets_dir
# Check if using Flatpak # Check if using Flatpak
if command -v flatpak &>/dev/null && flatpak list | grep -q "org.zealdocs.Zeal"; then if command -v flatpak &> /dev/null && flatpak list | grep -q "org.zealdocs.Zeal"; then
docsets_dir="$HOME/.var/app/org.zealdocs.Zeal/data/Zeal/Zeal/docsets" docsets_dir="$HOME/.var/app/org.zealdocs.Zeal/data/Zeal/Zeal/docsets"
else else
# Standard installation # Standard installation
docsets_dir="$HOME/.local/share/Zeal/Zeal/docsets" docsets_dir="$HOME/.local/share/Zeal/Zeal/docsets"
fi fi
mkdir -p "$docsets_dir" mkdir -p "$docsets_dir"
echo "$docsets_dir" echo "$docsets_dir"
} }
# Download a docset from Zeal feeds # Download a docset from Zeal feeds
download_docset() { download_docset() {
local name="$1" local name="$1"
local docsets_dir="$2" local docsets_dir="$2"
# Check if already installed # Check if already installed
if [ -d "$docsets_dir/${name}.docset" ]; then if [ -d "$docsets_dir/${name}.docset" ]; then
warn "$name docset already installed" warn "$name docset already installed"
return 0 return 0
fi fi
info "Downloading $name documentation..." info "Downloading $name documentation..."
# Use Zeal's built-in feed system via CLI or direct download # Use Zeal's built-in feed system via CLI or direct download
# Zeal stores docsets in .docset directories # Zeal stores docsets in .docset directories
# Try to get from dash-user-contributions or official feeds # Try to get from dash-user-contributions or official feeds
local download_url="" local download_url=""
case "$name" in case "$name" in
"C") "C")
download_url="http://kapeli.com/feeds/C.tgz" download_url="http://kapeli.com/feeds/C.tgz"
;; ;;
"C++") "C++")
download_url="http://kapeli.com/feeds/C%2B%2B.tgz" download_url="http://kapeli.com/feeds/C%2B%2B.tgz"
;; ;;
"JavaScript") "JavaScript")
download_url="http://kapeli.com/feeds/JavaScript.tgz" download_url="http://kapeli.com/feeds/JavaScript.tgz"
;; ;;
"TypeScript") "TypeScript")
download_url="http://kapeli.com/feeds/TypeScript.tgz" download_url="http://kapeli.com/feeds/TypeScript.tgz"
;; ;;
"Python_3") "Python_3")
download_url="http://kapeli.com/feeds/Python_3.tgz" download_url="http://kapeli.com/feeds/Python_3.tgz"
;; ;;
"Python_2") "Python_2")
download_url="http://kapeli.com/feeds/Python_2.tgz" download_url="http://kapeli.com/feeds/Python_2.tgz"
;; ;;
"Bash") "Bash")
download_url="http://kapeli.com/feeds/Bash.tgz" download_url="http://kapeli.com/feeds/Bash.tgz"
;; ;;
"HTML") "HTML")
download_url="http://kapeli.com/feeds/HTML.tgz" download_url="http://kapeli.com/feeds/HTML.tgz"
;; ;;
"CSS") "CSS")
download_url="http://kapeli.com/feeds/CSS.tgz" download_url="http://kapeli.com/feeds/CSS.tgz"
;; ;;
"NodeJS") "NodeJS")
download_url="http://kapeli.com/feeds/NodeJS.tgz" download_url="http://kapeli.com/feeds/NodeJS.tgz"
;; ;;
"React") "React")
download_url="http://kapeli.com/feeds/React.tgz" download_url="http://kapeli.com/feeds/React.tgz"
;; ;;
*) *)
warn "Unknown docset: $name" warn "Unknown docset: $name"
return 1 return 1
;; ;;
esac esac
# Download and extract # Download and extract
local temp_file local temp_file
temp_file=$(mktemp) temp_file=$(mktemp)
echo " URL: $download_url" echo " URL: $download_url"
if curl -fL --progress-bar "$download_url" -o "$temp_file"; then if curl -fL --progress-bar "$download_url" -o "$temp_file"; then
echo " Extracting to $docsets_dir..." echo " Extracting to $docsets_dir..."
tar -xzf "$temp_file" -C "$docsets_dir" tar -xzf "$temp_file" -C "$docsets_dir"
rm -f "$temp_file" rm -f "$temp_file"
success "$name documentation downloaded" success "$name documentation downloaded"
else else
rm -f "$temp_file" rm -f "$temp_file"
warn "Failed to download $name - you can install it from Zeal's UI" warn "Failed to download $name - you can install it from Zeal's UI"
return 1 return 1
fi fi
} }
# Main installation # Main installation
main() { main() {
# Step 1: Install Zeal # Step 1: Install Zeal
echo "" echo ""
echo "=== Step 1: Installing Zeal ===" echo "=== Step 1: Installing Zeal ==="
install_zeal || exit 1 install_zeal || exit 1
# Step 2: Get docsets directory # Step 2: Get docsets directory
echo "" echo ""
echo "=== Step 2: Preparing docsets directory ===" echo "=== Step 2: Preparing docsets directory ==="
local docsets_dir local docsets_dir
docsets_dir=$(get_docsets_dir) docsets_dir=$(get_docsets_dir)
success "Docsets directory: $docsets_dir" success "Docsets directory: $docsets_dir"
# Step 3: Download requested docsets # Step 3: Download requested docsets
echo "" echo ""
echo "=== Step 3: Downloading Documentation ===" echo "=== Step 3: Downloading Documentation ==="
echo "" echo ""
# Core requested languages # Core requested languages
local docsets=("C" "C++" "JavaScript" "TypeScript" "Python_3") local docsets=("C" "C++" "JavaScript" "TypeScript" "Python_3")
# Optional extras (comment out if not needed) # Optional extras (comment out if not needed)
local extras=("Bash" "HTML" "CSS" "NodeJS") local extras=("Bash" "HTML" "CSS" "NodeJS")
# Download core docsets # Download core docsets
for docset in "${docsets[@]}"; do for docset in "${docsets[@]}"; do
download_docset "$docset" "$docsets_dir" download_docset "$docset" "$docsets_dir"
done done
# Ask about extras # Ask about extras
echo "" echo ""
read -r -p "Install additional docsets (Bash, HTML, CSS, NodeJS)? [Y/n] " response read -r -p "Install additional docsets (Bash, HTML, CSS, NodeJS)? [Y/n] " response
if [[ ! "$response" =~ ^[Nn]$ ]]; then if [[ ! $response =~ ^[Nn]$ ]]; then
for docset in "${extras[@]}"; do for docset in "${extras[@]}"; do
download_docset "$docset" "$docsets_dir" download_docset "$docset" "$docsets_dir"
done done
fi fi
# Summary # Summary
echo "" echo ""
echo "==============================================" echo "=============================================="
echo " Installation Complete!" echo " Installation Complete!"
echo "==============================================" echo "=============================================="
echo "" echo ""
echo "Installed documentation:" echo "Installed documentation:"
for f in "$docsets_dir"/*.docset; do for f in "$docsets_dir"/*.docset; do
if [[ -d "$f" ]]; then if [[ -d $f ]]; then
echo "$(basename "$f" .docset)" echo "$(basename "$f" .docset)"
fi fi
done done
echo "" echo ""
echo "Usage:" echo "Usage:"
echo " Launch Zeal from your application menu, or run: zeal" echo " Launch Zeal from your application menu, or run: zeal"
echo "" echo ""
echo "To download additional docsets:" echo "To download additional docsets:"
echo " 1. Open Zeal" echo " 1. Open Zeal"
echo " 2. Go to Tools → Docsets" echo " 2. Go to Tools → Docsets"
echo " 3. Click 'Available' tab and download what you need" echo " 3. Click 'Available' tab and download what you need"
echo "" echo ""
echo "Keyboard shortcut tip:" echo "Keyboard shortcut tip:"
echo " Set a global hotkey in Zeal → Preferences → Global Shortcuts" echo " Set a global hotkey in Zeal → Preferences → Global Shortcuts"
echo " (e.g., Alt+Space for quick documentation lookup)" echo " (e.g., Alt+Space for quick documentation lookup)"
echo "" echo ""
echo "==============================================" echo "=============================================="
# Offer to launch Zeal # Offer to launch Zeal
read -r -p "Launch Zeal now? [y/N] " response read -r -p "Launch Zeal now? [y/N] " response
if [[ "$response" =~ ^[Yy]$ ]]; then if [[ $response =~ ^[Yy]$ ]]; then
nohup zeal &>/dev/null & nohup zeal &> /dev/null &
success "Zeal launched" success "Zeal launched"
fi fi
} }
main "$@" main "$@"

View File

@ -39,18 +39,18 @@ echo ""
echo "=== 1. Installing Python NLP-based Plagiarism Tools ===" echo "=== 1. Installing Python NLP-based Plagiarism Tools ==="
# Check for Python 3 # Check for Python 3
if ! command -v python3 &>/dev/null; then if ! command -v python3 &> /dev/null; then
error "Python 3 is required but not installed." error "Python 3 is required but not installed."
exit 1 exit 1
fi fi
# Create virtual environment # Create virtual environment
if [ ! -d "$VENV_DIR" ]; then if [ ! -d "$VENV_DIR" ]; then
echo "Creating Python virtual environment..." echo "Creating Python virtual environment..."
python3 -m venv "$VENV_DIR" python3 -m venv "$VENV_DIR"
success "Virtual environment created at $VENV_DIR" success "Virtual environment created at $VENV_DIR"
else else
warn "Virtual environment already exists at $VENV_DIR" warn "Virtual environment already exists at $VENV_DIR"
fi fi
# Activate and install packages # Activate and install packages
@ -60,19 +60,19 @@ echo "Installing Python packages for text similarity detection..."
pip install --upgrade pip pip install --upgrade pip
pip install --progress-bar on \ pip install --progress-bar on \
scikit-learn \ scikit-learn \
nltk \ nltk \
spacy \ spacy \
gensim \ gensim \
numpy \ numpy \
pandas \ pandas \
python-docx \ python-docx \
PyPDF2 \ PyPDF2 \
beautifulsoup4 \ beautifulsoup4 \
lxml \ lxml \
textdistance \ textdistance \
fuzzywuzzy \ fuzzywuzzy \
python-Levenshtein python-Levenshtein
success "Python NLP packages installed" success "Python NLP packages installed"
@ -90,11 +90,11 @@ success "NLTK data downloaded"
# Download spaCy English model (small) # Download spaCy English model (small)
echo "Downloading spaCy English model..." echo "Downloading spaCy English model..."
python3 -m spacy download en_core_web_sm 2>/dev/null || warn "spaCy model download may need manual install: python -m spacy download en_core_web_sm" python3 -m spacy download en_core_web_sm 2> /dev/null || warn "spaCy model download may need manual install: python -m spacy download en_core_web_sm"
success "spaCy model installed" success "spaCy model installed"
# Create a simple plagiarism checker script # Create a simple plagiarism checker script
cat >"$INSTALL_DIR/check_plagiarism.py" <<'PYEOF' cat > "$INSTALL_DIR/check_plagiarism.py" << 'PYEOF'
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Simple Text Plagiarism Checker Simple Text Plagiarism Checker
@ -325,7 +325,7 @@ success "Created plagiarism checker script at $INSTALL_DIR/check_plagiarism.py"
# Create convenience wrapper # Create convenience wrapper
mkdir -p "$HOME/.local/bin" mkdir -p "$HOME/.local/bin"
cat >"$HOME/.local/bin/plagcheck" <<WRAPEOF cat > "$HOME/.local/bin/plagcheck" << WRAPEOF
#!/usr/bin/env bash #!/usr/bin/env bash
# Wrapper for plagiarism checker # Wrapper for plagiarism checker
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
@ -344,14 +344,14 @@ echo "=== 2. Installing Sherlock Text Plagiarism Detector ==="
SHERLOCK_DIR="$INSTALL_DIR/sherlock" SHERLOCK_DIR="$INSTALL_DIR/sherlock"
if [ ! -d "$SHERLOCK_DIR" ]; then if [ ! -d "$SHERLOCK_DIR" ]; then
# There are several Sherlock implementations; using a popular Python one # There are several Sherlock implementations; using a popular Python one
if command -v git &>/dev/null; then if command -v git &> /dev/null; then
# Clone a text-based similarity tool # Clone a text-based similarity tool
git clone --depth 1 https://github.com/Zedeldi/sherlock-py.git "$SHERLOCK_DIR" 2>/dev/null || { git clone --depth 1 https://github.com/Zedeldi/sherlock-py.git "$SHERLOCK_DIR" 2> /dev/null || {
warn "Could not clone sherlock-py, trying alternative..." warn "Could not clone sherlock-py, trying alternative..."
# Alternative: Create a simple n-gram based sherlock # Alternative: Create a simple n-gram based sherlock
mkdir -p "$SHERLOCK_DIR" mkdir -p "$SHERLOCK_DIR"
cat >"$SHERLOCK_DIR/sherlock.py" <<'SHERLOCKEOF' cat > "$SHERLOCK_DIR/sherlock.py" << 'SHERLOCKEOF'
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Sherlock - Simple text plagiarism detector using n-gram fingerprinting. Sherlock - Simple text plagiarism detector using n-gram fingerprinting.
@ -443,14 +443,14 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() main()
SHERLOCKEOF SHERLOCKEOF
chmod +x "$SHERLOCK_DIR/sherlock.py" chmod +x "$SHERLOCK_DIR/sherlock.py"
} }
success "Sherlock installed at $SHERLOCK_DIR" success "Sherlock installed at $SHERLOCK_DIR"
else else
warn "Git not available, skipping Sherlock installation" warn "Git not available, skipping Sherlock installation"
fi fi
else else
warn "Sherlock already installed at $SHERLOCK_DIR" warn "Sherlock already installed at $SHERLOCK_DIR"
fi fi
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -459,17 +459,17 @@ fi
echo "" echo ""
echo "=== 3. Checking for Ferret (Java-based plagiarism tool) ===" echo "=== 3. Checking for Ferret (Java-based plagiarism tool) ==="
if command -v java &>/dev/null; then if command -v java &> /dev/null; then
FERRET_DIR="$INSTALL_DIR/ferret" FERRET_DIR="$INSTALL_DIR/ferret"
if [ ! -d "$FERRET_DIR" ]; then if [ ! -d "$FERRET_DIR" ]; then
mkdir -p "$FERRET_DIR" mkdir -p "$FERRET_DIR"
echo "Ferret is a Java-based tool from University of Hertfordshire." echo "Ferret is a Java-based tool from University of Hertfordshire."
echo "Download manually from: https://homepages.herts.ac.uk/~comqcln/Ferret/" echo "Download manually from: https://homepages.herts.ac.uk/~comqcln/Ferret/"
echo "Place JAR file in: $FERRET_DIR" echo "Place JAR file in: $FERRET_DIR"
warn "Ferret requires manual download (academic license)" warn "Ferret requires manual download (academic license)"
fi fi
else else
warn "Java not installed, skipping Ferret" warn "Java not installed, skipping Ferret"
fi fi
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -478,16 +478,16 @@ fi
echo "" echo ""
echo "=== 4. WCopyfind Information (Windows tool, needs Wine) ===" echo "=== 4. WCopyfind Information (Windows tool, needs Wine) ==="
if command -v wine &>/dev/null; then if command -v wine &> /dev/null; then
echo "Wine is available. WCopyfind can be run via Wine." echo "Wine is available. WCopyfind can be run via Wine."
echo "Download from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/" echo "Download from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/"
echo "Run with: wine /path/to/WCopyfind.exe" echo "Run with: wine /path/to/WCopyfind.exe"
warn "WCopyfind requires manual download" warn "WCopyfind requires manual download"
else else
echo "Wine not installed. To use WCopyfind:" echo "Wine not installed. To use WCopyfind:"
echo " 1. Install wine: sudo apt install wine (or equivalent)" echo " 1. Install wine: sudo apt install wine (or equivalent)"
echo " 2. Download WCopyfind from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/" echo " 2. Download WCopyfind from: https://plagiarism.bloomfieldmedia.com/software/wcopyfind/"
warn "WCopyfind skipped (Wine not available)" warn "WCopyfind skipped (Wine not available)"
fi fi
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -527,6 +527,6 @@ echo "=============================================="
# Add to PATH reminder # Add to PATH reminder
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
warn "Add ~/.local/bin to your PATH by adding this to ~/.bashrc or ~/.zshrc:" warn "Add ~/.local/bin to your PATH by adding this to ~/.bashrc or ~/.zshrc:"
echo ' export PATH="$HOME/.local/bin:$PATH"' echo ' export PATH="$HOME/.local/bin:$PATH"'
fi fi

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ OUTPUT_FORMAT="jpg"
PDF_FILES=() PDF_FILES=()
usage() { usage() {
cat <<EOF cat << EOF
Usage: Usage:
$(basename "$0") [OPTIONS] PDF_FILE [PDF_FILE...] $(basename "$0") [OPTIONS] PDF_FILE [PDF_FILE...]
@ -36,80 +36,80 @@ EOF
} }
ensure_magick() { ensure_magick() {
require_imagemagick "magick" || exit 1 require_imagemagick "magick" || exit 1
} }
parse_args() { parse_args() {
local opt local opt
OUTPUT_DIR="" OUTPUT_DIR=""
OUTPUT_FORMAT="jpg" OUTPUT_FORMAT="jpg"
PDF_FILES=() PDF_FILES=()
while getopts ":o:f:h" opt; do while getopts ":o:f:h" opt; do
case "$opt" in case "$opt" in
o) o)
OUTPUT_DIR="$OPTARG" OUTPUT_DIR="$OPTARG"
;; ;;
f) f)
OUTPUT_FORMAT="$OPTARG" OUTPUT_FORMAT="$OPTARG"
;; ;;
h) h)
usage usage
exit 0 exit 0
;; ;;
*) *)
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
shift $((OPTIND - 1)) shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then if [[ $# -lt 1 ]]; then
echo "Error: at least one PDF file must be specified." >&2 echo "Error: at least one PDF file must be specified." >&2
usage usage
exit 1 exit 1
fi fi
PDF_FILES=("$@") PDF_FILES=("$@")
if [[ -z ${OUTPUT_DIR:-} ]]; then if [[ -z ${OUTPUT_DIR:-} ]]; then
OUTPUT_DIR="${PWD}" OUTPUT_DIR="${PWD}"
fi fi
if [[ ! -d $OUTPUT_DIR ]]; then if [[ ! -d $OUTPUT_DIR ]]; then
mkdir -p "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR"
fi fi
} }
convert_pdf() { convert_pdf() {
local pdf_file="$1" local pdf_file="$1"
local base name out_pattern local base name out_pattern
name="$(basename "$pdf_file")" name="$(basename "$pdf_file")"
base="${name%.*}" base="${name%.*}"
out_pattern="${OUTPUT_DIR%/}/${base}_page-" out_pattern="${OUTPUT_DIR%/}/${base}_page-"
log "Converting '$pdf_file' to $OUTPUT_FORMAT using magick -> ${out_pattern}*.${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}" magick -density 300 "$pdf_file" -quality 90 "${out_pattern}%d.${OUTPUT_FORMAT}"
} }
main() { main() {
ensure_magick ensure_magick
parse_args "$@" parse_args "$@"
local pdf local pdf
for pdf in "${PDF_FILES[@]}"; do for pdf in "${PDF_FILES[@]}"; do
if [[ ! -f $pdf ]]; then if [[ ! -f $pdf ]]; then
echo "Warning: '$pdf' is not a regular file, skipping." >&2 echo "Warning: '$pdf' is not a regular file, skipping." >&2
continue continue
fi fi
convert_pdf "$pdf" convert_pdf "$pdf"
done done
log "Done converting PDFs to ${OUTPUT_FORMAT}. Output directory: $OUTPUT_DIR" log "Done converting PDFs to ${OUTPUT_FORMAT}. Output directory: $OUTPUT_DIR"
} }
main "$@" main "$@"

View File

@ -45,37 +45,37 @@ NC='\033[0m'
# Helper Functions (all print to stderr to not interfere with return values) # Helper Functions (all print to stderr to not interfere with return values)
#============================================================================== #==============================================================================
print_header() { print_header() {
echo -e "\n${BOLD}${CYAN}════════════════════════════════════════════════════════════${NC}" >&2 echo -e "\n${BOLD}${CYAN}════════════════════════════════════════════════════════════${NC}" >&2
echo -e "${BOLD}${CYAN} $1${NC}" >&2 echo -e "${BOLD}${CYAN} $1${NC}" >&2
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════${NC}\n" >&2 echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════${NC}\n" >&2
} }
print_step() { print_step() {
echo -e "${BOLD}${BLUE}$1${NC}" >&2 echo -e "${BOLD}${BLUE}$1${NC}" >&2
} }
print_success() { print_success() {
echo -e "${GREEN}$1${NC}" >&2 echo -e "${GREEN}$1${NC}" >&2
} }
print_error() { print_error() {
echo -e "${RED}$1${NC}" >&2 echo -e "${RED}$1${NC}" >&2
} }
print_info() { print_info() {
echo -e "${YELLOW}$1${NC}" >&2 echo -e "${YELLOW}$1${NC}" >&2
} }
cleanup() { cleanup() {
if [ -d "$WORK_DIR" ] && [ "$WORK_DIR" != "/" ]; then if [ -d "$WORK_DIR" ] && [ "$WORK_DIR" != "/" ]; then
rm -rf "$WORK_DIR" rm -rf "$WORK_DIR"
fi fi
} }
trap cleanup EXIT trap cleanup EXIT
usage() { usage() {
cat << EOF cat << EOF
repo_to_study.sh - Generate study materials from any repository repo_to_study.sh - Generate study materials from any repository
USAGE: USAGE:
@ -99,54 +99,54 @@ OUTPUT FILES:
analysis/ - Raw analysis data (imports, keywords, functions) analysis/ - Raw analysis data (imports, keywords, functions)
EOF EOF
exit 0 exit 0
} }
#============================================================================== #==============================================================================
# Check Dependencies # Check Dependencies
#============================================================================== #==============================================================================
check_dependencies() { check_dependencies() {
local missing=() local missing=()
# Check for required scripts # Check for required scripts
if [ ! -x "$ANALYZE_SCRIPT" ]; then if [ ! -x "$ANALYZE_SCRIPT" ]; then
missing+=("analyze_repo.sh not found at $ANALYZE_SCRIPT") missing+=("analyze_repo.sh not found at $ANALYZE_SCRIPT")
fi
if [ ! -x "$STUDY_SCRIPT" ]; then
missing+=("generate_study_materials.sh not found at $STUDY_SCRIPT")
fi
# Check for basic tools
for cmd in git curl grep sed awk; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi fi
done
if [ ! -x "$STUDY_SCRIPT" ]; then if [ ${#missing[@]} -gt 0 ]; then
missing+=("generate_study_materials.sh not found at $STUDY_SCRIPT") print_error "Missing dependencies:"
fi for dep in "${missing[@]}"; do
echo " - $dep"
# Check for basic tools
for cmd in git curl grep sed awk; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done done
exit 1
if [ ${#missing[@]} -gt 0 ]; then fi
print_error "Missing dependencies:"
for dep in "${missing[@]}"; do
echo " - $dep"
done
exit 1
fi
} }
#============================================================================== #==============================================================================
# Ensure Offline Docs are Available # Ensure Offline Docs are Available
#============================================================================== #==============================================================================
ensure_offline_docs() { ensure_offline_docs() {
local docs_dir="$HOME/.local/share/offline-docs" local docs_dir="$HOME/.local/share/offline-docs"
if [ ! -d "$docs_dir/python" ]; then if [ ! -d "$docs_dir/python" ]; then
print_info "Offline docs not found. Setting up Python documentation..." print_info "Offline docs not found. Setting up Python documentation..."
if [ -x "$SETUP_DOCS_SCRIPT" ]; then if [ -x "$SETUP_DOCS_SCRIPT" ]; then
"$SETUP_DOCS_SCRIPT" --python "$SETUP_DOCS_SCRIPT" --python
else else
print_info "Run setup_offline_docs.sh --all to enable offline documentation" print_info "Run setup_offline_docs.sh --all to enable offline documentation"
fi
fi fi
fi
} }
# Global to store repo name for cloned repos # Global to store repo name for cloned repos
@ -156,209 +156,209 @@ REPO_NAME=""
# Get Repository # Get Repository
#============================================================================== #==============================================================================
get_repo() { get_repo() {
local input="$1" local input="$1"
local repo_dir="" local repo_dir=""
# Check if it's a URL (git clone needed) # Check if it's a URL (git clone needed)
if [[ "$input" =~ ^https?:// ]] || [[ "$input" =~ ^git@ ]]; then if [[ $input =~ ^https?:// ]] || [[ $input =~ ^git@ ]]; then
print_step "Cloning repository..." print_step "Cloning repository..."
# Extract repo name from URL # Extract repo name from URL
REPO_NAME=$(basename "$input" .git) REPO_NAME=$(basename "$input" .git)
repo_dir="$WORK_DIR/$REPO_NAME" repo_dir="$WORK_DIR/$REPO_NAME"
mkdir -p "$WORK_DIR" mkdir -p "$WORK_DIR"
if git clone --depth 1 "$input" "$repo_dir" >&2 2>&1; then if git clone --depth 1 "$input" "$repo_dir" >&2 2>&1; then
print_success "Cloned: $input" print_success "Cloned: $input"
else
print_error "Failed to clone repository"
exit 1
fi
echo "$repo_dir"
# Local path
elif [ -d "$input" ]; then
# Convert to absolute path
repo_dir="$(cd "$input" && pwd)"
REPO_NAME=$(basename "$repo_dir")
print_success "Using local repository: $repo_dir"
echo "$repo_dir"
else else
print_error "Invalid input: '$input' is not a valid URL or directory" print_error "Failed to clone repository"
exit 1 exit 1
fi fi
echo "$repo_dir"
# Local path
elif [ -d "$input" ]; then
# Convert to absolute path
repo_dir="$(cd "$input" && pwd)"
REPO_NAME=$(basename "$repo_dir")
print_success "Using local repository: $repo_dir"
echo "$repo_dir"
else
print_error "Invalid input: '$input' is not a valid URL or directory"
exit 1
fi
} }
#============================================================================== #==============================================================================
# Analyze Repository # Analyze Repository
#============================================================================== #==============================================================================
analyze_repo() { analyze_repo() {
local repo_path="$1" local repo_path="$1"
local repo_name="$REPO_NAME" local repo_name="$REPO_NAME"
[ -z "$repo_name" ] && repo_name=$(basename "$repo_path") [ -z "$repo_name" ] && repo_name=$(basename "$repo_path")
print_step "Analyzing repository..." print_step "Analyzing repository..."
# Run the analyzer (it outputs to stderr/stdout, results go to /tmp/repo_analysis/) # Run the analyzer (it outputs to stderr/stdout, results go to /tmp/repo_analysis/)
"$ANALYZE_SCRIPT" "$repo_path" >&2 || true "$ANALYZE_SCRIPT" "$repo_path" >&2 || true
# Find the results directory # Find the results directory
local results_dir="/tmp/repo_analysis/results_${repo_name}" local results_dir="/tmp/repo_analysis/results_${repo_name}"
if [ ! -d "$results_dir" ]; then if [ ! -d "$results_dir" ]; then
# Try without prefix # Try without prefix
results_dir="/tmp/repo_analysis/results" results_dir="/tmp/repo_analysis/results"
fi fi
if [ ! -d "$results_dir" ] || [ ! -d "$results_dir/per_language" ]; then if [ ! -d "$results_dir" ] || [ ! -d "$results_dir/per_language" ]; then
print_error "Could not find analysis results at $results_dir" print_error "Could not find analysis results at $results_dir"
exit 1 exit 1
fi fi
print_success "Analysis complete: $results_dir" print_success "Analysis complete: $results_dir"
echo "$results_dir" echo "$results_dir"
} }
#============================================================================== #==============================================================================
# Generate Study Materials # Generate Study Materials
#============================================================================== #==============================================================================
generate_materials() { generate_materials() {
local analysis_dir="$1" local analysis_dir="$1"
local output_dir="$2" local output_dir="$2"
print_step "Generating study materials with offline documentation..." print_step "Generating study materials with offline documentation..."
# Run study materials generator # Run study materials generator
cd "$analysis_dir" cd "$analysis_dir"
if "$STUDY_SCRIPT" . 2>/dev/null | grep -E "^(Created|✓|Files created)" | head -5; then if "$STUDY_SCRIPT" . 2> /dev/null | grep -E "^(Created|✓|Files created)" | head -5; then
print_success "Study materials generated" print_success "Study materials generated"
else else
# Try anyway, might have succeeded # Try anyway, might have succeeded
true true
fi fi
# Create output directory and copy results # Create output directory and copy results
mkdir -p "$output_dir" mkdir -p "$output_dir"
# Copy generated files # Copy generated files
[ -f "documentation_links.md" ] && cp "documentation_links.md" "$output_dir/" [ -f "documentation_links.md" ] && cp "documentation_links.md" "$output_dir/"
[ -f "anki_cards.txt" ] && cp "anki_cards.txt" "$output_dir/" [ -f "anki_cards.txt" ] && cp "anki_cards.txt" "$output_dir/"
[ -f "llm_anki_prompt.md" ] && cp "llm_anki_prompt.md" "$output_dir/" [ -f "llm_anki_prompt.md" ] && cp "llm_anki_prompt.md" "$output_dir/"
# Copy analysis data # Copy analysis data
mkdir -p "$output_dir/analysis" mkdir -p "$output_dir/analysis"
[ -d "per_language" ] && cp -r "per_language" "$output_dir/analysis/" [ -d "per_language" ] && cp -r "per_language" "$output_dir/analysis/"
[ -f "grep_imports.txt" ] && cp "grep_imports.txt" "$output_dir/analysis/" [ -f "grep_imports.txt" ] && cp "grep_imports.txt" "$output_dir/analysis/"
[ -f "grep_keywords.txt" ] && cp "grep_keywords.txt" "$output_dir/analysis/" [ -f "grep_keywords.txt" ] && cp "grep_keywords.txt" "$output_dir/analysis/"
[ -f "grep_function_calls.txt" ] && cp "grep_function_calls.txt" "$output_dir/analysis/" [ -f "grep_function_calls.txt" ] && cp "grep_function_calls.txt" "$output_dir/analysis/"
print_success "Files saved to: $output_dir" print_success "Files saved to: $output_dir"
} }
#============================================================================== #==============================================================================
# Show Summary # Show Summary
#============================================================================== #==============================================================================
show_summary() { show_summary() {
local output_dir="$1" local output_dir="$1"
print_header "Study Materials Ready!" print_header "Study Materials Ready!"
echo -e "${BOLD}Output directory:${NC} $output_dir" echo -e "${BOLD}Output directory:${NC} $output_dir"
echo "" echo ""
echo -e "${BOLD}Generated files:${NC}" echo -e "${BOLD}Generated files:${NC}"
if [ -f "$output_dir/documentation_links.md" ]; then if [ -f "$output_dir/documentation_links.md" ]; then
local doc_lines local doc_lines
doc_lines=$(wc -l < "$output_dir/documentation_links.md") doc_lines=$(wc -l < "$output_dir/documentation_links.md")
echo -e " 📚 ${GREEN}documentation_links.md${NC} ($doc_lines lines)" echo -e " 📚 ${GREEN}documentation_links.md${NC} ($doc_lines lines)"
echo " Contains links to OFFLINE documentation" echo " Contains links to OFFLINE documentation"
fi fi
if [ -f "$output_dir/anki_cards.txt" ]; then if [ -f "$output_dir/anki_cards.txt" ]; then
local card_count local card_count
card_count=$(grep -c $'^\w' "$output_dir/anki_cards.txt" 2>/dev/null || echo "0") card_count=$(grep -c $'^\w' "$output_dir/anki_cards.txt" 2> /dev/null || echo "0")
echo -e " 🎴 ${GREEN}anki_cards.txt${NC} (~$card_count cards)" echo -e " 🎴 ${GREEN}anki_cards.txt${NC} (~$card_count cards)"
echo " Import to Anki: File → Import → Tab separated" echo " Import to Anki: File → Import → Tab separated"
fi fi
if [ -f "$output_dir/llm_anki_prompt.md" ]; then if [ -f "$output_dir/llm_anki_prompt.md" ]; then
echo -e " 🤖 ${GREEN}llm_anki_prompt.md${NC}" echo -e " 🤖 ${GREEN}llm_anki_prompt.md${NC}"
echo " Use with ChatGPT/Claude to generate more cards" echo " Use with ChatGPT/Claude to generate more cards"
fi fi
if [ -d "$output_dir/analysis" ]; then if [ -d "$output_dir/analysis" ]; then
echo -e " 📊 ${GREEN}analysis/${NC}" echo -e " 📊 ${GREEN}analysis/${NC}"
echo " Raw analysis data (imports, keywords, functions per language)" echo " Raw analysis data (imports, keywords, functions per language)"
fi fi
echo "" echo ""
echo -e "${BOLD}Quick preview of imports with offline docs:${NC}" echo -e "${BOLD}Quick preview of imports with offline docs:${NC}"
if [ -f "$output_dir/documentation_links.md" ]; then if [ -f "$output_dir/documentation_links.md" ]; then
grep -A20 "import/from" "$output_dir/documentation_links.md" 2>/dev/null | \ grep -A20 "import/from" "$output_dir/documentation_links.md" 2> /dev/null |
grep "^\| \`" | head -5 | \ grep "^\| \`" | head -5 |
sed 's/|/│/g' sed 's/|/│/g'
fi fi
echo "" echo ""
echo -e "${BOLD}Next steps:${NC}" echo -e "${BOLD}Next steps:${NC}"
echo " 1. Open documentation_links.md to browse offline docs" echo " 1. Open documentation_links.md to browse offline docs"
echo " 2. Import anki_cards.txt into Anki for spaced repetition" echo " 2. Import anki_cards.txt into Anki for spaced repetition"
echo " 3. Use llm_anki_prompt.md to generate more targeted cards" echo " 3. Use llm_anki_prompt.md to generate more targeted cards"
echo "" echo ""
echo -e "${CYAN}To view a doc:${NC} xdg-open 'file:///path/from/documentation_links.md'" echo -e "${CYAN}To view a doc:${NC} xdg-open 'file:///path/from/documentation_links.md'"
} }
#============================================================================== #==============================================================================
# Main # Main
#============================================================================== #==============================================================================
main() { main() {
# Handle help # Handle help
if [ $# -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then if [ $# -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
usage usage
fi fi
local input="$1" local input="$1"
local output_dir="${2:-}" # Will be set after we know repo name local output_dir="${2:-}" # Will be set after we know repo name
print_header "Repo → Study Materials Pipeline" print_header "Repo → Study Materials Pipeline"
# Setup # Setup
mkdir -p "$WORK_DIR" mkdir -p "$WORK_DIR"
check_dependencies check_dependencies
ensure_offline_docs ensure_offline_docs
# Step 1: Get repository # Step 1: Get repository
print_header "Step 1/3: Getting Repository" print_header "Step 1/3: Getting Repository"
local repo_path local repo_path
repo_path=$(get_repo "$input") repo_path=$(get_repo "$input")
# Extract repo name from path (since get_repo runs in subshell, REPO_NAME is lost) # Extract repo name from path (since get_repo runs in subshell, REPO_NAME is lost)
if [ -z "$REPO_NAME" ]; then if [ -z "$REPO_NAME" ]; then
REPO_NAME=$(basename "$repo_path") REPO_NAME=$(basename "$repo_path")
fi fi
# Set default output dir based on repo name # Set default output dir based on repo name
if [ -z "$output_dir" ]; then if [ -z "$output_dir" ]; then
output_dir="$STUDY_MATERIALS_BASE/$REPO_NAME" output_dir="$STUDY_MATERIALS_BASE/$REPO_NAME"
elif [[ "$output_dir" != /* ]]; then elif [[ $output_dir != /* ]]; then
# Convert relative to absolute # Convert relative to absolute
output_dir="$(pwd)/$output_dir" output_dir="$(pwd)/$output_dir"
fi fi
echo -e "${BOLD}Input:${NC} $input" >&2 echo -e "${BOLD}Input:${NC} $input" >&2
echo -e "${BOLD}Output:${NC} $output_dir" >&2 echo -e "${BOLD}Output:${NC} $output_dir" >&2
echo "" >&2 echo "" >&2
# Step 2: Analyze # Step 2: Analyze
print_header "Step 2/3: Analyzing Code" print_header "Step 2/3: Analyzing Code"
local analysis_dir local analysis_dir
analysis_dir=$(analyze_repo "$repo_path") analysis_dir=$(analyze_repo "$repo_path")
# Step 3: Generate materials # Step 3: Generate materials
print_header "Step 3/3: Generating Study Materials" print_header "Step 3/3: Generating Study Materials"
generate_materials "$analysis_dir" "$output_dir" generate_materials "$analysis_dir" "$output_dir"
# Show results # Show results
show_summary "$output_dir" show_summary "$output_dir"
} }
main "$@" main "$@"

File diff suppressed because it is too large Load Diff

View File

@ -13,36 +13,36 @@ source "$SCRIPT_DIR/../lib/android.sh"
init_android_script "$@" init_android_script "$@"
install_adaway() { install_adaway() {
print_header "Installing AdAway" print_header "Installing AdAway"
local adaway_apk="$WORK_DIR/adaway.apk" local adaway_apk="$WORK_DIR/adaway.apk"
local adaway_url="https://github.com/AdAway/AdAway/releases/latest/download/AdAway.apk" local adaway_url="https://github.com/AdAway/AdAway/releases/latest/download/AdAway.apk"
if [[ ! -f $adaway_apk ]]; then if [[ ! -f $adaway_apk ]]; then
log "Downloading AdAway APK..." log "Downloading AdAway APK..."
curl -L -o "$adaway_apk" "$adaway_url" || die "Failed to download AdAway" curl -L -o "$adaway_apk" "$adaway_url" || die "Failed to download AdAway"
else else
log "AdAway APK already downloaded" log "AdAway APK already downloaded"
fi fi
log "Installing AdAway..." log "Installing AdAway..."
if adb install -r "$adaway_apk" 2>&1 | grep -q "Success"; then if adb install -r "$adaway_apk" 2>&1 | grep -q "Success"; then
log "AdAway installed successfully" log "AdAway installed successfully"
else else
warn "AdAway installation may have failed or already installed" warn "AdAway installation may have failed or already installed"
fi fi
} }
setup_systemless_hosts() { setup_systemless_hosts() {
print_header "Setting up Systemless Hosts" print_header "Setting up Systemless Hosts"
log "Installing Systemless Hosts module..." log "Installing Systemless Hosts module..."
# Create systemless hosts module directory # Create systemless hosts module directory
adb shell "su -c 'mkdir -p /data/adb/modules/systemless_hosts/system/etc'" || die "Failed to create module directory" adb shell "su -c 'mkdir -p /data/adb/modules/systemless_hosts/system/etc'" || die "Failed to create module directory"
# Create module.prop # Create module.prop
cat >"$WORK_DIR/module.prop" <<'EOF' cat > "$WORK_DIR/module.prop" << 'EOF'
id=systemless_hosts id=systemless_hosts
name=Systemless Hosts name=Systemless Hosts
version=1.0 version=1.0
@ -51,122 +51,122 @@ author=Custom
description=Custom hosts file from StevenBlack with extensions description=Custom hosts file from StevenBlack with extensions
EOF EOF
adb push "$WORK_DIR/module.prop" /sdcard/module.prop adb push "$WORK_DIR/module.prop" /sdcard/module.prop
adb shell "su -c 'cp /sdcard/module.prop /data/adb/modules/systemless_hosts/'" || die "Failed to create module.prop" adb shell "su -c 'cp /sdcard/module.prop /data/adb/modules/systemless_hosts/'" || die "Failed to create module.prop"
adb shell "su -c 'rm /sdcard/module.prop'" adb shell "su -c 'rm /sdcard/module.prop'"
log "Module structure created" log "Module structure created"
} }
push_hosts_file() { push_hosts_file() {
print_header "Pushing Custom Hosts File" print_header "Pushing Custom Hosts File"
local hosts_file="$WORK_DIR/hosts" local hosts_file="$WORK_DIR/hosts"
# Use the StevenBlack cache or generate from /etc/hosts # Use the StevenBlack cache or generate from /etc/hosts
if [[ -f /etc/hosts.stevenblack ]]; then if [[ -f /etc/hosts.stevenblack ]]; then
log "Using StevenBlack hosts cache..." log "Using StevenBlack hosts cache..."
cp /etc/hosts.stevenblack "$hosts_file" cp /etc/hosts.stevenblack "$hosts_file"
elif [[ -f /etc/hosts ]]; then elif [[ -f /etc/hosts ]]; then
log "Using current /etc/hosts..." log "Using current /etc/hosts..."
cp /etc/hosts "$hosts_file" cp /etc/hosts "$hosts_file"
else else
die "No hosts file found" die "No hosts file found"
fi fi
# Show stats # Show stats
local total_entries local total_entries
total_entries=$(grep -c "^0\.0\.0\.0 " "$hosts_file" || echo 0) total_entries=$(grep -c "^0\.0\.0\.0 " "$hosts_file" || echo 0)
log "Hosts file contains $total_entries blocked domains" log "Hosts file contains $total_entries blocked domains"
log "Pushing hosts file to device..." log "Pushing hosts file to device..."
adb push "$hosts_file" /sdcard/hosts || die "Failed to push hosts file" adb push "$hosts_file" /sdcard/hosts || die "Failed to push hosts file"
log "Installing hosts file systemlessly..." log "Installing hosts file systemlessly..."
adb shell "su -c 'cp /sdcard/hosts /data/adb/modules/systemless_hosts/system/etc/hosts'" || die "Failed to install hosts file" adb shell "su -c 'cp /sdcard/hosts /data/adb/modules/systemless_hosts/system/etc/hosts'" || die "Failed to install hosts file"
adb shell "su -c 'chmod 644 /data/adb/modules/systemless_hosts/system/etc/hosts'" || die "Failed to set permissions" adb shell "su -c 'chmod 644 /data/adb/modules/systemless_hosts/system/etc/hosts'" || die "Failed to set permissions"
adb shell "su -c 'rm /sdcard/hosts'" adb shell "su -c 'rm /sdcard/hosts'"
log "Hosts file installed successfully" log "Hosts file installed successfully"
log "Total blocked domains: $total_entries" log "Total blocked domains: $total_entries"
} }
enable_module() { enable_module() {
print_header "Enabling Systemless Hosts Module" print_header "Enabling Systemless Hosts Module"
log "Removing disable flag if present..." log "Removing disable flag if present..."
adb shell "su -c 'rm -f /data/adb/modules/systemless_hosts/disable'" 2>/dev/null || true adb shell "su -c 'rm -f /data/adb/modules/systemless_hosts/disable'" 2> /dev/null || true
adb shell "su -c 'rm -f /data/adb/modules/systemless_hosts/remove'" 2>/dev/null || true adb shell "su -c 'rm -f /data/adb/modules/systemless_hosts/remove'" 2> /dev/null || true
log "Module enabled" log "Module enabled"
echo echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " REBOOT REQUIRED" echo " REBOOT REQUIRED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo echo
echo "The systemless hosts module requires a reboot to take effect." echo "The systemless hosts module requires a reboot to take effect."
echo echo
read -p "Reboot device now? [y/N]: " -n 1 -r read -p "Reboot device now? [y/N]: " -n 1 -r
echo echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
log "Rebooting device..." log "Rebooting device..."
adb reboot adb reboot
log "Device rebooting. Wait for boot to complete." log "Device rebooting. Wait for boot to complete."
else else
warn "Remember to reboot manually for changes to take effect!" warn "Remember to reboot manually for changes to take effect!"
fi fi
} }
verify_hosts() { verify_hosts() {
print_header "Verifying Hosts Installation" print_header "Verifying Hosts Installation"
log "Waiting for device to boot..." log "Waiting for device to boot..."
sleep 5 sleep 5
adb wait-for-device adb wait-for-device
sleep 10 sleep 10
log "Checking if hosts file is active..." log "Checking if hosts file is active..."
local test_domain="doubleclick.net" local test_domain="doubleclick.net"
local result local result
result=$(adb shell "su -c 'cat /system/etc/hosts | grep -c $test_domain'" 2>/dev/null || echo "0") result=$(adb shell "su -c 'cat /system/etc/hosts | grep -c $test_domain'" 2> /dev/null || echo "0")
if [[ $result -gt 0 ]]; then if [[ $result -gt 0 ]]; then
log "✓ Hosts file is active and blocking domains" log "✓ Hosts file is active and blocking domains"
else else
warn "Could not verify hosts file, but module should be installed" warn "Could not verify hosts file, but module should be installed"
fi fi
} }
main() { main() {
print_header "Android Ad Blocking Setup" print_header "Android Ad Blocking Setup"
check_device check_device
check_root check_root
echo "This will:" echo "This will:"
echo " 1. Install AdAway app (optional GUI management)" echo " 1. Install AdAway app (optional GUI management)"
echo " 2. Create systemless hosts module" echo " 2. Create systemless hosts module"
echo " 3. Push your custom hosts file (StevenBlack with extensions)" echo " 3. Push your custom hosts file (StevenBlack with extensions)"
echo " 4. Enable the module and reboot" echo " 4. Enable the module and reboot"
echo echo
read -p "Continue? [y/N]: " -n 1 -r read -p "Continue? [y/N]: " -n 1 -r
echo echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log "Cancelled by user" log "Cancelled by user"
exit 0 exit 0
fi fi
install_adaway install_adaway
setup_systemless_hosts setup_systemless_hosts
push_hosts_file push_hosts_file
enable_module enable_module
log "Setup complete!" log "Setup complete!"
log "After reboot, ads should be blocked system-wide" log "After reboot, ads should be blocked system-wide"
log "You can manage hosts in the AdAway app or by updating the module" log "You can manage hosts in the AdAway app or by updating the module"
} }
main "$@" main "$@"

View File

@ -12,33 +12,33 @@ SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
# Get the actual user (not root when running with sudo) # Get the actual user (not root when running with sudo)
if [[ -n ${SUDO_USER:-} ]]; then if [[ -n ${SUDO_USER:-} ]]; then
USER_NAME="$SUDO_USER" USER_NAME="$SUDO_USER"
else else
USER_NAME="$(whoami)" USER_NAME="$(whoami)"
fi fi
# Function to log messages # Function to log messages
log() { log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
} }
# Check if organize script exists # Check if organize script exists
if [[ ! -f $ORGANIZE_SCRIPT ]]; then if [[ ! -f $ORGANIZE_SCRIPT ]]; then
log "ERROR: organize_downloads.sh not found at $ORGANIZE_SCRIPT" log "ERROR: organize_downloads.sh not found at $ORGANIZE_SCRIPT"
exit 1 exit 1
fi fi
# Check if running as root for systemd service creation # Check if running as root for systemd service creation
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
log "This script needs to be run as root to create systemd service." log "This script needs to be run as root to create systemd service."
log "Re-executing with sudo..." log "Re-executing with sudo..."
exec sudo "$0" "$@" exec sudo "$0" "$@"
fi fi
log "Setting up media organizer startup service..." log "Setting up media organizer startup service..."
# Create systemd service file # Create systemd service file
cat >"$SERVICE_FILE" <<EOF cat > "$SERVICE_FILE" << EOF
[Unit] [Unit]
Description=Media File Organizer Description=Media File Organizer
After=graphical-session.target After=graphical-session.target

View File

@ -22,27 +22,27 @@ YELLOW='\033[1;33m'
NC='\033[0m' NC='\033[0m'
print_header() { print_header() {
echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} $1${NC}" echo -e "${GREEN} $1${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}"
} }
print_status() { print_status() {
echo -e "${YELLOW}${NC} $1" echo -e "${YELLOW}${NC} $1"
} }
print_success() { print_success() {
echo -e "${GREEN}${NC} $1" echo -e "${GREEN}${NC} $1"
} }
print_error() { print_error() {
echo -e "${RED}${NC} $1" echo -e "${RED}${NC} $1"
} }
# Create directory structure # Create directory structure
setup_dirs() { setup_dirs() {
mkdir -p "$DOCS_DIR"/{python,c_cpp,javascript,typescript,rust,go,ruby,java,shell} mkdir -p "$DOCS_DIR"/{python,c_cpp,javascript,typescript,rust,go,ruby,java,shell}
mkdir -p "$INDEX_DIR" mkdir -p "$INDEX_DIR"
} }
#============================================================================== #==============================================================================
@ -50,62 +50,62 @@ setup_dirs() {
# Source: https://docs.python.org/3/download.html # Source: https://docs.python.org/3/download.html
#============================================================================== #==============================================================================
download_python_docs() { download_python_docs() {
print_header "Python Documentation" print_header "Python Documentation"
local dest="$DOCS_DIR/python" local dest="$DOCS_DIR/python"
# Check if already downloaded # Check if already downloaded
if [ -f "$dest/library/index.html" ]; then if [ -f "$dest/library/index.html" ]; then
print_status "Python docs already present, checking for updates..." print_status "Python docs already present, checking for updates..."
fi fi
print_status "Downloading Python 3.12 documentation..." print_status "Downloading Python 3.12 documentation..."
# Download HTML documentation (most searchable) # Download HTML documentation (most searchable)
local url="https://www.python.org/ftp/python/doc/3.12.8/python-3.12.8-docs-html.tar.bz2" local url="https://www.python.org/ftp/python/doc/3.12.8/python-3.12.8-docs-html.tar.bz2"
local archive="/tmp/python-docs.tar.bz2" local archive="/tmp/python-docs.tar.bz2"
if curl -L -o "$archive" "$url" 2>/dev/null; then if curl -L -o "$archive" "$url" 2> /dev/null; then
print_status "Extracting..." print_status "Extracting..."
tar -xjf "$archive" -C "$dest" --strip-components=1 tar -xjf "$archive" -C "$dest" --strip-components=1
rm -f "$archive" rm -f "$archive"
print_success "Python documentation installed to $dest" print_success "Python documentation installed to $dest"
# Build index # Build index
build_python_index build_python_index
else else
print_error "Failed to download Python docs" print_error "Failed to download Python docs"
print_status "Alternative: Use 'python -m pydoc' for built-in docs" print_status "Alternative: Use 'python -m pydoc' for built-in docs"
fi fi
} }
build_python_index() { build_python_index() {
print_status "Building Python documentation index..." print_status "Building Python documentation index..."
local dest="$DOCS_DIR/python" local dest="$DOCS_DIR/python"
local index="$INDEX_DIR/python_index.txt" local index="$INDEX_DIR/python_index.txt"
# Create searchable index: term -> file path # Create searchable index: term -> file path
{ {
# Index library modules # Index library modules
find "$dest/library" -name "*.html" -exec basename {} .html \; 2>/dev/null | while read -r mod; do find "$dest/library" -name "*.html" -exec basename {} .html \; 2> /dev/null | while read -r mod; do
echo "$mod $dest/library/$mod.html" echo "$mod $dest/library/$mod.html"
done done
# Index built-in functions from functions.html # Index built-in functions from functions.html
if [ -f "$dest/library/functions.html" ]; then if [ -f "$dest/library/functions.html" ]; then
grep -oP '(?<=id=")[^"]+' "$dest/library/functions.html" 2>/dev/null | while read -r func; do grep -oP '(?<=id=")[^"]+' "$dest/library/functions.html" 2> /dev/null | while read -r func; do
echo "$func $dest/library/functions.html#$func" echo "$func $dest/library/functions.html#$func"
done done
fi fi
# Index from general index # Index from general index
if [ -f "$dest/genindex.html" ]; then if [ -f "$dest/genindex.html" ]; then
grep -oP 'href="([^"]+)"[^>]*>([^<]+)' "$dest/genindex.html" 2>/dev/null | \ grep -oP 'href="([^"]+)"[^>]*>([^<]+)' "$dest/genindex.html" 2> /dev/null |
sed -E 's/href="([^"]+)"[^>]*>([^<]+)/\2 \1/' | \ sed -E 's/href="([^"]+)"[^>]*>([^<]+)/\2 \1/' |
head -5000 head -5000
fi fi
} | sort -u > "$index" } | sort -u > "$index"
print_success "Python index created with $(wc -l < "$index") entries" print_success "Python index created with $(wc -l < "$index") entries"
} }
#============================================================================== #==============================================================================
@ -114,114 +114,114 @@ build_python_index() {
# Fallback: AUR cppreference package or direct download # Fallback: AUR cppreference package or direct download
#============================================================================== #==============================================================================
download_cpp_docs() { download_cpp_docs() {
print_header "C/C++ Documentation (cppreference)" print_header "C/C++ Documentation (cppreference)"
local dest="$DOCS_DIR/c_cpp" local dest="$DOCS_DIR/c_cpp"
if [ -f "$dest/en/index.html" ] || [ -d "$dest/reference" ] || [ -L "$dest/system" ]; then if [ -f "$dest/en/index.html" ] || [ -d "$dest/reference" ] || [ -L "$dest/system" ]; then
print_status "C/C++ docs already present" print_status "C/C++ docs already present"
return 0 return 0
fi fi
mkdir -p "$dest" mkdir -p "$dest"
# Method 1: Use cppman if available (best - fetches and caches on demand) # Method 1: Use cppman if available (best - fetches and caches on demand)
if command -v cppman &>/dev/null; then if command -v cppman &> /dev/null; then
print_status "Found cppman, caching common C++ references..." print_status "Found cppman, caching common C++ references..."
cppman -s cppreference.com 2>/dev/null cppman -s cppreference.com 2> /dev/null
cppman -c 2>/dev/null # Cache all pages cppman -c 2> /dev/null # Cache all pages
print_success "cppman configured - use 'cppman <term>' for lookups" print_success "cppman configured - use 'cppman <term>' for lookups"
print_status "Cppman cache at: ~/.cache/cppman/" print_status "Cppman cache at: ~/.cache/cppman/"
ln -sf ~/.cache/cppman "$dest/cppman_cache" 2>/dev/null ln -sf ~/.cache/cppman "$dest/cppman_cache" 2> /dev/null
build_cpp_index build_cpp_index
return 0 return 0
fi fi
# Method 2: Check if system package already installed # Method 2: Check if system package already installed
if [ -d /usr/share/doc/cppreference/en ]; then if [ -d /usr/share/doc/cppreference/en ]; then
print_status "Found system cppreference package" print_status "Found system cppreference package"
ln -sf /usr/share/doc/cppreference "$dest/system"
print_success "C/C++ documentation linked from system package"
build_cpp_index
return 0
fi
# Method 3: Try AUR package (Arch Linux)
if command -v yay &> /dev/null; then
print_status "Installing cppreference from AUR..."
if yay -S --noconfirm cppreference 2> /dev/null; then
# Link to installed docs (the package uses /en not /html)
if [ -d /usr/share/doc/cppreference/en ]; then
ln -sf /usr/share/doc/cppreference "$dest/system" ln -sf /usr/share/doc/cppreference "$dest/system"
print_success "C/C++ documentation linked from system package" print_success "C/C++ documentation linked from system package"
build_cpp_index build_cpp_index
return 0 return 0
fi
fi fi
fi
# Method 3: Try AUR package (Arch Linux) # Method 4: Direct download (try multiple mirrors)
if command -v yay &>/dev/null; then print_status "Downloading cppreference offline archive..."
print_status "Installing cppreference from AUR..." local archive="/tmp/cppreference.tar.xz"
if yay -S --noconfirm cppreference 2>/dev/null; then local urls=(
# Link to installed docs (the package uses /en not /html) "https://upload.cppreference.com/mwiki/images/1/16/html_book_20241110.tar.xz"
if [ -d /usr/share/doc/cppreference/en ]; then "https://github.com/nicovank/cppreference-doc/releases/latest/download/html_book.tar.xz"
ln -sf /usr/share/doc/cppreference "$dest/system" )
print_success "C/C++ documentation linked from system package"
build_cpp_index for url in "${urls[@]}"; do
return 0 print_status "Trying: $url"
fi if curl -fL -o "$archive" "$url" 2> /dev/null; then
fi print_status "Extracting (this may take a while)..."
if tar -xJf "$archive" -C "$dest" 2> /dev/null; then
rm -f "$archive"
print_success "C/C++ documentation installed to $dest"
build_cpp_index
return 0
fi
fi fi
done
# Method 4: Direct download (try multiple mirrors) print_error "Failed to download cppreference"
print_status "Downloading cppreference offline archive..." print_status "Manual install: yay -S cppreference OR yay -S cppman"
local archive="/tmp/cppreference.tar.xz" return 1
local urls=(
"https://upload.cppreference.com/mwiki/images/1/16/html_book_20241110.tar.xz"
"https://github.com/nicovank/cppreference-doc/releases/latest/download/html_book.tar.xz"
)
for url in "${urls[@]}"; do
print_status "Trying: $url"
if curl -fL -o "$archive" "$url" 2>/dev/null; then
print_status "Extracting (this may take a while)..."
if tar -xJf "$archive" -C "$dest" 2>/dev/null; then
rm -f "$archive"
print_success "C/C++ documentation installed to $dest"
build_cpp_index
return 0
fi
fi
done
print_error "Failed to download cppreference"
print_status "Manual install: yay -S cppreference OR yay -S cppman"
return 1
} }
build_cpp_index() { build_cpp_index() {
print_status "Building C/C++ documentation index..." print_status "Building C/C++ documentation index..."
local dest="$DOCS_DIR/c_cpp" local dest="$DOCS_DIR/c_cpp"
local index="$INDEX_DIR/cpp_index.txt" local index="$INDEX_DIR/cpp_index.txt"
# Resolve symlink if present # Resolve symlink if present
local search_dir="$dest" local search_dir="$dest"
[ -L "$dest/system" ] && search_dir="$dest/system" [ -L "$dest/system" ] && search_dir="$dest/system"
{ {
# Find all HTML files and extract identifiers # Find all HTML files and extract identifiers
# Format: term|filepath (using | as separator to handle spaces) # Format: term|filepath (using | as separator to handle spaces)
find "$search_dir" -name "*.html" -type f 2>/dev/null | while read -r file; do find "$search_dir" -name "*.html" -type f 2> /dev/null | while read -r file; do
# Extract meaningful term from path (e.g., /en/cpp/container/vector.html -> vector) # Extract meaningful term from path (e.g., /en/cpp/container/vector.html -> vector)
local term local term
term=$(basename "$file" .html) term=$(basename "$file" .html)
# Skip index files and overly generic names # Skip index files and overly generic names
[[ "$term" == "index" ]] && continue [[ $term == "index" ]] && continue
echo "${term}|${file}" echo "${term}|${file}"
done done
# Also index by path components for better discoverability # Also index by path components for better discoverability
# e.g., cpp/container/vector -> vector # e.g., cpp/container/vector -> vector
find "$search_dir/en" -name "*.html" -type f 2>/dev/null | while read -r file; do find "$search_dir/en" -name "*.html" -type f 2> /dev/null | while read -r file; do
# Extract path relative to en/ and create searchable term # Extract path relative to en/ and create searchable term
local relpath local relpath
relpath=$(echo "$file" | sed "s|$search_dir/en/||" | sed 's|\.html$||') relpath=$(echo "$file" | sed "s|$search_dir/en/||" | sed 's|\.html$||')
# Get the last component as primary term # Get the last component as primary term
local term local term
term=$(basename "$relpath") term=$(basename "$relpath")
[[ "$term" == "index" ]] && continue [[ $term == "index" ]] && continue
# Also add the full path as a searchable term (cpp/vector, c/stdlib/malloc) # Also add the full path as a searchable term (cpp/vector, c/stdlib/malloc)
echo "${relpath}|${file}" echo "${relpath}|${file}"
done done
} | sort -u > "$index" } | sort -u > "$index"
print_success "C/C++ index created with $(wc -l < "$index") entries" print_success "C/C++ index created with $(wc -l < "$index") entries"
} }
#============================================================================== #==============================================================================
@ -230,31 +230,31 @@ build_cpp_index() {
# https://github.com/mdn/content # https://github.com/mdn/content
#============================================================================== #==============================================================================
download_js_docs() { download_js_docs() {
print_header "JavaScript/MDN Documentation" print_header "JavaScript/MDN Documentation"
local dest="$DOCS_DIR/javascript" local dest="$DOCS_DIR/javascript"
local mdn_repo="$DOCS_DIR/mdn-content" local mdn_repo="$DOCS_DIR/mdn-content"
# Check if already cloned # Check if already cloned
if [ -d "$mdn_repo/files/en-us/web/javascript" ]; then if [ -d "$mdn_repo/files/en-us/web/javascript" ]; then
print_status "MDN content already present" print_status "MDN content already present"
build_js_index build_js_index
return 0 return 0
fi fi
print_status "Cloning MDN content repository (sparse checkout for web docs)..." print_status "Cloning MDN content repository (sparse checkout for web docs)..."
print_status "This may take a few minutes on first run..." print_status "This may take a few minutes on first run..."
mkdir -p "$mdn_repo" mkdir -p "$mdn_repo"
cd "$mdn_repo" || exit 1 cd "$mdn_repo" || exit 1
# Initialize sparse checkout to only get what we need # Initialize sparse checkout to only get what we need
if [ ! -d ".git" ]; then if [ ! -d ".git" ]; then
git init git init
git remote add origin https://github.com/mdn/content.git git remote add origin https://github.com/mdn/content.git
git config core.sparseCheckout true git config core.sparseCheckout true
# Only checkout web-related documentation (JS, HTML, CSS, Web APIs) # Only checkout web-related documentation (JS, HTML, CSS, Web APIs)
cat > .git/info/sparse-checkout << 'SPARSE' cat > .git/info/sparse-checkout << 'SPARSE'
/files/en-us/web/javascript/ /files/en-us/web/javascript/
/files/en-us/web/api/ /files/en-us/web/api/
/files/en-us/web/html/ /files/en-us/web/html/
@ -262,205 +262,205 @@ download_js_docs() {
/files/en-us/glossary/ /files/en-us/glossary/
SPARSE SPARSE
print_status "Fetching MDN content (JavaScript, HTML, CSS, Web APIs)..." print_status "Fetching MDN content (JavaScript, HTML, CSS, Web APIs)..."
git fetch --depth 1 origin main git fetch --depth 1 origin main
git checkout main git checkout main
else else
print_status "Updating MDN content..." print_status "Updating MDN content..."
git pull --depth 1 origin main 2>/dev/null || true git pull --depth 1 origin main 2> /dev/null || true
fi fi
cd - > /dev/null || exit 1 cd - > /dev/null || exit 1
# Create symlink for easier access # Create symlink for easier access
mkdir -p "$dest" mkdir -p "$dest"
ln -sf "$mdn_repo/files/en-us/web/javascript" "$dest/javascript" ln -sf "$mdn_repo/files/en-us/web/javascript" "$dest/javascript"
ln -sf "$mdn_repo/files/en-us/web/api" "$dest/web-api" ln -sf "$mdn_repo/files/en-us/web/api" "$dest/web-api"
ln -sf "$mdn_repo/files/en-us/web/html" "$dest/html" ln -sf "$mdn_repo/files/en-us/web/html" "$dest/html"
ln -sf "$mdn_repo/files/en-us/web/css" "$dest/css" ln -sf "$mdn_repo/files/en-us/web/css" "$dest/css"
ln -sf "$mdn_repo/files/en-us/glossary" "$dest/glossary" ln -sf "$mdn_repo/files/en-us/glossary" "$dest/glossary"
build_js_index build_js_index
print_success "MDN offline documentation ready" print_success "MDN offline documentation ready"
local doc_count local doc_count
doc_count=$(find "$mdn_repo/files" -name "index.md" 2>/dev/null | wc -l) doc_count=$(find "$mdn_repo/files" -name "index.md" 2> /dev/null | wc -l)
print_status "Downloaded $doc_count documentation pages" print_status "Downloaded $doc_count documentation pages"
} }
build_js_index() { build_js_index() {
print_status "Building MDN documentation index..." print_status "Building MDN documentation index..."
local mdn_repo="$DOCS_DIR/mdn-content" local mdn_repo="$DOCS_DIR/mdn-content"
local index="$INDEX_DIR/js_index.txt" local index="$INDEX_DIR/js_index.txt"
if [ ! -d "$mdn_repo/files" ]; then if [ ! -d "$mdn_repo/files" ]; then
print_error "MDN content not found" print_error "MDN content not found"
return 1 return 1
fi fi
# Build comprehensive index from MDN markdown files # Build comprehensive index from MDN markdown files
{ {
# Index JavaScript reference # Index JavaScript reference
find "$mdn_repo/files/en-us/web/javascript/reference" -name "index.md" 2>/dev/null | while read -r file; do find "$mdn_repo/files/en-us/web/javascript/reference" -name "index.md" 2> /dev/null | while read -r file; do
local dir local dir
dir=$(dirname "$file") dir=$(dirname "$file")
local term local term
term=$(basename "$dir") term=$(basename "$dir")
# Extract title from frontmatter if available # Extract title from frontmatter if available
local title local title
title=$(grep -m1 "^title:" "$file" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') title=$(grep -m1 "^title:" "$file" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "${term}|${file}|${title:-$term}" echo "${term}|${file}|${title:-$term}"
done done
# Index Web APIs # Index Web APIs
find "$mdn_repo/files/en-us/web/api" -name "index.md" 2>/dev/null | while read -r file; do find "$mdn_repo/files/en-us/web/api" -name "index.md" 2> /dev/null | while read -r file; do
local dir local dir
dir=$(dirname "$file") dir=$(dirname "$file")
local term local term
term=$(basename "$dir") term=$(basename "$dir")
local title local title
title=$(grep -m1 "^title:" "$file" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') title=$(grep -m1 "^title:" "$file" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "${term}|${file}|${title:-$term}" echo "${term}|${file}|${title:-$term}"
done done
# Index HTML elements # Index HTML elements
find "$mdn_repo/files/en-us/web/html/element" -name "index.md" 2>/dev/null | while read -r file; do find "$mdn_repo/files/en-us/web/html/element" -name "index.md" 2> /dev/null | while read -r file; do
local dir local dir
dir=$(dirname "$file") dir=$(dirname "$file")
local term local term
term=$(basename "$dir") term=$(basename "$dir")
echo "${term}|${file}|HTML <${term}> element" echo "${term}|${file}|HTML <${term}> element"
done done
# Index CSS properties # Index CSS properties
find "$mdn_repo/files/en-us/web/css" -maxdepth 2 -name "index.md" 2>/dev/null | while read -r file; do find "$mdn_repo/files/en-us/web/css" -maxdepth 2 -name "index.md" 2> /dev/null | while read -r file; do
local dir local dir
dir=$(dirname "$file") dir=$(dirname "$file")
local term local term
term=$(basename "$dir") term=$(basename "$dir")
local title local title
title=$(grep -m1 "^title:" "$file" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') title=$(grep -m1 "^title:" "$file" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "${term}|${file}|${title:-$term}" echo "${term}|${file}|${title:-$term}"
done done
# Index Glossary # Index Glossary
find "$mdn_repo/files/en-us/glossary" -name "index.md" 2>/dev/null | while read -r file; do find "$mdn_repo/files/en-us/glossary" -name "index.md" 2> /dev/null | while read -r file; do
local dir local dir
dir=$(dirname "$file") dir=$(dirname "$file")
local term local term
term=$(basename "$dir") term=$(basename "$dir")
local title local title
title=$(grep -m1 "^title:" "$file" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') title=$(grep -m1 "^title:" "$file" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "${term}|${file}|${title:-$term}" echo "${term}|${file}|${title:-$term}"
done done
} | sort -t'|' -k1,1 -u > "$index" } | sort -t'|' -k1,1 -u > "$index"
local count local count
count=$(wc -l < "$index") count=$(wc -l < "$index")
print_success "MDN index created with $count entries" print_success "MDN index created with $count entries"
} }
#============================================================================== #==============================================================================
# Rust Documentation (via rustup) # Rust Documentation (via rustup)
#============================================================================== #==============================================================================
download_rust_docs() { download_rust_docs() {
print_header "Rust Documentation" print_header "Rust Documentation"
local dest="$DOCS_DIR/rust" local dest="$DOCS_DIR/rust"
if command -v rustup &>/dev/null; then if command -v rustup &> /dev/null; then
print_status "Rust docs available via 'rustup doc'" print_status "Rust docs available via 'rustup doc'"
# Get the rust doc path # Get the rust doc path
local rust_doc_path local rust_doc_path
rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null) rust_doc_path=$(rustup doc --path 2> /dev/null | head -1 | xargs dirname 2> /dev/null)
if [ -n "$rust_doc_path" ] && [ -d "$rust_doc_path" ]; then if [ -n "$rust_doc_path" ] && [ -d "$rust_doc_path" ]; then
ln -sf "$rust_doc_path" "$dest/std" ln -sf "$rust_doc_path" "$dest/std"
print_success "Linked Rust std docs from $rust_doc_path" print_success "Linked Rust std docs from $rust_doc_path"
build_rust_index build_rust_index
fi
else
print_status "Rust not installed. Install with: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
fi fi
else
print_status "Rust not installed. Install with: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
fi
} }
build_rust_index() { build_rust_index() {
print_status "Building Rust documentation index..." print_status "Building Rust documentation index..."
local index="$INDEX_DIR/rust_index.txt" local index="$INDEX_DIR/rust_index.txt"
if command -v rustup &>/dev/null; then if command -v rustup &> /dev/null; then
local rust_doc_path local rust_doc_path
rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null) rust_doc_path=$(rustup doc --path 2> /dev/null | head -1 | xargs dirname 2> /dev/null)
if [ -d "$rust_doc_path/std" ]; then if [ -d "$rust_doc_path/std" ]; then
find "$rust_doc_path/std" -name "*.html" 2>/dev/null | head -2000 | while read -r file; do find "$rust_doc_path/std" -name "*.html" 2> /dev/null | head -2000 | while read -r file; do
basename "$file" .html basename "$file" .html
done | sort -u > "$index" done | sort -u > "$index"
fi
fi fi
fi
print_success "Rust index created" print_success "Rust index created"
} }
#============================================================================== #==============================================================================
# Go Documentation # Go Documentation
#============================================================================== #==============================================================================
download_go_docs() { download_go_docs() {
print_header "Go Documentation" print_header "Go Documentation"
local dest="$DOCS_DIR/go" local dest="$DOCS_DIR/go"
if command -v go &>/dev/null; then if command -v go &> /dev/null; then
print_status "Go docs available via 'go doc'" print_status "Go docs available via 'go doc'"
# Create a reference of standard library packages # Create a reference of standard library packages
mkdir -p "$dest" mkdir -p "$dest"
go list std 2>/dev/null > "$dest/stdlib_packages.txt" go list std 2> /dev/null > "$dest/stdlib_packages.txt"
print_success "Go stdlib package list created" print_success "Go stdlib package list created"
build_go_index build_go_index
else else
print_status "Go not installed" print_status "Go not installed"
fi fi
} }
build_go_index() { build_go_index() {
print_status "Building Go documentation index..." print_status "Building Go documentation index..."
local dest="$DOCS_DIR/go" local dest="$DOCS_DIR/go"
local index="$INDEX_DIR/go_index.txt" local index="$INDEX_DIR/go_index.txt"
if [ -f "$dest/stdlib_packages.txt" ]; then if [ -f "$dest/stdlib_packages.txt" ]; then
cp "$dest/stdlib_packages.txt" "$index" cp "$dest/stdlib_packages.txt" "$index"
fi fi
print_success "Go index created" print_success "Go index created"
} }
#============================================================================== #==============================================================================
# Shell/Bash Documentation (man pages + built-in help) # Shell/Bash Documentation (man pages + built-in help)
#============================================================================== #==============================================================================
download_shell_docs() { download_shell_docs() {
print_header "Shell/Bash Documentation" print_header "Shell/Bash Documentation"
local dest="$DOCS_DIR/shell" local dest="$DOCS_DIR/shell"
mkdir -p "$dest" mkdir -p "$dest"
print_status "Extracting bash built-in help..." print_status "Extracting bash built-in help..."
# Extract help for all bash builtins # Extract help for all bash builtins
{ {
echo "# Bash Built-in Commands Reference" echo "# Bash Built-in Commands Reference"
echo "# Generated from 'help' command" echo "# Generated from 'help' command"
echo "" echo ""
# Get list of builtins # Get list of builtins
compgen -b 2>/dev/null | while read -r builtin; do compgen -b 2> /dev/null | while read -r builtin; do
echo "=== $builtin ===" echo "=== $builtin ==="
help "$builtin" 2>/dev/null || echo "No help available" help "$builtin" 2> /dev/null || echo "No help available"
echo "" echo ""
done done
} > "$dest/bash_builtins.txt" } > "$dest/bash_builtins.txt"
# Create quick reference for common commands # Create quick reference for common commands
cat > "$dest/common_commands.txt" << 'SHELLREF' cat > "$dest/common_commands.txt" << 'SHELLREF'
# Common Shell Commands Quick Reference # Common Shell Commands Quick Reference
## File Operations ## File Operations
@ -555,53 +555,53 @@ eval - Evaluate arguments
exec - Execute command exec - Execute command
SHELLREF SHELLREF
print_success "Shell documentation created" print_success "Shell documentation created"
build_shell_index build_shell_index
} }
build_shell_index() { build_shell_index() {
print_status "Building Shell documentation index..." print_status "Building Shell documentation index..."
local dest="$DOCS_DIR/shell" local dest="$DOCS_DIR/shell"
local index="$INDEX_DIR/shell_index.txt" local index="$INDEX_DIR/shell_index.txt"
{ {
# Bash builtins # Bash builtins
compgen -b 2>/dev/null | while read -r cmd; do compgen -b 2> /dev/null | while read -r cmd; do
echo "$cmd $dest/bash_builtins.txt" echo "$cmd $dest/bash_builtins.txt"
done done
# Common commands from man pages # Common commands from man pages
for cmd in ls cd cp mv rm mkdir cat grep sed awk find sort curl wget tar chmod; do for cmd in ls cd cp mv rm mkdir cat grep sed awk find sort curl wget tar chmod; do
man_path=$(man -w "$cmd" 2>/dev/null) man_path=$(man -w "$cmd" 2> /dev/null)
[ -n "$man_path" ] && echo "$cmd $man_path" [ -n "$man_path" ] && echo "$cmd $man_path"
done done
} | sort -u > "$index" } | sort -u > "$index"
print_success "Shell index created" print_success "Shell index created"
} }
#============================================================================== #==============================================================================
# Zeal Docsets (cross-platform dash alternative) # Zeal Docsets (cross-platform dash alternative)
#============================================================================== #==============================================================================
setup_zeal_docsets() { setup_zeal_docsets() {
print_header "Zeal Docsets (Optional)" print_header "Zeal Docsets (Optional)"
if ! command -v zeal &>/dev/null; then if ! command -v zeal &> /dev/null; then
print_status "Zeal not installed." print_status "Zeal not installed."
print_status "Install with: pacman -S zeal (or your package manager)" print_status "Install with: pacman -S zeal (or your package manager)"
print_status "Zeal provides a GUI for offline documentation" print_status "Zeal provides a GUI for offline documentation"
return 0 return 0
fi fi
print_status "Zeal is installed. You can download docsets from within Zeal." print_status "Zeal is installed. You can download docsets from within Zeal."
print_status "Recommended docsets: Python 3, JavaScript, TypeScript, C, C++" print_status "Recommended docsets: Python 3, JavaScript, TypeScript, C, C++"
} }
#============================================================================== #==============================================================================
# Main # Main
#============================================================================== #==============================================================================
usage() { usage() {
cat << EOF cat << EOF
Usage: $0 [OPTIONS] Usage: $0 [OPTIONS]
Download and setup offline documentation for programming languages. Download and setup offline documentation for programming languages.
@ -628,86 +628,86 @@ EOF
} }
show_status() { show_status() {
print_header "Offline Documentation Status" print_header "Offline Documentation Status"
echo "Documentation directory: $DOCS_DIR" echo "Documentation directory: $DOCS_DIR"
echo "" echo ""
for lang in python c_cpp javascript rust go shell; do for lang in python c_cpp javascript rust go shell; do
dir="$DOCS_DIR/$lang" dir="$DOCS_DIR/$lang"
if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then if [ -d "$dir" ] && [ "$(ls -A "$dir" 2> /dev/null)" ]; then
size=$(du -sh "$dir" 2>/dev/null | cut -f1) size=$(du -sh "$dir" 2> /dev/null | cut -f1)
print_success "$lang: installed ($size)" print_success "$lang: installed ($size)"
else else
print_error "$lang: not installed" print_error "$lang: not installed"
fi fi
done done
echo "" echo ""
echo "Index files:" echo "Index files:"
ls -la "$INDEX_DIR"/*.txt 2>/dev/null || echo "No indexes built yet" ls -la "$INDEX_DIR"/*.txt 2> /dev/null || echo "No indexes built yet"
} }
main() { main() {
setup_dirs setup_dirs
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
usage
exit 0
fi
while [ $# -gt 0 ]; do
case "$1" in
--all)
download_python_docs
download_cpp_docs
download_js_docs
download_rust_docs
download_go_docs
download_shell_docs
setup_zeal_docsets
;;
--python)
download_python_docs
;;
--cpp | --c | --c++)
download_cpp_docs
;;
--js | --javascript)
download_js_docs
;;
--rust)
download_rust_docs
;;
--go)
download_go_docs
;;
--shell | --bash)
download_shell_docs
;;
--zeal)
setup_zeal_docsets
;;
--status)
show_status
;;
--help | -h)
usage usage
exit 0 exit 0
fi ;;
*)
print_error "Unknown option: $1"
usage
exit 1
;;
esac
shift
done
while [ $# -gt 0 ]; do echo ""
case "$1" in print_header "Setup Complete"
--all) echo "Documentation stored in: $DOCS_DIR"
download_python_docs echo ""
download_cpp_docs echo "Use 'lookup_docs.sh <term> [language]' to search documentation"
download_js_docs
download_rust_docs
download_go_docs
download_shell_docs
setup_zeal_docsets
;;
--python)
download_python_docs
;;
--cpp|--c|--c++)
download_cpp_docs
;;
--js|--javascript)
download_js_docs
;;
--rust)
download_rust_docs
;;
--go)
download_go_docs
;;
--shell|--bash)
download_shell_docs
;;
--zeal)
setup_zeal_docsets
;;
--status)
show_status
;;
--help|-h)
usage
exit 0
;;
*)
print_error "Unknown option: $1"
usage
exit 1
;;
esac
shift
done
echo ""
print_header "Setup Complete"
echo "Documentation stored in: $DOCS_DIR"
echo ""
echo "Use 'lookup_docs.sh <term> [language]' to search documentation"
} }
main "$@" main "$@"

View File

@ -14,10 +14,10 @@ set -euo pipefail
# Source common library if available # Source common library if available
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
if [[ -f "$SCRIPT_DIR/../lib/common.sh" ]]; then if [[ -f "$SCRIPT_DIR/../lib/common.sh" ]]; then
# shellcheck source=../lib/common.sh # shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh" source "$SCRIPT_DIR/../lib/common.sh"
else else
log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; } log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
fi fi
# Configuration # Configuration
@ -25,34 +25,34 @@ KEEPASS_DIR="${1:-$HOME/Keepass}"
BACKUP_DIR="$KEEPASS_DIR/.backup_$(date +%Y%m%d_%H%M%S)" BACKUP_DIR="$KEEPASS_DIR/.backup_$(date +%Y%m%d_%H%M%S)"
# Ensure keepassxc-cli is installed # Ensure keepassxc-cli is installed
if ! command -v keepassxc-cli &>/dev/null; then if ! command -v keepassxc-cli &> /dev/null; then
log "ERROR: 'keepassxc-cli' is not installed. Install with: sudo pacman -S keepassxc" log "ERROR: 'keepassxc-cli' is not installed. Install with: sudo pacman -S keepassxc"
exit 1 exit 1
fi fi
# Check if directory exists # Check if directory exists
if [[ ! -d "$KEEPASS_DIR" ]]; then if [[ ! -d $KEEPASS_DIR ]]; then
log "ERROR: Directory does not exist: $KEEPASS_DIR" log "ERROR: Directory does not exist: $KEEPASS_DIR"
exit 1 exit 1
fi fi
# Find all .kdbx files # Find all .kdbx files
mapfile -t KDBX_FILES < <(find "$KEEPASS_DIR" -maxdepth 1 -name "*.kdbx" -type f | sort) mapfile -t KDBX_FILES < <(find "$KEEPASS_DIR" -maxdepth 1 -name "*.kdbx" -type f | sort)
if [[ ${#KDBX_FILES[@]} -eq 0 ]]; then if [[ ${#KDBX_FILES[@]} -eq 0 ]]; then
log "No .kdbx files found in $KEEPASS_DIR" log "No .kdbx files found in $KEEPASS_DIR"
exit 0 exit 0
fi fi
if [[ ${#KDBX_FILES[@]} -eq 1 ]]; then if [[ ${#KDBX_FILES[@]} -eq 1 ]]; then
log "Only one .kdbx file found. Nothing to merge." log "Only one .kdbx file found. Nothing to merge."
log "File: ${KDBX_FILES[0]}" log "File: ${KDBX_FILES[0]}"
exit 0 exit 0
fi fi
log "Found ${#KDBX_FILES[@]} .kdbx files in $KEEPASS_DIR:" log "Found ${#KDBX_FILES[@]} .kdbx files in $KEEPASS_DIR:"
for f in "${KDBX_FILES[@]}"; do for f in "${KDBX_FILES[@]}"; do
echo " - $(basename "$f")" echo " - $(basename "$f")"
done done
# Create backup directory # Create backup directory
@ -61,7 +61,7 @@ log "Creating backups in: $BACKUP_DIR"
# Backup all files before any operation # Backup all files before any operation
for f in "${KDBX_FILES[@]}"; do for f in "${KDBX_FILES[@]}"; do
cp -v "$f" "$BACKUP_DIR/" cp -v "$f" "$BACKUP_DIR/"
done done
log "All files backed up successfully." log "All files backed up successfully."
@ -74,9 +74,9 @@ echo "Backups are stored in: $BACKUP_DIR"
echo "==============================================" echo "=============================================="
echo "" echo ""
read -rp "Do you want to continue? (yes/no): " CONFIRM read -rp "Do you want to continue? (yes/no): " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then if [[ $CONFIRM != "yes" ]]; then
log "Aborted by user." log "Aborted by user."
exit 1 exit 1
fi fi
# Select the target database (the one to merge INTO) # Select the target database (the one to merge INTO)
@ -93,9 +93,9 @@ read -rsp "Enter master password for TARGET database ($(basename "$TARGET_DB")):
echo "" echo ""
# Verify target password works # Verify target password works
if ! echo "$TARGET_PASSWORD" | keepassxc-cli ls "$TARGET_DB" &>/dev/null; then if ! echo "$TARGET_PASSWORD" | keepassxc-cli ls "$TARGET_DB" &> /dev/null; then
log "ERROR: Failed to open target database. Wrong password?" log "ERROR: Failed to open target database. Wrong password?"
exit 1 exit 1
fi fi
log "Target database password verified." log "Target database password verified."
@ -107,41 +107,41 @@ SAME_PASSWORD="${SAME_PASSWORD,,}" # lowercase
# Merge each source database into the target # Merge each source database into the target
MERGE_COUNT=0 MERGE_COUNT=0
for ((i = 1; i < ${#KDBX_FILES[@]}; i++)); do for ((i = 1; i < ${#KDBX_FILES[@]}; i++)); do
SOURCE_DB="${KDBX_FILES[$i]}" SOURCE_DB="${KDBX_FILES[$i]}"
log "" log ""
log "Merging $(basename "$SOURCE_DB") into $(basename "$TARGET_DB")..." log "Merging $(basename "$SOURCE_DB") into $(basename "$TARGET_DB")..."
# Reuse target password if user confirmed all are the same # Reuse target password if user confirmed all are the same
if [[ "$SAME_PASSWORD" == "y" || "$SAME_PASSWORD" == "yes" ]]; then if [[ $SAME_PASSWORD == "y" || $SAME_PASSWORD == "yes" ]]; then
SOURCE_PASSWORD="$TARGET_PASSWORD" SOURCE_PASSWORD="$TARGET_PASSWORD"
else else
# Ask for source password (might be different) # Ask for source password (might be different)
echo "" echo ""
read -rsp "Enter master password for SOURCE database ($(basename "$SOURCE_DB")): " SOURCE_PASSWORD read -rsp "Enter master password for SOURCE database ($(basename "$SOURCE_DB")): " SOURCE_PASSWORD
echo "" echo ""
fi fi
# Verify source password # Verify source password
if ! echo "$SOURCE_PASSWORD" | keepassxc-cli ls "$SOURCE_DB" &>/dev/null; then if ! echo "$SOURCE_PASSWORD" | keepassxc-cli ls "$SOURCE_DB" &> /dev/null; then
log "ERROR: Failed to open source database $(basename "$SOURCE_DB"). Wrong password?" log "ERROR: Failed to open source database $(basename "$SOURCE_DB"). Wrong password?"
log "Skipping this database. You can try again later." log "Skipping this database. You can try again later."
continue continue
fi fi
# Perform the merge # Perform the merge
# keepassxc-cli merge requires: target_db source_db # keepassxc-cli merge requires: target_db source_db
# It will prompt for passwords # It will prompt for passwords
if echo -e "${TARGET_PASSWORD}\n${SOURCE_PASSWORD}" | keepassxc-cli merge "$TARGET_DB" "$SOURCE_DB"; then if echo -e "${TARGET_PASSWORD}\n${SOURCE_PASSWORD}" | keepassxc-cli merge "$TARGET_DB" "$SOURCE_DB"; then
log "Successfully merged $(basename "$SOURCE_DB")" log "Successfully merged $(basename "$SOURCE_DB")"
# Delete the source database after successful merge # Delete the source database after successful merge
log "Deleting source database: $(basename "$SOURCE_DB")" log "Deleting source database: $(basename "$SOURCE_DB")"
rm -v "$SOURCE_DB" rm -v "$SOURCE_DB"
((MERGE_COUNT++)) || true ((MERGE_COUNT++)) || true
else else
log "ERROR: Failed to merge $(basename "$SOURCE_DB")" log "ERROR: Failed to merge $(basename "$SOURCE_DB")"
log "Source database NOT deleted. Check the backup and try manually." log "Source database NOT deleted. Check the backup and try manually."
fi fi
done done
echo "" echo ""
@ -159,15 +159,15 @@ find "$KEEPASS_DIR" -maxdepth 1 -name "*.kdbx" -type f -exec basename {} \;
# Rename to clean name if desired # Rename to clean name if desired
FINAL_COUNT=$(find "$KEEPASS_DIR" -maxdepth 1 -name "*.kdbx" -type f | wc -l) FINAL_COUNT=$(find "$KEEPASS_DIR" -maxdepth 1 -name "*.kdbx" -type f | wc -l)
if [[ $FINAL_COUNT -eq 1 ]]; then if [[ $FINAL_COUNT -eq 1 ]]; then
log "" log ""
FINAL_NAME="$KEEPASS_DIR/Passwords.kdbx" FINAL_NAME="$KEEPASS_DIR/Passwords.kdbx"
if [[ "$TARGET_DB" != "$FINAL_NAME" ]]; then if [[ $TARGET_DB != "$FINAL_NAME" ]]; then
read -rp "Rename final database to 'Passwords.kdbx'? (y/n): " RENAME_CONFIRM read -rp "Rename final database to 'Passwords.kdbx'? (y/n): " RENAME_CONFIRM
if [[ "$RENAME_CONFIRM" == "y" ]]; then if [[ $RENAME_CONFIRM == "y" ]]; then
mv -v "$TARGET_DB" "$FINAL_NAME" mv -v "$TARGET_DB" "$FINAL_NAME"
log "Final database: $FINAL_NAME" log "Final database: $FINAL_NAME"
fi fi
fi fi
log "" log ""
log "SUCCESS: You now have exactly ONE KeePassXC database!" log "SUCCESS: You now have exactly ONE KeePassXC database!"
fi fi

View File

@ -10,34 +10,34 @@ source "$SCRIPT_DIR/../lib/common.sh"
# Configuration ----------------------------------------------------------------- # Configuration -----------------------------------------------------------------
TARGET_SESSION_NAME="Xfce Session" TARGET_SESSION_NAME="Xfce Session"
TARGET_PACKAGES=( TARGET_PACKAGES=(
xfwm4 # Compositing window manager with XFCE integration xfwm4 # Compositing window manager with XFCE integration
xfce4-session # Provides the Xfce session entry for display managers xfce4-session # Provides the Xfce session entry for display managers
xfce4-panel # Panel with system tray support xfce4-panel # Panel with system tray support
xfce4-settings # Settings daemon (enables compositing toggle, theming, etc.) xfce4-settings # Settings daemon (enables compositing toggle, theming, etc.)
xfce4-terminal # Handy default terminal for the new environment xfce4-terminal # Handy default terminal for the new environment
) )
# Utility functions -------------------------------------------------------------- # Utility functions --------------------------------------------------------------
info() { echo "[INFO] $*"; } info() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; } warn() { echo "[WARN] $*" >&2; }
error() { error() {
echo "[ERROR] $*" >&2 echo "[ERROR] $*" >&2
exit 1 exit 1
} }
ensure_pacman() { ensure_pacman() {
require_command pacman "pacman" || error "Required command 'pacman' not found." require_command pacman "pacman" || error "Required command 'pacman' not found."
if ! grep -qi "arch" /etc/os-release 2>/dev/null; then if ! grep -qi "arch" /etc/os-release 2> /dev/null; then
warn "This script was designed for Arch Linux; continuing anyway." warn "This script was designed for Arch Linux; continuing anyway."
fi fi
} }
install_packages() { install_packages() {
install_missing_pacman_packages "${TARGET_PACKAGES[@]}" install_missing_pacman_packages "${TARGET_PACKAGES[@]}"
} }
print_post_install_tips() { print_post_install_tips() {
cat <<EOF cat << EOF
------------------------------------------------------------------------ ------------------------------------------------------------------------
XFCE session installed. XFCE session installed.
@ -54,31 +54,31 @@ EOF
} }
logout_user() { logout_user() {
local session_id="${XDG_SESSION_ID:-}" local session_id="${XDG_SESSION_ID:-}"
if [[ -n $session_id ]] && loginctl show-session "$session_id" >/dev/null 2>&1; then if [[ -n $session_id ]] && loginctl show-session "$session_id" > /dev/null 2>&1; then
info "Terminating current session (ID: $session_id) via loginctl." info "Terminating current session (ID: $session_id) via loginctl."
loginctl terminate-session "$session_id" loginctl terminate-session "$session_id"
return return
fi fi
if loginctl list-sessions 2>/dev/null | awk '{print $1" "$3}' | grep -q " $USER$"; then if loginctl list-sessions 2> /dev/null | awk '{print $1" "$3}' | grep -q " $USER$"; then
info "Terminating all sessions for user '$USER' via loginctl." info "Terminating all sessions for user '$USER' via loginctl."
loginctl terminate-user "$USER" loginctl terminate-user "$USER"
return return
fi fi
warn "loginctl could not terminate the session; attempting fallback logout." warn "loginctl could not terminate the session; attempting fallback logout."
pkill -KILL -u "$USER" || error "Failed to terminate user sessions. Please log out manually." pkill -KILL -u "$USER" || error "Failed to terminate user sessions. Please log out manually."
} }
main() { main() {
ensure_pacman ensure_pacman
install_packages install_packages
print_post_install_tips print_post_install_tips
# Give the user a moment to read the instructions before logging out. # Give the user a moment to read the instructions before logging out.
logout_user logout_user
} }
main "$@" main "$@"

View File

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

View File

@ -16,32 +16,32 @@ MODULE_DEST="/data/adb/modules/android_guardian"
# Ensure android-tools (adb) is installed # Ensure android-tools (adb) is installed
ensure_adb_installed() { ensure_adb_installed() {
if command -v adb &>/dev/null; then if command -v adb &> /dev/null; then
return 0 return 0
fi fi
log "adb not found, installing android-tools..." log "adb not found, installing android-tools..."
if command -v pacman &>/dev/null; then if command -v pacman &> /dev/null; then
sudo pacman -S --noconfirm android-tools || die "Failed to install android-tools" sudo pacman -S --noconfirm android-tools || die "Failed to install android-tools"
elif command -v apt-get &>/dev/null; then elif command -v apt-get &> /dev/null; then
sudo apt-get update && sudo apt-get install -y adb || die "Failed to install adb" sudo apt-get update && sudo apt-get install -y adb || die "Failed to install adb"
elif command -v dnf &>/dev/null; then elif command -v dnf &> /dev/null; then
sudo dnf install -y android-tools || die "Failed to install android-tools" sudo dnf install -y android-tools || die "Failed to install android-tools"
else else
die "adb not found and could not determine package manager. Please install android-tools manually." die "adb not found and could not determine package manager. Please install android-tools manually."
fi fi
# Verify installation # Verify installation
if ! command -v adb &>/dev/null; then if ! command -v adb &> /dev/null; then
die "adb installation failed" die "adb installation failed"
fi fi
log "android-tools installed successfully" log "android-tools installed successfully"
} }
show_usage() { show_usage() {
cat <<EOF cat << EOF
Usage: $(basename "$0") [COMMAND] Usage: $(basename "$0") [COMMAND]
Commands: Commands:
@ -81,239 +81,239 @@ WIRELESS_CONFIG="$HOME/.config/android_guardian_wireless"
# Discover Android devices on the network using mDNS # Discover Android devices on the network using mDNS
discover_android_device() { discover_android_device() {
local found_address="" local found_address=""
# Ensure avahi-browse is available # Ensure avahi-browse is available
if ! command -v avahi-browse &>/dev/null; then if ! command -v avahi-browse &> /dev/null; then
if command -v pacman &>/dev/null; then if command -v pacman &> /dev/null; then
echo "Installing avahi for device discovery..." >&2 echo "Installing avahi for device discovery..." >&2
sudo pacman -S --noconfirm avahi nss-mdns &>/dev/null || true sudo pacman -S --noconfirm avahi nss-mdns &> /dev/null || true
sudo systemctl enable --now avahi-daemon &>/dev/null || true sudo systemctl enable --now avahi-daemon &> /dev/null || true
elif command -v apt-get &>/dev/null; then elif command -v apt-get &> /dev/null; then
sudo apt-get install -y avahi-utils &>/dev/null || true sudo apt-get install -y avahi-utils &> /dev/null || true
fi fi
fi fi
if command -v avahi-browse &>/dev/null; then if command -v avahi-browse &> /dev/null; then
echo "Scanning for Android devices (5 seconds)..." >&2 echo "Scanning for Android devices (5 seconds)..." >&2
# Android wireless debugging advertises as _adb-tls-connect._tcp # Android wireless debugging advertises as _adb-tls-connect._tcp
local discovery_result local discovery_result
discovery_result=$(timeout 5 avahi-browse -rpt _adb-tls-connect._tcp 2>/dev/null | grep "^=" | head -1) discovery_result=$(timeout 5 avahi-browse -rpt _adb-tls-connect._tcp 2> /dev/null | grep "^=" | head -1)
if [[ -n "$discovery_result" ]]; then if [[ -n $discovery_result ]]; then
# Parse: =;eth0;IPv4;adb-...;_adb-tls-connect._tcp;local;hostname.local;192.168.x.x;port;... # Parse: =;eth0;IPv4;adb-...;_adb-tls-connect._tcp;local;hostname.local;192.168.x.x;port;...
local ip port local ip port
ip=$(echo "$discovery_result" | cut -d';' -f8) ip=$(echo "$discovery_result" | cut -d';' -f8)
port=$(echo "$discovery_result" | cut -d';' -f9) port=$(echo "$discovery_result" | cut -d';' -f9)
if [[ -n "$ip" && -n "$port" ]]; then if [[ -n $ip && -n $port ]]; then
found_address="$ip:$port" found_address="$ip:$port"
echo "✓ Found device: $found_address" >&2 echo "✓ Found device: $found_address" >&2
fi fi
fi fi
fi fi
# Fallback: try adb's mdns discovery # Fallback: try adb's mdns discovery
if [[ -z "$found_address" ]]; then if [[ -z $found_address ]]; then
echo "Trying adb mdns discovery..." >&2 echo "Trying adb mdns discovery..." >&2
# adb can discover devices via mdns # adb can discover devices via mdns
local mdns_result local mdns_result
mdns_result=$(timeout 5 adb mdns services 2>/dev/null | grep -E "adb-tls-connect|_adb\._tcp" | head -1) mdns_result=$(timeout 5 adb mdns services 2> /dev/null | grep -E "adb-tls-connect|_adb\._tcp" | head -1)
if [[ -n "$mdns_result" ]]; then if [[ -n $mdns_result ]]; then
# Try to extract IP:port from the result # Try to extract IP:port from the result
local service_name local service_name
service_name=$(echo "$mdns_result" | awk '{print $1}') service_name=$(echo "$mdns_result" | awk '{print $1}')
if [[ -n "$service_name" ]]; then if [[ -n $service_name ]]; then
# Try connecting via service name # Try connecting via service name
echo "Found service: $service_name" >&2 echo "Found service: $service_name" >&2
fi fi
fi fi
fi fi
# Return found address (or empty) # Return found address (or empty)
echo "$found_address" echo "$found_address"
} }
# Pair with device over WiFi (Android 11+) # Pair with device over WiFi (Android 11+)
cmd_pair() { cmd_pair() {
ensure_adb_installed ensure_adb_installed
echo "" echo ""
echo "=== Wireless ADB Pairing (Android 11+) ===" echo "=== Wireless ADB Pairing (Android 11+) ==="
echo "" echo ""
echo "On your phone:" echo "On your phone:"
echo " 1. Go to Settings > Developer Options > Wireless debugging" echo " 1. Go to Settings > Developer Options > Wireless debugging"
echo " 2. Enable Wireless debugging" echo " 2. Enable Wireless debugging"
echo " 3. Tap 'Pair device with pairing code'" echo " 3. Tap 'Pair device with pairing code'"
echo " 4. Note the IP:port and pairing code shown" echo " 4. Note the IP:port and pairing code shown"
echo "" echo ""
read -rp "Enter pairing IP:port (e.g., 192.168.1.100:37123): " pair_address read -rp "Enter pairing IP:port (e.g., 192.168.1.100:37123): " pair_address
read -rp "Enter pairing code: " pair_code read -rp "Enter pairing code: " pair_code
if [[ -z "$pair_address" || -z "$pair_code" ]]; then if [[ -z $pair_address || -z $pair_code ]]; then
die "Pairing address and code are required" die "Pairing address and code are required"
fi fi
log "Pairing with device at $pair_address..." log "Pairing with device at $pair_address..."
if adb pair "$pair_address" "$pair_code"; then if adb pair "$pair_address" "$pair_code"; then
echo "" echo ""
echo "✓ Pairing successful!" echo "✓ Pairing successful!"
echo "" echo ""
echo "Now get the connection address:" echo "Now get the connection address:"
echo " On phone: Wireless debugging screen shows IP:port under 'IP address & Port'" echo " On phone: Wireless debugging screen shows IP:port under 'IP address & Port'"
echo " (This is DIFFERENT from the pairing port)" echo " (This is DIFFERENT from the pairing port)"
echo "" echo ""
read -rp "Enter connection IP:port (e.g., 192.168.1.100:41567): " connect_address read -rp "Enter connection IP:port (e.g., 192.168.1.100:41567): " connect_address
if [[ -n "$connect_address" ]]; then if [[ -n $connect_address ]]; then
# Save for future connections # Save for future connections
mkdir -p "$(dirname "$WIRELESS_CONFIG")" mkdir -p "$(dirname "$WIRELESS_CONFIG")"
echo "$connect_address" >"$WIRELESS_CONFIG" echo "$connect_address" > "$WIRELESS_CONFIG"
log "Saved connection address for future use" log "Saved connection address for future use"
# Connect now # Connect now
cmd_connect cmd_connect
fi fi
else else
die "Pairing failed. Make sure the code is correct and you're on the same network." die "Pairing failed. Make sure the code is correct and you're on the same network."
fi fi
} }
# Connect to already-paired device # Connect to already-paired device
cmd_connect() { cmd_connect() {
ensure_adb_installed ensure_adb_installed
local connect_address="" local connect_address=""
# Check for saved address # Check for saved address
if [[ -f "$WIRELESS_CONFIG" ]]; then if [[ -f $WIRELESS_CONFIG ]]; then
connect_address=$(cat "$WIRELESS_CONFIG") connect_address=$(cat "$WIRELESS_CONFIG")
log "Using saved address: $connect_address" log "Using saved address: $connect_address"
fi fi
# Try auto-discovery if no saved address # Try auto-discovery if no saved address
if [[ -z "$connect_address" ]]; then if [[ -z $connect_address ]]; then
echo "" echo ""
log "Searching for Android devices on network..." log "Searching for Android devices on network..."
connect_address=$(discover_android_device) connect_address=$(discover_android_device)
fi fi
# Manual fallback # Manual fallback
if [[ -z "$connect_address" ]]; then if [[ -z $connect_address ]]; then
echo "" echo ""
echo "Auto-discovery failed. Enter address manually." echo "Auto-discovery failed. Enter address manually."
echo "On phone: Settings > Developer Options > Wireless debugging" echo "On phone: Settings > Developer Options > Wireless debugging"
echo "Look for IP address & Port (NOT the pairing port)" echo "Look for IP address & Port (NOT the pairing port)"
echo "" echo ""
read -rp "Enter connection IP:port (e.g., 192.168.1.100:41567): " connect_address read -rp "Enter connection IP:port (e.g., 192.168.1.100:41567): " connect_address
if [[ -z "$connect_address" ]]; then if [[ -z $connect_address ]]; then
die "Connection address is required" die "Connection address is required"
fi fi
fi fi
# Save for future # Save for future
mkdir -p "$(dirname "$WIRELESS_CONFIG")" mkdir -p "$(dirname "$WIRELESS_CONFIG")"
echo "$connect_address" >"$WIRELESS_CONFIG" echo "$connect_address" > "$WIRELESS_CONFIG"
log "Connecting to $connect_address..." log "Connecting to $connect_address..."
if adb connect "$connect_address" | grep -q "connected"; then if adb connect "$connect_address" | grep -q "connected"; then
echo "" echo ""
echo "✓ Connected to device wirelessly!" echo "✓ Connected to device wirelessly!"
echo "" echo ""
# Verify connection # Verify connection
if adb devices | grep -q "$connect_address"; then if adb devices | grep -q "$connect_address"; then
echo "Device ready. You can now run other commands." echo "Device ready. You can now run other commands."
fi fi
else else
echo "" echo ""
echo "Connection failed. Possible issues:" echo "Connection failed. Possible issues:"
echo " - Wireless debugging not enabled on phone" echo " - Wireless debugging not enabled on phone"
echo " - Phone and PC not on same WiFi network" echo " - Phone and PC not on same WiFi network"
echo " - Port changed (check Wireless debugging screen)" echo " - Port changed (check Wireless debugging screen)"
echo " - May need to pair first: $0 pair" echo " - May need to pair first: $0 pair"
echo "" echo ""
# Clear saved config since it failed # Clear saved config since it failed
rm -f "$WIRELESS_CONFIG" rm -f "$WIRELESS_CONFIG"
exit 1 exit 1
fi fi
} }
# Disconnect wireless ADB # Disconnect wireless ADB
cmd_disconnect() { cmd_disconnect() {
ensure_adb_installed ensure_adb_installed
log "Disconnecting all wireless devices..." log "Disconnecting all wireless devices..."
adb disconnect adb disconnect
echo "✓ Disconnected" echo "✓ Disconnected"
} }
# Check device connection and root # Check device connection and root
ensure_device_ready() { ensure_device_ready() {
ensure_adb_installed ensure_adb_installed
# Check if any device is connected # Check if any device is connected
if ! adb devices | grep -qE "device$|:.*device$"; then if ! adb devices | grep -qE "device$|:.*device$"; then
echo "" echo ""
echo "No device connected!" echo "No device connected!"
echo "" echo ""
echo "Options:" echo "Options:"
echo " 1. Connect USB cable with debugging enabled" echo " 1. Connect USB cable with debugging enabled"
echo " 2. Use wireless: $0 pair (first time) or $0 connect" echo " 2. Use wireless: $0 pair (first time) or $0 connect"
echo "" echo ""
# Check if we have a saved wireless config # Check if we have a saved wireless config
if [[ -f "$WIRELESS_CONFIG" ]]; then if [[ -f $WIRELESS_CONFIG ]]; then
read -rp "Try connecting to saved wireless device? [Y/n]: " try_wireless read -rp "Try connecting to saved wireless device? [Y/n]: " try_wireless
if [[ "${try_wireless,,}" != "n" ]]; then if [[ ${try_wireless,,} != "n" ]]; then
cmd_connect cmd_connect
else else
exit 1 exit 1
fi fi
else else
exit 1 exit 1
fi fi
fi fi
check_adb_device check_adb_device
check_adb_root check_adb_root
} }
# Build the module zip # Build the module zip
build_module() { build_module() {
local tmp_dir="$WORK_DIR/guardian_module" local tmp_dir="$WORK_DIR/guardian_module"
local module_zip="$WORK_DIR/android_guardian.zip" local module_zip="$WORK_DIR/android_guardian.zip"
echo "[BUILD] Building Android Guardian module..." >&2 echo "[BUILD] Building Android Guardian module..." >&2
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
mkdir -p "$tmp_dir/system/etc" mkdir -p "$tmp_dir/system/etc"
# Copy module files # Copy module files
cp "$GUARDIAN_MODULE_DIR/module.prop" "$tmp_dir/" cp "$GUARDIAN_MODULE_DIR/module.prop" "$tmp_dir/"
cp "$GUARDIAN_MODULE_DIR/service.sh" "$tmp_dir/" cp "$GUARDIAN_MODULE_DIR/service.sh" "$tmp_dir/"
cp "$GUARDIAN_MODULE_DIR/post-fs-data.sh" "$tmp_dir/" cp "$GUARDIAN_MODULE_DIR/post-fs-data.sh" "$tmp_dir/"
cp "$GUARDIAN_MODULE_DIR/uninstall.sh" "$tmp_dir/" cp "$GUARDIAN_MODULE_DIR/uninstall.sh" "$tmp_dir/"
# Build hosts file # Build hosts file
local hosts_file="$tmp_dir/system/etc/hosts" local hosts_file="$tmp_dir/system/etc/hosts"
if [[ -f /etc/hosts.stevenblack ]]; then if [[ -f /etc/hosts.stevenblack ]]; then
echo "[BUILD] Using StevenBlack hosts cache..." >&2 echo "[BUILD] Using StevenBlack hosts cache..." >&2
cp /etc/hosts.stevenblack "$hosts_file" cp /etc/hosts.stevenblack "$hosts_file"
elif [[ -f /etc/hosts ]]; then elif [[ -f /etc/hosts ]]; then
echo "[BUILD] Using /etc/hosts..." >&2 echo "[BUILD] Using /etc/hosts..." >&2
cp /etc/hosts "$hosts_file" cp /etc/hosts "$hosts_file"
else else
die "No hosts file found" die "No hosts file found"
fi fi
# Append custom blocking entries # Append custom blocking entries
cat >>"$hosts_file" <<'CUSTOM_EOF' cat >> "$hosts_file" << 'CUSTOM_EOF'
# ============================================ # ============================================
# Custom blocking entries - Android Guardian # Custom blocking entries - Android Guardian
@ -382,252 +382,252 @@ build_module() {
0.0.0.0 www.dominos.com 0.0.0.0 www.dominos.com
CUSTOM_EOF CUSTOM_EOF
local total_entries local total_entries
total_entries=$(grep -c "^0\.0\.0\.0 " "$hosts_file" || echo 0) total_entries=$(grep -c "^0\.0\.0\.0 " "$hosts_file" || echo 0)
echo "[BUILD] Hosts file contains $total_entries blocked domains" >&2 echo "[BUILD] Hosts file contains $total_entries blocked domains" >&2
# Create zip # Create zip
(cd "$tmp_dir" && zip -r "$module_zip" . -x "*.DS_Store") >/dev/null (cd "$tmp_dir" && zip -r "$module_zip" . -x "*.DS_Store") > /dev/null
echo "$module_zip" echo "$module_zip"
} }
# Install/update the guardian module # Install/update the guardian module
cmd_install() { cmd_install() {
ensure_device_ready ensure_device_ready
local module_zip local module_zip
module_zip=$(build_module) module_zip=$(build_module)
log "Pushing module to device..." log "Pushing module to device..."
adb push "$module_zip" /sdcard/android_guardian.zip || die "Failed to push module" adb push "$module_zip" /sdcard/android_guardian.zip || die "Failed to push module"
log "Installing module..." log "Installing module..."
adb shell "su -c 'mkdir -p $MODULE_DEST'" || die "Failed to create module directory" adb shell "su -c 'mkdir -p $MODULE_DEST'" || die "Failed to create module directory"
adb shell "su -c 'cd $MODULE_DEST && unzip -o /sdcard/android_guardian.zip'" || die "Failed to extract module" adb shell "su -c 'cd $MODULE_DEST && unzip -o /sdcard/android_guardian.zip'" || die "Failed to extract module"
adb shell "su -c 'chmod 755 $MODULE_DEST/*.sh'" adb shell "su -c 'chmod 755 $MODULE_DEST/*.sh'"
adb shell "su -c 'rm /sdcard/android_guardian.zip'" adb shell "su -c 'rm /sdcard/android_guardian.zip'"
# Set up guardian data directory # Set up guardian data directory
log "Setting up guardian data..." log "Setting up guardian data..."
adb shell "su -c 'mkdir -p $GUARDIAN_DATA_DIR'" adb shell "su -c 'mkdir -p $GUARDIAN_DATA_DIR'"
adb shell "su -c 'echo ENABLED > $GUARDIAN_DATA_DIR/control'" adb shell "su -c 'echo ENABLED > $GUARDIAN_DATA_DIR/control'"
# Copy blocked apps list # Copy blocked apps list
adb push "$GUARDIAN_MODULE_DIR/blocked_apps.txt" /sdcard/blocked_apps.txt || die "Failed to push blocked apps list" adb push "$GUARDIAN_MODULE_DIR/blocked_apps.txt" /sdcard/blocked_apps.txt || die "Failed to push blocked apps list"
adb shell "su -c 'cp /sdcard/blocked_apps.txt $GUARDIAN_DATA_DIR/blocked_apps.txt'" adb shell "su -c 'cp /sdcard/blocked_apps.txt $GUARDIAN_DATA_DIR/blocked_apps.txt'"
adb shell "su -c 'rm /sdcard/blocked_apps.txt'" adb shell "su -c 'rm /sdcard/blocked_apps.txt'"
# Create hosts backup for tamper protection # Create hosts backup for tamper protection
adb shell "su -c 'cp $MODULE_DEST/system/etc/hosts $GUARDIAN_DATA_DIR/hosts.backup'" adb shell "su -c 'cp $MODULE_DEST/system/etc/hosts $GUARDIAN_DATA_DIR/hosts.backup'"
# Immediately uninstall any currently installed blocked apps # Immediately uninstall any currently installed blocked apps
log "Checking for blocked apps to remove..." log "Checking for blocked apps to remove..."
uninstall_blocked_apps uninstall_blocked_apps
echo "" echo ""
echo "==========================================" echo "=========================================="
echo " ✓ Android Guardian installed!" echo " ✓ Android Guardian installed!"
echo "==========================================" echo "=========================================="
echo "" echo ""
echo "Features enabled:" echo "Features enabled:"
echo " • Hosts-based ad/tracker blocking" echo " • Hosts-based ad/tracker blocking"
echo " • App installation blocking" echo " • App installation blocking"
echo " • Tamper protection" echo " • Tamper protection"
echo "" echo ""
echo "⚠️ This can ONLY be controlled via ADB:" echo "⚠️ This can ONLY be controlled via ADB:"
echo " Disable: $0 disable" echo " Disable: $0 disable"
echo " Enable: $0 enable" echo " Enable: $0 enable"
echo " Status: $0 status" echo " Status: $0 status"
echo "" echo ""
echo "Reboot your device to activate the module." echo "Reboot your device to activate the module."
echo "" echo ""
} }
# Uninstall currently installed blocked apps # Uninstall currently installed blocked apps
uninstall_blocked_apps() { uninstall_blocked_apps() {
local blocked_apps local blocked_apps
blocked_apps=$(grep -v '^#' "$GUARDIAN_MODULE_DIR/blocked_apps.txt" | grep -v '^$' || true) blocked_apps=$(grep -v '^#' "$GUARDIAN_MODULE_DIR/blocked_apps.txt" | grep -v '^$' || true)
for package in $blocked_apps; do for package in $blocked_apps; do
if adb shell "pm list packages" 2>/dev/null | grep -q "package:$package"; then if adb shell "pm list packages" 2> /dev/null | grep -q "package:$package"; then
log "Uninstalling blocked app: $package" log "Uninstalling blocked app: $package"
adb shell "pm uninstall $package" 2>/dev/null || true adb shell "pm uninstall $package" 2> /dev/null || true
fi fi
done done
} }
# Show status # Show status
cmd_status() { cmd_status() {
ensure_device_ready ensure_device_ready
echo "" echo ""
echo "=== Android Guardian Status ===" echo "=== Android Guardian Status ==="
echo "" echo ""
# Check if module is installed # Check if module is installed
if adb shell "su -c 'test -d $MODULE_DEST'" 2>/dev/null; then if adb shell "su -c 'test -d $MODULE_DEST'" 2> /dev/null; then
echo "Module: INSTALLED" echo "Module: INSTALLED"
else else
echo "Module: NOT INSTALLED" echo "Module: NOT INSTALLED"
return return
fi fi
# Check control status # Check control status
local status local status
status=$(adb shell "su -c 'cat $GUARDIAN_DATA_DIR/control 2>/dev/null || echo UNKNOWN'" | tr -d '\r') status=$(adb shell "su -c 'cat $GUARDIAN_DATA_DIR/control 2>/dev/null || echo UNKNOWN'" | tr -d '\r')
echo "Status: $status" echo "Status: $status"
# Check if module is "disabled" in Magisk UI (should be auto-fixed by watchdog) # Check if module is "disabled" in Magisk UI (should be auto-fixed by watchdog)
local magisk_disabled local magisk_disabled
if adb shell "su -c 'test -f $MODULE_DEST/disable'" 2>/dev/null; then if adb shell "su -c 'test -f $MODULE_DEST/disable'" 2> /dev/null; then
magisk_disabled="YES (watchdog should fix this)" magisk_disabled="YES (watchdog should fix this)"
else else
magisk_disabled="No" magisk_disabled="No"
fi fi
echo "Magisk UI disabled: $magisk_disabled" echo "Magisk UI disabled: $magisk_disabled"
# Check if watchdog is running # Check if watchdog is running
local watchdog_running local watchdog_running
watchdog_running=$(adb shell "su -c 'pgrep -f watchdog.sh 2>/dev/null | wc -l'" | tr -d '\r') watchdog_running=$(adb shell "su -c 'pgrep -f watchdog.sh 2>/dev/null | wc -l'" | tr -d '\r')
if [ "$watchdog_running" -gt 0 ] 2>/dev/null; then if [ "$watchdog_running" -gt 0 ] 2> /dev/null; then
echo "Watchdog: RUNNING ($watchdog_running processes)" echo "Watchdog: RUNNING ($watchdog_running processes)"
else else
echo "Watchdog: NOT RUNNING (reboot phone to start)" echo "Watchdog: NOT RUNNING (reboot phone to start)"
fi fi
# Check hosts file # Check hosts file
local hosts_entries local hosts_entries
hosts_entries=$(adb shell "su -c 'grep -c \"^0.0.0.0\" /system/etc/hosts 2>/dev/null || echo 0'" | tr -d '\r') hosts_entries=$(adb shell "su -c 'grep -c \"^0.0.0.0\" /system/etc/hosts 2>/dev/null || echo 0'" | tr -d '\r')
echo "Blocked domains: $hosts_entries" echo "Blocked domains: $hosts_entries"
# Check blocked apps count # Check blocked apps count
local blocked_count local blocked_count
blocked_count=$(adb shell "su -c 'grep -v \"^#\" $GUARDIAN_DATA_DIR/blocked_apps.txt 2>/dev/null | grep -v \"^$\" | wc -l || echo 0'" | tr -d '\r') blocked_count=$(adb shell "su -c 'grep -v \"^#\" $GUARDIAN_DATA_DIR/blocked_apps.txt 2>/dev/null | grep -v \"^$\" | wc -l || echo 0'" | tr -d '\r')
echo "Blocked app rules: $blocked_count packages" echo "Blocked app rules: $blocked_count packages"
echo "" echo ""
echo "Protection: Module cannot be disabled from Magisk UI" echo "Protection: Module cannot be disabled from Magisk UI"
echo " Only controllable via: $0 disable/enable" echo " Only controllable via: $0 disable/enable"
echo "" echo ""
} }
# Disable guardian # Disable guardian
cmd_disable() { cmd_disable() {
ensure_device_ready ensure_device_ready
log "Disabling Android Guardian..." log "Disabling Android Guardian..."
adb shell "su -c 'echo DISABLED > $GUARDIAN_DATA_DIR/control'" || die "Failed to disable guardian" adb shell "su -c 'echo DISABLED > $GUARDIAN_DATA_DIR/control'" || die "Failed to disable guardian"
echo "" echo ""
echo "✓ Guardian DISABLED" echo "✓ Guardian DISABLED"
echo " Hosts blocking still active until reboot" echo " Hosts blocking still active until reboot"
echo " App blocking service paused" echo " App blocking service paused"
echo "" echo ""
echo "To re-enable: $0 enable" echo "To re-enable: $0 enable"
echo "" echo ""
} }
# Enable guardian # Enable guardian
cmd_enable() { cmd_enable() {
ensure_device_ready ensure_device_ready
log "Enabling Android Guardian..." log "Enabling Android Guardian..."
adb shell "su -c 'echo ENABLED > $GUARDIAN_DATA_DIR/control'" || die "Failed to enable guardian" adb shell "su -c 'echo ENABLED > $GUARDIAN_DATA_DIR/control'" || die "Failed to enable guardian"
echo "" echo ""
echo "✓ Guardian ENABLED" echo "✓ Guardian ENABLED"
echo "" echo ""
} }
# Uninstall module # Uninstall module
cmd_uninstall() { cmd_uninstall() {
ensure_device_ready ensure_device_ready
# Check if disabled first # Check if disabled first
local status local status
status=$(adb shell "su -c 'cat $GUARDIAN_DATA_DIR/control 2>/dev/null || echo ENABLED'" | tr -d '\r') status=$(adb shell "su -c 'cat $GUARDIAN_DATA_DIR/control 2>/dev/null || echo ENABLED'" | tr -d '\r')
if [[ "$status" != "DISABLED" ]]; then if [[ $status != "DISABLED" ]]; then
echo "" echo ""
echo "⚠️ Guardian must be disabled before uninstalling!" echo "⚠️ Guardian must be disabled before uninstalling!"
echo " Run: $0 disable" echo " Run: $0 disable"
echo " Then: $0 uninstall" echo " Then: $0 uninstall"
echo "" echo ""
exit 1 exit 1
fi fi
log "Removing Android Guardian..." log "Removing Android Guardian..."
adb shell "su -c 'rm -rf $MODULE_DEST'" adb shell "su -c 'rm -rf $MODULE_DEST'"
adb shell "su -c 'rm -rf $GUARDIAN_DATA_DIR'" adb shell "su -c 'rm -rf $GUARDIAN_DATA_DIR'"
echo "" echo ""
echo "✓ Guardian uninstalled" echo "✓ Guardian uninstalled"
echo " Reboot to remove hosts blocking" echo " Reboot to remove hosts blocking"
echo "" echo ""
} }
# Show logs # Show logs
cmd_logs() { cmd_logs() {
ensure_device_ready ensure_device_ready
echo "=== Guardian Logs ===" echo "=== Guardian Logs ==="
adb shell "su -c 'cat $GUARDIAN_DATA_DIR/guardian.log 2>/dev/null || echo \"No logs yet\"'" adb shell "su -c 'cat $GUARDIAN_DATA_DIR/guardian.log 2>/dev/null || echo \"No logs yet\"'"
} }
# Block an app # Block an app
cmd_block_app() { cmd_block_app() {
local package="${1:-}" local package="${1:-}"
if [[ -z "$package" ]]; then if [[ -z $package ]]; then
echo "Usage: $0 block-app <package.name>" echo "Usage: $0 block-app <package.name>"
echo "Example: $0 block-app com.ubercab.eats" echo "Example: $0 block-app com.ubercab.eats"
exit 1 exit 1
fi fi
ensure_device_ready ensure_device_ready
log "Adding $package to block list..." log "Adding $package to block list..."
adb shell "su -c 'echo \"$package\" >> $GUARDIAN_DATA_DIR/blocked_apps.txt'" adb shell "su -c 'echo \"$package\" >> $GUARDIAN_DATA_DIR/blocked_apps.txt'"
# Also add to local file # Also add to local file
echo "$package" >>"$GUARDIAN_MODULE_DIR/blocked_apps.txt" echo "$package" >> "$GUARDIAN_MODULE_DIR/blocked_apps.txt"
# Try to uninstall if currently installed # Try to uninstall if currently installed
if adb shell "pm list packages" 2>/dev/null | grep -q "package:$package"; then if adb shell "pm list packages" 2> /dev/null | grep -q "package:$package"; then
log "Uninstalling $package..." log "Uninstalling $package..."
adb shell "pm uninstall $package" 2>/dev/null || true adb shell "pm uninstall $package" 2> /dev/null || true
fi fi
echo "$package added to block list" echo "$package added to block list"
} }
# Unblock an app # Unblock an app
cmd_unblock_app() { cmd_unblock_app() {
local package="${1:-}" local package="${1:-}"
if [[ -z "$package" ]]; then if [[ -z $package ]]; then
echo "Usage: $0 unblock-app <package.name>" echo "Usage: $0 unblock-app <package.name>"
exit 1 exit 1
fi fi
ensure_device_ready ensure_device_ready
log "Removing $package from block list..." log "Removing $package from block list..."
adb shell "su -c 'grep -v \"^$package\$\" $GUARDIAN_DATA_DIR/blocked_apps.txt > $GUARDIAN_DATA_DIR/blocked_apps.tmp && mv $GUARDIAN_DATA_DIR/blocked_apps.tmp $GUARDIAN_DATA_DIR/blocked_apps.txt'" adb shell "su -c 'grep -v \"^$package\$\" $GUARDIAN_DATA_DIR/blocked_apps.txt > $GUARDIAN_DATA_DIR/blocked_apps.tmp && mv $GUARDIAN_DATA_DIR/blocked_apps.tmp $GUARDIAN_DATA_DIR/blocked_apps.txt'"
# Also remove from local file # Also remove from local file
grep -v "^$package$" "$GUARDIAN_MODULE_DIR/blocked_apps.txt" >"$GUARDIAN_MODULE_DIR/blocked_apps.tmp" && mv "$GUARDIAN_MODULE_DIR/blocked_apps.tmp" "$GUARDIAN_MODULE_DIR/blocked_apps.txt" grep -v "^$package$" "$GUARDIAN_MODULE_DIR/blocked_apps.txt" > "$GUARDIAN_MODULE_DIR/blocked_apps.tmp" && mv "$GUARDIAN_MODULE_DIR/blocked_apps.tmp" "$GUARDIAN_MODULE_DIR/blocked_apps.txt"
echo "$package removed from block list" echo "$package removed from block list"
} }
# List blocked apps # List blocked apps
cmd_list_blocked() { cmd_list_blocked() {
ensure_device_ready ensure_device_ready
echo "=== Blocked Apps ===" echo "=== Blocked Apps ==="
adb shell "su -c 'cat $GUARDIAN_DATA_DIR/blocked_apps.txt 2>/dev/null'" | grep -v "^#" | grep -v "^$" || echo "No blocked apps" adb shell "su -c 'cat $GUARDIAN_DATA_DIR/blocked_apps.txt 2>/dev/null'" | grep -v "^#" | grep -v "^$" || echo "No blocked apps"
} }
# Main # Main
@ -638,48 +638,48 @@ COMMAND="${1:-install}"
shift || true shift || true
case "$COMMAND" in case "$COMMAND" in
install) install)
cmd_install cmd_install
;; ;;
status) status)
cmd_status cmd_status
;; ;;
disable) disable)
cmd_disable cmd_disable
;; ;;
enable) enable)
cmd_enable cmd_enable
;; ;;
uninstall) uninstall)
cmd_uninstall cmd_uninstall
;; ;;
logs) logs)
cmd_logs cmd_logs
;; ;;
block-app) block-app)
cmd_block_app "$@" cmd_block_app "$@"
;; ;;
unblock-app) unblock-app)
cmd_unblock_app "$@" cmd_unblock_app "$@"
;; ;;
list-blocked) list-blocked)
cmd_list_blocked cmd_list_blocked
;; ;;
pair) pair)
cmd_pair cmd_pair
;; ;;
connect) connect)
cmd_connect cmd_connect
;; ;;
disconnect) disconnect)
cmd_disconnect cmd_disconnect
;; ;;
-h | --help | help) -h | --help | help)
show_usage show_usage
;; ;;
*) *)
echo "Unknown command: $COMMAND" echo "Unknown command: $COMMAND"
show_usage show_usage
exit 1 exit 1
;; ;;
esac esac