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
if: success()
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
[ "${GPU_VENDOR}" = "amd" ] || {
echo "AMD installer invoked but GPU_VENDOR=${GPU_VENDOR}"
exit 0
echo "AMD installer invoked but GPU_VENDOR=${GPU_VENDOR}"
exit 0
}
AMD_INSTALL_XF86=${AMD_INSTALL_XF86:-0}
@ -32,9 +32,9 @@ warn() { echo "[amd][warn] $*" >&2; }
# Detect multilib enabled
if grep -q '^\[multilib\]' /etc/pacman.conf; then
MULTILIB_ENABLED=1
MULTILIB_ENABLED=1
else
MULTILIB_ENABLED=0
MULTILIB_ENABLED=0
fi
# Basic packages
@ -58,49 +58,49 @@ LIB32_AMDVLK_PKG="lib32-amdvlk"
# Simple AUR builder (reused from NVIDIA script style)
_build_aur_pkg() {
local pkg="$1"
local url="https://aur.archlinux.org/${pkg}.git"
mkdir -p "$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
cd "$pkg"
rm -f -- *.pkg.tar.* 2>/dev/null || true
yes | makepkg -s -c -C --noconfirm --needed
local built=(*.pkg.tar.zst)
yes | sudo pacman -U --noconfirm "${built[@]}"
local pkg="$1"
local url="https://aur.archlinux.org/${pkg}.git"
mkdir -p "$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
cd "$pkg"
rm -f -- *.pkg.tar.* 2> /dev/null || true
yes | makepkg -s -c -C --noconfirm --needed
local built=(*.pkg.tar.zst)
yes | sudo pacman -U --noconfirm "${built[@]}"
}
_install_repo_or_aur() {
local pkg="$1"
if pacman -Si "$pkg" >/dev/null 2>&1; then
if pacman -Qi "$pkg" >/dev/null 2>&1; then
vlog "$pkg already installed"
else
yes | sudo pacman -Sy --noconfirm "$pkg"
fi
else
info "Building AUR package: $pkg"
_build_aur_pkg "$pkg"
fi
local pkg="$1"
if pacman -Si "$pkg" > /dev/null 2>&1; then
if pacman -Qi "$pkg" > /dev/null 2>&1; then
vlog "$pkg already installed"
else
yes | sudo pacman -Sy --noconfirm "$pkg"
fi
else
info "Building AUR package: $pkg"
_build_aur_pkg "$pkg"
fi
}
info "Installing AMD GPU stack"
for p in "${BASE_PKGS[@]}" "$VULKAN_PKG"; do _install_repo_or_aur "$p"; done
if [ "$AMD_INSTALL_XF86" = 1 ]; then
_install_repo_or_aur "$XF86_PKG"
_install_repo_or_aur "$XF86_PKG"
fi
# AMDVLK optional (install after vulkan-radeon if requested)
if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then
_install_repo_or_aur "$AMDVLK_PKG"
_install_repo_or_aur "$AMDVLK_PKG"
fi
if [ $MULTILIB_ENABLED = 1 ] || [ "$AMD_INSTALL_LIB32" = 1 ]; then
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
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
else
vlog "Skipping 32-bit packages (multilib disabled)"
vlog "Skipping 32-bit packages (multilib disabled)"
fi
# 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
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)"
TMP_CONF=$(mktemp)
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"
sudo mkdir -p /etc/modprobe.d
sudo cp "$TMP_CONF" /etc/modprobe.d/10-amdgpu-si-cik.conf
rm -f "$TMP_CONF"
# Ensure amdgpu early in MODULES
if [ -f /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
fi
if ! grep -q 'modconf' /etc/mkinitcpio.conf; then
warn "modconf hook not found in mkinitcpio.conf (needed for module options)"
fi
if [ "$AMD_SKIP_INITRAMFS" != 1 ]; then
info "Regenerating initramfs (mkinitcpio -P)"
sudo mkinitcpio -P || warn "mkinitcpio failed; review manually"
else
info "Skipping initramfs regeneration per AMD_SKIP_INITRAMFS=1"
fi
else
warn "/etc/mkinitcpio.conf not found; skipping MODULES update"
fi
info "Configuring amdgpu for SI/CIK (IS_SI=$IS_SI IS_CIK=$IS_CIK)"
TMP_CONF=$(mktemp)
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"
sudo mkdir -p /etc/modprobe.d
sudo cp "$TMP_CONF" /etc/modprobe.d/10-amdgpu-si-cik.conf
rm -f "$TMP_CONF"
# Ensure amdgpu early in MODULES
if [ -f /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
fi
if ! grep -q 'modconf' /etc/mkinitcpio.conf; then
warn "modconf hook not found in mkinitcpio.conf (needed for module options)"
fi
if [ "$AMD_SKIP_INITRAMFS" != 1 ]; then
info "Regenerating initramfs (mkinitcpio -P)"
sudo mkinitcpio -P || warn "mkinitcpio failed; review manually"
else
info "Skipping initramfs regeneration per AMD_SKIP_INITRAMFS=1"
fi
else
warn "/etc/mkinitcpio.conf not found; skipping MODULES update"
fi
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
# 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}')
info "Kernel driver in use: ${KDRV:-unknown}"
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
export AMD_STACK_DONE=1

View File

@ -13,8 +13,8 @@
set -e
[ "$GPU_VENDOR" = "intel" ] || {
echo "Intel installer invoked but GPU_VENDOR=$GPU_VENDOR"
exit 0
echo "Intel installer invoked but GPU_VENDOR=$GPU_VENDOR"
exit 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
if [ "$INTEL_USE_AMBER" = 1 ]; then
BASE_MESA=mesa-amber
LIB32_BASE=lib32-mesa-amber
BASE_MESA=mesa-amber
LIB32_BASE=lib32-mesa-amber
else
BASE_MESA=mesa
LIB32_BASE=lib32-mesa
BASE_MESA=mesa
LIB32_BASE=lib32-mesa
fi
install_pkg() {
local pkg="$1"
if pacman -Qi "$pkg" >/dev/null 2>&1; then
vlog "$pkg already installed"
else
if pacman -Si "$pkg" >/dev/null 2>&1; then
yes | sudo pacman -Sy --noconfirm "$pkg"
else
warn "Package $pkg not found in repos (not handling AUR here)"
fi
fi
local pkg="$1"
if pacman -Qi "$pkg" > /dev/null 2>&1; then
vlog "$pkg already installed"
else
if pacman -Si "$pkg" > /dev/null 2>&1; then
yes | sudo pacman -Sy --noconfirm "$pkg"
else
warn "Package $pkg not found in repos (not handling AUR here)"
fi
fi
}
info "Installing Intel GPU stack"
@ -60,47 +60,47 @@ install_pkg "$BASE_MESA"
# 32-bit mesa
if { [ "$INTEL_INSTALL_LIB32" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32" = 1 ]; then
install_pkg "$LIB32_BASE"
install_pkg "$LIB32_BASE"
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
# Vulkan
if [ "$INTEL_INSTALL_VULKAN" = 1 ]; then
install_pkg vulkan-intel
if { [ "$INTEL_INSTALL_LIB32_VK" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32_VK" = 1 ]; then
install_pkg lib32-vulkan-intel
else
vlog "Skipping 32-bit vulkan (INTEL_INSTALL_LIB32_VK=$INTEL_INSTALL_LIB32_VK MULTILIB=$MULTILIB)"
fi
install_pkg vulkan-intel
if { [ "$INTEL_INSTALL_LIB32_VK" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32_VK" = 1 ]; then
install_pkg lib32-vulkan-intel
else
vlog "Skipping 32-bit vulkan (INTEL_INSTALL_LIB32_VK=$INTEL_INSTALL_LIB32_VK MULTILIB=$MULTILIB)"
fi
fi
# Legacy xf86-video-intel (not recommended)
if [ "$INTEL_INSTALL_XF86" = 1 ]; then
install_pkg xf86-video-intel
install_pkg xf86-video-intel
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
# GuC / HuC enablement
if [ -n "$INTEL_ENABLE_GUC" ]; then
if ! echo "$INTEL_ENABLE_GUC" | grep -Eq '^[0-3]$'; then
warn "INTEL_ENABLE_GUC must be 0..3; ignoring"
else
info "Configuring enable_guc=$INTEL_ENABLE_GUC"
sudo mkdir -p /etc/modprobe.d
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
info "Regenerating initramfs (mkinitcpio -P) for GuC/HuC change"
sudo mkinitcpio -P || warn "mkinitcpio failed; continue manually"
else
info "Skipping initramfs regeneration (INTEL_SKIP_INITRAMFS=$INTEL_SKIP_INITRAMFS)"
fi
fi
if ! echo "$INTEL_ENABLE_GUC" | grep -Eq '^[0-3]$'; then
warn "INTEL_ENABLE_GUC must be 0..3; ignoring"
else
info "Configuring enable_guc=$INTEL_ENABLE_GUC"
sudo mkdir -p /etc/modprobe.d
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
info "Regenerating initramfs (mkinitcpio -P) for GuC/HuC change"
sudo mkinitcpio -P || warn "mkinitcpio failed; continue manually"
else
info "Skipping initramfs regeneration (INTEL_SKIP_INITRAMFS=$INTEL_SKIP_INITRAMFS)"
fi
fi
fi
# 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}')
info "Kernel driver in use: ${KDRV:-unknown}"

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ collapse_mounts
# Ensure writable by remounting if still read-only
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
log_hook "pre" "unlocking(done)"

View File

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

View File

@ -7,32 +7,32 @@ SHUTDOWN_CONFIG="/etc/shutdown-schedule.conf"
# Function to show error state in i3blocks and exit
show_error() {
local message="$1"
echo "$message"
echo "⏻"
echo "#FF79C6" # Pink/magenta for config errors
exit 0
local message="$1"
echo "$message"
echo "⏻"
echo "#FF79C6" # Pink/magenta for config errors
exit 0
}
# Validate and load config file
if [[ ! -f "$SHUTDOWN_CONFIG" ]]; then
show_error "NO CONFIG"
if [[ ! -f $SHUTDOWN_CONFIG ]]; then
show_error "NO CONFIG"
fi
# Source the config file to get MON_WED_HOUR and THU_SUN_HOUR
# shellcheck source=/dev/null
if ! source "$SHUTDOWN_CONFIG" 2>/dev/null; then
show_error "BAD CONFIG"
if ! source "$SHUTDOWN_CONFIG" 2> /dev/null; then
show_error "BAD CONFIG"
fi
# Validate that required variables are set
if [[ -z "${MON_WED_HOUR:-}" ]] || [[ -z "${THU_SUN_HOUR:-}" ]]; then
show_error "MISSING VARS"
if [[ -z ${MON_WED_HOUR:-} ]] || [[ -z ${THU_SUN_HOUR:-} ]]; then
show_error "MISSING VARS"
fi
# Validate that values are numbers
if ! [[ "$MON_WED_HOUR" =~ ^[0-9]+$ ]] || ! [[ "$THU_SUN_HOUR" =~ ^[0-9]+$ ]]; then
show_error "INVALID HOURS"
if ! [[ $MON_WED_HOUR =~ ^[0-9]+$ ]] || ! [[ $THU_SUN_HOUR =~ ^[0-9]+$ ]]; then
show_error "INVALID HOURS"
fi
# 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
if [[ $day_of_week -ge 1 ]] && [[ $day_of_week -le 3 ]]; then
# Monday-Wednesday
shutdown_hour=$MON_WED_HOUR
# Monday-Wednesday
shutdown_hour=$MON_WED_HOUR
else
# Thursday-Sunday
shutdown_hour=$THU_SUN_HOUR
# Thursday-Sunday
shutdown_hour=$THU_SUN_HOUR
fi
shutdown_time_minutes=$((shutdown_hour * 60))
# 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
# We're in shutdown window - show warning
echo "⏻ SHUTDOWN"
echo "⏻"
echo "#FF5555"
exit 0
# We're in shutdown window - show warning
echo "⏻ SHUTDOWN"
echo "⏻"
echo "#FF5555"
exit 0
fi
# Calculate minutes until shutdown
@ -70,28 +70,28 @@ minutes=$((minutes_until_shutdown % 60))
# Format output
if [[ $hours -gt 0 ]]; then
time_str="${hours}h ${minutes}m"
time_str="${hours}h ${minutes}m"
else
time_str="${minutes}m"
time_str="${minutes}m"
fi
# Color based on time remaining
if [[ $minutes_until_shutdown -le 30 ]]; then
# Less than 30 min - red warning
color="#FF5555"
icon="⏻"
# Less than 30 min - red warning
color="#FF5555"
icon="⏻"
elif [[ $minutes_until_shutdown -le 60 ]]; then
# Less than 1 hour - orange warning
color="#FFB86C"
icon="⏻"
# Less than 1 hour - orange warning
color="#FFB86C"
icon="⏻"
elif [[ $minutes_until_shutdown -le 120 ]]; then
# Less than 2 hours - yellow
color="#F1FA8C"
icon="⏻"
# Less than 2 hours - yellow
color="#F1FA8C"
icon="⏻"
else
# More than 2 hours - normal
color="#6272A4"
icon="⏻"
# More than 2 hours - normal
color="#6272A4"
icon="⏻"
fi
# 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" "$*"; }
run() {
if [[ $DRY_RUN -eq 1 ]]; then
echo -e "${YELLOW}DRY-RUN:${NC} $*"
return 0
else
"$@"
fi
if [[ $DRY_RUN -eq 1 ]]; then
echo -e "${YELLOW}DRY-RUN:${NC} $*"
return 0
else
"$@"
fi
}
require_root() {
if [[ $EUID -ne 0 ]]; then
echo "This script requires root privileges."
echo "Re-executing with sudo..."
exec sudo -E bash "$0" "$@"
fi
if [[ $EUID -ne 0 ]]; then
echo "This script requires root privileges."
echo "Re-executing with sudo..."
exec sudo -E bash "$0" "$@"
fi
}
usage() {
cat <<'EOF'
cat << 'EOF'
Check and Enable Digital Wellbeing Services
============================================
@ -90,25 +90,25 @@ EOF
######################################################################
ORIGINAL_ARGS=("$@")
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=1
shift
;;
--status)
STATUS_ONLY=1
shift
;;
-h | --help)
usage
exit 0
;;
*)
err "Unknown option: $1"
usage
exit 1
;;
esac
case "$1" in
--dry-run)
DRY_RUN=1
shift
;;
--status)
STATUS_ONLY=1
shift
;;
-h | --help)
usage
exit 0
;;
*)
err "Unknown option: $1"
usage
exit 1
;;
esac
done
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...]
######################################################################
report_and_fix() {
local -n _issues=$1
local -n _status=$2
local status_key="$3"
local fix_note="$4"
local setup_script="$5"
local verify_service="${6:-}"
shift 6
local script_args=("$@")
local -n _issues=$1
local -n _status=$2
local status_key="$3"
local fix_note="$4"
local setup_script="$5"
local verify_service="${6:-}"
shift 6
local script_args=("$@")
if [[ $_status != "ok" ]]; then
for issue in "${_issues[@]}"; do
if [[ $_status == "error" ]]; then
err "$issue"
else
warn "$issue"
fi
done
((ISSUES_FOUND++)) || true
if [[ $_status != "ok" ]]; then
for issue in "${_issues[@]}"; do
if [[ $_status == "error" ]]; then
err "$issue"
else
warn "$issue"
fi
done
((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 && $_status == "error" ]]; then
note "$fix_note"
if [[ -f $setup_script ]]; then
run bash "$setup_script" "${script_args[@]}"
((FIXES_APPLIED++)) || true
# Re-verify after fix
if [[ $DRY_RUN -eq 0 && -n $verify_service ]] && systemctl is-enabled "$verify_service" &>/dev/null; then
_status="ok"
fi
else
err "Setup script not found: $setup_script"
fi
fi
fi
if [[ $STATUS_ONLY -eq 0 && $_status == "error" ]]; then
note "$fix_note"
if [[ -f $setup_script ]]; then
run bash "$setup_script" "${script_args[@]}"
((FIXES_APPLIED++)) || true
# Re-verify after fix
if [[ $DRY_RUN -eq 0 && -n $verify_service ]] && systemctl is-enabled "$verify_service" &> /dev/null; then
_status="ok"
fi
else
err "Setup script not found: $setup_script"
fi
fi
fi
SERVICE_STATUS["$status_key"]=$_status
SERVICE_STATUS["$status_key"]=$_status
}
######################################################################
@ -167,443 +167,443 @@ report_and_fix() {
######################################################################
check_pacman_wrapper() {
header "Pacman Wrapper"
header "Pacman Wrapper"
local status="ok"
local issues=()
local status="ok"
local issues=()
# Check if wrapper is installed
if [[ -L /usr/bin/pacman ]]; then
local target
target=$(readlink -f /usr/bin/pacman)
if [[ $target == "/usr/local/bin/pacman_wrapper" ]]; then
msg "Pacman symlink points to wrapper"
else
issues+=("Pacman symlink points to: $target (expected /usr/local/bin/pacman_wrapper)")
status="error"
fi
else
issues+=("Pacman is not a symlink (wrapper not installed)")
status="error"
fi
# Check if wrapper is installed
if [[ -L /usr/bin/pacman ]]; then
local target
target=$(readlink -f /usr/bin/pacman)
if [[ $target == "/usr/local/bin/pacman_wrapper" ]]; then
msg "Pacman symlink points to wrapper"
else
issues+=("Pacman symlink points to: $target (expected /usr/local/bin/pacman_wrapper)")
status="error"
fi
else
issues+=("Pacman is not a symlink (wrapper not installed)")
status="error"
fi
# Check if original pacman is backed up
if [[ -f /usr/bin/pacman.orig ]]; then
msg "Original pacman backed up at /usr/bin/pacman.orig"
else
issues+=("Original pacman backup not found at /usr/bin/pacman.orig")
status="error"
fi
# Check if original pacman is backed up
if [[ -f /usr/bin/pacman.orig ]]; then
msg "Original pacman backed up at /usr/bin/pacman.orig"
else
issues+=("Original pacman backup not found at /usr/bin/pacman.orig")
status="error"
fi
# Check if wrapper script exists
if [[ -f /usr/local/bin/pacman_wrapper ]]; then
msg "Wrapper script exists at /usr/local/bin/pacman_wrapper"
else
issues+=("Wrapper script not found at /usr/local/bin/pacman_wrapper")
status="error"
fi
# Check if wrapper script exists
if [[ -f /usr/local/bin/pacman_wrapper ]]; then
msg "Wrapper script exists at /usr/local/bin/pacman_wrapper"
else
issues+=("Wrapper script not found at /usr/local/bin/pacman_wrapper")
status="error"
fi
# Check supporting files
for file in words.txt pacman_blocked_keywords.txt pacman_whitelist.txt; do
if [[ -f "/usr/local/bin/$file" ]]; then
msg "Supporting file exists: /usr/local/bin/$file"
else
warn "Supporting file missing: /usr/local/bin/$file"
fi
done
# Check supporting files
for file in words.txt pacman_blocked_keywords.txt pacman_whitelist.txt; do
if [[ -f "/usr/local/bin/$file" ]]; then
msg "Supporting file exists: /usr/local/bin/$file"
else
warn "Supporting file missing: /usr/local/bin/$file"
fi
done
# Report and fix
if [[ $status == "error" ]]; then
for issue in "${issues[@]}"; do
err "$issue"
done
((ISSUES_FOUND++)) || true
# Report and fix
if [[ $status == "error" ]]; then
for issue in "${issues[@]}"; do
err "$issue"
done
((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 ]]; then
note "Installing pacman wrapper..."
if [[ -f $PACMAN_WRAPPER_INSTALL ]]; then
run bash "$PACMAN_WRAPPER_INSTALL"
((FIXES_APPLIED++)) || true
# 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
status="ok"
fi
else
err "Installer script not found: $PACMAN_WRAPPER_INSTALL"
fi
fi
fi
if [[ $STATUS_ONLY -eq 0 ]]; then
note "Installing pacman wrapper..."
if [[ -f $PACMAN_WRAPPER_INSTALL ]]; then
run bash "$PACMAN_WRAPPER_INSTALL"
((FIXES_APPLIED++)) || true
# 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
status="ok"
fi
else
err "Installer script not found: $PACMAN_WRAPPER_INSTALL"
fi
fi
fi
SERVICE_STATUS["pacman_wrapper"]=$status
SERVICE_STATUS["pacman_wrapper"]=$status
}
check_midnight_shutdown() {
header "Midnight Shutdown (Day-Specific Auto-Shutdown)"
header "Midnight Shutdown (Day-Specific Auto-Shutdown)"
local status="ok"
local issues=()
local status="ok"
local issues=()
# Check timer
if systemctl is-enabled day-specific-shutdown.timer &>/dev/null; then
msg "day-specific-shutdown.timer is enabled"
else
issues+=("day-specific-shutdown.timer is not enabled")
status="error"
fi
# Check timer
if systemctl is-enabled day-specific-shutdown.timer &> /dev/null; then
msg "day-specific-shutdown.timer is enabled"
else
issues+=("day-specific-shutdown.timer is not enabled")
status="error"
fi
if systemctl is-active day-specific-shutdown.timer &>/dev/null; then
msg "day-specific-shutdown.timer is active"
else
issues+=("day-specific-shutdown.timer is not active")
status="warning"
fi
if systemctl is-active day-specific-shutdown.timer &> /dev/null; then
msg "day-specific-shutdown.timer is active"
else
issues+=("day-specific-shutdown.timer is not active")
status="warning"
fi
# Check service file exists
if [[ -f /etc/systemd/system/day-specific-shutdown.service ]]; then
msg "day-specific-shutdown.service file exists"
else
issues+=("day-specific-shutdown.service file missing")
status="error"
fi
# Check service file exists
if [[ -f /etc/systemd/system/day-specific-shutdown.service ]]; then
msg "day-specific-shutdown.service file exists"
else
issues+=("day-specific-shutdown.service file missing")
status="error"
fi
# Check management script
if [[ -f /usr/local/bin/day-specific-shutdown-manager.sh ]]; then
msg "Shutdown manager script exists"
else
issues+=("day-specific-shutdown-manager.sh not found")
status="error"
fi
# Check management script
if [[ -f /usr/local/bin/day-specific-shutdown-manager.sh ]]; then
msg "Shutdown manager script exists"
else
issues+=("day-specific-shutdown-manager.sh not found")
status="error"
fi
report_and_fix issues status "midnight_shutdown" \
"Setting up midnight shutdown..." \
"$MIDNIGHT_SHUTDOWN_SCRIPT" \
"day-specific-shutdown.timer" \
enable
report_and_fix issues status "midnight_shutdown" \
"Setting up midnight shutdown..." \
"$MIDNIGHT_SHUTDOWN_SCRIPT" \
"day-specific-shutdown.timer" \
enable
}
check_startup_monitor() {
header "PC Startup Monitor"
header "PC Startup Monitor"
local status="ok"
local issues=()
local status="ok"
local issues=()
# Check timer (the timer triggers the service, so we check the timer)
if systemctl is-enabled pc-startup-monitor.timer &>/dev/null; then
msg "pc-startup-monitor.timer is enabled"
else
issues+=("pc-startup-monitor.timer is not enabled")
status="error"
fi
# Check timer (the timer triggers the service, so we check the timer)
if systemctl is-enabled pc-startup-monitor.timer &> /dev/null; then
msg "pc-startup-monitor.timer is enabled"
else
issues+=("pc-startup-monitor.timer is not enabled")
status="error"
fi
if systemctl is-active pc-startup-monitor.timer &>/dev/null; then
msg "pc-startup-monitor.timer is active"
else
issues+=("pc-startup-monitor.timer is not active")
status="warning"
fi
if systemctl is-active pc-startup-monitor.timer &> /dev/null; then
msg "pc-startup-monitor.timer is active"
else
issues+=("pc-startup-monitor.timer is not active")
status="warning"
fi
# Check service file exists
if [[ -f /etc/systemd/system/pc-startup-monitor.service ]]; then
msg "pc-startup-monitor.service file exists"
else
issues+=("pc-startup-monitor.service file missing")
status="error"
fi
# Check service file exists
if [[ -f /etc/systemd/system/pc-startup-monitor.service ]]; then
msg "pc-startup-monitor.service file exists"
else
issues+=("pc-startup-monitor.service file missing")
status="error"
fi
# Check monitor script
if [[ -f /usr/local/bin/pc-startup-check.sh ]]; then
msg "Startup check script exists"
else
issues+=("pc-startup-check.sh not found")
status="error"
fi
# Check monitor script
if [[ -f /usr/local/bin/pc-startup-check.sh ]]; then
msg "Startup check script exists"
else
issues+=("pc-startup-check.sh not found")
status="error"
fi
report_and_fix issues status "startup_monitor" \
"Setting up startup monitor..." \
"$STARTUP_MONITOR_SCRIPT" \
"pc-startup-monitor.timer"
report_and_fix issues status "startup_monitor" \
"Setting up startup monitor..." \
"$STARTUP_MONITOR_SCRIPT" \
"pc-startup-monitor.timer"
}
check_periodic_systems() {
header "Periodic System Maintenance"
header "Periodic System Maintenance"
local status="ok"
local issues=()
local status="ok"
local issues=()
# Check timer
if systemctl is-enabled periodic-system-maintenance.timer &>/dev/null; then
msg "periodic-system-maintenance.timer is enabled"
else
issues+=("periodic-system-maintenance.timer is not enabled")
status="error"
fi
# Check timer
if systemctl is-enabled periodic-system-maintenance.timer &> /dev/null; then
msg "periodic-system-maintenance.timer is enabled"
else
issues+=("periodic-system-maintenance.timer is not enabled")
status="error"
fi
if systemctl is-active periodic-system-maintenance.timer &>/dev/null; then
msg "periodic-system-maintenance.timer is active"
else
issues+=("periodic-system-maintenance.timer is not active")
status="warning"
fi
if systemctl is-active periodic-system-maintenance.timer &> /dev/null; then
msg "periodic-system-maintenance.timer is active"
else
issues+=("periodic-system-maintenance.timer is not active")
status="warning"
fi
# Check startup service
if systemctl is-enabled periodic-system-startup.service &>/dev/null; then
msg "periodic-system-startup.service is enabled"
else
issues+=("periodic-system-startup.service is not enabled")
status="error"
fi
# Check startup service
if systemctl is-enabled periodic-system-startup.service &> /dev/null; then
msg "periodic-system-startup.service is enabled"
else
issues+=("periodic-system-startup.service is not enabled")
status="error"
fi
# Check hosts file monitor
if systemctl is-enabled hosts-file-monitor.service &>/dev/null; then
msg "hosts-file-monitor.service is enabled"
else
issues+=("hosts-file-monitor.service is not enabled")
status="error"
fi
# Check hosts file monitor
if systemctl is-enabled hosts-file-monitor.service &> /dev/null; then
msg "hosts-file-monitor.service is enabled"
else
issues+=("hosts-file-monitor.service is not enabled")
status="error"
fi
if systemctl is-active hosts-file-monitor.service &>/dev/null; then
msg "hosts-file-monitor.service is active"
else
issues+=("hosts-file-monitor.service is not active")
status="warning"
fi
if systemctl is-active hosts-file-monitor.service &> /dev/null; then
msg "hosts-file-monitor.service is active"
else
issues+=("hosts-file-monitor.service is not active")
status="warning"
fi
# Check maintenance script
if [[ -f /usr/local/bin/periodic-system-maintenance.sh ]]; then
msg "Maintenance script exists"
else
issues+=("periodic-system-maintenance.sh not found")
status="error"
fi
# Check maintenance script
if [[ -f /usr/local/bin/periodic-system-maintenance.sh ]]; then
msg "Maintenance script exists"
else
issues+=("periodic-system-maintenance.sh not found")
status="error"
fi
report_and_fix issues status "periodic_systems" \
"Setting up periodic systems..." \
"$PERIODIC_SYSTEM_SCRIPT" \
"periodic-system-maintenance.timer"
report_and_fix issues status "periodic_systems" \
"Setting up periodic systems..." \
"$PERIODIC_SYSTEM_SCRIPT" \
"periodic-system-maintenance.timer"
}
check_hosts() {
header "Hosts File and Guards"
header "Hosts File and Guards"
local status="ok"
local issues=()
local status="ok"
local issues=()
# Check /etc/hosts exists and has content
if [[ -f /etc/hosts ]]; then
local line_count
line_count=$(wc -l </etc/hosts)
if [[ $line_count -gt 100 ]]; then
msg "/etc/hosts exists with $line_count lines (StevenBlack list likely installed)"
else
issues+=("/etc/hosts has only $line_count lines (StevenBlack list may not be installed)")
status="warning"
fi
else
issues+=("/etc/hosts does not exist")
status="error"
fi
# Check /etc/hosts exists and has content
if [[ -f /etc/hosts ]]; then
local line_count
line_count=$(wc -l < /etc/hosts)
if [[ $line_count -gt 100 ]]; then
msg "/etc/hosts exists with $line_count lines (StevenBlack list likely installed)"
else
issues+=("/etc/hosts has only $line_count lines (StevenBlack list may not be installed)")
status="warning"
fi
else
issues+=("/etc/hosts does not exist")
status="error"
fi
# Check if hosts file is immutable
local attrs
attrs=$(lsattr /etc/hosts 2>/dev/null | cut -d' ' -f1 || echo "")
if [[ $attrs == *"i"* ]]; then
msg "/etc/hosts has immutable attribute set"
else
issues+=("/etc/hosts is not immutable")
status="warning"
fi
# Check if hosts file is immutable
local attrs
attrs=$(lsattr /etc/hosts 2> /dev/null | cut -d' ' -f1 || echo "")
if [[ $attrs == *"i"* ]]; then
msg "/etc/hosts has immutable attribute set"
else
issues+=("/etc/hosts is not immutable")
status="warning"
fi
# Check cached hosts file
if [[ -f /etc/hosts.stevenblack ]]; then
msg "StevenBlack cache exists at /etc/hosts.stevenblack"
else
issues+=("StevenBlack cache not found")
status="warning"
fi
# Check cached hosts file
if [[ -f /etc/hosts.stevenblack ]]; then
msg "StevenBlack cache exists at /etc/hosts.stevenblack"
else
issues+=("StevenBlack cache not found")
status="warning"
fi
# Check hosts guard path watcher
if systemctl is-enabled hosts-guard.path &>/dev/null; then
msg "hosts-guard.path is enabled"
else
issues+=("hosts-guard.path is not enabled")
status="error"
fi
# Check hosts guard path watcher
if systemctl is-enabled hosts-guard.path &> /dev/null; then
msg "hosts-guard.path is enabled"
else
issues+=("hosts-guard.path is not enabled")
status="error"
fi
if systemctl is-active hosts-guard.path &>/dev/null; then
msg "hosts-guard.path is active"
else
issues+=("hosts-guard.path is not active")
status="warning"
fi
if systemctl is-active hosts-guard.path &> /dev/null; then
msg "hosts-guard.path is active"
else
issues+=("hosts-guard.path is not active")
status="warning"
fi
# Check hosts bind mount service
if systemctl is-enabled hosts-bind-mount.service &>/dev/null; then
msg "hosts-bind-mount.service is enabled"
else
issues+=("hosts-bind-mount.service is not enabled")
status="warning"
fi
# Check hosts bind mount service
if systemctl is-enabled hosts-bind-mount.service &> /dev/null; then
msg "hosts-bind-mount.service is enabled"
else
issues+=("hosts-bind-mount.service is not enabled")
status="warning"
fi
# Check enforcement script
if [[ -f /usr/local/sbin/enforce-hosts.sh ]]; then
msg "Enforcement script exists at /usr/local/sbin/enforce-hosts.sh"
else
issues+=("enforce-hosts.sh not found")
status="error"
fi
# Check enforcement script
if [[ -f /usr/local/sbin/enforce-hosts.sh ]]; then
msg "Enforcement script exists at /usr/local/sbin/enforce-hosts.sh"
else
issues+=("enforce-hosts.sh not found")
status="error"
fi
# Check unlock script
if [[ -f /usr/local/sbin/unlock-hosts ]]; then
msg "Unlock script exists at /usr/local/sbin/unlock-hosts"
else
issues+=("unlock-hosts not found")
status="warning"
fi
# Check unlock script
if [[ -f /usr/local/sbin/unlock-hosts ]]; then
msg "Unlock script exists at /usr/local/sbin/unlock-hosts"
else
issues+=("unlock-hosts not found")
status="warning"
fi
# Check locked hosts snapshot
if [[ -f /usr/local/share/locked-hosts ]]; then
msg "Canonical hosts snapshot exists at /usr/local/share/locked-hosts"
else
issues+=("Canonical hosts snapshot not found")
status="error"
fi
# Check locked hosts snapshot
if [[ -f /usr/local/share/locked-hosts ]]; then
msg "Canonical hosts snapshot exists at /usr/local/share/locked-hosts"
else
issues+=("Canonical hosts snapshot not found")
status="error"
fi
# 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
msg "Pacman hooks installed"
else
issues+=("Pacman hooks not installed")
status="warning"
fi
# 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
msg "Pacman hooks installed"
else
issues+=("Pacman hooks not installed")
status="warning"
fi
# Report issues
if [[ $status != "ok" ]]; then
for issue in "${issues[@]}"; do
if [[ $status == "error" ]]; then
err "$issue"
else
warn "$issue"
fi
done
((ISSUES_FOUND++)) || true
# Report issues
if [[ $status != "ok" ]]; then
for issue in "${issues[@]}"; do
if [[ $status == "error" ]]; then
err "$issue"
else
warn "$issue"
fi
done
((ISSUES_FOUND++)) || true
if [[ $STATUS_ONLY -eq 0 ]]; then
# Run hosts install first
if [[ ! -f /etc/hosts ]] || [[ $(wc -l </etc/hosts) -lt 100 ]]; then
note "Installing hosts file..."
if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then
run bash "$HOSTS_INSTALL_SCRIPT"
((FIXES_APPLIED++)) || true
else
err "Hosts install script not found: $HOSTS_INSTALL_SCRIPT"
fi
fi
if [[ $STATUS_ONLY -eq 0 ]]; then
# Run hosts install first
if [[ ! -f /etc/hosts ]] || [[ $(wc -l < /etc/hosts) -lt 100 ]]; then
note "Installing hosts file..."
if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then
run bash "$HOSTS_INSTALL_SCRIPT"
((FIXES_APPLIED++)) || true
else
err "Hosts install script not found: $HOSTS_INSTALL_SCRIPT"
fi
fi
# Run hosts guard setup
if ! systemctl is-enabled hosts-guard.path &>/dev/null || [[ ! -f /usr/local/sbin/enforce-hosts.sh ]]; then
note "Setting up hosts guard..."
if [[ -f $HOSTS_GUARD_SCRIPT ]]; then
run bash "$HOSTS_GUARD_SCRIPT"
((FIXES_APPLIED++)) || true
else
err "Hosts guard script not found: $HOSTS_GUARD_SCRIPT"
fi
fi
# Run hosts guard setup
if ! systemctl is-enabled hosts-guard.path &> /dev/null || [[ ! -f /usr/local/sbin/enforce-hosts.sh ]]; then
note "Setting up hosts guard..."
if [[ -f $HOSTS_GUARD_SCRIPT ]]; then
run bash "$HOSTS_GUARD_SCRIPT"
((FIXES_APPLIED++)) || true
else
err "Hosts guard script not found: $HOSTS_GUARD_SCRIPT"
fi
fi
# Install pacman hooks if missing
if [[ ! -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then
note "Installing pacman hooks..."
if [[ -f $HOSTS_PACMAN_HOOKS_SCRIPT ]]; then
run bash "$HOSTS_PACMAN_HOOKS_SCRIPT"
((FIXES_APPLIED++)) || true
else
err "Pacman hooks script not found: $HOSTS_PACMAN_HOOKS_SCRIPT"
fi
fi
# Install pacman hooks if missing
if [[ ! -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then
note "Installing pacman hooks..."
if [[ -f $HOSTS_PACMAN_HOOKS_SCRIPT ]]; then
run bash "$HOSTS_PACMAN_HOOKS_SCRIPT"
((FIXES_APPLIED++)) || true
else
err "Pacman hooks script not found: $HOSTS_PACMAN_HOOKS_SCRIPT"
fi
fi
# Re-verify after fixes
if [[ $DRY_RUN -eq 0 ]]; then
if systemctl is-enabled hosts-guard.path &>/dev/null &&
[[ -f /usr/local/sbin/enforce-hosts.sh ]] &&
[[ -f /usr/local/share/locked-hosts ]] &&
[[ -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then
# Downgrade to warning if only minor issues remain (immutable attr, etc.)
status="ok"
fi
fi
fi
fi
# Re-verify after fixes
if [[ $DRY_RUN -eq 0 ]]; then
if systemctl is-enabled hosts-guard.path &> /dev/null &&
[[ -f /usr/local/sbin/enforce-hosts.sh ]] &&
[[ -f /usr/local/share/locked-hosts ]] &&
[[ -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then
# Downgrade to warning if only minor issues remain (immutable attr, etc.)
status="ok"
fi
fi
fi
fi
SERVICE_STATUS["hosts"]=$status
SERVICE_STATUS["hosts"]=$status
}
######################################################################
# Summary
######################################################################
print_summary() {
header "Summary"
header "Summary"
echo ""
printf "%-25s %s\n" "Service" "Status"
printf "%-25s %s\n" "-------" "------"
echo ""
printf "%-25s %s\n" "Service" "Status"
printf "%-25s %s\n" "-------" "------"
for service in pacman_wrapper midnight_shutdown startup_monitor periodic_systems hosts; do
local status="${SERVICE_STATUS[$service]:-unknown}"
local color
case "$status" in
ok) color=$GREEN ;;
warning) color=$YELLOW ;;
error) color=$RED ;;
*) color=$NC ;;
esac
printf "%-25s ${color}%s${NC}\n" "$service" "$status"
done
for service in pacman_wrapper midnight_shutdown startup_monitor periodic_systems hosts; do
local status="${SERVICE_STATUS[$service]:-unknown}"
local color
case "$status" in
ok) color=$GREEN ;;
warning) color=$YELLOW ;;
error) color=$RED ;;
*) color=$NC ;;
esac
printf "%-25s ${color}%s${NC}\n" "$service" "$status"
done
echo ""
if [[ $DRY_RUN -eq 1 ]]; then
note "DRY RUN - No changes were made"
fi
echo ""
if [[ $DRY_RUN -eq 1 ]]; then
note "DRY RUN - No changes were made"
fi
if [[ $ISSUES_FOUND -eq 0 ]]; then
msg "All services are properly configured!"
else
if [[ $STATUS_ONLY -eq 1 ]]; then
warn "Found $ISSUES_FOUND service(s) with issues"
note "Run without --status to fix issues"
else
if [[ $FIXES_APPLIED -gt 0 ]]; then
msg "Applied $FIXES_APPLIED fix(es)"
else
warn "Found $ISSUES_FOUND issue(s) but no fixes were applied"
fi
fi
fi
if [[ $ISSUES_FOUND -eq 0 ]]; then
msg "All services are properly configured!"
else
if [[ $STATUS_ONLY -eq 1 ]]; then
warn "Found $ISSUES_FOUND service(s) with issues"
note "Run without --status to fix issues"
else
if [[ $FIXES_APPLIED -gt 0 ]]; then
msg "Applied $FIXES_APPLIED fix(es)"
else
warn "Found $ISSUES_FOUND issue(s) but no fixes were applied"
fi
fi
fi
}
######################################################################
# Main
######################################################################
main() {
echo ""
echo "Digital Wellbeing Services Status Check"
echo "========================================"
echo "Date: $(date)"
echo "User: ${SUDO_USER:-$USER}"
if [[ $DRY_RUN -eq 1 ]]; then
echo "Mode: DRY RUN (no changes will be made)"
elif [[ $STATUS_ONLY -eq 1 ]]; then
echo "Mode: STATUS ONLY (no changes will be made)"
else
echo "Mode: CHECK AND FIX"
fi
echo ""
echo "Digital Wellbeing Services Status Check"
echo "========================================"
echo "Date: $(date)"
echo "User: ${SUDO_USER:-$USER}"
if [[ $DRY_RUN -eq 1 ]]; then
echo "Mode: DRY RUN (no changes will be made)"
elif [[ $STATUS_ONLY -eq 1 ]]; then
echo "Mode: STATUS ONLY (no changes will be made)"
else
echo "Mode: CHECK AND FIX"
fi
check_pacman_wrapper
check_midnight_shutdown
check_startup_monitor
check_periodic_systems
check_hosts
check_pacman_wrapper
check_midnight_shutdown
check_startup_monitor
check_periodic_systems
check_hosts
print_summary
print_summary
}
main

View File

@ -12,14 +12,14 @@ set -euo pipefail
# Send desktop notification (inlined from common.sh to avoid dependency issues
# when script is installed to /usr/local/bin)
notify() {
local title="$1"
local message="$2"
local urgency="${3:-normal}"
local timeout="${4:-5000}"
local title="$1"
local message="$2"
local urgency="${3:-normal}"
local timeout="${4:-5000}"
if command -v notify-send &>/dev/null; then
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2>/dev/null || true
fi
if command -v notify-send &> /dev/null; then
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2> /dev/null || true
fi
}
# Configuration
@ -29,165 +29,165 @@ LOG_FILE="$STATE_DIR/compulsive-block.log"
# Apps to limit (name -> binary path)
# These are the primary wrapper locations (what the user calls)
declare -A APPS=(
["beeper"]="/usr/bin/beeper"
["signal-desktop"]="/usr/bin/signal-desktop"
["discord"]="/usr/bin/discord"
["beeper"]="/usr/bin/beeper"
["signal-desktop"]="/usr/bin/signal-desktop"
["discord"]="/usr/bin/discord"
)
# Actual executable paths (the real binaries to exec after wrapper check)
# These are where the real code lives
declare -A REAL_BINARIES=(
["beeper"]="/opt/beeper/beepertexts"
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
["discord"]="/opt/discord/Discord"
["beeper"]="/opt/beeper/beepertexts"
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
["discord"]="/opt/discord/Discord"
)
# Ensure state directory exists
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() {
local msg
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
echo "$msg" >&2
echo "$msg" >>"$LOG_FILE" 2>/dev/null || true
local msg
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
echo "$msg" >&2
echo "$msg" >> "$LOG_FILE" 2> /dev/null || true
}
# Get current hour key (YYYY-MM-DD-HH format)
get_hour_key() {
date '+%Y-%m-%d-%H'
date '+%Y-%m-%d-%H'
}
# Get state file path for an app
get_state_file() {
local app="$1"
echo "$STATE_DIR/${app}.lastopen"
local app="$1"
echo "$STATE_DIR/${app}.lastopen"
}
# Check if app was already opened this hour
was_opened_this_hour() {
local app="$1"
local state_file
state_file=$(get_state_file "$app")
local current_hour
current_hour=$(get_hour_key)
local app="$1"
local state_file
state_file=$(get_state_file "$app")
local current_hour
current_hour=$(get_hour_key)
if [[ -f $state_file ]]; then
local last_hour
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
if [[ $last_hour == "$current_hour" ]]; then
return 0 # Was opened this hour
fi
fi
return 1 # Not opened this hour
if [[ -f $state_file ]]; then
local last_hour
last_hour=$(cat "$state_file" 2> /dev/null || echo "")
if [[ $last_hour == "$current_hour" ]]; then
return 0 # Was opened this hour
fi
fi
return 1 # Not opened this hour
}
# Record app opening
record_opening() {
local app="$1"
local state_file
state_file=$(get_state_file "$app")
local current_hour
current_hour=$(get_hour_key)
local app="$1"
local state_file
state_file=$(get_state_file "$app")
local current_hour
current_hour=$(get_hour_key)
echo "$current_hour" >"$state_file"
log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
echo "$current_hour" > "$state_file"
log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
}
# Block app and notify
block_app() {
local app="$1"
local current_hour
current_hour=$(get_hour_key)
local app="$1"
local current_hour
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
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
# Send notification using common library
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
}
# Get real binary path for an app
get_real_binary() {
local app="$1"
local wrapper_path="${APPS[$app]}"
local real_binary="${REAL_BINARIES[$app]}"
local app="$1"
local wrapper_path="${APPS[$app]}"
local real_binary="${REAL_BINARIES[$app]}"
# Check if wrapper is installed (original moved to .orig)
if [[ -f "${wrapper_path}.orig" ]]; then
# Wrapper installed, return the actual executable
echo "$real_binary"
return 0
fi
# Check if wrapper is installed (original moved to .orig)
if [[ -f "${wrapper_path}.orig" ]]; then
# Wrapper installed, return the actual executable
echo "$real_binary"
return 0
fi
return 1
return 1
}
# Main wrapper function - called when wrapping app launches
wrapper_main() {
local app="$1"
shift
local app="$1"
shift
ensure_state_dir
ensure_state_dir
local real_binary
if ! real_binary=$(get_real_binary "$app"); then
log_message "ERROR: Real binary not found for $app"
echo "Error: Real binary for $app not found. Was the installer run?" >&2
exit 1
fi
local real_binary
if ! real_binary=$(get_real_binary "$app"); then
log_message "ERROR: Real binary not found for $app"
echo "Error: Real binary for $app not found. Was the installer run?" >&2
exit 1
fi
if was_opened_this_hour "$app"; then
block_app "$app"
exit 1
fi
if was_opened_this_hour "$app"; then
block_app "$app"
exit 1
fi
record_opening "$app"
exec "$real_binary" "$@"
record_opening "$app"
exec "$real_binary" "$@"
}
# Install wrapper for a specific app
install_wrapper() {
local app="$1"
local wrapper_path="${APPS[$app]}"
local real_binary="${REAL_BINARIES[$app]}"
local app="$1"
local wrapper_path="${APPS[$app]}"
local real_binary="${REAL_BINARIES[$app]}"
# Check if already wrapped
if [[ -f "${wrapper_path}.orig" ]]; then
echo "$app already wrapped"
return 0
fi
# Check if already wrapped
if [[ -f "${wrapper_path}.orig" ]]; then
echo "$app already wrapped"
return 0
fi
# Check if wrapper location exists (file or symlink)
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
echo "$app not installed ($wrapper_path not found)"
return 1
fi
# Check if wrapper location exists (file or symlink)
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
echo "$app not installed ($wrapper_path not found)"
return 1
fi
# Check if real binary exists
if [[ ! -x $real_binary ]]; then
echo "$app real binary not found ($real_binary)"
return 1
fi
# Check if real binary exists
if [[ ! -x $real_binary ]]; then
echo "$app real binary not found ($real_binary)"
return 1
fi
echo " Installing wrapper for $app..."
echo " Installing wrapper for $app..."
# Handle symlinks: save the symlink itself, not the target
if [[ -L $wrapper_path ]]; then
local link_target
link_target=$(readlink "$wrapper_path")
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
# Remove symlink and create .orig that stores the link target info
echo "SYMLINK:$link_target" >"${wrapper_path}.orig"
rm "$wrapper_path"
else
echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
mv "$wrapper_path" "${wrapper_path}.orig"
fi
# Handle symlinks: save the symlink itself, not the target
if [[ -L $wrapper_path ]]; then
local link_target
link_target=$(readlink "$wrapper_path")
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
# Remove symlink and create .orig that stores the link target info
echo "SYMLINK:$link_target" > "${wrapper_path}.orig"
rm "$wrapper_path"
else
echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
mv "$wrapper_path" "${wrapper_path}.orig"
fi
echo " Creating wrapper at $wrapper_path"
cat >"$wrapper_path" <<WRAPPER_EOF
echo " Creating wrapper at $wrapper_path"
cat > "$wrapper_path" << WRAPPER_EOF
#!/bin/bash
# Auto-generated wrapper for $app - blocks compulsive opening
# Real binary: $real_binary
@ -195,88 +195,88 @@ install_wrapper() {
exec /usr/local/bin/block-compulsive-opening.sh wrapper "$app" "\$@"
WRAPPER_EOF
chmod +x "$wrapper_path"
echo "$app wrapper installed"
chmod +x "$wrapper_path"
echo "$app wrapper installed"
}
# Uninstall wrapper for a specific app
uninstall_wrapper() {
local app="$1"
local wrapper_path="${APPS[$app]}"
local app="$1"
local wrapper_path="${APPS[$app]}"
if [[ ! -f "${wrapper_path}.orig" ]]; then
echo "$app wrapper not found"
return 1
fi
if [[ ! -f "${wrapper_path}.orig" ]]; then
echo "$app wrapper not found"
return 1
fi
echo " Removing wrapper for $app..."
rm -f "$wrapper_path"
echo " Removing wrapper for $app..."
rm -f "$wrapper_path"
# Check if it was a symlink (stored as SYMLINK:target in .orig)
local orig_content
orig_content=$(cat "${wrapper_path}.orig" 2>/dev/null || echo "")
if [[ $orig_content == SYMLINK:* ]]; then
local link_target="${orig_content#SYMLINK:}"
echo " Restoring symlink $wrapper_path -> $link_target"
ln -s "$link_target" "$wrapper_path"
rm "${wrapper_path}.orig"
else
echo " Restoring original file"
mv "${wrapper_path}.orig" "$wrapper_path"
fi
echo "$app restored"
# Check if it was a symlink (stored as SYMLINK:target in .orig)
local orig_content
orig_content=$(cat "${wrapper_path}.orig" 2> /dev/null || echo "")
if [[ $orig_content == SYMLINK:* ]]; then
local link_target="${orig_content#SYMLINK:}"
echo " Restoring symlink $wrapper_path -> $link_target"
ln -s "$link_target" "$wrapper_path"
rm "${wrapper_path}.orig"
else
echo " Restoring original file"
mv "${wrapper_path}.orig" "$wrapper_path"
fi
echo "$app restored"
}
# Install all wrappers
install_all() {
echo "Installing compulsive opening blockers..."
echo ""
echo "Installing compulsive opening blockers..."
echo ""
# Install main script to /usr/local/bin
local script_path
script_path="$(readlink -f "$0")"
local install_path="/usr/local/bin/block-compulsive-opening.sh"
# Install main script to /usr/local/bin
local script_path
script_path="$(readlink -f "$0")"
local install_path="/usr/local/bin/block-compulsive-opening.sh"
if [[ $script_path != "$install_path" ]]; then
echo "Installing main script to $install_path..."
cp "$script_path" "$install_path"
chmod +x "$install_path"
echo "✓ Main script installed"
else
echo "Main script already at $install_path"
fi
echo ""
if [[ $script_path != "$install_path" ]]; then
echo "Installing main script to $install_path..."
cp "$script_path" "$install_path"
chmod +x "$install_path"
echo "✓ Main script installed"
else
echo "Main script already at $install_path"
fi
echo ""
# Install wrappers for each app
local installed=0
for app in "${!APPS[@]}"; do
if install_wrapper "$app"; then
((installed++)) || true
fi
done
# Install wrappers for each app
local installed=0
for app in "${!APPS[@]}"; do
if install_wrapper "$app"; then
((installed++)) || true
fi
done
echo ""
echo "Installation complete. $installed app(s) wrapped."
echo ""
echo "Each app can now only be opened once per hour."
echo "State files stored in: $STATE_DIR"
echo "Logs stored in: $LOG_FILE"
echo ""
echo "Installation complete. $installed app(s) wrapped."
echo ""
echo "Each app can now only be opened once per hour."
echo "State files stored in: $STATE_DIR"
echo "Logs stored in: $LOG_FILE"
# Install pacman hook to re-wrap after package updates
install_pacman_hook
# Install pacman hook to re-wrap after package updates
install_pacman_hook
}
# Install pacman hook to re-install wrappers after package updates
install_pacman_hook() {
local hook_dir="/etc/pacman.d/hooks"
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
local hook_dir="/etc/pacman.d/hooks"
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
echo ""
echo "Installing pacman hook..."
echo ""
echo "Installing pacman hook..."
mkdir -p "$hook_dir"
mkdir -p "$hook_dir"
cat >"$hook_file" <<'HOOK_EOF'
cat > "$hook_file" << 'HOOK_EOF'
[Trigger]
Operation = Upgrade
Operation = Install
@ -291,131 +291,131 @@ When = PostTransaction
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
HOOK_EOF
chmod 644 "$hook_file"
echo "✓ Pacman hook installed: $hook_file"
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
chmod 644 "$hook_file"
echo "✓ Pacman hook installed: $hook_file"
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
}
# Uninstall pacman hook
uninstall_pacman_hook() {
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
if [[ -f "$hook_file" ]]; then
rm -f "$hook_file"
echo "✓ Pacman hook removed"
fi
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
if [[ -f $hook_file ]]; then
rm -f "$hook_file"
echo "✓ Pacman hook removed"
fi
}
# Quietly re-wrap apps (for pacman hook - no interactive output)
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
local wrapper_path="${APPS[$app]}"
for app in "${!APPS[@]}"; do
local wrapper_path="${APPS[$app]}"
# 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
# Wrapper was overwritten by package update
log_message "REWRAP: $app wrapper was overwritten, re-installing"
# 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
# Wrapper was overwritten by package update
log_message "REWRAP: $app wrapper was overwritten, re-installing"
# Remove old .orig if exists (it's now stale)
rm -f "${wrapper_path}.orig"
# Remove old .orig if exists (it's now stale)
rm -f "${wrapper_path}.orig"
# Re-install wrapper
install_wrapper "$app" >>"$LOG_FILE" 2>&1 || true
fi
done
# Re-install wrapper
install_wrapper "$app" >> "$LOG_FILE" 2>&1 || true
fi
done
log_message "REWRAP: Complete"
log_message "REWRAP: Complete"
}
# Uninstall all wrappers
uninstall_all() {
echo "Removing compulsive opening blockers..."
echo ""
echo "Removing compulsive opening blockers..."
echo ""
for app in "${!APPS[@]}"; do
uninstall_wrapper "$app" || true
done
for app in "${!APPS[@]}"; do
uninstall_wrapper "$app" || true
done
rm -f "/usr/local/bin/block-compulsive-opening.sh"
rm -f "/usr/local/bin/block-compulsive-opening.sh"
# Remove pacman hook
uninstall_pacman_hook
# Remove pacman hook
uninstall_pacman_hook
echo ""
echo "Uninstallation complete."
echo ""
echo "Uninstallation complete."
}
# Show status of all apps
show_status() {
ensure_state_dir
local current_hour
current_hour=$(get_hour_key)
ensure_state_dir
local current_hour
current_hour=$(get_hour_key)
echo "Compulsive Opening Blocker Status"
echo "=================================="
echo "Current hour: $current_hour"
echo ""
echo "Compulsive Opening Blocker Status"
echo "=================================="
echo "Current hour: $current_hour"
echo ""
for app in "${!APPS[@]}"; do
local state_file
state_file=$(get_state_file "$app")
local status="not opened this hour"
local icon="○"
for app in "${!APPS[@]}"; do
local state_file
state_file=$(get_state_file "$app")
local status="not opened this hour"
local icon="○"
if [[ -f $state_file ]]; then
local last_hour
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
if [[ $last_hour == "$current_hour" ]]; then
status="already opened (blocked until next hour)"
icon="●"
else
status="last opened: $last_hour"
fi
fi
if [[ -f $state_file ]]; then
local last_hour
last_hour=$(cat "$state_file" 2> /dev/null || echo "")
if [[ $last_hour == "$current_hour" ]]; then
status="already opened (blocked until next hour)"
icon="●"
else
status="last opened: $last_hour"
fi
fi
# Check if wrapped
local wrapped="not installed"
local wrapper_path="${APPS[$app]}"
if [[ -f "${wrapper_path}.orig" ]]; then
wrapped="wrapped"
elif [[ -f $wrapper_path ]]; then
wrapped="installed (not wrapped)"
fi
# Check if wrapped
local wrapped="not installed"
local wrapper_path="${APPS[$app]}"
if [[ -f "${wrapper_path}.orig" ]]; then
wrapped="wrapped"
elif [[ -f $wrapper_path ]]; then
wrapped="installed (not wrapped)"
fi
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
done
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
done
echo ""
echo "State directory: $STATE_DIR"
echo ""
echo "State directory: $STATE_DIR"
}
# Reset state for an app (allow opening again)
reset_app() {
local app="$1"
local state_file
state_file=$(get_state_file "$app")
local app="$1"
local state_file
state_file=$(get_state_file "$app")
if [[ -f $state_file ]]; then
rm -f "$state_file"
echo "Reset $app - can be opened again this hour"
log_message "RESET: $app state cleared by user"
else
echo "$app was not marked as opened"
fi
if [[ -f $state_file ]]; then
rm -f "$state_file"
echo "Reset $app - can be opened again this hour"
log_message "RESET: $app state cleared by user"
else
echo "$app was not marked as opened"
fi
}
# Clear all state
reset_all() {
ensure_state_dir
rm -f "$STATE_DIR"/*.lastopen
echo "All apps reset - can be opened again this hour"
log_message "RESET: All app states cleared by user"
ensure_state_dir
rm -f "$STATE_DIR"/*.lastopen
echo "All apps reset - can be opened again this hour"
log_message "RESET: All app states cleared by user"
}
# Show usage
show_usage() {
cat <<EOF
cat << EOF
Block Compulsive Opening Script
================================
@ -447,60 +447,60 @@ EOF
# Main entry point
main() {
case "${1:-help}" in
install)
if [[ $EUID -ne 0 ]]; then
echo "Error: install requires root privileges"
echo "Run: sudo $0 install"
exit 1
fi
install_all
;;
uninstall)
if [[ $EUID -ne 0 ]]; then
echo "Error: uninstall requires root privileges"
echo "Run: sudo $0 uninstall"
exit 1
fi
uninstall_all
;;
status)
show_status
;;
reset)
if [[ -z ${2:-} ]]; then
echo "Error: specify app to reset"
echo "Apps: ${!APPS[*]}"
exit 1
fi
reset_app "$2"
;;
reset-all)
reset_all
;;
rewrap-quiet)
# Called by pacman hook - quietly re-wrap apps after package updates
if [[ $EUID -ne 0 ]]; then
exit 1
fi
rewrap_quiet
;;
wrapper)
if [[ -z ${2:-} ]]; then
echo "Error: wrapper requires app name"
exit 1
fi
wrapper_main "${@:2}"
;;
help | -h | --help)
show_usage
;;
*)
echo "Unknown command: $1"
show_usage
exit 1
;;
esac
case "${1:-help}" in
install)
if [[ $EUID -ne 0 ]]; then
echo "Error: install requires root privileges"
echo "Run: sudo $0 install"
exit 1
fi
install_all
;;
uninstall)
if [[ $EUID -ne 0 ]]; then
echo "Error: uninstall requires root privileges"
echo "Run: sudo $0 uninstall"
exit 1
fi
uninstall_all
;;
status)
show_status
;;
reset)
if [[ -z ${2:-} ]]; then
echo "Error: specify app to reset"
echo "Apps: ${!APPS[*]}"
exit 1
fi
reset_app "$2"
;;
reset-all)
reset_all
;;
rewrap-quiet)
# Called by pacman hook - quietly re-wrap apps after package updates
if [[ $EUID -ne 0 ]]; then
exit 1
fi
rewrap_quiet
;;
wrapper)
if [[ -z ${2:-} ]]; then
echo "Error: wrapper requires app name"
exit 1
fi
wrapper_main "${@:2}"
;;
help | -h | --help)
show_usage
;;
*)
echo "Unknown command: $1"
show_usage
exit 1
;;
esac
}
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" "$*"; }
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
err "Missing dependency: $1"
MISSING=1
fi
if ! command -v "$1" > /dev/null 2>&1; then
err "Missing dependency: $1"
MISSING=1
fi
}
usage() {
cat <<EOF
cat << EOF
${SCRIPT_NAME} — Download and wire up LeechBlockNG from GitHub
Usage: ${SCRIPT_NAME} [--version vX.Y[.Z]] [--force] [--install-firefox]
@ -44,29 +44,29 @@ VERSION=""
FORCE=0
AUTO_FIREFOX=0
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
VERSION="$2"
shift 2
;;
--force)
FORCE=1
shift
;;
--install-firefox)
AUTO_FIREFOX=1
shift
;;
-h | --help)
usage
exit 0
;;
*)
err "Unrecognized option: $1"
usage
exit 2
;;
esac
case "$1" in
--version)
VERSION="$2"
shift 2
;;
--force)
FORCE=1
shift
;;
--install-firefox)
AUTO_FIREFOX=1
shift
;;
-h | --help)
usage
exit 0
;;
*)
err "Unrecognized option: $1"
usage
exit 2
;;
esac
done
# Dependencies
@ -76,45 +76,45 @@ require_cmd tar
require_cmd find
require_cmd sed
require_cmd awk
if ! command -v jq >/dev/null 2>&1; then
warn "jq not found — will fall back to a simpler tag detection method."
if ! command -v jq > /dev/null 2>&1; then
warn "jq not found — will fall back to a simpler tag detection method."
fi
[[ $MISSING -eq 1 ]] && {
err "Please install missing tools and re-run."
exit 1
err "Please install missing tools and re-run."
exit 1
}
REPO_OWNER="proginosko"
REPO_NAME="LeechBlockNG"
get_latest_tag() {
local tag
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)
if [[ -n $tag && $tag != "null" ]]; then
echo "$tag"
return 0
fi
fi
# 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)
if [[ -n $tag ]]; then
echo "$tag"
return 0
fi
return 1
local tag
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)
if [[ -n $tag && $tag != "null" ]]; then
echo "$tag"
return 0
fi
fi
# 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)
if [[ -n $tag ]]; then
echo "$tag"
return 0
fi
return 1
}
if [[ -z $VERSION ]]; then
info "Resolving latest release tag from GitHub…"
if ! VERSION=$(get_latest_tag); then
err "Failed to determine latest version tag"
exit 1
fi
info "Resolving latest release tag from GitHub…"
if ! VERSION=$(get_latest_tag); then
err "Failed to determine latest version tag"
exit 1
fi
fi
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
VERSION=${VERSION#v} # strip leading v for folder names
@ -126,40 +126,40 @@ VERSION_DIR="$INSTALL_ROOT/$VERSION"
CURRENT_LINK="$INSTALL_ROOT/current"
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
info "Downloading LeechBlockNG $TAG source from GitHub…"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
ARCHIVE_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${TAG}.tar.gz"
ARCHIVE_FILE="$tmpdir/${REPO_NAME}-${TAG}.tar.gz"
curl -fL --retry 3 -o "$ARCHIVE_FILE" "$ARCHIVE_URL"
info "Extracting…"
mkdir -p "$tmpdir/extract"
tar -xzf "$ARCHIVE_FILE" -C "$tmpdir/extract"
# The archive usually extracts to REPO_NAME-TAG/ …
src_root=$(find "$tmpdir/extract" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n1 || true)
[[ -z $src_root ]] && {
err "Could not locate extracted source root"
exit 1
}
info "Downloading LeechBlockNG $TAG source from GitHub…"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
ARCHIVE_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${TAG}.tar.gz"
ARCHIVE_FILE="$tmpdir/${REPO_NAME}-${TAG}.tar.gz"
curl -fL --retry 3 -o "$ARCHIVE_FILE" "$ARCHIVE_URL"
info "Extracting…"
mkdir -p "$tmpdir/extract"
tar -xzf "$ARCHIVE_FILE" -C "$tmpdir/extract"
# The archive usually extracts to REPO_NAME-TAG/ …
src_root=$(find "$tmpdir/extract" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n1 || true)
[[ -z $src_root ]] && {
err "Could not locate extracted source root"
exit 1
}
# 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)
if [[ -z $manifest_path ]]; then
err "manifest.json not found in the extracted archive. The project layout may have changed."
exit 1
fi
ext_dir=$(dirname "$manifest_path")
# 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)
if [[ -z $manifest_path ]]; then
err "manifest.json not found in the extracted archive. The project layout may have changed."
exit 1
fi
ext_dir=$(dirname "$manifest_path")
mkdir -p "$INSTALL_ROOT"
rm -rf "$VERSION_DIR"
info "Installing to $VERSION_DIR"
mkdir -p "$VERSION_DIR"
# 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/"
mkdir -p "$INSTALL_ROOT"
rm -rf "$VERSION_DIR"
info "Installing to $VERSION_DIR"
mkdir -p "$VERSION_DIR"
# 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/"
ln -sfn "$VERSION_DIR" "$CURRENT_LINK"
ln -sfn "$VERSION_DIR" "$CURRENT_LINK"
fi
EXT_PATH="$CURRENT_LINK" # stable path used by wrappers
@ -167,21 +167,21 @@ EXT_PATH="$CURRENT_LINK" # stable path used by wrappers
# Detect browsers
declare -A BROWSERS
BROWSERS=(
[chromium]="Chromium"
[google - chrome - stable]="Google Chrome"
[google - chrome]="Google Chrome"
[brave - browser]="Brave"
[vivaldi - stable]="Vivaldi"
[vivaldi]="Vivaldi"
[opera]="Opera"
[thorium - browser]="Thorium"
[chromium]="Chromium"
[google - chrome - stable]="Google Chrome"
[google - chrome]="Google Chrome"
[brave - browser]="Brave"
[vivaldi - stable]="Vivaldi"
[vivaldi]="Vivaldi"
[opera]="Opera"
[thorium - browser]="Thorium"
)
declare -A FIREFOXES
FIREFOXES=(
[firefox]="Firefox"
[firefox - developer - edition]="Firefox Developer Edition"
[librewolf]="LibreWolf"
[firefox]="Firefox"
[firefox - developer - edition]="Firefox Developer Edition"
[librewolf]="LibreWolf"
)
found_any=0
@ -193,36 +193,36 @@ user_apps_dir="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
mkdir -p "$user_apps_dir"
create_wrapper_and_desktop() {
local bin="$1"
shift
local pretty="$1"
shift
local wrapper="$wrap_bin_dir/${bin}-with-leechblock"
local bin="$1"
shift
local pretty="$1"
shift
local wrapper="$wrap_bin_dir/${bin}-with-leechblock"
local real_bin
real_bin=$(command -v "$bin" || true)
[[ -z $real_bin ]] && return
local real_bin
real_bin=$(command -v "$bin" || true)
[[ -z $real_bin ]] && return
cat >"$wrapper" <<WRAP
cat > "$wrapper" << WRAP
#!/usr/bin/env bash
exec "$real_bin" --load-extension="$EXT_PATH" "$@"
WRAP
chmod +x "$wrapper"
chmod +x "$wrapper"
# Try to reuse icon from an existing desktop file if available
local sys_desktop existing_icon existing_name categories
sys_desktop=$(grep -RIl "^Exec=.*${bin}" /usr/share/applications 2>/dev/null | head -n1 || true)
if [[ -n $sys_desktop ]]; then
existing_icon=$(awk -F= '/^Icon=/{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)
fi
[[ -z $existing_icon ]] && existing_icon="$bin"
[[ -z $existing_name ]] && existing_name="$pretty"
[[ -z $categories ]] && categories="Network;WebBrowser;"
# Try to reuse icon from an existing desktop file if available
local sys_desktop existing_icon existing_name categories
sys_desktop=$(grep -RIl "^Exec=.*${bin}" /usr/share/applications 2> /dev/null | head -n1 || true)
if [[ -n $sys_desktop ]]; then
existing_icon=$(awk -F= '/^Icon=/{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)
fi
[[ -z $existing_icon ]] && existing_icon="$bin"
[[ -z $existing_name ]] && existing_name="$pretty"
[[ -z $categories ]] && categories="Network;WebBrowser;"
local desktop_file="$user_apps_dir/${bin}-with-leechblock.desktop"
cat >"$desktop_file" <<DESK
local desktop_file="$user_apps_dir/${bin}-with-leechblock.desktop"
cat > "$desktop_file" << DESK
[Desktop Entry]
Name=${existing_name} (LeechBlock)
Exec=${wrapper} %U
@ -233,35 +233,35 @@ Categories=${categories}
StartupNotify=true
DESK
info "Created wrapper: $wrapper"
info "Created launcher: $desktop_file"
found_any=1
info "Created wrapper: $wrapper"
info "Created launcher: $desktop_file"
found_any=1
}
info "Detecting installed browsers…"
for bin in "${!BROWSERS[@]}"; do
if command -v "$bin" >/dev/null 2>&1; then
create_wrapper_and_desktop "$bin" "${BROWSERS[$bin]}"
fi
if command -v "$bin" > /dev/null 2>&1; then
create_wrapper_and_desktop "$bin" "${BROWSERS[$bin]}"
fi
done
ff_found=0
for bin in "${!FIREFOXES[@]}"; do
if command -v "$bin" >/dev/null 2>&1; then
ff_found=1
fi
if command -v "$bin" > /dev/null 2>&1; then
ff_found=1
fi
done
echo
if [[ $found_any -eq 1 ]]; then
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."
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."
fi
if [[ $ff_found -eq 1 ]]; then
echo
warn "Detected Firefox-based browser(s). Permanent install from GitHub source isn't possible on stable builds due to required signing."
cat <<FF
echo
warn "Detected Firefox-based browser(s). Permanent install from GitHub source isn't possible on stable builds due to required signing."
cat << FF
Options:
1) Install from Mozilla Add-ons (recommended):
https://addons.mozilla.org/firefox/addon/leechblock-ng/
@ -276,8 +276,8 @@ FF
fi
if [[ $found_any -eq 0 && $ff_found -eq 0 ]]; then
warn "No supported browsers detected. We placed the extension at: $VERSION_DIR"
echo "Supported (auto-wired): ${!BROWSERS[*]}. Detected Firefox variants will show guidance only."
warn "No supported browsers detected. We placed the extension at: $VERSION_DIR"
echo "Supported (auto-wired): ${!BROWSERS[*]}. Detected Firefox variants will show guidance only."
fi
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 [[ $AUTO_FIREFOX -eq 1 && $ff_found -eq 1 ]]; then
echo
info "Attempting Firefox auto-install via Enterprise Policies (requires sudo)."
# AMO info
ADDON_ID="leechblockng@proginosko.com"
ADDON_AMO_URL="https://addons.mozilla.org/firefox/downloads/latest/leechblock-ng/latest.xpi"
echo
info "Attempting Firefox auto-install via Enterprise Policies (requires sudo)."
# AMO info
ADDON_ID="leechblockng@proginosko.com"
ADDON_AMO_URL="https://addons.mozilla.org/firefox/downloads/latest/leechblock-ng/latest.xpi"
# Determine policy directories for detected Firefox-like browsers
declare -a POLICY_DIRS
POLICY_DIRS=()
if command -v firefox >/dev/null 2>&1; then
POLICY_DIRS+=("/etc/firefox/policies" "/usr/lib/firefox/distribution")
fi
if command -v firefox-developer-edition >/dev/null 2>&1; then
POLICY_DIRS+=("/etc/firefox-developer-edition/policies" "/usr/lib/firefox-developer-edition/distribution")
fi
if command -v librewolf >/dev/null 2>&1; then
POLICY_DIRS+=("/etc/librewolf/policies" "/usr/lib/librewolf/distribution")
fi
# Generic mozilla path as fallback
POLICY_DIRS+=("/usr/lib/mozilla/distribution")
# Determine policy directories for detected Firefox-like browsers
declare -a POLICY_DIRS
POLICY_DIRS=()
if command -v firefox > /dev/null 2>&1; then
POLICY_DIRS+=("/etc/firefox/policies" "/usr/lib/firefox/distribution")
fi
if command -v firefox-developer-edition > /dev/null 2>&1; then
POLICY_DIRS+=("/etc/firefox-developer-edition/policies" "/usr/lib/firefox-developer-edition/distribution")
fi
if command -v librewolf > /dev/null 2>&1; then
POLICY_DIRS+=("/etc/librewolf/policies" "/usr/lib/librewolf/distribution")
fi
# Generic mozilla path as fallback
POLICY_DIRS+=("/usr/lib/mozilla/distribution")
updated_any=0
for pol_target in "${POLICY_DIRS[@]}"; do
tmp_pol=$(mktemp)
existing="${pol_target}/policies.json"
if sudo test -f "$existing"; then
info "Merging into existing policies.json at $existing"
sudo cp "$existing" "$tmp_pol"
if command -v jq >/dev/null 2>&1; then
merged=$(jq --arg id "$ADDON_ID" --arg url "$ADDON_AMO_URL" '
updated_any=0
for pol_target in "${POLICY_DIRS[@]}"; do
tmp_pol=$(mktemp)
existing="${pol_target}/policies.json"
if sudo test -f "$existing"; then
info "Merging into existing policies.json at $existing"
sudo cp "$existing" "$tmp_pol"
if command -v jq > /dev/null 2>&1; then
merged=$(jq --arg id "$ADDON_ID" --arg url "$ADDON_AMO_URL" '
.policies |= (. // {}) |
.policies.ExtensionSettings |= (. // {}) |
.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].install_url = $url
' "$tmp_pol") || merged=""
if [[ -n $merged ]]; then
printf '%s\n' "$merged" >"$tmp_pol"
else
warn "jq merge failed; skipping $pol_target"
rm -f "$tmp_pol"
continue
fi
else
warn "jq not available; creating minimal policies.json (existing file will be backed up)."
sudo cp "$existing" "${existing}.bak.$(date +%s)"
cat >"$tmp_pol" <<JSON
if [[ -n $merged ]]; then
printf '%s\n' "$merged" > "$tmp_pol"
else
warn "jq merge failed; skipping $pol_target"
rm -f "$tmp_pol"
continue
fi
else
warn "jq not available; creating minimal policies.json (existing file will be backed up)."
sudo cp "$existing" "${existing}.bak.$(date +%s)"
cat > "$tmp_pol" << JSON
{
"policies": {
"ExtensionSettings": {
@ -345,10 +345,10 @@ if [[ $AUTO_FIREFOX -eq 1 && $ff_found -eq 1 ]]; then
}
}
JSON
fi
else
info "Creating new policies.json at $pol_target"
cat >"$tmp_pol" <<JSON
fi
else
info "Creating new policies.json at $pol_target"
cat > "$tmp_pol" << JSON
{
"policies": {
"ExtensionSettings": {
@ -361,18 +361,18 @@ JSON
}
}
JSON
fi
fi
sudo mkdir -p "$pol_target"
sudo cp "$tmp_pol" "$pol_target/policies.json"
rm -f "$tmp_pol"
updated_any=1
done
sudo mkdir -p "$pol_target"
sudo cp "$tmp_pol" "$pol_target/policies.json"
rm -f "$tmp_pol"
updated_any=1
done
if [[ $updated_any -eq 1 ]]; then
info "Firefox policies updated. Restart Firefox/LibreWolf to complete installation of LeechBlock NG."
else
warn "No Firefox policy locations updated. You may not have a supported Firefox installed."
fi
info "Firefox policy updated. Restart Firefox to complete installation of LeechBlock NG."
if [[ $updated_any -eq 1 ]]; then
info "Firefox policies updated. Restart Firefox/LibreWolf to complete installation of LeechBlock NG."
else
warn "No Firefox policy locations updated. You may not have a supported Firefox installed."
fi
info "Firefox policy updated. Restart Firefox to complete installation of LeechBlock NG."
fi

View File

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

View File

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

View File

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

View File

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

View File

@ -13,33 +13,33 @@ echo -e "${BLUE}=== Unreal MCP Installer for Arch Linux ===${NC}"
# Check dependencies
echo -e "${BLUE}Checking dependencies...${NC}"
for cmd in git python pip; do
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}"
exit 1
fi
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}"
exit 1
fi
done
# Get Unreal Project Path
PROJECT_PATH="$1"
if [ -z "$PROJECT_PATH" ]; then
echo -e "${YELLOW}Please enter the path to your Unreal Engine Project (the folder containing .uproject file):${NC}"
read -r -e -p "> " PROJECT_PATH
echo -e "${YELLOW}Please enter the path to your Unreal Engine Project (the folder containing .uproject file):${NC}"
read -r -e -p "> " PROJECT_PATH
fi
# Validate path
# Expand tilde if present
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
echo -e "${RED}Error: Invalid directory: $PROJECT_PATH${NC}"
exit 1
echo -e "${RED}Error: Invalid directory: $PROJECT_PATH${NC}"
exit 1
fi
UPROJECT_FILES=("$PROJECT_PATH"/*.uproject)
if [ ! -e "${UPROJECT_FILES[0]}" ]; then
echo -e "${RED}Error: No .uproject file found in $PROJECT_PATH${NC}"
exit 1
echo -e "${RED}Error: No .uproject file found in $PROJECT_PATH${NC}"
exit 1
fi
echo -e "${GREEN}Target Project: $PROJECT_PATH${NC}"
@ -51,12 +51,12 @@ mkdir -p "$PLUGINS_DIR"
# Clone UnrealMCP
MCP_PLUGIN_DIR="$PLUGINS_DIR/UnrealMCP"
if [ -d "$MCP_PLUGIN_DIR" ]; then
echo -e "${BLUE}UnrealMCP already exists. Updating...${NC}"
cd "$MCP_PLUGIN_DIR"
git pull
echo -e "${BLUE}UnrealMCP already exists. Updating...${NC}"
cd "$MCP_PLUGIN_DIR"
git pull
else
echo -e "${BLUE}Cloning UnrealMCP...${NC}"
git clone https://github.com/kvick-games/UnrealMCP.git "$MCP_PLUGIN_DIR"
echo -e "${BLUE}Cloning UnrealMCP...${NC}"
git clone https://github.com/kvick-games/UnrealMCP.git "$MCP_PLUGIN_DIR"
fi
# Setup Python Environment
@ -64,41 +64,41 @@ echo -e "${BLUE}Setting up Python environment...${NC}"
MCP_DIR="$MCP_PLUGIN_DIR/MCP"
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}"
exit 1
echo -e "${RED}Error: unreal_mcp_bridge.py not found in $MCP_DIR. Repository structure might have changed.${NC}"
exit 1
fi
VENV_DIR="$MCP_DIR/python_env"
if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..."
python -m venv "$VENV_DIR"
echo "Creating virtual environment..."
python -m venv "$VENV_DIR"
fi
# Install requirements
echo "Installing dependencies in virtual environment..."
# shellcheck source=/dev/null
source "$VENV_DIR/bin/activate"
pip install --upgrade pip >/dev/null
pip install "mcp>=0.1.0" >/dev/null
pip install --upgrade pip > /dev/null
pip install "mcp>=0.1.0" > /dev/null
# Patch unreal_mcp_bridge.py for newer mcp package compatibility
# The newer mcp package (1.x) renamed 'description' parameter to 'instructions'
BRIDGE_SCRIPT="$MCP_DIR/unreal_mcp_bridge.py"
if grep -q 'description="Unreal Engine integration' "$BRIDGE_SCRIPT" 2>/dev/null; then
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"
if grep -q 'description="Unreal Engine integration' "$BRIDGE_SCRIPT" 2> /dev/null; then
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"
fi
# Fix case-sensitive includes for Linux (Windows is case-insensitive, Linux is not)
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
RUN_SCRIPT="$MCP_DIR/run_unreal_mcp.sh"
echo -e "${BLUE}Creating run script at $RUN_SCRIPT...${NC}"
cat <<EOF >"$RUN_SCRIPT"
cat << EOF > "$RUN_SCRIPT"
#!/bin/bash
set -e
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
@ -115,7 +115,7 @@ echo -e "${BLUE}=== Configuration Setup ===${NC}"
# Python script to update JSON configs
CONFIG_UPDATER_SCRIPT=$(mktemp)
cat <<EOF >"$CONFIG_UPDATER_SCRIPT"
cat << EOF > "$CONFIG_UPDATER_SCRIPT"
import json
import os
import sys
@ -164,18 +164,18 @@ CLAUDE_CONFIG="$HOME/.config/Claude/claude_desktop_config.json"
# Function to ask and update
update_config() {
local path="$1"
local type="$2"
local name="$3"
local path="$1"
local type="$2"
local name="$3"
if [ -f "$path" ] || [ -d "$(dirname "$path")" ]; then
echo -e "Found $name configuration at: $path"
read -p "Do you want to add UnrealMCP to this config? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
python "$CONFIG_UPDATER_SCRIPT" "$path" "$RUN_SCRIPT" "$type"
fi
fi
if [ -f "$path" ] || [ -d "$(dirname "$path")" ]; then
echo -e "Found $name configuration at: $path"
read -p "Do you want to add UnrealMCP to this config? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
python "$CONFIG_UPDATER_SCRIPT" "$path" "$RUN_SCRIPT" "$type"
fi
fi
}
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"
if [ ! -f "$MCP_JSON" ]; then
echo -e "${BLUE}Creating workspace MCP config at $MCP_JSON...${NC}"
cat <<EOF >"$MCP_JSON"
echo -e "${BLUE}Creating workspace MCP config at $MCP_JSON...${NC}"
cat << EOF > "$MCP_JSON"
{
"mcpServers": {
"unreal": {
@ -201,23 +201,23 @@ if [ ! -f "$MCP_JSON" ]; then
}
EOF
else
echo -e "${YELLOW}Workspace MCP config already exists at $MCP_JSON. Skipping overwrite.${NC}"
echo "Ensure it contains the following configuration:"
echo "\"unreal\": { \"command\": \"$RUN_SCRIPT\", \"args\": [] }"
echo -e "${YELLOW}Workspace MCP config already exists at $MCP_JSON. Skipping overwrite.${NC}"
echo "Ensure it contains the following configuration:"
echo "\"unreal\": { \"command\": \"$RUN_SCRIPT\", \"args\": [] }"
fi
echo -e "${BLUE}=== Build Instructions ===${NC}"
echo "1. You need to regenerate project files."
if [ -f "$PROJECT_PATH/GenerateProjectFiles.sh" ]; then
echo " Found GenerateProjectFiles.sh in project root."
read -p " Do you want to run it now? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
cd "$PROJECT_PATH"
./GenerateProjectFiles.sh
fi
echo " Found GenerateProjectFiles.sh in project root."
read -p " Do you want to run it now? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
cd "$PROJECT_PATH"
./GenerateProjectFiles.sh
fi
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
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 "For VS Code (User Settings), add this to your settings.json:"
echo -e "${GREEN}"
cat <<EOF
cat << EOF
"mcpServers": {
"unreal": {
"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
check_sudo() {
if [[ $EUID -ne 0 ]] && [[ $1 == "install" ]]; then
echo "Package installation requires sudo privileges."
echo "Requesting sudo access..."
exec sudo "$0" "$@"
fi
if [[ $EUID -ne 0 ]] && [[ $1 == "install" ]]; then
echo "Package installation requires sudo privileges."
echo "Requesting sudo access..."
exec sudo "$0" "$@"
fi
}
# Get the actual user (even when running with sudo)
@ -31,180 +31,180 @@ echo "User home: $USER_HOME"
# Function to check if ActivityWatch is installed
check_activitywatch_installed() {
echo ""
echo "1. Checking ActivityWatch Installation..."
echo "========================================"
echo ""
echo "1. Checking ActivityWatch Installation..."
echo "========================================"
# Check if activitywatch-bin is installed via pacman
if pacman -Qi activitywatch-bin &>/dev/null; then
echo "✓ activitywatch-bin package is installed"
return 0
fi
# Check if activitywatch-bin is installed via pacman
if pacman -Qi activitywatch-bin &> /dev/null; then
echo "✓ activitywatch-bin package is installed"
return 0
fi
# Check if aw-qt binary exists in common locations
local common_paths=(
"/usr/bin/aw-qt"
"/usr/local/bin/aw-qt"
"$USER_HOME/.local/bin/aw-qt"
"$USER_HOME/activitywatch/aw-qt"
)
# Check if aw-qt binary exists in common locations
local common_paths=(
"/usr/bin/aw-qt"
"/usr/local/bin/aw-qt"
"$USER_HOME/.local/bin/aw-qt"
"$USER_HOME/activitywatch/aw-qt"
)
for path in "${common_paths[@]}"; do
if [[ -x $path ]]; then
echo "✓ ActivityWatch found at: $path"
return 0
fi
done
for path in "${common_paths[@]}"; do
if [[ -x $path ]]; then
echo "✓ ActivityWatch found at: $path"
return 0
fi
done
echo "✗ ActivityWatch not found"
return 1
echo "✗ ActivityWatch not found"
return 1
}
# Function to install ActivityWatch
install_activitywatch() {
echo ""
echo "2. Installing ActivityWatch..."
echo "============================="
echo ""
echo "2. Installing ActivityWatch..."
echo "============================="
# Check if we need sudo for installation
check_sudo "install"
# Check if we need sudo for installation
check_sudo "install"
echo "Installing activitywatch-bin from AUR..."
echo "Installing activitywatch-bin from AUR..."
# Check if an AUR helper is available
local aur_helpers=("yay" "paru" "makepkg")
local helper_found=""
# Check if an AUR helper is available
local aur_helpers=("yay" "paru" "makepkg")
local helper_found=""
for helper in "${aur_helpers[@]}"; do
if command -v "$helper" &>/dev/null; then
helper_found="$helper"
break
fi
done
for helper in "${aur_helpers[@]}"; do
if command -v "$helper" &> /dev/null; then
helper_found="$helper"
break
fi
done
if [[ -n $helper_found && $helper_found != "makepkg" ]]; then
echo "Using AUR helper: $helper_found"
if [[ $EUID -eq 0 ]]; then
# Running as root, need to install as user
sudo -u "$ACTUAL_USER" "$helper_found" -S --noconfirm activitywatch-bin
else
"$helper_found" -S --noconfirm activitywatch-bin
fi
else
echo "No AUR helper found. Installing manually with makepkg..."
install_activitywatch_manual
fi
if [[ -n $helper_found && $helper_found != "makepkg" ]]; then
echo "Using AUR helper: $helper_found"
if [[ $EUID -eq 0 ]]; then
# Running as root, need to install as user
sudo -u "$ACTUAL_USER" "$helper_found" -S --noconfirm activitywatch-bin
else
"$helper_found" -S --noconfirm activitywatch-bin
fi
else
echo "No AUR helper found. Installing manually with makepkg..."
install_activitywatch_manual
fi
echo "✓ ActivityWatch installation completed"
echo "✓ ActivityWatch installation completed"
}
# Function to manually install ActivityWatch via makepkg
install_activitywatch_manual() {
local temp_dir="/tmp/activitywatch-install"
local original_user="$ACTUAL_USER"
local temp_dir="/tmp/activitywatch-install"
local original_user="$ACTUAL_USER"
# Create temp directory
mkdir -p "$temp_dir"
cd "$temp_dir"
# Create temp directory
mkdir -p "$temp_dir"
cd "$temp_dir"
# Download PKGBUILD
if command -v git &>/dev/null; then
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
else
echo "Installing git..."
pacman -S --noconfirm git
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
fi
# Download PKGBUILD
if command -v git &> /dev/null; then
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
else
echo "Installing git..."
pacman -S --noconfirm git
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
fi
# Build and install package
sudo -u "$original_user" makepkg -si --noconfirm
# Build and install package
sudo -u "$original_user" makepkg -si --noconfirm
# Cleanup
cd /
rm -rf "$temp_dir"
# Cleanup
cd /
rm -rf "$temp_dir"
}
# Function to check if ActivityWatch is running
check_activitywatch_running() {
echo ""
echo "3. Checking ActivityWatch Status..."
echo "=================================="
echo ""
echo "3. Checking ActivityWatch Status..."
echo "=================================="
# Check for aw-qt process
if pgrep -f "aw-qt" >/dev/null; then
echo "✓ ActivityWatch (aw-qt) is running"
return 0
fi
# Check for aw-qt process
if pgrep -f "aw-qt" > /dev/null; then
echo "✓ ActivityWatch (aw-qt) is running"
return 0
fi
# Check for aw-server process
if pgrep -f "aw-server" >/dev/null; then
echo "✓ ActivityWatch server is running"
return 0
fi
# Check for aw-server process
if pgrep -f "aw-server" > /dev/null; then
echo "✓ ActivityWatch server is running"
return 0
fi
echo "✗ ActivityWatch is not running"
return 1
echo "✗ ActivityWatch is not running"
return 1
}
# Function to start ActivityWatch
start_activitywatch() {
echo ""
echo "4. Starting ActivityWatch..."
echo "==========================="
echo ""
echo "4. Starting ActivityWatch..."
echo "==========================="
# Find aw-qt executable
local aw_qt_path=""
# Find aw-qt executable
local aw_qt_path=""
if command -v aw-qt &>/dev/null; then
aw_qt_path="$(which aw-qt)"
elif [[ -x "/usr/bin/aw-qt" ]]; then
aw_qt_path="/usr/bin/aw-qt"
else
echo "✗ Could not find aw-qt executable"
return 1
fi
if command -v aw-qt &> /dev/null; then
aw_qt_path="$(which aw-qt)"
elif [[ -x "/usr/bin/aw-qt" ]]; then
aw_qt_path="/usr/bin/aw-qt"
else
echo "✗ Could not find aw-qt executable"
return 1
fi
echo "Starting ActivityWatch as user: $ACTUAL_USER"
echo "Using aw-qt from: $aw_qt_path"
echo "Starting ActivityWatch as user: $ACTUAL_USER"
echo "Using aw-qt from: $aw_qt_path"
# Start as the actual user in the background
if [[ $EUID -eq 0 ]]; then
# Running as root, start as user
sudo -u "$ACTUAL_USER" env DISPLAY=:0 "$aw_qt_path" &
else
# Running as user
"$aw_qt_path" &
fi
# Start as the actual user in the background
if [[ $EUID -eq 0 ]]; then
# Running as root, start as user
sudo -u "$ACTUAL_USER" env DISPLAY=:0 "$aw_qt_path" &
else
# Running as user
"$aw_qt_path" &
fi
# Give it time to start
sleep 3
# Give it time to start
sleep 3
if check_activitywatch_running >/dev/null 2>&1; then
echo "✓ ActivityWatch started successfully"
else
echo "! ActivityWatch may be starting (check system tray)"
fi
if check_activitywatch_running > /dev/null 2>&1; then
echo "✓ ActivityWatch started successfully"
else
echo "! ActivityWatch may be starting (check system tray)"
fi
}
# Function to setup autostart
setup_autostart() {
echo ""
echo "5. Setting Up Autostart..."
echo "========================="
echo ""
echo "5. Setting Up Autostart..."
echo "========================="
local autostart_dir="$USER_HOME/.config/autostart"
local desktop_file="$autostart_dir/activitywatch.desktop"
local i3_config="$USER_HOME/.config/i3/config"
local autostart_dir="$USER_HOME/.config/autostart"
local desktop_file="$autostart_dir/activitywatch.desktop"
local i3_config="$USER_HOME/.config/i3/config"
# Method 1: XDG Autostart (works with most desktop environments)
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" mkdir -p "$autostart_dir"
else
mkdir -p "$autostart_dir"
fi
# Method 1: XDG Autostart (works with most desktop environments)
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" mkdir -p "$autostart_dir"
else
mkdir -p "$autostart_dir"
fi
# Create desktop file for autostart
cat >"$desktop_file" <<EOF
# Create desktop file for autostart
cat > "$desktop_file" << EOF
[Desktop Entry]
Type=Application
Name=ActivityWatch
@ -219,60 +219,60 @@ Terminal=false
Categories=Utility;
EOF
# Set proper ownership if running as root
if [[ $EUID -eq 0 ]]; then
chown "$ACTUAL_USER:$ACTUAL_USER" "$desktop_file"
fi
# Set proper ownership if running as root
if [[ $EUID -eq 0 ]]; then
chown "$ACTUAL_USER:$ACTUAL_USER" "$desktop_file"
fi
echo "✓ Created XDG autostart entry: $desktop_file"
echo "✓ Created XDG autostart entry: $desktop_file"
# Method 2: i3 config autostart (specific to i3)
if [[ -f $i3_config ]]; then
# Check if autostart entry already exists
if ! grep -q "aw-qt" "$i3_config"; then
# Add autostart entry to i3 config
if [[ $EUID -eq 0 ]]; then
# Running as root
sudo -u "$ACTUAL_USER" bash -c "cat <<'EOF' >> '$i3_config'
# Method 2: i3 config autostart (specific to i3)
if [[ -f $i3_config ]]; then
# Check if autostart entry already exists
if ! grep -q "aw-qt" "$i3_config"; then
# Add autostart entry to i3 config
if [[ $EUID -eq 0 ]]; then
# Running as root
sudo -u "$ACTUAL_USER" bash -c "cat <<'EOF' >> '$i3_config'
# Auto-start ActivityWatch
exec --no-startup-id aw-qt
EOF"
else
{
printf '\n'
printf '# Auto-start ActivityWatch\n'
printf 'exec --no-startup-id aw-qt\n'
} >>"$i3_config"
fi
else
{
printf '\n'
printf '# Auto-start ActivityWatch\n'
printf 'exec --no-startup-id aw-qt\n'
} >> "$i3_config"
fi
echo "✓ Added ActivityWatch to i3 config autostart"
else
echo "✓ ActivityWatch autostart already exists in i3 config"
fi
else
echo "! i3 config not found at $i3_config"
fi
echo "✓ Added ActivityWatch to i3 config autostart"
else
echo "✓ ActivityWatch autostart already exists in i3 config"
fi
else
echo "! i3 config not found at $i3_config"
fi
}
# Function to create i3blocks status script
create_i3blocks_status() {
echo ""
echo "6. Creating i3blocks Status Script..."
echo "===================================="
echo ""
echo "6. Creating i3blocks Status Script..."
echo "===================================="
local i3blocks_dir="$USER_HOME/.config/i3blocks"
local status_script="$i3blocks_dir/activitywatch_status.sh"
local i3blocks_dir="$USER_HOME/.config/i3blocks"
local status_script="$i3blocks_dir/activitywatch_status.sh"
# Create i3blocks directory if it doesn't exist
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" mkdir -p "$i3blocks_dir"
else
mkdir -p "$i3blocks_dir"
fi
# Create i3blocks directory if it doesn't exist
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" mkdir -p "$i3blocks_dir"
else
mkdir -p "$i3blocks_dir"
fi
# Create the status script
cat >"$status_script" <<'EOF'
# Create the status script
cat > "$status_script" << 'EOF'
#!/bin/bash
# ActivityWatch status script for i3blocks
# Shows ActivityWatch installation and running status
@ -323,134 +323,134 @@ else
fi
EOF
chmod +x "$status_script"
chmod +x "$status_script"
# Set proper ownership if running as root
if [[ $EUID -eq 0 ]]; then
chown "$ACTUAL_USER:$ACTUAL_USER" "$status_script"
fi
# Set proper ownership if running as root
if [[ $EUID -eq 0 ]]; then
chown "$ACTUAL_USER:$ACTUAL_USER" "$status_script"
fi
echo "✓ Created i3blocks status script: $status_script"
echo "✓ Created i3blocks status script: $status_script"
# Show configuration instructions
echo ""
echo "To add to your i3blocks config, add this block:"
echo ""
echo "[activitywatch]"
echo "command=~/.config/i3blocks/activitywatch_status.sh"
echo "interval=10"
echo "color=#FFFFFF"
echo ""
# Show configuration instructions
echo ""
echo "To add to your i3blocks config, add this block:"
echo ""
echo "[activitywatch]"
echo "command=~/.config/i3blocks/activitywatch_status.sh"
echo "interval=10"
echo "color=#FFFFFF"
echo ""
}
# Function to test the setup
test_setup() {
echo ""
echo "7. Testing Setup..."
echo "=================="
echo ""
echo "7. Testing Setup..."
echo "=================="
echo "Installation status:"
if check_activitywatch_installed >/dev/null 2>&1; then
echo "✓ ActivityWatch is installed"
else
echo "✗ ActivityWatch is not installed"
fi
echo "Installation status:"
if check_activitywatch_installed > /dev/null 2>&1; then
echo "✓ ActivityWatch is installed"
else
echo "✗ ActivityWatch is not installed"
fi
echo "Running status:"
if check_activitywatch_running >/dev/null 2>&1; then
echo "✓ ActivityWatch is running"
else
echo "✗ ActivityWatch is not running"
fi
echo "Running status:"
if check_activitywatch_running > /dev/null 2>&1; then
echo "✓ ActivityWatch is running"
else
echo "✗ ActivityWatch is not running"
fi
echo "Autostart files:"
if [[ -f "$USER_HOME/.config/autostart/activitywatch.desktop" ]]; then
echo "✓ XDG autostart file exists"
else
echo "✗ XDG autostart file missing"
fi
echo "Autostart files:"
if [[ -f "$USER_HOME/.config/autostart/activitywatch.desktop" ]]; then
echo "✓ XDG autostart file exists"
else
echo "✗ XDG autostart file missing"
fi
if [[ -f "$USER_HOME/.config/i3/config" ]] && grep -q "aw-qt" "$USER_HOME/.config/i3/config"; then
echo "✓ i3 autostart configured"
else
echo "! i3 autostart may not be configured"
fi
if [[ -f "$USER_HOME/.config/i3/config" ]] && grep -q "aw-qt" "$USER_HOME/.config/i3/config"; then
echo "✓ i3 autostart configured"
else
echo "! i3 autostart may not be configured"
fi
echo "i3blocks status script:"
if [[ -x "$USER_HOME/.config/i3blocks/activitywatch_status.sh" ]]; then
echo "✓ i3blocks status script created"
echo "Testing status script:"
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" "$USER_HOME/.config/i3blocks/activitywatch_status.sh"
else
"$USER_HOME/.config/i3blocks/activitywatch_status.sh"
fi
else
echo "✗ i3blocks status script missing"
fi
echo "i3blocks status script:"
if [[ -x "$USER_HOME/.config/i3blocks/activitywatch_status.sh" ]]; then
echo "✓ i3blocks status script created"
echo "Testing status script:"
if [[ $EUID -eq 0 ]]; then
sudo -u "$ACTUAL_USER" "$USER_HOME/.config/i3blocks/activitywatch_status.sh"
else
"$USER_HOME/.config/i3blocks/activitywatch_status.sh"
fi
else
echo "✗ i3blocks status script missing"
fi
}
# Function to show final instructions
show_instructions() {
echo ""
echo "=========================================="
echo "ActivityWatch Setup Complete"
echo "=========================================="
echo "Summary:"
echo "✓ ActivityWatch installation checked/completed"
echo "✓ ActivityWatch startup configured"
echo "✓ Autostart configured (XDG + i3)"
echo "✓ i3blocks status script created"
echo ""
echo "Next steps:"
echo "1. Add the i3blocks configuration to your config file:"
echo " ~/.config/i3blocks/config"
echo ""
echo "2. Reload i3 configuration:"
echo " Super+Shift+R"
echo ""
echo "3. ActivityWatch web interface should be available at:"
echo " http://localhost:5600"
echo ""
echo "4. Check system tray for ActivityWatch icon"
echo ""
echo "Files created:"
echo " ~/.config/autostart/activitywatch.desktop"
echo " ~/.config/i3blocks/activitywatch_status.sh"
echo " ~/.config/i3/config (modified)"
echo ""
echo ""
echo "=========================================="
echo "ActivityWatch Setup Complete"
echo "=========================================="
echo "Summary:"
echo "✓ ActivityWatch installation checked/completed"
echo "✓ ActivityWatch startup configured"
echo "✓ Autostart configured (XDG + i3)"
echo "✓ i3blocks status script created"
echo ""
echo "Next steps:"
echo "1. Add the i3blocks configuration to your config file:"
echo " ~/.config/i3blocks/config"
echo ""
echo "2. Reload i3 configuration:"
echo " Super+Shift+R"
echo ""
echo "3. ActivityWatch web interface should be available at:"
echo " http://localhost:5600"
echo ""
echo "4. Check system tray for ActivityWatch icon"
echo ""
echo "Files created:"
echo " ~/.config/autostart/activitywatch.desktop"
echo " ~/.config/i3blocks/activitywatch_status.sh"
echo " ~/.config/i3/config (modified)"
echo ""
}
# Main execution flow
main() {
local need_install=false
local need_start=false
local need_install=false
local need_start=false
# Check installation
if ! check_activitywatch_installed; then
need_install=true
fi
# Check installation
if ! check_activitywatch_installed; then
need_install=true
fi
# Install if needed
if [[ $need_install == true ]]; then
install_activitywatch
fi
# Install if needed
if [[ $need_install == true ]]; then
install_activitywatch
fi
# Check if running
if ! check_activitywatch_running; then
need_start=true
fi
# Check if running
if ! check_activitywatch_running; then
need_start=true
fi
# Start if needed
if [[ $need_start == true ]]; then
start_activitywatch
fi
# Start if needed
if [[ $need_start == true ]]; then
start_activitywatch
fi
# Always set up autostart and i3blocks (in case they're missing)
setup_autostart
create_i3blocks_status
test_setup
show_instructions
# Always set up autostart and i3blocks (in case they're missing)
setup_autostart
create_i3blocks_status
test_setup
show_instructions
}
# Run main function

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,106 +12,106 @@ CHECK_INTERVAL=30
# Log with timestamp (shutdown-timer-monitor specific)
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
timer_needs_restoration() {
# Check if timer is enabled
if ! systemctl is-enabled "$TIMER_NAME" &>/dev/null; then
log_message "Timer $TIMER_NAME is not enabled"
return 0
fi
# Check if timer is enabled
if ! systemctl is-enabled "$TIMER_NAME" &> /dev/null; then
log_message "Timer $TIMER_NAME is not enabled"
return 0
fi
# Check if timer is active
if ! systemctl is-active "$TIMER_NAME" &>/dev/null; then
log_message "Timer $TIMER_NAME is not active"
return 0
fi
# Check if timer is active
if ! systemctl is-active "$TIMER_NAME" &> /dev/null; then
log_message "Timer $TIMER_NAME is not active"
return 0
fi
# Check if timer unit file exists
if [[ ! -f "/etc/systemd/system/$TIMER_NAME" ]]; then
log_message "Timer unit file missing: /etc/systemd/system/$TIMER_NAME"
return 0
fi
# Check if timer unit file exists
if [[ ! -f "/etc/systemd/system/$TIMER_NAME" ]]; then
log_message "Timer unit file missing: /etc/systemd/system/$TIMER_NAME"
return 0
fi
# Check if service unit file exists
if [[ ! -f "/etc/systemd/system/$SERVICE_NAME" ]]; then
log_message "Service unit file missing: /etc/systemd/system/$SERVICE_NAME"
return 0
fi
# Check if service unit file exists
if [[ ! -f "/etc/systemd/system/$SERVICE_NAME" ]]; then
log_message "Service unit file missing: /etc/systemd/system/$SERVICE_NAME"
return 0
fi
# Check if check script exists
if [[ ! -f "/usr/local/bin/day-specific-shutdown-check.sh" ]]; then
log_message "Check script missing: /usr/local/bin/day-specific-shutdown-check.sh"
return 0
fi
# Check if check script exists
if [[ ! -f "/usr/local/bin/day-specific-shutdown-check.sh" ]]; then
log_message "Check script missing: /usr/local/bin/day-specific-shutdown-check.sh"
return 0
fi
return 1 # Timer is properly configured
return 1 # Timer is properly configured
}
# Function to 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
systemctl daemon-reload
# Reload systemd daemon in case unit files were modified
systemctl daemon-reload
# Re-enable timer if disabled
if ! systemctl is-enabled "$TIMER_NAME" &>/dev/null; then
log_message "Re-enabling $TIMER_NAME"
systemctl enable "$TIMER_NAME" 2>/dev/null || true
fi
# Re-enable timer if disabled
if ! systemctl is-enabled "$TIMER_NAME" &> /dev/null; then
log_message "Re-enabling $TIMER_NAME"
systemctl enable "$TIMER_NAME" 2> /dev/null || true
fi
# Re-start timer if not active
if ! systemctl is-active "$TIMER_NAME" &>/dev/null; then
log_message "Re-starting $TIMER_NAME"
systemctl start "$TIMER_NAME" 2>/dev/null || true
fi
# Re-start timer if not active
if ! systemctl is-active "$TIMER_NAME" &> /dev/null; then
log_message "Re-starting $TIMER_NAME"
systemctl start "$TIMER_NAME" 2> /dev/null || true
fi
# Verify restoration
if systemctl is-active "$TIMER_NAME" &>/dev/null; then
log_message "Timer restoration completed successfully"
else
log_message "WARNING: Timer restoration may have failed"
fi
# Verify restoration
if systemctl is-active "$TIMER_NAME" &> /dev/null; then
log_message "Timer restoration completed successfully"
else
log_message "WARNING: Timer restoration may have failed"
fi
}
# Function to monitor timer with systemd events
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
# Fall back to polling if this fails
if command -v busctl &>/dev/null; then
# Monitor for unit state changes
busctl monitor --system org.freedesktop.systemd1 2>/dev/null |
while read -r line; do
# Check if the line mentions our timer
if echo "$line" | grep -q "$TIMER_NAME\|$SERVICE_NAME"; then
log_message "Systemd event detected for shutdown timer"
sleep 2
if timer_needs_restoration; then
restore_timer
fi
fi
done
else
log_message "busctl not available, falling back to polling"
monitor_with_polling
fi
# Use busctl to monitor systemd unit changes
# Fall back to polling if this fails
if command -v busctl &> /dev/null; then
# Monitor for unit state changes
busctl monitor --system org.freedesktop.systemd1 2> /dev/null |
while read -r line; do
# Check if the line mentions our timer
if echo "$line" | grep -q "$TIMER_NAME\|$SERVICE_NAME"; then
log_message "Systemd event detected for shutdown timer"
sleep 2
if timer_needs_restoration; then
restore_timer
fi
fi
done
else
log_message "busctl not available, falling back to polling"
monitor_with_polling
fi
}
# Function to monitor with polling (primary method for reliability)
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
if timer_needs_restoration; then
restore_timer
fi
sleep "$CHECK_INTERVAL"
done
while true; do
if timer_needs_restoration; then
restore_timer
fi
sleep "$CHECK_INTERVAL"
done
}
# Main execution
@ -121,10 +121,10 @@ log_message "Monitoring service: $SERVICE_NAME"
# Initial check
if timer_needs_restoration; then
log_message "Initial check: Timer needs restoration"
restore_timer
log_message "Initial check: Timer needs restoration"
restore_timer
else
log_message "Initial check: Timer is properly configured"
log_message "Initial check: Timer is properly configured"
fi
# 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"
# 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
cat >"$WATCHDOG_SCRIPT" <<'WATCHDOG'
cat > "$WATCHDOG_SCRIPT" << 'WATCHDOG'
#!/system/bin/sh
# Secondary watchdog - runs independently of module state
# Even if module is "disabled" in Magisk UI, this keeps running and undoes it
@ -59,5 +59,5 @@ WATCHDOG
chmod 755 "$WATCHDOG_SCRIPT"
# Start watchdog as a separate background process
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"
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"

View File

@ -20,83 +20,83 @@ REMOVE_FILE="$MODULE_DIR/remove"
mkdir -p "$GUARDIAN_DIR"
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
[ ! -f "$CONTROL_FILE" ] && echo "ENABLED" >"$CONTROL_FILE"
[ ! -f "$CONTROL_FILE" ] && echo "ENABLED" > "$CONTROL_FILE"
log "=== Android Guardian starting ==="
# Function to check if guardian is enabled (via ADB control, not Magisk UI)
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
protect_module() {
# Remove disable file if someone tried to disable via Magisk
if [ -f "$DISABLE_FILE" ]; then
log "Module disable attempt detected via Magisk UI! Re-enabling..."
rm -f "$DISABLE_FILE"
log "Module re-enabled"
fi
# Remove disable file if someone tried to disable via Magisk
if [ -f "$DISABLE_FILE" ]; then
log "Module disable attempt detected via Magisk UI! Re-enabling..."
rm -f "$DISABLE_FILE"
log "Module re-enabled"
fi
# Remove remove file if someone tried to uninstall via Magisk
if [ -f "$REMOVE_FILE" ]; then
log "Module removal attempt detected via Magisk UI! Blocking..."
rm -f "$REMOVE_FILE"
log "Module removal blocked"
fi
# Remove remove file if someone tried to uninstall via Magisk
if [ -f "$REMOVE_FILE" ]; then
log "Module removal attempt detected via Magisk UI! Blocking..."
rm -f "$REMOVE_FILE"
log "Module removal blocked"
fi
}
# Function to restore hosts file if tampered
protect_hosts() {
if [ -f "$HOSTS_BACKUP" ]; then
current_hash=$(md5sum /system/etc/hosts 2>/dev/null | cut -d' ' -f1)
backup_hash=$(md5sum "$HOSTS_BACKUP" 2>/dev/null | cut -d' ' -f1)
if [ -f "$HOSTS_BACKUP" ]; then
current_hash=$(md5sum /system/etc/hosts 2> /dev/null | cut -d' ' -f1)
backup_hash=$(md5sum "$HOSTS_BACKUP" 2> /dev/null | cut -d' ' -f1)
if [ "$current_hash" != "$backup_hash" ]; then
log "Hosts file tampering detected! Restoring..."
cp "$HOSTS_BACKUP" "$MODDIR/system/etc/hosts"
log "Hosts file restored"
fi
fi
if [ "$current_hash" != "$backup_hash" ]; then
log "Hosts file tampering detected! Restoring..."
cp "$HOSTS_BACKUP" "$MODDIR/system/etc/hosts"
log "Hosts file restored"
fi
fi
}
# Function to uninstall blocked apps
check_blocked_apps() {
if [ ! -f "$BLOCKED_APPS_FILE" ]; then
return
fi
if [ ! -f "$BLOCKED_APPS_FILE" ]; then
return
fi
while IFS= read -r package || [ -n "$package" ]; do
# Skip comments and empty lines
case "$package" in
\#* | "") continue ;;
esac
while IFS= read -r package || [ -n "$package" ]; do
# Skip comments and empty lines
case "$package" in
\#* | "") continue ;;
esac
# Check if package is installed
if pm list packages 2>/dev/null | grep -q "package:$package"; then
log "Blocked app detected: $package - Uninstalling..."
pm uninstall "$package" 2>/dev/null && log "Uninstalled: $package" || log "Failed to uninstall: $package"
fi
done <"$BLOCKED_APPS_FILE"
# Check if package is installed
if pm list packages 2> /dev/null | grep -q "package:$package"; then
log "Blocked app detected: $package - Uninstalling..."
pm uninstall "$package" 2> /dev/null && log "Uninstalled: $package" || log "Failed to uninstall: $package"
fi
done < "$BLOCKED_APPS_FILE"
}
# Main monitoring loop - runs every 5 seconds for faster protection
while true; do
# ALWAYS protect module from UI disabling (even if guardian is "disabled" via ADB)
# This ensures only ADB can control the guardian
protect_module
# ALWAYS protect module from UI disabling (even if guardian is "disabled" via ADB)
# This ensures only ADB can control the guardian
protect_module
if is_enabled; then
protect_hosts
check_blocked_apps
fi
if is_enabled; then
protect_hosts
check_blocked_apps
fi
# Check every 5 seconds (faster response to disable attempts)
sleep 5
# Check every 5 seconds (faster response to disable attempts)
sleep 5
done &
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
if [ -f "$GUARDIAN_DIR/control" ]; then
status=$(cat "$GUARDIAN_DIR/control")
if [ "$status" != "DISABLED" ]; then
echo "Guardian is still enabled! Use ADB to disable first:"
echo " adb shell 'echo DISABLED > /data/adb/android_guardian/control'"
exit 1
fi
status=$(cat "$GUARDIAN_DIR/control")
if [ "$status" != "DISABLED" ]; then
echo "Guardian is still enabled! Use ADB to disable first:"
echo " adb shell 'echo DISABLED > /data/adb/android_guardian/control'"
exit 1
fi
fi
# 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")
usage() {
cat <<EOF
cat << EOF
Usage:
$(basename "$0") [OPTIONS] PATH
@ -47,193 +47,193 @@ EOF
}
ensure_ffmpeg() {
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "Error: 'ffmpeg' is not installed or not in PATH." >&2
exit 1
fi
if ! command -v ffmpeg > /dev/null 2>&1; then
echo "Error: 'ffmpeg' is not installed or not in PATH." >&2
exit 1
fi
}
get_video_extensions_except() {
local exclude="$1"
local exts=()
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ ${ext,,} != "${exclude,,}" ]]; then
exts+=("$ext")
fi
done
echo "${exts[@]}"
local exclude="$1"
local exts=()
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ ${ext,,} != "${exclude,,}" ]]; then
exts+=("$ext")
fi
done
echo "${exts[@]}"
}
is_video_file() {
local file="$1"
local ext="${file##*.}"
ext="${ext,,}" # lowercase
local file="$1"
local ext="${file##*.}"
ext="${ext,,}" # lowercase
for video_ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ $ext == "${video_ext,,}" ]]; then
return 0
fi
done
return 1
for video_ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ $ext == "${video_ext,,}" ]]; then
return 0
fi
done
return 1
}
convert_video() {
local input_file="$1"
local output_file="${input_file%.*}.${TARGET_FORMAT}"
local input_file="$1"
local output_file="${input_file%.*}.${TARGET_FORMAT}"
# Skip if output already exists
if [[ -f $output_file ]]; then
log "Skipping '$input_file': output '$output_file' already exists"
return 0
fi
# Skip if output already exists
if [[ -f $output_file ]]; then
log "Skipping '$input_file': output '$output_file' already exists"
return 0
fi
log "Converting '$input_file' -> '$output_file'"
log "Converting '$input_file' -> '$output_file'"
local ffmpeg_args=()
ffmpeg_args+=(-hide_banner -loglevel warning -i "$input_file")
local ffmpeg_args=()
ffmpeg_args+=(-hide_banner -loglevel warning -i "$input_file")
if [[ $TARGET_FORMAT == "mp4" ]]; then
# H.264 codec for video and AAC for audio (maximum compatibility)
ffmpeg_args+=(-c:v libx264 -crf "$CRF" -preset "$PRESET")
ffmpeg_args+=(-c:a aac -b:a 192k)
ffmpeg_args+=(-movflags +faststart)
elif [[ $TARGET_FORMAT == "webm" ]]; then
# VP9 codec for video and Opus for audio
ffmpeg_args+=(-c:v libvpx-vp9 -crf "$CRF" -b:v 0)
ffmpeg_args+=(-c:a libopus -b:a 128k)
fi
if [[ $TARGET_FORMAT == "mp4" ]]; then
# H.264 codec for video and AAC for audio (maximum compatibility)
ffmpeg_args+=(-c:v libx264 -crf "$CRF" -preset "$PRESET")
ffmpeg_args+=(-c:a aac -b:a 192k)
ffmpeg_args+=(-movflags +faststart)
elif [[ $TARGET_FORMAT == "webm" ]]; then
# VP9 codec for video and Opus for audio
ffmpeg_args+=(-c:v libvpx-vp9 -crf "$CRF" -b:v 0)
ffmpeg_args+=(-c:a libopus -b:a 128k)
fi
ffmpeg_args+=("$output_file")
ffmpeg_args+=("$output_file")
if ffmpeg "${ffmpeg_args[@]}"; then
log "Successfully converted '$input_file'"
if ffmpeg "${ffmpeg_args[@]}"; then
log "Successfully converted '$input_file'"
if [[ $DELETE_ORIGINAL == true ]]; then
log "Deleting original: '$input_file'"
rm "$input_file"
fi
else
log "Error converting '$input_file'"
[[ -f $output_file ]] && rm "$output_file"
return 1
fi
if [[ $DELETE_ORIGINAL == true ]]; then
log "Deleting original: '$input_file'"
rm "$input_file"
fi
else
log "Error converting '$input_file'"
[[ -f $output_file ]] && rm "$output_file"
return 1
fi
}
process_directory() {
local dir="$1"
local count=0
local failed=0
local dir="$1"
local count=0
local failed=0
log "Searching for video files in '$dir'..."
log "Searching for video files in '$dir'..."
# Build find command dynamically
local find_args=(-type f \()
local first=true
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ ${ext,,} != "${TARGET_FORMAT,,}" ]]; then
if [[ $first == true ]]; then
first=false
else
find_args+=(-o)
fi
find_args+=(-iname "*.$ext")
fi
done
find_args+=(\) -print0)
# Build find command dynamically
local find_args=(-type f \()
local first=true
for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do
if [[ ${ext,,} != "${TARGET_FORMAT,,}" ]]; then
if [[ $first == true ]]; then
first=false
else
find_args+=(-o)
fi
find_args+=(-iname "*.$ext")
fi
done
find_args+=(\) -print0)
while IFS= read -r -d '' file; do
((count++)) || true
if ! convert_video "$file"; then
((failed++)) || true
fi
done < <(find "$dir" "${find_args[@]}" 2>/dev/null)
while IFS= read -r -d '' file; do
((count++)) || true
if ! convert_video "$file"; then
((failed++)) || true
fi
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
log "No video files found in '$dir'"
fi
if [[ $count -eq 0 ]]; then
log "No video files found in '$dir'"
fi
}
parse_args() {
while getopts ":f:c:p:dh" opt; do
case "$opt" in
f)
TARGET_FORMAT="${OPTARG,,}"
if [[ $TARGET_FORMAT != "mp4" && $TARGET_FORMAT != "webm" ]]; then
echo "Error: Format must be 'mp4' or 'webm'" >&2
exit 1
fi
;;
c) CRF="$OPTARG" ;;
p) PRESET="$OPTARG" ;;
d) DELETE_ORIGINAL=true ;;
h)
usage
exit 0
;;
:)
echo "Error: Option -$OPTARG requires an argument." >&2
usage
exit 1
;;
\?)
echo "Error: Invalid option -$OPTARG" >&2
usage
exit 1
;;
esac
done
shift $((OPTIND - 1))
while getopts ":f:c:p:dh" opt; do
case "$opt" in
f)
TARGET_FORMAT="${OPTARG,,}"
if [[ $TARGET_FORMAT != "mp4" && $TARGET_FORMAT != "webm" ]]; then
echo "Error: Format must be 'mp4' or 'webm'" >&2
exit 1
fi
;;
c) CRF="$OPTARG" ;;
p) PRESET="$OPTARG" ;;
d) DELETE_ORIGINAL=true ;;
h)
usage
exit 0
;;
:)
echo "Error: Option -$OPTARG requires an argument." >&2
usage
exit 1
;;
\?)
echo "Error: Invalid option -$OPTARG" >&2
usage
exit 1
;;
esac
done
shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then
echo "Error: No path specified." >&2
usage
exit 1
fi
if [[ $# -lt 1 ]]; then
echo "Error: No path specified." >&2
usage
exit 1
fi
TARGET_PATH="$1"
TARGET_PATH="$1"
# Set default CRF based on format if not specified
if [[ -z $CRF ]]; then
if [[ $TARGET_FORMAT == "mp4" ]]; then
CRF=23
else
CRF=30
fi
fi
# Set default CRF based on format if not specified
if [[ -z $CRF ]]; then
if [[ $TARGET_FORMAT == "mp4" ]]; then
CRF=23
else
CRF=30
fi
fi
}
main() {
ensure_ffmpeg
parse_args "$@"
ensure_ffmpeg
parse_args "$@"
if [[ ! -e $TARGET_PATH ]]; then
echo "Error: Path '$TARGET_PATH' does not exist." >&2
exit 1
fi
if [[ ! -e $TARGET_PATH ]]; then
echo "Error: Path '$TARGET_PATH' does not exist." >&2
exit 1
fi
if [[ -f $TARGET_PATH ]]; then
# Single file
if [[ ${TARGET_PATH,,} == *."$TARGET_FORMAT" ]]; then
log "File '$TARGET_PATH' is already in $TARGET_FORMAT format, skipping."
exit 0
fi
if [[ -f $TARGET_PATH ]]; then
# Single file
if [[ ${TARGET_PATH,,} == *."$TARGET_FORMAT" ]]; then
log "File '$TARGET_PATH' is already in $TARGET_FORMAT format, skipping."
exit 0
fi
if is_video_file "$TARGET_PATH"; then
convert_video "$TARGET_PATH"
else
echo "Error: '$TARGET_PATH' is not a recognized video file." >&2
exit 1
fi
elif [[ -d $TARGET_PATH ]]; then
process_directory "$TARGET_PATH"
else
echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2
exit 1
fi
if is_video_file "$TARGET_PATH"; then
convert_video "$TARGET_PATH"
else
echo "Error: '$TARGET_PATH' is not a recognized video file." >&2
exit 1
fi
elif [[ -d $TARGET_PATH ]]; then
process_directory "$TARGET_PATH"
else
echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2
exit 1
fi
log "Done!"
log "Done!"
}
main "$@"

View File

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

View File

@ -10,10 +10,10 @@ set -euo pipefail
# Source common library if available
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
if [[ -f "$SCRIPT_DIR/../lib/common.sh" ]]; then
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
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
# Configuration
@ -22,9 +22,9 @@ SEARCH_ROOT="/"
TIMEOUT_SECONDS=30
# Ensure fd is installed
if ! command -v fd &>/dev/null; then
log "ERROR: 'fd' is not installed. Install with: sudo pacman -S fd"
exit 1
if ! command -v fd &> /dev/null; then
log "ERROR: 'fd' is not installed. Install with: sudo pacman -S fd"
exit 1
fi
# 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
# Exclude /proc, /sys, /dev, /run, /tmp, /var/cache, /var/tmp for speed
FOUND_FILES=$(timeout "$TIMEOUT_SECONDS" fd \
-e kdbx \
-u \
-a \
--exclude '/proc' \
--exclude '/sys' \
--exclude '/dev' \
--exclude '/run' \
--exclude '/tmp' \
--exclude '/var/cache' \
--exclude '/var/tmp' \
--exclude '/snap' \
--exclude '/.snapshots' \
--exclude '/lost+found' \
. "$SEARCH_ROOT" 2>/dev/null || true)
-e kdbx \
-u \
-a \
--exclude '/proc' \
--exclude '/sys' \
--exclude '/dev' \
--exclude '/run' \
--exclude '/tmp' \
--exclude '/var/cache' \
--exclude '/var/tmp' \
--exclude '/snap' \
--exclude '/.snapshots' \
--exclude '/lost+found' \
. "$SEARCH_ROOT" 2> /dev/null || true)
if [[ -z "$FOUND_FILES" ]]; then
log "No .kdbx files found."
exit 0
if [[ -z $FOUND_FILES ]]; then
log "No .kdbx files found."
exit 0
fi
# Count and display found files
FILE_COUNT=$(echo "$FOUND_FILES" | wc -l)
log "Found $FILE_COUNT .kdbx file(s):"
echo "$FOUND_FILES" | while read -r file; do
echo " - $file"
echo " - $file"
done
# Move files to destination
@ -74,51 +74,51 @@ MOVED_COUNT=0
SKIPPED_COUNT=0
while IFS= read -r src_file; do
[[ -z "$src_file" ]] && continue
[[ -z $src_file ]] && continue
# Skip if file is already in destination
if [[ "$(dirname "$src_file")" == "$DEST_DIR" ]]; then
log "Skipping (already in destination): $src_file"
((SKIPPED_COUNT++)) || true
continue
fi
# Skip if file is already in destination
if [[ "$(dirname "$src_file")" == "$DEST_DIR" ]]; then
log "Skipping (already in destination): $src_file"
((SKIPPED_COUNT++)) || true
continue
fi
# Get the base filename
base_name=$(basename "$src_file")
dest_file="$DEST_DIR/$base_name"
# Get the base filename
base_name=$(basename "$src_file")
dest_file="$DEST_DIR/$base_name"
# Handle filename conflicts by adding a number suffix
if [[ -f "$dest_file" ]]; then
# Check if it's the exact same file (by content)
if cmp -s "$src_file" "$dest_file"; then
log "Skipping (identical file exists): $src_file"
# Remove the duplicate source file
rm -v "$src_file"
((SKIPPED_COUNT++)) || true
continue
fi
# Handle filename conflicts by adding a number suffix
if [[ -f $dest_file ]]; then
# Check if it's the exact same file (by content)
if cmp -s "$src_file" "$dest_file"; then
log "Skipping (identical file exists): $src_file"
# Remove the duplicate source file
rm -v "$src_file"
((SKIPPED_COUNT++)) || true
continue
fi
# Different file with same name - add suffix
counter=1
name_without_ext="${base_name%.kdbx}"
while [[ -f "$dest_file" ]]; do
dest_file="$DEST_DIR/${name_without_ext} ($counter).kdbx"
((counter++))
done
log "Renaming to avoid conflict: $base_name -> $(basename "$dest_file")"
fi
# Different file with same name - add suffix
counter=1
name_without_ext="${base_name%.kdbx}"
while [[ -f $dest_file ]]; do
dest_file="$DEST_DIR/${name_without_ext} ($counter).kdbx"
((counter++))
done
log "Renaming to avoid conflict: $base_name -> $(basename "$dest_file")"
fi
# Move the file
if mv -v "$src_file" "$dest_file"; then
((MOVED_COUNT++)) || true
else
log "ERROR: Failed to move $src_file"
fi
done <<<"$FOUND_FILES"
# Move the file
if mv -v "$src_file" "$dest_file"; then
((MOVED_COUNT++)) || true
else
log "ERROR: Failed to move $src_file"
fi
done <<< "$FOUND_FILES"
log "Done! Moved $MOVED_COUNT file(s), skipped $SKIPPED_COUNT file(s)."
log "All KeePassXC databases are now in: $DEST_DIR"
# List final contents
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
usage() {
cat <<EOF
cat << EOF
Usage: $0 <input_image> [resolution] [output_image]
Arguments:
@ -31,7 +31,7 @@ Examples:
Note: Requires ImageMagick (convert command)
EOF
exit 1
exit 1
}
# Check if ImageMagick is installed
@ -39,8 +39,8 @@ require_imagemagick "convert" || exit 1
# Parse arguments
if [[ $# -lt 1 ]]; then
echo "Error: Missing required argument <input_image>"
usage
echo "Error: Missing required argument <input_image>"
usage
fi
INPUT_IMAGE="$1"
@ -49,20 +49,20 @@ OUTPUT_IMAGE="${3:-}"
# Validate input image exists
if [[ ! -f ${INPUT_IMAGE} ]]; then
echo "Error: Input image '${INPUT_IMAGE}' does not exist."
exit 1
echo "Error: Input image '${INPUT_IMAGE}' does not exist."
exit 1
fi
# Validate resolution format (WIDTHxHEIGHT)
if ! validate_resolution "$RESOLUTION"; then
echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1
echo "Error: Invalid resolution format '${RESOLUTION}'"
echo "Expected format: WIDTHxHEIGHT (e.g., 320x240, 1920x1080)"
exit 1
fi
# Generate output filename if not provided
if [[ -z ${OUTPUT_IMAGE} ]]; then
OUTPUT_IMAGE=$(generate_output_filename "${INPUT_IMAGE}" "_${RESOLUTION}")
OUTPUT_IMAGE=$(generate_output_filename "${INPUT_IMAGE}" "_${RESOLUTION}")
fi
# Perform the conversion
@ -70,15 +70,15 @@ echo "Converting '${INPUT_IMAGE}' to ${RESOLUTION}..."
echo "Output will be saved to: ${OUTPUT_IMAGE}"
if convert "${INPUT_IMAGE}" -resize "${RESOLUTION}!" "${OUTPUT_IMAGE}"; then
echo "✓ Successfully converted image to ${RESOLUTION}"
echo "Output: ${OUTPUT_IMAGE}"
echo "✓ Successfully converted image to ${RESOLUTION}"
echo "Output: ${OUTPUT_IMAGE}"
# Show file sizes
INPUT_SIZE=$(du -h "${INPUT_IMAGE}" | cut -f1)
OUTPUT_SIZE=$(du -h "${OUTPUT_IMAGE}" | cut -f1)
echo "Input size: ${INPUT_SIZE}"
echo "Output size: ${OUTPUT_SIZE}"
# Show file sizes
INPUT_SIZE=$(du -h "${INPUT_IMAGE}" | cut -f1)
OUTPUT_SIZE=$(du -h "${OUTPUT_IMAGE}" | cut -f1)
echo "Input size: ${INPUT_SIZE}"
echo "Output size: ${OUTPUT_SIZE}"
else
echo "✗ Error: Conversion failed"
exit 1
echo "✗ Error: Conversion failed"
exit 1
fi

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -45,37 +45,37 @@ NC='\033[0m'
# Helper Functions (all print to stderr to not interfere with return values)
#==============================================================================
print_header() {
echo -e "\n${BOLD}${CYAN}════════════════════════════════════════════════════════════${NC}" >&2
echo -e "${BOLD}${CYAN} $1${NC}" >&2
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════${NC}\n" >&2
echo -e "\n${BOLD}${CYAN}════════════════════════════════════════════════════════════${NC}" >&2
echo -e "${BOLD}${CYAN} $1${NC}" >&2
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════${NC}\n" >&2
}
print_step() {
echo -e "${BOLD}${BLUE}$1${NC}" >&2
echo -e "${BOLD}${BLUE}$1${NC}" >&2
}
print_success() {
echo -e "${GREEN}$1${NC}" >&2
echo -e "${GREEN}$1${NC}" >&2
}
print_error() {
echo -e "${RED}$1${NC}" >&2
echo -e "${RED}$1${NC}" >&2
}
print_info() {
echo -e "${YELLOW}$1${NC}" >&2
echo -e "${YELLOW}$1${NC}" >&2
}
cleanup() {
if [ -d "$WORK_DIR" ] && [ "$WORK_DIR" != "/" ]; then
rm -rf "$WORK_DIR"
fi
if [ -d "$WORK_DIR" ] && [ "$WORK_DIR" != "/" ]; then
rm -rf "$WORK_DIR"
fi
}
trap cleanup EXIT
usage() {
cat << EOF
cat << EOF
repo_to_study.sh - Generate study materials from any repository
USAGE:
@ -99,54 +99,54 @@ OUTPUT FILES:
analysis/ - Raw analysis data (imports, keywords, functions)
EOF
exit 0
exit 0
}
#==============================================================================
# Check Dependencies
#==============================================================================
check_dependencies() {
local missing=()
# Check for required scripts
if [ ! -x "$ANALYZE_SCRIPT" ]; then
missing+=("analyze_repo.sh not found at $ANALYZE_SCRIPT")
local missing=()
# Check for required scripts
if [ ! -x "$ANALYZE_SCRIPT" ]; then
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
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
done
if [ ${#missing[@]} -gt 0 ]; then
print_error "Missing dependencies:"
for dep in "${missing[@]}"; do
echo " - $dep"
done
if [ ${#missing[@]} -gt 0 ]; then
print_error "Missing dependencies:"
for dep in "${missing[@]}"; do
echo " - $dep"
done
exit 1
fi
exit 1
fi
}
#==============================================================================
# Ensure Offline Docs are Available
#==============================================================================
ensure_offline_docs() {
local docs_dir="$HOME/.local/share/offline-docs"
if [ ! -d "$docs_dir/python" ]; then
print_info "Offline docs not found. Setting up Python documentation..."
if [ -x "$SETUP_DOCS_SCRIPT" ]; then
"$SETUP_DOCS_SCRIPT" --python
else
print_info "Run setup_offline_docs.sh --all to enable offline documentation"
fi
local docs_dir="$HOME/.local/share/offline-docs"
if [ ! -d "$docs_dir/python" ]; then
print_info "Offline docs not found. Setting up Python documentation..."
if [ -x "$SETUP_DOCS_SCRIPT" ]; then
"$SETUP_DOCS_SCRIPT" --python
else
print_info "Run setup_offline_docs.sh --all to enable offline documentation"
fi
fi
}
# Global to store repo name for cloned repos
@ -156,209 +156,209 @@ REPO_NAME=""
# Get Repository
#==============================================================================
get_repo() {
local input="$1"
local repo_dir=""
# Check if it's a URL (git clone needed)
if [[ "$input" =~ ^https?:// ]] || [[ "$input" =~ ^git@ ]]; then
print_step "Cloning repository..."
# Extract repo name from URL
REPO_NAME=$(basename "$input" .git)
repo_dir="$WORK_DIR/$REPO_NAME"
mkdir -p "$WORK_DIR"
if git clone --depth 1 "$input" "$repo_dir" >&2 2>&1; then
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"
local input="$1"
local repo_dir=""
# Check if it's a URL (git clone needed)
if [[ $input =~ ^https?:// ]] || [[ $input =~ ^git@ ]]; then
print_step "Cloning repository..."
# Extract repo name from URL
REPO_NAME=$(basename "$input" .git)
repo_dir="$WORK_DIR/$REPO_NAME"
mkdir -p "$WORK_DIR"
if git clone --depth 1 "$input" "$repo_dir" >&2 2>&1; then
print_success "Cloned: $input"
else
print_error "Invalid input: '$input' is not a valid URL or directory"
exit 1
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
print_error "Invalid input: '$input' is not a valid URL or directory"
exit 1
fi
}
#==============================================================================
# Analyze Repository
#==============================================================================
analyze_repo() {
local repo_path="$1"
local repo_name="$REPO_NAME"
[ -z "$repo_name" ] && repo_name=$(basename "$repo_path")
print_step "Analyzing repository..."
# Run the analyzer (it outputs to stderr/stdout, results go to /tmp/repo_analysis/)
"$ANALYZE_SCRIPT" "$repo_path" >&2 || true
# Find the results directory
local results_dir="/tmp/repo_analysis/results_${repo_name}"
if [ ! -d "$results_dir" ]; then
# Try without prefix
results_dir="/tmp/repo_analysis/results"
fi
if [ ! -d "$results_dir" ] || [ ! -d "$results_dir/per_language" ]; then
print_error "Could not find analysis results at $results_dir"
exit 1
fi
print_success "Analysis complete: $results_dir"
echo "$results_dir"
local repo_path="$1"
local repo_name="$REPO_NAME"
[ -z "$repo_name" ] && repo_name=$(basename "$repo_path")
print_step "Analyzing repository..."
# Run the analyzer (it outputs to stderr/stdout, results go to /tmp/repo_analysis/)
"$ANALYZE_SCRIPT" "$repo_path" >&2 || true
# Find the results directory
local results_dir="/tmp/repo_analysis/results_${repo_name}"
if [ ! -d "$results_dir" ]; then
# Try without prefix
results_dir="/tmp/repo_analysis/results"
fi
if [ ! -d "$results_dir" ] || [ ! -d "$results_dir/per_language" ]; then
print_error "Could not find analysis results at $results_dir"
exit 1
fi
print_success "Analysis complete: $results_dir"
echo "$results_dir"
}
#==============================================================================
# Generate Study Materials
#==============================================================================
generate_materials() {
local analysis_dir="$1"
local output_dir="$2"
print_step "Generating study materials with offline documentation..."
# Run study materials generator
cd "$analysis_dir"
if "$STUDY_SCRIPT" . 2>/dev/null | grep -E "^(Created|✓|Files created)" | head -5; then
print_success "Study materials generated"
else
# Try anyway, might have succeeded
true
fi
# Create output directory and copy results
mkdir -p "$output_dir"
# Copy generated files
[ -f "documentation_links.md" ] && cp "documentation_links.md" "$output_dir/"
[ -f "anki_cards.txt" ] && cp "anki_cards.txt" "$output_dir/"
[ -f "llm_anki_prompt.md" ] && cp "llm_anki_prompt.md" "$output_dir/"
# Copy analysis data
mkdir -p "$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_keywords.txt" ] && cp "grep_keywords.txt" "$output_dir/analysis/"
[ -f "grep_function_calls.txt" ] && cp "grep_function_calls.txt" "$output_dir/analysis/"
print_success "Files saved to: $output_dir"
local analysis_dir="$1"
local output_dir="$2"
print_step "Generating study materials with offline documentation..."
# Run study materials generator
cd "$analysis_dir"
if "$STUDY_SCRIPT" . 2> /dev/null | grep -E "^(Created|✓|Files created)" | head -5; then
print_success "Study materials generated"
else
# Try anyway, might have succeeded
true
fi
# Create output directory and copy results
mkdir -p "$output_dir"
# Copy generated files
[ -f "documentation_links.md" ] && cp "documentation_links.md" "$output_dir/"
[ -f "anki_cards.txt" ] && cp "anki_cards.txt" "$output_dir/"
[ -f "llm_anki_prompt.md" ] && cp "llm_anki_prompt.md" "$output_dir/"
# Copy analysis data
mkdir -p "$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_keywords.txt" ] && cp "grep_keywords.txt" "$output_dir/analysis/"
[ -f "grep_function_calls.txt" ] && cp "grep_function_calls.txt" "$output_dir/analysis/"
print_success "Files saved to: $output_dir"
}
#==============================================================================
# Show Summary
#==============================================================================
show_summary() {
local output_dir="$1"
print_header "Study Materials Ready!"
echo -e "${BOLD}Output directory:${NC} $output_dir"
echo ""
echo -e "${BOLD}Generated files:${NC}"
if [ -f "$output_dir/documentation_links.md" ]; then
local doc_lines
doc_lines=$(wc -l < "$output_dir/documentation_links.md")
echo -e " 📚 ${GREEN}documentation_links.md${NC} ($doc_lines lines)"
echo " Contains links to OFFLINE documentation"
fi
if [ -f "$output_dir/anki_cards.txt" ]; then
local card_count
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 " Import to Anki: File → Import → Tab separated"
fi
if [ -f "$output_dir/llm_anki_prompt.md" ]; then
echo -e " 🤖 ${GREEN}llm_anki_prompt.md${NC}"
echo " Use with ChatGPT/Claude to generate more cards"
fi
if [ -d "$output_dir/analysis" ]; then
echo -e " 📊 ${GREEN}analysis/${NC}"
echo " Raw analysis data (imports, keywords, functions per language)"
fi
echo ""
echo -e "${BOLD}Quick preview of imports with offline docs:${NC}"
if [ -f "$output_dir/documentation_links.md" ]; then
grep -A20 "import/from" "$output_dir/documentation_links.md" 2>/dev/null | \
grep "^\| \`" | head -5 | \
sed 's/|/│/g'
fi
echo ""
echo -e "${BOLD}Next steps:${NC}"
echo " 1. Open documentation_links.md to browse offline docs"
echo " 2. Import anki_cards.txt into Anki for spaced repetition"
echo " 3. Use llm_anki_prompt.md to generate more targeted cards"
echo ""
echo -e "${CYAN}To view a doc:${NC} xdg-open 'file:///path/from/documentation_links.md'"
local output_dir="$1"
print_header "Study Materials Ready!"
echo -e "${BOLD}Output directory:${NC} $output_dir"
echo ""
echo -e "${BOLD}Generated files:${NC}"
if [ -f "$output_dir/documentation_links.md" ]; then
local doc_lines
doc_lines=$(wc -l < "$output_dir/documentation_links.md")
echo -e " 📚 ${GREEN}documentation_links.md${NC} ($doc_lines lines)"
echo " Contains links to OFFLINE documentation"
fi
if [ -f "$output_dir/anki_cards.txt" ]; then
local card_count
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 " Import to Anki: File → Import → Tab separated"
fi
if [ -f "$output_dir/llm_anki_prompt.md" ]; then
echo -e " 🤖 ${GREEN}llm_anki_prompt.md${NC}"
echo " Use with ChatGPT/Claude to generate more cards"
fi
if [ -d "$output_dir/analysis" ]; then
echo -e " 📊 ${GREEN}analysis/${NC}"
echo " Raw analysis data (imports, keywords, functions per language)"
fi
echo ""
echo -e "${BOLD}Quick preview of imports with offline docs:${NC}"
if [ -f "$output_dir/documentation_links.md" ]; then
grep -A20 "import/from" "$output_dir/documentation_links.md" 2> /dev/null |
grep "^\| \`" | head -5 |
sed 's/|/│/g'
fi
echo ""
echo -e "${BOLD}Next steps:${NC}"
echo " 1. Open documentation_links.md to browse offline docs"
echo " 2. Import anki_cards.txt into Anki for spaced repetition"
echo " 3. Use llm_anki_prompt.md to generate more targeted cards"
echo ""
echo -e "${CYAN}To view a doc:${NC} xdg-open 'file:///path/from/documentation_links.md'"
}
#==============================================================================
# Main
#==============================================================================
main() {
# Handle help
if [ $# -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
usage
fi
local input="$1"
local output_dir="${2:-}" # Will be set after we know repo name
print_header "Repo → Study Materials Pipeline"
# Setup
mkdir -p "$WORK_DIR"
check_dependencies
ensure_offline_docs
# Step 1: Get repository
print_header "Step 1/3: Getting Repository"
local repo_path
repo_path=$(get_repo "$input")
# Extract repo name from path (since get_repo runs in subshell, REPO_NAME is lost)
if [ -z "$REPO_NAME" ]; then
REPO_NAME=$(basename "$repo_path")
fi
# Set default output dir based on repo name
if [ -z "$output_dir" ]; then
output_dir="$STUDY_MATERIALS_BASE/$REPO_NAME"
elif [[ "$output_dir" != /* ]]; then
# Convert relative to absolute
output_dir="$(pwd)/$output_dir"
fi
echo -e "${BOLD}Input:${NC} $input" >&2
echo -e "${BOLD}Output:${NC} $output_dir" >&2
echo "" >&2
# Step 2: Analyze
print_header "Step 2/3: Analyzing Code"
local analysis_dir
analysis_dir=$(analyze_repo "$repo_path")
# Step 3: Generate materials
print_header "Step 3/3: Generating Study Materials"
generate_materials "$analysis_dir" "$output_dir"
# Show results
show_summary "$output_dir"
# Handle help
if [ $# -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
usage
fi
local input="$1"
local output_dir="${2:-}" # Will be set after we know repo name
print_header "Repo → Study Materials Pipeline"
# Setup
mkdir -p "$WORK_DIR"
check_dependencies
ensure_offline_docs
# Step 1: Get repository
print_header "Step 1/3: Getting Repository"
local repo_path
repo_path=$(get_repo "$input")
# Extract repo name from path (since get_repo runs in subshell, REPO_NAME is lost)
if [ -z "$REPO_NAME" ]; then
REPO_NAME=$(basename "$repo_path")
fi
# Set default output dir based on repo name
if [ -z "$output_dir" ]; then
output_dir="$STUDY_MATERIALS_BASE/$REPO_NAME"
elif [[ $output_dir != /* ]]; then
# Convert relative to absolute
output_dir="$(pwd)/$output_dir"
fi
echo -e "${BOLD}Input:${NC} $input" >&2
echo -e "${BOLD}Output:${NC} $output_dir" >&2
echo "" >&2
# Step 2: Analyze
print_header "Step 2/3: Analyzing Code"
local analysis_dir
analysis_dir=$(analyze_repo "$repo_path")
# Step 3: Generate materials
print_header "Step 3/3: Generating Study Materials"
generate_materials "$analysis_dir" "$output_dir"
# Show results
show_summary "$output_dir"
}
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 "$@"
install_adaway() {
print_header "Installing AdAway"
print_header "Installing AdAway"
local adaway_apk="$WORK_DIR/adaway.apk"
local adaway_url="https://github.com/AdAway/AdAway/releases/latest/download/AdAway.apk"
local adaway_apk="$WORK_DIR/adaway.apk"
local adaway_url="https://github.com/AdAway/AdAway/releases/latest/download/AdAway.apk"
if [[ ! -f $adaway_apk ]]; then
log "Downloading AdAway APK..."
curl -L -o "$adaway_apk" "$adaway_url" || die "Failed to download AdAway"
else
log "AdAway APK already downloaded"
fi
if [[ ! -f $adaway_apk ]]; then
log "Downloading AdAway APK..."
curl -L -o "$adaway_apk" "$adaway_url" || die "Failed to download AdAway"
else
log "AdAway APK already downloaded"
fi
log "Installing AdAway..."
if adb install -r "$adaway_apk" 2>&1 | grep -q "Success"; then
log "AdAway installed successfully"
else
warn "AdAway installation may have failed or already installed"
fi
log "Installing AdAway..."
if adb install -r "$adaway_apk" 2>&1 | grep -q "Success"; then
log "AdAway installed successfully"
else
warn "AdAway installation may have failed or already installed"
fi
}
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
adb shell "su -c 'mkdir -p /data/adb/modules/systemless_hosts/system/etc'" || die "Failed to create 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"
# Create module.prop
cat >"$WORK_DIR/module.prop" <<'EOF'
# Create module.prop
cat > "$WORK_DIR/module.prop" << 'EOF'
id=systemless_hosts
name=Systemless Hosts
version=1.0
@ -51,122 +51,122 @@ author=Custom
description=Custom hosts file from StevenBlack with extensions
EOF
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 'rm /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 'rm /sdcard/module.prop'"
log "Module structure created"
log "Module structure created"
}
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
if [[ -f /etc/hosts.stevenblack ]]; then
log "Using StevenBlack hosts cache..."
cp /etc/hosts.stevenblack "$hosts_file"
elif [[ -f /etc/hosts ]]; then
log "Using current /etc/hosts..."
cp /etc/hosts "$hosts_file"
else
die "No hosts file found"
fi
# Use the StevenBlack cache or generate from /etc/hosts
if [[ -f /etc/hosts.stevenblack ]]; then
log "Using StevenBlack hosts cache..."
cp /etc/hosts.stevenblack "$hosts_file"
elif [[ -f /etc/hosts ]]; then
log "Using current /etc/hosts..."
cp /etc/hosts "$hosts_file"
else
die "No hosts file found"
fi
# Show stats
local total_entries
total_entries=$(grep -c "^0\.0\.0\.0 " "$hosts_file" || echo 0)
log "Hosts file contains $total_entries blocked domains"
# Show stats
local total_entries
total_entries=$(grep -c "^0\.0\.0\.0 " "$hosts_file" || echo 0)
log "Hosts file contains $total_entries blocked domains"
log "Pushing hosts file to device..."
adb push "$hosts_file" /sdcard/hosts || die "Failed to push hosts file"
log "Pushing hosts file to device..."
adb push "$hosts_file" /sdcard/hosts || die "Failed to push hosts file"
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 'chmod 644 /data/adb/modules/systemless_hosts/system/etc/hosts'" || die "Failed to set permissions"
adb shell "su -c 'rm /sdcard/hosts'"
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 'chmod 644 /data/adb/modules/systemless_hosts/system/etc/hosts'" || die "Failed to set permissions"
adb shell "su -c 'rm /sdcard/hosts'"
log "Hosts file installed successfully"
log "Total blocked domains: $total_entries"
log "Hosts file installed successfully"
log "Total blocked domains: $total_entries"
}
enable_module() {
print_header "Enabling Systemless Hosts Module"
print_header "Enabling Systemless Hosts Module"
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/remove'" 2>/dev/null || true
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/remove'" 2> /dev/null || true
log "Module enabled"
log "Module enabled"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " REBOOT REQUIRED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
echo "The systemless hosts module requires a reboot to take effect."
echo
read -p "Reboot device now? [y/N]: " -n 1 -r
echo
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " REBOOT REQUIRED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
echo "The systemless hosts module requires a reboot to take effect."
echo
read -p "Reboot device now? [y/N]: " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
log "Rebooting device..."
adb reboot
log "Device rebooting. Wait for boot to complete."
else
warn "Remember to reboot manually for changes to take effect!"
fi
if [[ $REPLY =~ ^[Yy]$ ]]; then
log "Rebooting device..."
adb reboot
log "Device rebooting. Wait for boot to complete."
else
warn "Remember to reboot manually for changes to take effect!"
fi
}
verify_hosts() {
print_header "Verifying Hosts Installation"
print_header "Verifying Hosts Installation"
log "Waiting for device to boot..."
sleep 5
adb wait-for-device
sleep 10
log "Waiting for device to boot..."
sleep 5
adb wait-for-device
sleep 10
log "Checking if hosts file is active..."
local test_domain="doubleclick.net"
local result
result=$(adb shell "su -c 'cat /system/etc/hosts | grep -c $test_domain'" 2>/dev/null || echo "0")
log "Checking if hosts file is active..."
local test_domain="doubleclick.net"
local result
result=$(adb shell "su -c 'cat /system/etc/hosts | grep -c $test_domain'" 2> /dev/null || echo "0")
if [[ $result -gt 0 ]]; then
log "✓ Hosts file is active and blocking domains"
else
warn "Could not verify hosts file, but module should be installed"
fi
if [[ $result -gt 0 ]]; then
log "✓ Hosts file is active and blocking domains"
else
warn "Could not verify hosts file, but module should be installed"
fi
}
main() {
print_header "Android Ad Blocking Setup"
print_header "Android Ad Blocking Setup"
check_device
check_root
check_device
check_root
echo "This will:"
echo " 1. Install AdAway app (optional GUI management)"
echo " 2. Create systemless hosts module"
echo " 3. Push your custom hosts file (StevenBlack with extensions)"
echo " 4. Enable the module and reboot"
echo
read -p "Continue? [y/N]: " -n 1 -r
echo
echo "This will:"
echo " 1. Install AdAway app (optional GUI management)"
echo " 2. Create systemless hosts module"
echo " 3. Push your custom hosts file (StevenBlack with extensions)"
echo " 4. Enable the module and reboot"
echo
read -p "Continue? [y/N]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log "Cancelled by user"
exit 0
fi
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log "Cancelled by user"
exit 0
fi
install_adaway
setup_systemless_hosts
push_hosts_file
enable_module
install_adaway
setup_systemless_hosts
push_hosts_file
enable_module
log "Setup complete!"
log "After reboot, ads should be blocked system-wide"
log "You can manage hosts in the AdAway app or by updating the module"
log "Setup complete!"
log "After reboot, ads should be blocked system-wide"
log "You can manage hosts in the AdAway app or by updating the module"
}
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)
if [[ -n ${SUDO_USER:-} ]]; then
USER_NAME="$SUDO_USER"
USER_NAME="$SUDO_USER"
else
USER_NAME="$(whoami)"
USER_NAME="$(whoami)"
fi
# Function to log messages
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Check if organize script exists
if [[ ! -f $ORGANIZE_SCRIPT ]]; then
log "ERROR: organize_downloads.sh not found at $ORGANIZE_SCRIPT"
exit 1
log "ERROR: organize_downloads.sh not found at $ORGANIZE_SCRIPT"
exit 1
fi
# Check if running as root for systemd service creation
if [[ $EUID -ne 0 ]]; then
log "This script needs to be run as root to create systemd service."
log "Re-executing with sudo..."
exec sudo "$0" "$@"
log "This script needs to be run as root to create systemd service."
log "Re-executing with sudo..."
exec sudo "$0" "$@"
fi
log "Setting up media organizer startup service..."
# Create systemd service file
cat >"$SERVICE_FILE" <<EOF
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Media File Organizer
After=graphical-session.target

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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