From 18b9f020bb9e627bef358539eeb78de6d866e72c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:52:20 +0100 Subject: [PATCH] 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> --- .github/BRANCH_PROTECTION.md | 71 + .github/workflows/shell-check.yml | 7 + fresh-install/install_amd_driver.sh | 114 +- fresh-install/install_intel_driver.sh | 80 +- fresh-install/main.sh | 384 ++-- .../guard/pacman-hooks/hosts-guard-common.sh | 98 +- .../pacman-hooks/pacman-post-relock-hosts.sh | 2 +- .../pacman-hooks/pacman-pre-unlock-hosts.sh | 2 +- hosts/install.sh | 318 +-- .../i3blocks/shutdown_countdown.sh | 72 +- scripts/check_and_enable_services.sh | 850 ++++---- .../block_compulsive_opening.sh | 598 +++--- .../digital_wellbeing/install_leechblock.sh | 376 ++-- .../digital_wellbeing/music_parallelism.sh | 530 ++--- .../pacman/install_pacman_wrapper.sh | 32 +- .../pacman/pacman_wrapper.sh | 860 ++++---- .../setup_midnight_shutdown.sh | 1110 +++++------ .../setup_pc_startup_monitor.sh | 476 ++--- .../youtube-music-wrapper.sh | 6 +- scripts/features/install_unreal_mcp.sh | 490 ++--- scripts/features/install_unreal_mcp_kvick.sh | 106 +- scripts/features/raspberry_pi_flash_sd.sh | 810 ++++---- scripts/features/raspberry_pi_nextcloud.sh | 1476 +++++++------- scripts/features/setup_activitywatch.sh | 552 +++--- scripts/features/setup_nextcloud_raspberry.sh | 1754 ++++++++--------- scripts/fixes/fix_anki.sh | 292 +-- scripts/fixes/fix_systemctl.sh | 22 +- scripts/fixes/fix_thorium.sh | 474 ++--- scripts/fixes/fix_virtualbox.sh | 258 +-- scripts/fixes/fix_yay_aur_database.sh | 68 +- scripts/fixes/nvidia_troubleshoot.sh | 338 ++-- scripts/lib/android.sh | 46 +- scripts/lib/common.sh | 472 ++--- scripts/meta/shell_check.sh | 604 +++--- .../testsAndMisc-bash/fix_thorium_unity.sh | 156 +- scripts/misc/testsAndMisc-bash/fix_unity.sh | 386 ++-- .../testsAndMisc-bash/get_rnnoise_model.sh | 184 +- .../install_ffmpeg_with_arnndn.sh | 196 +- .../testsAndMisc-bash/install_unity_mcp.sh | 232 +-- .../misc/testsAndMisc-bash/libre_translate.sh | 680 +++---- scripts/misc/testsAndMisc-bash/transcribe.sh | 780 ++++---- scripts/setup_periodic_system.sh | 316 +-- scripts/setup_thorium_startup.sh | 378 ++-- .../bin/hosts-file-monitor.sh | 124 +- .../bin/shutdown-timer-monitor.sh | 152 +- scripts/utils/analyze_repo.sh | 897 ++++----- .../utils/android_guardian/post-fs-data.sh | 8 +- scripts/utils/android_guardian/service.sh | 94 +- scripts/utils/android_guardian/uninstall.sh | 12 +- scripts/utils/convert_video.sh | 300 +-- scripts/utils/download_exercism_bulk.sh | 52 +- scripts/utils/find_keepassxc.sh | 122 +- scripts/utils/generate_study_materials.sh | 1233 ++++++------ scripts/utils/image_to_resolution.sh | 38 +- scripts/utils/install_exercism.sh | 450 ++--- scripts/utils/install_offline_docs.sh | 328 +-- scripts/utils/install_plagiarism_tools.sh | 118 +- scripts/utils/lookup_docs.sh | 1614 +++++++-------- scripts/utils/pdf_to_image.sh | 110 +- scripts/utils/repo_to_study.sh | 442 ++--- scripts/utils/root_bl9000.sh | 1612 +++++++-------- scripts/utils/setup_android_adblock.sh | 212 +- scripts/utils/setup_media_organizer.sh | 18 +- scripts/utils/setup_offline_docs.sh | 926 ++++----- scripts/utils/sync_keepassxc.sh | 128 +- scripts/utils/toggle_window_manager.sh | 62 +- scripts/utils/txt_to_image.sh | 106 +- scripts/utils/update_android_hosts.sh | 820 ++++---- 68 files changed, 13558 insertions(+), 13476 deletions(-) create mode 100644 .github/BRANCH_PROTECTION.md diff --git a/.github/BRANCH_PROTECTION.md b/.github/BRANCH_PROTECTION.md new file mode 100644 index 0000000..04fb4cc --- /dev/null +++ b/.github/BRANCH_PROTECTION.md @@ -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. diff --git a/.github/workflows/shell-check.yml b/.github/workflows/shell-check.yml index 4f85582..0fb7877 100644 --- a/.github/workflows/shell-check.yml +++ b/.github/workflows/shell-check.yml @@ -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." diff --git a/fresh-install/install_amd_driver.sh b/fresh-install/install_amd_driver.sh index 083403d..76c5677 100755 --- a/fresh-install/install_amd_driver.sh +++ b/fresh-install/install_amd_driver.sh @@ -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 diff --git a/fresh-install/install_intel_driver.sh b/fresh-install/install_intel_driver.sh index 4dab607..e606754 100755 --- a/fresh-install/install_intel_driver.sh +++ b/fresh-install/install_intel_driver.sh @@ -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}" diff --git a/fresh-install/main.sh b/fresh-install/main.sh index 4132421..ba964ea 100755 --- a/fresh-install/main.sh +++ b/fresh-install/main.sh @@ -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 diff --git a/hosts/guard/pacman-hooks/hosts-guard-common.sh b/hosts/guard/pacman-hooks/hosts-guard-common.sh index 9872cc3..404d618 100644 --- a/hosts/guard/pacman-hooks/hosts-guard-common.sh +++ b/hosts/guard/pacman-hooks/hosts-guard-common.sh @@ -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 } diff --git a/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh b/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh index 9f6cee2..3817803 100644 --- a/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh +++ b/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh @@ -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 diff --git a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh index f2ad308..1530ef1 100644 --- a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh +++ b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh @@ -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)" diff --git a/hosts/install.sh b/hosts/install.sh index 1628e34..0d2d902 100755 --- a/hosts/install.sh +++ b/hosts/install.sh @@ -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 "" diff --git a/i3-configuration/i3blocks/shutdown_countdown.sh b/i3-configuration/i3blocks/shutdown_countdown.sh index ed57d57..9744595 100755 --- a/i3-configuration/i3blocks/shutdown_countdown.sh +++ b/i3-configuration/i3blocks/shutdown_countdown.sh @@ -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) diff --git a/scripts/check_and_enable_services.sh b/scripts/check_and_enable_services.sh index 19fe11d..dd75461 100755 --- a/scripts/check_and_enable_services.sh +++ b/scripts/check_and_enable_services.sh @@ -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 /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 /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 diff --git a/scripts/digital_wellbeing/block_compulsive_opening.sh b/scripts/digital_wellbeing/block_compulsive_opening.sh index 1475301..426658b 100755 --- a/scripts/digital_wellbeing/block_compulsive_opening.sh +++ b/scripts/digital_wellbeing/block_compulsive_opening.sh @@ -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_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 </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 </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" < "$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" < "$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 </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" < "$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" < "$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 diff --git a/scripts/digital_wellbeing/music_parallelism.sh b/scripts/digital_wellbeing/music_parallelism.sh index 8abc751..63323c8 100755 --- a/scripts/digital_wellbeing/music_parallelism.sh +++ b/scripts/digital_wellbeing/music_parallelism.sh @@ -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 diff --git a/scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh b/scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh index 0a400fe..406b3aa 100755 --- a/scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh +++ b/scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh @@ -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 diff --git a/scripts/digital_wellbeing/pacman/pacman_wrapper.sh b/scripts/digital_wellbeing/pacman/pacman_wrapper.sh index e7d6a47..0cca9e1 100755 --- a/scripts/digital_wellbeing/pacman/pacman_wrapper.sh +++ b/scripts/digital_wellbeing/pacman/pacman_wrapper.sh @@ -19,572 +19,572 @@ declare -a GREYLISTED_KEYWORDS_LIST=() POLICY_LISTS_LOADED=0 load_policy_lists() { - if [[ $POLICY_LISTS_LOADED -eq 1 ]]; then - return - fi + if [[ $POLICY_LISTS_LOADED -eq 1 ]]; then + return + fi - local script_dir - script_dir="$(dirname "$(readlink -f "$0")")" - local blocked_file="$script_dir/pacman_blocked_keywords.txt" - local whitelist_file="$script_dir/pacman_whitelist.txt" - local greylist_file="$script_dir/pacman_greylist.txt" + local script_dir + script_dir="$(dirname "$(readlink -f "$0")")" + local blocked_file="$script_dir/pacman_blocked_keywords.txt" + local whitelist_file="$script_dir/pacman_whitelist.txt" + local greylist_file="$script_dir/pacman_greylist.txt" - if [[ -f $blocked_file ]]; then - mapfile -t BLOCKED_KEYWORDS_LIST < <(sed 's/\r$//' "$blocked_file" | grep -Ev '^[[:space:]]*(#|$)' || true) - else - BLOCKED_KEYWORDS_LIST=() - echo -e "${YELLOW}Warning:${NC} Missing blocked keywords file at $blocked_file" >&2 - fi + if [[ -f $blocked_file ]]; then + mapfile -t BLOCKED_KEYWORDS_LIST < <(sed 's/\r$//' "$blocked_file" | grep -Ev '^[[:space:]]*(#|$)' || true) + else + BLOCKED_KEYWORDS_LIST=() + echo -e "${YELLOW}Warning:${NC} Missing blocked keywords file at $blocked_file" >&2 + fi - if [[ -f $whitelist_file ]]; then - mapfile -t WHITELISTED_NAMES_LIST < <(sed 's/\r$//' "$whitelist_file" | grep -Ev '^[[:space:]]*(#|$)' || true) - else - WHITELISTED_NAMES_LIST=() - fi + if [[ -f $whitelist_file ]]; then + mapfile -t WHITELISTED_NAMES_LIST < <(sed 's/\r$//' "$whitelist_file" | grep -Ev '^[[:space:]]*(#|$)' || true) + else + WHITELISTED_NAMES_LIST=() + fi - if [[ -f $greylist_file ]]; then - mapfile -t GREYLISTED_KEYWORDS_LIST < <(sed 's/\r$//' "$greylist_file" | grep -Ev '^[[:space:]]*(#|$)' || true) - else - GREYLISTED_KEYWORDS_LIST=() - fi + if [[ -f $greylist_file ]]; then + mapfile -t GREYLISTED_KEYWORDS_LIST < <(sed 's/\r$//' "$greylist_file" | grep -Ev '^[[:space:]]*(#|$)' || true) + else + GREYLISTED_KEYWORDS_LIST=() + fi - for i in "${!BLOCKED_KEYWORDS_LIST[@]}"; do - BLOCKED_KEYWORDS_LIST[i]="${BLOCKED_KEYWORDS_LIST[i],,}" - done + for i in "${!BLOCKED_KEYWORDS_LIST[@]}"; do + BLOCKED_KEYWORDS_LIST[i]="${BLOCKED_KEYWORDS_LIST[i],,}" + done - for i in "${!WHITELISTED_NAMES_LIST[@]}"; do - WHITELISTED_NAMES_LIST[i]="${WHITELISTED_NAMES_LIST[i],,}" - done + for i in "${!WHITELISTED_NAMES_LIST[@]}"; do + WHITELISTED_NAMES_LIST[i]="${WHITELISTED_NAMES_LIST[i],,}" + done - for i in "${!GREYLISTED_KEYWORDS_LIST[@]}"; do - GREYLISTED_KEYWORDS_LIST[i]="${GREYLISTED_KEYWORDS_LIST[i],,}" - done + for i in "${!GREYLISTED_KEYWORDS_LIST[@]}"; do + GREYLISTED_KEYWORDS_LIST[i]="${GREYLISTED_KEYWORDS_LIST[i],,}" + done - POLICY_LISTS_LOADED=1 + POLICY_LISTS_LOADED=1 } # Determine if this invocation may perform a transaction (upgrade/install/remove) needs_unlock() { - # If args include -S (install/upgrade), -U (local install), or -R (remove), we unlock - # Also include -Su/-Syu/-Syuu when -S is part of the combined flag - for arg in "$@"; do - case "$arg" in - -S* | -U | -R | --sync | --upgrade | --remove) - return 0 - ;; - esac - done - return 1 + # If args include -S (install/upgrade), -U (local install), or -R (remove), we unlock + # Also include -Su/-Syu/-Syuu when -S is part of the combined flag + for arg in "$@"; do + case "$arg" in + -S* | -U | -R | --sync | --upgrade | --remove) + return 0 + ;; + esac + done + return 1 } # Run pre/post hooks for /etc/hosts guard if present pre_unlock_hosts() { - local pre="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh" - if [[ -x $pre ]]; then - echo -e "${CYAN}[hosts-guard] Preparing /etc/hosts for transaction...${NC}" >&2 - /bin/bash "$pre" || true - fi + local pre="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh" + if [[ -x $pre ]]; then + echo -e "${CYAN}[hosts-guard] Preparing /etc/hosts for transaction...${NC}" >&2 + /bin/bash "$pre" || true + fi } post_relock_hosts() { - local post="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh" - if [[ -x $post ]]; then - /bin/bash "$post" || true - echo -e "${CYAN}[hosts-guard] Protections re-applied to /etc/hosts.${NC}" >&2 - fi + local post="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh" + if [[ -x $post ]]; then + /bin/bash "$post" || true + echo -e "${CYAN}[hosts-guard] Protections re-applied to /etc/hosts.${NC}" >&2 + fi } # Ensure periodic system services (timer/monitor) are set up; if not, trigger setup ensure_periodic_maintenance() { - # Only proceed if systemd/systemctl is available - if ! command -v systemctl >/dev/null 2>&1; then - return 0 - fi + # Only proceed if systemd/systemctl is available + if ! command -v systemctl > /dev/null 2>&1; then + return 0 + fi - local timer_unit="periodic-system-maintenance.timer" - local startup_unit="periodic-system-startup.service" - local monitor_unit="hosts-file-monitor.service" - local needs_setup=0 + local timer_unit="periodic-system-maintenance.timer" + local startup_unit="periodic-system-startup.service" + local monitor_unit="hosts-file-monitor.service" + local needs_setup=0 - # Timer should be enabled and active - systemctl --quiet is-enabled "$timer_unit" || needs_setup=1 - systemctl --quiet is-active "$timer_unit" || needs_setup=1 + # Timer should be enabled and active + systemctl --quiet is-enabled "$timer_unit" || needs_setup=1 + systemctl --quiet is-active "$timer_unit" || needs_setup=1 - # Monitor should be enabled and active - systemctl --quiet is-enabled "$monitor_unit" || needs_setup=1 - systemctl --quiet is-active "$monitor_unit" || needs_setup=1 + # Monitor should be enabled and active + systemctl --quiet is-enabled "$monitor_unit" || needs_setup=1 + systemctl --quiet is-active "$monitor_unit" || needs_setup=1 - # Startup service should be enabled (it’s oneshot and may not be active except at boot) - systemctl --quiet is-enabled "$startup_unit" || needs_setup=1 + # Startup service should be enabled (it’s oneshot and may not be active except at boot) + systemctl --quiet is-enabled "$startup_unit" || needs_setup=1 - if [[ $needs_setup -eq 0 ]]; then - return 0 - fi + if [[ $needs_setup -eq 0 ]]; then + return 0 + fi - echo -e "${YELLOW}Periodic maintenance services missing or inactive. Running setup...${NC}" >&2 + echo -e "${YELLOW}Periodic maintenance services missing or inactive. Running setup...${NC}" >&2 - # Try to locate setup_periodic_system.sh - local setup_script="" - local self_dir - self_dir="$(dirname "$(readlink -f "$0")")" - if [[ -f "$self_dir/setup_periodic_system.sh" ]]; then - setup_script="$self_dir/setup_periodic_system.sh" - elif [[ -f "$HOME/linux-configuration/scripts/setup_periodic_system.sh" ]]; then - setup_script="$HOME/linux-configuration/scripts/setup_periodic_system.sh" - fi + # Try to locate setup_periodic_system.sh + local setup_script="" + local self_dir + self_dir="$(dirname "$(readlink -f "$0")")" + if [[ -f "$self_dir/setup_periodic_system.sh" ]]; then + setup_script="$self_dir/setup_periodic_system.sh" + elif [[ -f "$HOME/linux-configuration/scripts/setup_periodic_system.sh" ]]; then + setup_script="$HOME/linux-configuration/scripts/setup_periodic_system.sh" + fi - if [[ -n $setup_script ]]; then - if [[ $EUID -ne 0 ]]; then - sudo bash "$setup_script" - else - bash "$setup_script" - fi - echo -e "${CYAN}Tip:${NC} To disable these later:" >&2 - echo " sudo systemctl disable periodic-system-maintenance.timer" >&2 - echo " sudo systemctl disable periodic-system-startup.service" >&2 - echo " sudo systemctl disable hosts-file-monitor.service" >&2 - else - echo -e "${RED}Could not locate setup_periodic_system.sh to configure services automatically.${NC}" >&2 - fi + if [[ -n $setup_script ]]; then + if [[ $EUID -ne 0 ]]; then + sudo bash "$setup_script" + else + bash "$setup_script" + fi + echo -e "${CYAN}Tip:${NC} To disable these later:" >&2 + echo " sudo systemctl disable periodic-system-maintenance.timer" >&2 + echo " sudo systemctl disable periodic-system-startup.service" >&2 + echo " sudo systemctl disable hosts-file-monitor.service" >&2 + else + echo -e "${RED}Could not locate setup_periodic_system.sh to configure services automatically.${NC}" >&2 + fi } # Function to display help function show_help() { - echo -e "${BOLD}Pacman Wrapper Help${NC}" - echo "This wrapper adds helpful features while preserving all pacman functionality." - echo "" - echo "Additional commands:" - echo " --help-wrapper Show this help message" + echo -e "${BOLD}Pacman Wrapper Help${NC}" + echo "This wrapper adds helpful features while preserving all pacman functionality." + echo "" + echo "Additional commands:" + echo " --help-wrapper Show this help message" } # Function to display a message before executing function display_operation() { - case "$1" in - -S | -Sy | -S\ *) - echo -e "${BLUE}Installing packages...${NC}" >&2 - ;; - -Syu | -Syyu) - echo -e "${BLUE}Updating system...${NC}" >&2 - ;; - -R | -Rs | -Rns | -R\ *) - echo -e "${YELLOW}Removing packages...${NC}" >&2 - ;; - -Ss | -Ss\ *) - echo -e "${CYAN}Searching for packages...${NC}" >&2 - ;; - -Q | -Qs | -Qi | -Ql | -Q\ *) - echo -e "${CYAN}Querying package database...${NC}" >&2 - ;; - -U | -U\ *) - echo -e "${BLUE}Installing local packages...${NC}" >&2 - ;; - -Scc) - echo -e "${YELLOW}Cleaning package cache...${NC}" >&2 - ;; - *) - echo -e "${CYAN}Executing pacman command...${NC}" >&2 - ;; - esac + case "$1" in + -S | -Sy | -S\ *) + echo -e "${BLUE}Installing packages...${NC}" >&2 + ;; + -Syu | -Syyu) + echo -e "${BLUE}Updating system...${NC}" >&2 + ;; + -R | -Rs | -Rns | -R\ *) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -Ss | -Ss\ *) + echo -e "${CYAN}Searching for packages...${NC}" >&2 + ;; + -Q | -Qs | -Qi | -Ql | -Q\ *) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -U | -U\ *) + echo -e "${BLUE}Installing local packages...${NC}" >&2 + ;; + -Scc) + echo -e "${YELLOW}Cleaning package cache...${NC}" >&2 + ;; + *) + echo -e "${CYAN}Executing pacman command...${NC}" >&2 + ;; + esac } # Helper: return 0 if the given package name is blocked by policy function is_blocked_package_name() { - load_policy_lists - local normalized="${1,,}" + load_policy_lists + local normalized="${1,,}" - for allowed in "${WHITELISTED_NAMES_LIST[@]}"; do - if [[ $normalized == "$allowed" ]]; then - return 1 - fi - done + for allowed in "${WHITELISTED_NAMES_LIST[@]}"; do + if [[ $normalized == "$allowed" ]]; then + return 1 + fi + done - for keyword in "${BLOCKED_KEYWORDS_LIST[@]}"; do - if [[ $normalized == *"$keyword"* ]]; then - return 0 - fi - done + for keyword in "${BLOCKED_KEYWORDS_LIST[@]}"; do + if [[ $normalized == *"$keyword"* ]]; then + return 0 + fi + done - return 1 + return 1 } # Helper: return 0 if the given package name is greylisted (challenge required) function is_greylisted_package_name() { - load_policy_lists - local normalized="${1,,}" + load_policy_lists + local normalized="${1,,}" - for keyword in "${GREYLISTED_KEYWORDS_LIST[@]}"; do - if [[ $normalized == *"$keyword"* ]]; then - return 0 - fi - done + for keyword in "${GREYLISTED_KEYWORDS_LIST[@]}"; do + if [[ $normalized == *"$keyword"* ]]; then + return 0 + fi + done - return 1 + return 1 } # Helper: detect if current invocation includes --noconfirm function has_noconfirm_flag() { - for arg in "$@"; do - if [[ $arg == "--noconfirm" ]]; then - return 0 - fi - done - return 1 + for arg in "$@"; do + if [[ $arg == "--noconfirm" ]]; then + return 0 + fi + done + return 1 } # Helper: get list of PIDs holding a lock file (excluding our own PID) # Populates the $holders array get_lock_holders() { - local lock_file="$1" - holders=() - if command -v fuser >/dev/null 2>&1; then - mapfile -t holders < <(fuser "$lock_file" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true) - elif command -v lsof >/dev/null 2>&1; then - mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null | grep -E '^[0-9]+$' || true) - fi - # Filter out our own PID - if [[ ${#holders[@]} -gt 0 ]]; then - local -a filtered=() - for pid in "${holders[@]}"; do - [[ $pid -eq $$ ]] && continue - filtered+=("$pid") - done - holders=("${filtered[@]}") - fi + local lock_file="$1" + holders=() + if command -v fuser > /dev/null 2>&1; then + mapfile -t holders < <(fuser "$lock_file" 2> /dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true) + elif command -v lsof > /dev/null 2>&1; then + mapfile -t holders < <(lsof -t "$lock_file" 2> /dev/null | grep -E '^[0-9]+$' || true) + fi + # Filter out our own PID + if [[ ${#holders[@]} -gt 0 ]]; then + local -a filtered=() + for pid in "${holders[@]}"; do + [[ $pid -eq $$ ]] && continue + filtered+=("$pid") + done + holders=("${filtered[@]}") + fi } # Handle stale pacman database lock if present and no package managers are running check_and_handle_db_lock() { - local lock_file="/var/lib/pacman/db.lck" - # Quick exit if no lock - if [[ ! -e $lock_file ]]; then - return 0 - fi + local lock_file="/var/lib/pacman/db.lck" + # Quick exit if no lock + if [[ ! -e $lock_file ]]; then + return 0 + fi - # Determine which processes actually have the lock open - local -a holders=() - get_lock_holders "$lock_file" + # Determine which processes actually have the lock open + local -a holders=() + get_lock_holders "$lock_file" - if [[ ${#holders[@]} -gt 0 ]]; then - local pac_holder=0 - local gui_holder=0 - for pid in "${holders[@]}"; do - local comm args lower - comm=$(ps -p "$pid" -o comm= 2>/dev/null || true) - args=$(ps -p "$pid" -o args= 2>/dev/null || true) - lower="${comm,,} ${args,,}" - if [[ $lower == *" pacman"* || $lower == pacman* || $lower == *"/pacman "* || $lower == *" pamac"* ]]; then - pac_holder=1 - elif [[ $lower == *packagekit* || $lower == *gnome-software* || $lower == *discover* ]]; then - gui_holder=1 - fi - done + if [[ ${#holders[@]} -gt 0 ]]; then + local pac_holder=0 + local gui_holder=0 + for pid in "${holders[@]}"; do + local comm args lower + comm=$(ps -p "$pid" -o comm= 2> /dev/null || true) + args=$(ps -p "$pid" -o args= 2> /dev/null || true) + lower="${comm,,} ${args,,}" + if [[ $lower == *" pacman"* || $lower == pacman* || $lower == *"/pacman "* || $lower == *" pamac"* ]]; then + pac_holder=1 + elif [[ $lower == *packagekit* || $lower == *gnome-software* || $lower == *discover* ]]; then + gui_holder=1 + fi + done - if [[ $pac_holder -eq 1 ]]; then - echo -e "${RED}Another pacman/pamac transaction is holding the database lock. Try again later.${NC}" >&2 - return 1 - fi + if [[ $pac_holder -eq 1 ]]; then + echo -e "${RED}Another pacman/pamac transaction is holding the database lock. Try again later.${NC}" >&2 + return 1 + fi - if [[ $gui_holder -eq 1 ]]; then - echo -e "${YELLOW}A background software updater is holding the pacman lock. Attempting to stop it...${NC}" >&2 - if command -v systemctl >/dev/null 2>&1; then - systemctl --quiet stop packagekit.service 2>/dev/null || true - systemctl --quiet stop packagekit 2>/dev/null || true - fi - pkill -x packagekitd 2>/dev/null || true - pkill -f gnome-software 2>/dev/null || true - pkill -f discover 2>/dev/null || true - sleep 1 + if [[ $gui_holder -eq 1 ]]; then + echo -e "${YELLOW}A background software updater is holding the pacman lock. Attempting to stop it...${NC}" >&2 + if command -v systemctl > /dev/null 2>&1; then + systemctl --quiet stop packagekit.service 2> /dev/null || true + systemctl --quiet stop packagekit 2> /dev/null || true + fi + pkill -x packagekitd 2> /dev/null || true + pkill -f gnome-software 2> /dev/null || true + pkill -f discover 2> /dev/null || true + sleep 1 - # Re-check holders - get_lock_holders "$lock_file" - if [[ ${#holders[@]} -gt 0 ]]; then - echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2 - return 1 - fi - fi - fi + # Re-check holders + get_lock_holders "$lock_file" + if [[ ${#holders[@]} -gt 0 ]]; then + echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2 + return 1 + fi + fi + fi - # Helper to remove a file with sudo if needed - remove_file_as_root() { - local f="$1" - if [[ $EUID -ne 0 ]]; then - sudo rm -f "$f" - else - rm -f "$f" - fi - } + # Helper to remove a file with sudo if needed + remove_file_as_root() { + local f="$1" + if [[ $EUID -ne 0 ]]; then + sudo rm -f "$f" + else + rm -f "$f" + fi + } - # Decide whether to remove the lock - local now epoch age - if epoch=$(stat -c %Y "$lock_file" 2>/dev/null); then - now=$(date +%s) - age=$((now - epoch)) - else - age=999999 - fi + # Decide whether to remove the lock + local now epoch age + if epoch=$(stat -c %Y "$lock_file" 2> /dev/null); then + now=$(date +%s) + age=$((now - epoch)) + else + age=999999 + fi - # Auto-remove in non-interactive mode (--noconfirm) or if the lock is older than 10 minutes - if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then - echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2 - remove_file_as_root "$lock_file" || return 1 - return 0 - fi + # Auto-remove in non-interactive mode (--noconfirm) or if the lock is older than 10 minutes + if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then + echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2 + remove_file_as_root "$lock_file" || return 1 + return 0 + fi - # Interactive prompt (15s timeout) - echo -e "${YELLOW}A pacman lock exists but no active pacman is running.${NC}" >&2 - echo -e "${CYAN}Lock path:${NC} $lock_file (age: ${age}s)" >&2 - read -r -t 15 -p $'Remove stale lock and continue? [y/N]: ' reply || reply="n" - if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then - remove_file_as_root "$lock_file" || return 1 - return 0 - fi - echo -e "${RED}Aborting due to existing pacman lock. Close other updaters and retry, or run with --noconfirm to auto-clear stale locks.${NC}" >&2 - return 1 + # Interactive prompt (15s timeout) + echo -e "${YELLOW}A pacman lock exists but no active pacman is running.${NC}" >&2 + echo -e "${CYAN}Lock path:${NC} $lock_file (age: ${age}s)" >&2 + read -r -t 15 -p $'Remove stale lock and continue? [y/N]: ' reply || reply="n" + if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then + remove_file_as_root "$lock_file" || return 1 + return 0 + fi + echo -e "${RED}Aborting due to existing pacman lock. Close other updaters and retry, or run with --noconfirm to auto-clear stale locks.${NC}" >&2 + return 1 } # Generic function to remove installed packages matching a filter # Args: check_function label_prefix function remove_installed_packages_matching() { - local check_function="$1" - local label="$2" + local check_function="$1" + local label="$2" - mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null) - local to_remove=() - for name in "${installed_names[@]}"; do - if "$check_function" "$name"; then - to_remove+=("$name") - fi - done + mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2> /dev/null) + local to_remove=() + for name in "${installed_names[@]}"; do + if "$check_function" "$name"; then + to_remove+=("$name") + fi + done - if [[ ${#to_remove[@]} -eq 0 ]]; then - return 0 - fi + if [[ ${#to_remove[@]} -eq 0 ]]; then + return 0 + fi - echo -e "${YELLOW}${label} cleanup:${NC} Removing packages: ${BOLD}${to_remove[*]}${NC}" >&2 - "$PACMAN_BIN" -Rns --noconfirm "${to_remove[@]}" - local rc=$? - if [[ $rc -ne 0 ]]; then - echo -e "${RED}${label} cleanup removal failed with exit code ${rc}.${NC}" >&2 - else - echo -e "${GREEN}${label} cleanup removal completed for: ${to_remove[*]}${NC}" >&2 - fi - return $rc + echo -e "${YELLOW}${label} cleanup:${NC} Removing packages: ${BOLD}${to_remove[*]}${NC}" >&2 + "$PACMAN_BIN" -Rns --noconfirm "${to_remove[@]}" + local rc=$? + if [[ $rc -ne 0 ]]; then + echo -e "${RED}${label} cleanup removal failed with exit code ${rc}.${NC}" >&2 + else + echo -e "${GREEN}${label} cleanup removal completed for: ${to_remove[*]}${NC}" >&2 + fi + return $rc } # Cleanup: remove any installed blocked packages function remove_installed_blocked_packages() { - remove_installed_packages_matching is_blocked_package_name "Policy" + remove_installed_packages_matching is_blocked_package_name "Policy" } # Cleanup: remove any installed greylisted packages function remove_installed_greylisted_packages() { - remove_installed_packages_matching is_greylisted_package_name "Greylist" + remove_installed_packages_matching is_greylisted_package_name "Greylist" } # Helper: Check if this is an install command and run a filter on each package name # Usage: check_install_for filter_func "$@" # Returns 0 if filter_func matches any package function check_install_for() { - local filter_func="$1" - shift - # Check if the command is an installation command - if [[ ${1:-} == "-S" || ${1:-} == "-Sy" || ${1:-} == "-Syu" || ${1:-} == "-Syyu" || ${1:-} == "-U" ]]; then - for arg in "$@"; do - # Strip repository prefix if present (like extra/ or community/) - local package_name="${arg##*/}" - if "$filter_func" "$package_name"; then - return 0 - fi - done - fi - return 1 + local filter_func="$1" + shift + # Check if the command is an installation command + if [[ ${1:-} == "-S" || ${1:-} == "-Sy" || ${1:-} == "-Syu" || ${1:-} == "-Syyu" || ${1:-} == "-U" ]]; then + for arg in "$@"; do + # Strip repository prefix if present (like extra/ or community/) + local package_name="${arg##*/}" + if "$filter_func" "$package_name"; then + return 0 + fi + done + fi + return 1 } # Function to check if user is trying to install packages that are always blocked function check_for_always_blocked() { - check_install_for is_blocked_package_name "$@" + check_install_for is_blocked_package_name "$@" } # Helper to check if a package name is steam function is_steam_package() { - [[ $1 == "steam" ]] + [[ $1 == "steam" ]] } # Function to check if user is trying to install steam (challenge-eligible package) function check_for_steam() { - check_install_for is_steam_package "$@" + check_install_for is_steam_package "$@" } # Function to check if current day is a weekday (after 4PM Friday until midnight Sunday) function is_weekday() { - local day_of_week - day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7) - local hour - hour=$(date +%H) # %H gives hour in 24-hour format (00-23) + local day_of_week + day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7) + local hour + hour=$(date +%H) # %H gives hour in 24-hour format (00-23) - # Monday through Thursday are always weekdays - if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then - return 0 # Is weekday - # Friday before 4PM is weekday, after 4PM is weekend - elif [[ $day_of_week -eq 5 ]]; then - if [[ $hour -lt 14 ]]; then - return 0 # Is weekday (Friday before 4PM) - else - return 1 # Is weekend (Friday after 4PM) - fi - # Saturday and Sunday are weekend - else - return 1 # Is weekend - fi + # Monday through Thursday are always weekdays + if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then + return 0 # Is weekday + # Friday before 4PM is weekday, after 4PM is weekend + elif [[ $day_of_week -eq 5 ]]; then + if [[ $hour -lt 14 ]]; then + return 0 # Is weekday (Friday before 4PM) + else + return 1 # Is weekend (Friday after 4PM) + fi + # Saturday and Sunday are weekend + else + return 1 # Is weekend + fi } # Unified word unscrambling challenge function # Args: challenge_name word_length words_count timeout_seconds initial_delay_max post_delay_min post_delay_range function run_word_challenge() { - local challenge_name="$1" - local word_length="$2" - local words_count="$3" - local timeout_seconds="$4" - local initial_delay_max="${5:-20}" - local post_delay_min="${6:-0}" - local post_delay_range="${7:-20}" + local challenge_name="$1" + local word_length="$2" + local words_count="$3" + local timeout_seconds="$4" + local initial_delay_max="${5:-20}" + local post_delay_min="${6:-0}" + local post_delay_range="${7:-20}" - echo -e "${YELLOW}${challenge_name} challenge will begin shortly...${NC}" + echo -e "${YELLOW}${challenge_name} challenge will begin shortly...${NC}" - # Initial delay - local sleep_duration=$((RANDOM % initial_delay_max)) - sleep "$sleep_duration" + # Initial delay + local sleep_duration=$((RANDOM % initial_delay_max)) + sleep "$sleep_duration" - # Load words file - local script_dir words_file - script_dir="$(dirname "$(readlink -f "$0")")" - words_file="$script_dir/words.txt" + # Load words file + local script_dir words_file + script_dir="$(dirname "$(readlink -f "$0")")" + words_file="$script_dir/words.txt" - if [[ ! -f $words_file ]]; then - echo -e "${RED}Error: words.txt file not found at $words_file${NC}" - return 1 - fi + if [[ ! -f $words_file ]]; then + echo -e "${RED}Error: words.txt file not found at $words_file${NC}" + return 1 + fi - echo -e "${CYAN}Challenge: Words with ${word_length} letters${NC}" + echo -e "${CYAN}Challenge: Words with ${word_length} letters${NC}" - # Load random words of specified length - local -a selected_words - mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count") + # Load random words of specified length + local -a selected_words + mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count") - if [[ ${#selected_words[@]} -lt $words_count ]]; then - echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}" - words_count=${#selected_words[@]} - if [[ $words_count -eq 0 ]]; then - echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}" - return 1 - fi - fi + if [[ ${#selected_words[@]} -lt $words_count ]]; then + echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}" + words_count=${#selected_words[@]} + if [[ $words_count -eq 0 ]]; then + echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}" + return 1 + fi + fi - # Convert to uppercase - for i in "${!selected_words[@]}"; do - selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]') - done + # Convert to uppercase + for i in "${!selected_words[@]}"; do + selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]') + done - echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}" + echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}" - # Display words in grid - for ((i = 0; i < words_count; i++)); do - printf "${BLUE}%-15s${NC}" "${selected_words[i]}" - if (((i + 1) % 4 == 0)); then - echo "" - fi - done + # Display words in grid + for ((i = 0; i < words_count; i++)); do + printf "${BLUE}%-15s${NC}" "${selected_words[i]}" + if (((i + 1) % 4 == 0)); then + echo "" + fi + done - # Select and scramble a word - local target_index target_word scrambled_word - target_index=$((RANDOM % words_count)) - target_word="${selected_words[target_index]}" - scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n') + # Select and scramble a word + local target_index target_word scrambled_word + target_index=$((RANDOM % words_count)) + target_word="${selected_words[target_index]}" + scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n') - if [[ $scrambled_word == "$target_word" ]]; then - scrambled_word=$(echo "$target_word" | rev) - fi + if [[ $scrambled_word == "$target_word" ]]; then + scrambled_word=$(echo "$target_word" | rev) + fi - echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}" - echo -e "${YELLOW}Unscramble the word to proceed (you have $timeout_seconds seconds):${NC}" + echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}" + echo -e "${YELLOW}Unscramble the word to proceed (you have $timeout_seconds seconds):${NC}" - # Timer display background process - ( - local start_time current_time elapsed remaining - start_time=$(date +%s) - while true; do - current_time=$(date +%s) - elapsed=$((current_time - start_time)) - remaining=$((timeout_seconds - elapsed)) - if [[ $remaining -le 0 ]]; then - echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} " - break - fi - echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} " - sleep 1 - done - ) & - local display_pid=$! + # Timer display background process + ( + local start_time current_time elapsed remaining + start_time=$(date +%s) + while true; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + remaining=$((timeout_seconds - elapsed)) + if [[ $remaining -le 0 ]]; then + echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} " + break + fi + echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} " + sleep 1 + done + ) & + local display_pid=$! - # Read input with timeout - local user_input read_status - read -t "$timeout_seconds" -r user_input - read_status=$? + # Read input with timeout + local user_input read_status + read -t "$timeout_seconds" -r user_input + read_status=$? - kill "$display_pid" 2>/dev/null - wait "$display_pid" 2>/dev/null - echo + kill "$display_pid" 2> /dev/null + wait "$display_pid" 2> /dev/null + echo - if [[ $read_status -ne 0 ]]; then - echo -e "${RED}Time's up! Challenge failed. The correct word was '$target_word'.${NC}" - return 1 - fi + if [[ $read_status -ne 0 ]]; then + echo -e "${RED}Time's up! Challenge failed. The correct word was '$target_word'.${NC}" + return 1 + fi - user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs) + user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs) - if [[ $user_input == "$target_word" ]]; then - echo -e "${GREEN}Correct! Proceeding with installation...${NC}" - local post_challenge_sleep=$((RANDOM % post_delay_range + post_delay_min)) - [[ $post_challenge_sleep -gt 0 ]] && sleep "$post_challenge_sleep" - return 0 - else - echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}" - return 1 - fi + if [[ $user_input == "$target_word" ]]; then + echo -e "${GREEN}Correct! Proceeding with installation...${NC}" + local post_challenge_sleep=$((RANDOM % post_delay_range + post_delay_min)) + [[ $post_challenge_sleep -gt 0 ]] && sleep "$post_challenge_sleep" + return 0 + else + echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}" + return 1 + fi } # Function to prompt for solving a word unscrambling challenge (only for steam) function prompt_for_steam_challenge() { - echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}" + echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}" - # Check if it's a weekday and block completely - if is_weekday; then - local day_name - day_name=$(date +%A) - echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}" - echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}" - return 1 - fi + # Check if it's a weekday and block completely + if is_weekday; then + local day_name + day_name=$(date +%A) + echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}" + echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}" + return 1 + fi - # word_length=5, words_count=160, timeout=60s, initial_delay=20, post_delay=0-20 - run_word_challenge "Weekend Steam" 5 160 60 20 0 20 + # word_length=5, words_count=160, timeout=60s, initial_delay=20, post_delay=0-20 + run_word_challenge "Weekend Steam" 5 160 60 20 0 20 } function check_for_greylisted() { - check_install_for is_greylisted_package_name "$@" + check_install_for is_greylisted_package_name "$@" } # Function to prompt for solving a word unscrambling challenge (for greylisted packages - always active) function prompt_for_greylist_challenge() { - echo -e "${YELLOW}WARNING: You are trying to install a greylisted package.${NC}" + echo -e "${YELLOW}WARNING: You are trying to install a greylisted package.${NC}" - # word_length=6, words_count=120, timeout=90s, initial_delay=30, post_delay=15-35 - run_word_challenge "Greylist" 6 120 90 30 15 20 + # word_length=6, words_count=120, timeout=90s, initial_delay=30, post_delay=15-35 + run_word_challenge "Greylist" 6 120 90 30 15 20 } # Check for wrapper-specific commands if [[ $1 == "--help-wrapper" ]]; then - show_help - exit 0 + show_help + exit 0 fi # Before any pacman action, ensure maintenance services exist @@ -592,25 +592,25 @@ ensure_periodic_maintenance # Check for always blocked packages first (highest priority) if check_for_always_blocked "$@"; then - echo -e "${RED}Installation BLOCKED: This package is permanently restricted and cannot be installed.${NC}" - echo -e "${RED}Package installation has been denied by system policy.${NC}" - # Regardless of the attempted action, enforce cleanup of any installed blocked packages - remove_installed_blocked_packages "$@" - exit 1 + echo -e "${RED}Installation BLOCKED: This package is permanently restricted and cannot be installed.${NC}" + echo -e "${RED}Package installation has been denied by system policy.${NC}" + # Regardless of the attempted action, enforce cleanup of any installed blocked packages + remove_installed_blocked_packages "$@" + exit 1 fi # Check for steam (challenge-eligible package) if check_for_steam "$@"; then - if ! prompt_for_steam_challenge; then - exit 1 - fi + if ! prompt_for_steam_challenge; then + exit 1 + fi fi # Check for greylisted packages (challenge-eligible) if check_for_greylisted "$@"; then - if ! prompt_for_greylist_challenge; then - exit 1 - fi + if ! prompt_for_greylist_challenge; then + exit 1 + fi fi # Display operation @@ -624,19 +624,19 @@ start_time=$(date +%s) # Execute the real pacman command (with /etc/hosts guard handling) if needs_unlock "$@"; then - pre_unlock_hosts + pre_unlock_hosts fi # Handle a possible stale DB lock before executing if ! check_and_handle_db_lock "$@"; then - exit 1 + exit 1 fi "$PACMAN_BIN" "$@" exit_code=$? if needs_unlock "$@"; then - post_relock_hosts + post_relock_hosts fi # Record end time for statistics @@ -645,9 +645,9 @@ duration=$((end_time - start_time)) # Display results if [ $exit_code -eq 0 ]; then - echo -e "${GREEN}Command completed successfully in ${duration}s.${NC}" >&2 + echo -e "${GREEN}Command completed successfully in ${duration}s.${NC}" >&2 else - echo -e "${RED}Command failed with exit code ${exit_code}.${NC}" >&2 + echo -e "${RED}Command failed with exit code ${exit_code}.${NC}" >&2 fi # After any operation, remove installed blocked packages as part of policy enforcement @@ -658,11 +658,11 @@ remove_installed_greylisted_packages "$@" # Display some helpful tips depending on the operation if [[ $1 == "-S" || $1 == "-S "* ]] && [ $exit_code -eq 0 ]; then - echo -e "${CYAN}Tip:${NC} You may need to log out or restart to use some newly installed software." + echo -e "${CYAN}Tip:${NC} You may need to log out or restart to use some newly installed software." fi if [[ $1 == "-Syu" || $1 == "-Syyu" ]] && [ $exit_code -eq 0 ]; then - echo -e "${CYAN}Tip:${NC} Consider restarting after major updates." + echo -e "${CYAN}Tip:${NC} Consider restarting after major updates." fi exit $exit_code diff --git a/scripts/digital_wellbeing/setup_midnight_shutdown.sh b/scripts/digital_wellbeing/setup_midnight_shutdown.sh index b11b053..86c1b51 100755 --- a/scripts/digital_wellbeing/setup_midnight_shutdown.sh +++ b/scripts/digital_wellbeing/setup_midnight_shutdown.sh @@ -31,119 +31,119 @@ CANONICAL_CONFIG="/usr/local/share/locked-shutdown-schedule.conf" # Check if trying to make schedule more lenient (later shutdown / earlier morning end) check_schedule_protection() { - # Skip check if no canonical config exists (first install) - if [[ ! -f "$CANONICAL_CONFIG" ]]; then - return 0 - fi + # Skip check if no canonical config exists (first install) + if [[ ! -f $CANONICAL_CONFIG ]]; then + return 0 + fi - # Load canonical values - local canonical_mon_wed canonical_thu_sun canonical_morning_end - # shellcheck source=/dev/null - source "$CANONICAL_CONFIG" 2>/dev/null || return 0 - canonical_mon_wed="${MON_WED_HOUR:-}" - canonical_thu_sun="${THU_SUN_HOUR:-}" - canonical_morning_end="${MORNING_END_HOUR:-}" + # Load canonical values + local canonical_mon_wed canonical_thu_sun canonical_morning_end + # shellcheck source=/dev/null + source "$CANONICAL_CONFIG" 2> /dev/null || return 0 + canonical_mon_wed="${MON_WED_HOUR:-}" + canonical_thu_sun="${THU_SUN_HOUR:-}" + canonical_morning_end="${MORNING_END_HOUR:-}" - # If canonical values are empty, skip check - if [[ -z "$canonical_mon_wed" ]] || [[ -z "$canonical_thu_sun" ]] || [[ -z "$canonical_morning_end" ]]; then - return 0 - fi + # If canonical values are empty, skip check + if [[ -z $canonical_mon_wed ]] || [[ -z $canonical_thu_sun ]] || [[ -z $canonical_morning_end ]]; then + return 0 + fi - local violations=() + local violations=() - # Check if Mon-Wed hour is being made LATER (more lenient) - if [[ $SCHEDULE_MON_WED_HOUR -gt $canonical_mon_wed ]]; then - violations+=("Mon-Wed shutdown: ${canonical_mon_wed}:00 → ${SCHEDULE_MON_WED_HOUR}:00 (later)") - fi + # Check if Mon-Wed hour is being made LATER (more lenient) + if [[ $SCHEDULE_MON_WED_HOUR -gt $canonical_mon_wed ]]; then + violations+=("Mon-Wed shutdown: ${canonical_mon_wed}:00 → ${SCHEDULE_MON_WED_HOUR}:00 (later)") + fi - # Check if Thu-Sun hour is being made LATER (more lenient) - if [[ $SCHEDULE_THU_SUN_HOUR -gt $canonical_thu_sun ]]; then - violations+=("Thu-Sun shutdown: ${canonical_thu_sun}:00 → ${SCHEDULE_THU_SUN_HOUR}:00 (later)") - fi + # Check if Thu-Sun hour is being made LATER (more lenient) + if [[ $SCHEDULE_THU_SUN_HOUR -gt $canonical_thu_sun ]]; then + violations+=("Thu-Sun shutdown: ${canonical_thu_sun}:00 → ${SCHEDULE_THU_SUN_HOUR}:00 (later)") + fi - # Check if morning end is being made EARLIER (more lenient - shorter shutdown window) - if [[ $SCHEDULE_MORNING_END_HOUR -lt $canonical_morning_end ]]; then - violations+=("Morning end: 0${canonical_morning_end}:00 → 0${SCHEDULE_MORNING_END_HOUR}:00 (earlier)") - fi + # Check if morning end is being made EARLIER (more lenient - shorter shutdown window) + if [[ $SCHEDULE_MORNING_END_HOUR -lt $canonical_morning_end ]]; then + violations+=("Morning end: 0${canonical_morning_end}:00 → 0${SCHEDULE_MORNING_END_HOUR}:00 (earlier)") + fi - if [[ ${#violations[@]} -gt 0 ]]; then - echo "" - echo "╔══════════════════════════════════════════════════════════════════╗" - echo "║ ❌ SCHEDULE MODIFICATION BLOCKED - CHEATING DETECTED! ❌ ║" - echo "╚══════════════════════════════════════════════════════════════════╝" - echo "" - echo "You modified the script to make the shutdown schedule MORE LENIENT:" - echo "" - for v in "${violations[@]}"; do - echo " • $v" - done - echo "" - echo "Current protected schedule:" - echo " Monday-Wednesday: ${canonical_mon_wed}:00 - 0${canonical_morning_end}:00" - echo " Thursday-Sunday: ${canonical_thu_sun}:00 - 0${canonical_morning_end}:00" - echo "" - echo "Nice try! But this is exactly the kind of late-night bargaining" - echo "that this protection is designed to prevent. 😉" - echo "" - echo "If you REALLY need to change the schedule, use the proper unlock:" - echo " sudo /usr/local/sbin/unlock-shutdown-schedule" - echo "" - echo "This requires waiting through a psychological delay to give you" - echo "time to reconsider whether you actually need more screen time." - echo "" - exit 1 - fi + if [[ ${#violations[@]} -gt 0 ]]; then + echo "" + echo "╔══════════════════════════════════════════════════════════════════╗" + echo "║ ❌ SCHEDULE MODIFICATION BLOCKED - CHEATING DETECTED! ❌ ║" + echo "╚══════════════════════════════════════════════════════════════════╝" + echo "" + echo "You modified the script to make the shutdown schedule MORE LENIENT:" + echo "" + for v in "${violations[@]}"; do + echo " • $v" + done + echo "" + echo "Current protected schedule:" + echo " Monday-Wednesday: ${canonical_mon_wed}:00 - 0${canonical_morning_end}:00" + echo " Thursday-Sunday: ${canonical_thu_sun}:00 - 0${canonical_morning_end}:00" + echo "" + echo "Nice try! But this is exactly the kind of late-night bargaining" + echo "that this protection is designed to prevent. 😉" + echo "" + echo "If you REALLY need to change the schedule, use the proper unlock:" + echo " sudo /usr/local/sbin/unlock-shutdown-schedule" + echo "" + echo "This requires waiting through a psychological delay to give you" + echo "time to reconsider whether you actually need more screen time." + echo "" + exit 1 + fi - # Making schedule STRICTER is always allowed - local stricter=() - if [[ $SCHEDULE_MON_WED_HOUR -lt $canonical_mon_wed ]]; then - stricter+=("Mon-Wed: ${canonical_mon_wed}:00 → ${SCHEDULE_MON_WED_HOUR}:00 (earlier)") - fi - if [[ $SCHEDULE_THU_SUN_HOUR -lt $canonical_thu_sun ]]; then - stricter+=("Thu-Sun: ${canonical_thu_sun}:00 → ${SCHEDULE_THU_SUN_HOUR}:00 (earlier)") - fi - if [[ $SCHEDULE_MORNING_END_HOUR -gt $canonical_morning_end ]]; then - stricter+=("Morning end: 0${canonical_morning_end}:00 → 0${SCHEDULE_MORNING_END_HOUR}:00 (later)") - fi + # Making schedule STRICTER is always allowed + local stricter=() + if [[ $SCHEDULE_MON_WED_HOUR -lt $canonical_mon_wed ]]; then + stricter+=("Mon-Wed: ${canonical_mon_wed}:00 → ${SCHEDULE_MON_WED_HOUR}:00 (earlier)") + fi + if [[ $SCHEDULE_THU_SUN_HOUR -lt $canonical_thu_sun ]]; then + stricter+=("Thu-Sun: ${canonical_thu_sun}:00 → ${SCHEDULE_THU_SUN_HOUR}:00 (earlier)") + fi + if [[ $SCHEDULE_MORNING_END_HOUR -gt $canonical_morning_end ]]; then + stricter+=("Morning end: 0${canonical_morning_end}:00 → 0${SCHEDULE_MORNING_END_HOUR}:00 (later)") + fi - if [[ ${#stricter[@]} -gt 0 ]]; then - echo "" - echo "ℹ️ Schedule is being made STRICTER (allowed without unlock):" - for s in "${stricter[@]}"; do - echo " • $s" - done - echo "" - fi + if [[ ${#stricter[@]} -gt 0 ]]; then + echo "" + echo "ℹ️ Schedule is being made STRICTER (allowed without unlock):" + for s in "${stricter[@]}"; do + echo " • $s" + done + echo "" + fi - return 0 + return 0 } # Function to show usage show_usage() { - echo "Day-Specific Auto-Shutdown Setup for Arch Linux" - echo "===============================================" - echo "Usage: $0 [enable|status]" - echo "" - echo "Commands:" - echo " enable - Set up automatic shutdown with day-specific windows (default)" - echo " status - Show current status" - echo "" - echo "Shutdown Schedule:" - echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" - echo " Thursday-Sunday: ${SCHEDULE_THU_SUN_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" - echo "" - echo "NOTE: There is no 'disable' option. This is intentional." - echo " The shutdown timer is protected by a monitor service." - echo "" + echo "Day-Specific Auto-Shutdown Setup for Arch Linux" + echo "===============================================" + echo "Usage: $0 [enable|status]" + echo "" + echo "Commands:" + echo " enable - Set up automatic shutdown with day-specific windows (default)" + echo " status - Show current status" + echo "" + echo "Shutdown Schedule:" + echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" + echo " Thursday-Sunday: ${SCHEDULE_THU_SUN_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" + echo "" + echo "NOTE: There is no 'disable' option. This is intentional." + echo " The shutdown timer is protected by a monitor service." + echo "" } # Function to check and request sudo privileges check_sudo() { - if [[ $EUID -ne 0 ]]; then - echo "This script requires sudo privileges to manage systemd services." - echo "Requesting sudo access..." - exec sudo "$0" "$@" - fi + if [[ $EUID -ne 0 ]]; then + echo "This script requires sudo privileges to manage systemd services." + echo "Requesting sudo access..." + exec sudo "$0" "$@" + fi } # Get the actual user (even when running with sudo) @@ -151,149 +151,149 @@ set_actual_user_vars # Function to show current status show_current_status() { - echo "Day-Specific Auto-Shutdown Status" - echo "=================================" - echo "Current Date: $(date)" - echo "User: $ACTUAL_USER" - echo "" + echo "Day-Specific Auto-Shutdown Status" + echo "=================================" + echo "Current Date: $(date)" + echo "User: $ACTUAL_USER" + echo "" - local timer_exists=false + local timer_exists=false - # Check if files exist - if [[ -f "/etc/systemd/system/day-specific-shutdown.timer" ]]; then - timer_exists=true - echo "✓ Timer file exists" - else - echo "✗ Timer file missing" - fi + # Check if files exist + if [[ -f "/etc/systemd/system/day-specific-shutdown.timer" ]]; then + timer_exists=true + echo "✓ Timer file exists" + else + echo "✗ Timer file missing" + fi - if [[ -f "/etc/systemd/system/day-specific-shutdown.service" ]]; then - echo "✓ Service file exists" - else - echo "✗ Service file missing" - fi + if [[ -f "/etc/systemd/system/day-specific-shutdown.service" ]]; then + echo "✓ Service file exists" + else + echo "✗ Service file missing" + fi - if [[ -f "/usr/local/bin/day-specific-shutdown-manager.sh" ]]; then - echo "✓ Management script exists" - else - echo "✗ Management script missing" - fi + if [[ -f "/usr/local/bin/day-specific-shutdown-manager.sh" ]]; then + echo "✓ Management script exists" + else + echo "✗ Management script missing" + fi - if [[ -f "/usr/local/bin/shutdown-timer-monitor.sh" ]]; then - echo "✓ Monitor script exists" - else - echo "✗ Monitor script missing" - fi + if [[ -f "/usr/local/bin/shutdown-timer-monitor.sh" ]]; then + echo "✓ Monitor script exists" + else + echo "✗ Monitor script missing" + fi - echo "" + echo "" - # Check systemd status - if $timer_exists; then - if systemctl is-enabled day-specific-shutdown.timer &>/dev/null; then - echo "✓ Timer is enabled" - if systemctl is-active day-specific-shutdown.timer &>/dev/null; then - echo "✓ Timer is active" - echo "" - echo "Next scheduled shutdown check:" - systemctl list-timers day-specific-shutdown.timer --no-pager 2>/dev/null | grep day-specific-shutdown || echo "Timer information not available" - else - echo "✗ Timer is not active" - fi - else - echo "✗ Timer is not enabled" - fi - else - echo "Status: NOT CONFIGURED" - fi + # Check systemd status + if $timer_exists; then + if systemctl is-enabled day-specific-shutdown.timer &> /dev/null; then + echo "✓ Timer is enabled" + if systemctl is-active day-specific-shutdown.timer &> /dev/null; then + echo "✓ Timer is active" + echo "" + echo "Next scheduled shutdown check:" + systemctl list-timers day-specific-shutdown.timer --no-pager 2> /dev/null | grep day-specific-shutdown || echo "Timer information not available" + else + echo "✗ Timer is not active" + fi + else + echo "✗ Timer is not enabled" + fi + else + echo "Status: NOT CONFIGURED" + fi - echo "" + echo "" - # Check monitor service status - echo "Monitor Service Status:" - if systemctl is-enabled shutdown-timer-monitor.service &>/dev/null; then - echo "✓ Monitor is enabled" - if systemctl is-active shutdown-timer-monitor.service &>/dev/null; then - echo "✓ Monitor is active (will re-enable timer if disabled)" - else - echo "✗ Monitor is not active" - fi - else - echo "✗ Monitor is not enabled" - fi + # Check monitor service status + echo "Monitor Service Status:" + if systemctl is-enabled shutdown-timer-monitor.service &> /dev/null; then + echo "✓ Monitor is enabled" + if systemctl is-active shutdown-timer-monitor.service &> /dev/null; then + echo "✓ Monitor is active (will re-enable timer if disabled)" + else + echo "✗ Monitor is not active" + fi + else + echo "✗ Monitor is not enabled" + fi - echo "" + echo "" - # Check config file protection status - echo "Config File Protection Status:" - local config_file="/etc/shutdown-schedule.conf" - local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" + # Check config file protection status + echo "Config File Protection Status:" + local config_file="/etc/shutdown-schedule.conf" + local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" - if [[ -f "$config_file" ]]; then - echo "✓ Config file exists" - # Check immutable attribute - if lsattr "$config_file" 2>/dev/null | grep -q '^....i'; then - echo "✓ Config file is immutable (chattr +i)" - else - echo "✗ Config file is NOT immutable" - fi - else - echo "✗ Config file missing" - fi + if [[ -f $config_file ]]; then + echo "✓ Config file exists" + # Check immutable attribute + if lsattr "$config_file" 2> /dev/null | grep -q '^....i'; then + echo "✓ Config file is immutable (chattr +i)" + else + echo "✗ Config file is NOT immutable" + fi + else + echo "✗ Config file missing" + fi - if [[ -f "$canonical_file" ]]; then - echo "✓ Canonical copy exists" - else - echo "✗ Canonical copy missing" - fi + if [[ -f $canonical_file ]]; then + echo "✓ Canonical copy exists" + else + echo "✗ Canonical copy missing" + fi - if systemctl is-enabled shutdown-schedule-guard.path &>/dev/null; then - echo "✓ Config path watcher is enabled" - if systemctl is-active shutdown-schedule-guard.path &>/dev/null; then - echo "✓ Config path watcher is active" - else - echo "✗ Config path watcher is not active" - fi - else - echo "✗ Config path watcher is not enabled" - fi + if systemctl is-enabled shutdown-schedule-guard.path &> /dev/null; then + echo "✓ Config path watcher is enabled" + if systemctl is-active shutdown-schedule-guard.path &> /dev/null; then + echo "✓ Config path watcher is active" + else + echo "✗ Config path watcher is not active" + fi + else + echo "✗ Config path watcher is not enabled" + fi - if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then - echo "✓ Unlock script exists" - else - echo "✗ Unlock script missing" - fi + if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then + echo "✓ Unlock script exists" + else + echo "✗ Unlock script missing" + fi - echo "" - echo "Shutdown Schedule:" - echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" - echo " Thursday-Sunday: ${SCHEDULE_THU_SUN_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" - echo "" - echo "NOTE: The shutdown timer is protected by a monitor service." - echo " If you try to disable the timer, it will be automatically re-enabled." - echo "" - echo "NOTE: The config file is protected by:" - echo " - Immutable attribute (chattr +i)" - echo " - Canonical copy that auto-restores on modification" - echo " - Path watcher service" - echo " To modify: sudo /usr/local/sbin/unlock-shutdown-schedule" - echo "" + echo "" + echo "Shutdown Schedule:" + echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" + echo " Thursday-Sunday: ${SCHEDULE_THU_SUN_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" + echo "" + echo "NOTE: The shutdown timer is protected by a monitor service." + echo " If you try to disable the timer, it will be automatically re-enabled." + echo "" + echo "NOTE: The config file is protected by:" + echo " - Immutable attribute (chattr +i)" + echo " - Canonical copy that auto-restores on modification" + echo " - Path watcher service" + echo " To modify: sudo /usr/local/sbin/unlock-shutdown-schedule" + echo "" } # Function to create shutdown schedule config file (shared with i3blocks countdown) # Also creates a canonical (protected) copy and sets immutable attribute create_shutdown_config() { - echo "" - echo "1. Creating Shutdown Schedule Config..." - echo "=======================================" + echo "" + echo "1. Creating Shutdown Schedule Config..." + echo "=======================================" - local config_file="/etc/shutdown-schedule.conf" - local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" + local config_file="/etc/shutdown-schedule.conf" + local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" - # Remove immutable attribute if it exists (to allow update) - chattr -i "$config_file" 2>/dev/null || true - chattr -i "$canonical_file" 2>/dev/null || true + # Remove immutable attribute if it exists (to allow update) + chattr -i "$config_file" 2> /dev/null || true + chattr -i "$canonical_file" 2> /dev/null || true - cat >"$config_file" < "$config_file" << EOF # Shutdown schedule configuration # This file is managed by setup_midnight_shutdown.sh # Used by: day-specific-shutdown-check.sh, shutdown_countdown.sh (i3blocks) @@ -319,32 +319,32 @@ THU_SUN_HOUR=${SCHEDULE_THU_SUN_HOUR} MORNING_END_HOUR=${SCHEDULE_MORNING_END_HOUR} EOF - chmod 644 "$config_file" - echo "✓ Created shutdown schedule config: $config_file" + chmod 644 "$config_file" + echo "✓ Created shutdown schedule config: $config_file" - # Create canonical (protected) copy - install -m 644 -D "$config_file" "$canonical_file" - echo "✓ Created canonical copy: $canonical_file" + # Create canonical (protected) copy + install -m 644 -D "$config_file" "$canonical_file" + echo "✓ Created canonical copy: $canonical_file" - # Set immutable attribute on both files - chattr +i "$config_file" || echo "⚠ Warning: Could not set immutable attribute on $config_file" - chattr +i "$canonical_file" || echo "⚠ Warning: Could not set immutable attribute on $canonical_file" - echo "✓ Set immutable attribute (chattr +i) on config files" + # Set immutable attribute on both files + chattr +i "$config_file" || echo "⚠ Warning: Could not set immutable attribute on $config_file" + chattr +i "$canonical_file" || echo "⚠ Warning: Could not set immutable attribute on $canonical_file" + echo "✓ Set immutable attribute (chattr +i) on config files" } # Function to create config guard (path watcher + enforcement + unlock script) create_config_guard() { - echo "" - echo "2. Creating Config Guard (Path Watcher + Enforcement)..." - echo "========================================================" + echo "" + echo "2. Creating Config Guard (Path Watcher + Enforcement)..." + echo "========================================================" - local enforce_script="/usr/local/sbin/enforce-shutdown-schedule.sh" - local unlock_script="/usr/local/sbin/unlock-shutdown-schedule" - local guard_service="/etc/systemd/system/shutdown-schedule-guard.service" - local guard_path="/etc/systemd/system/shutdown-schedule-guard.path" + local enforce_script="/usr/local/sbin/enforce-shutdown-schedule.sh" + local unlock_script="/usr/local/sbin/unlock-shutdown-schedule" + local guard_service="/etc/systemd/system/shutdown-schedule-guard.service" + local guard_path="/etc/systemd/system/shutdown-schedule-guard.path" - # Create enforcement script - cat >"$enforce_script" <<'EOF' + # Create enforcement script + cat > "$enforce_script" << 'EOF' #!/bin/bash # Enforce canonical /etc/shutdown-schedule.conf contents # This script restores the config from canonical copy if tampered @@ -382,11 +382,11 @@ chattr +i "$TARGET" || log "Failed to set immutable attribute" log "Enforcement complete" EOF - chmod +x "$enforce_script" - echo "✓ Created enforcement script: $enforce_script" + chmod +x "$enforce_script" + echo "✓ Created enforcement script: $enforce_script" - # Create unlock script with psychological delay - cat >"$unlock_script" <<'EOF' + # Create unlock script with psychological delay + cat > "$unlock_script" << 'EOF' #!/bin/bash # Unlock shutdown schedule config for editing with smart friction # This script: @@ -598,11 +598,11 @@ echo " Thursday-Sunday: ${THU_SUN_HOUR:-??}:00 - 0${MORNING_END_HOUR:-?}:00" echo "" EOF - chmod +x "$unlock_script" - echo "✓ Created unlock script: $unlock_script" + chmod +x "$unlock_script" + echo "✓ Created unlock script: $unlock_script" - # Create path watcher unit - cat >"$guard_path" <<'EOF' + # Create path watcher unit + cat > "$guard_path" << 'EOF' [Unit] Description=Watch /etc/shutdown-schedule.conf and trigger enforcement @@ -614,10 +614,10 @@ Unit=shutdown-schedule-guard.service WantedBy=multi-user.target EOF - echo "✓ Created path watcher: $guard_path" + echo "✓ Created path watcher: $guard_path" - # Create enforcement service - cat >"$guard_service" <<'EOF' + # Create enforcement service + cat > "$guard_service" << 'EOF' [Unit] Description=Enforce canonical /etc/shutdown-schedule.conf contents After=local-fs.target @@ -632,27 +632,27 @@ IOSchedulingClass=idle WantedBy=multi-user.target EOF - echo "✓ Created guard service: $guard_service" + echo "✓ Created guard service: $guard_service" - # Reload and enable - systemctl daemon-reload - systemctl enable --now shutdown-schedule-guard.path - echo "✓ Enabled and started shutdown-schedule-guard.path" + # Reload and enable + systemctl daemon-reload + systemctl enable --now shutdown-schedule-guard.path + echo "✓ Enabled and started shutdown-schedule-guard.path" - # Run initial enforcement - "$enforce_script" || echo "⚠ Warning: Initial enforcement returned non-zero" - echo "✓ Ran initial enforcement" + # Run initial enforcement + "$enforce_script" || echo "⚠ Warning: Initial enforcement returned non-zero" + echo "✓ Ran initial enforcement" } # Function to create the shutdown service create_shutdown_service() { - echo "" - echo "3. Creating Systemd Shutdown Service..." - echo "======================================" + echo "" + echo "3. Creating Systemd Shutdown Service..." + echo "======================================" - local service_file="/etc/systemd/system/day-specific-shutdown.service" + local service_file="/etc/systemd/system/day-specific-shutdown.service" - cat >"$service_file" <<'EOF' + cat > "$service_file" << 'EOF' [Unit] Description=Automatic PC shutdown with day-specific time windows DefaultDependencies=false @@ -666,48 +666,48 @@ StandardOutput=journal StandardError=journal EOF - echo "✓ Created systemd service: $service_file" + echo "✓ Created systemd service: $service_file" } # Function to create the shutdown timer create_shutdown_timer() { - echo "" - echo "4. Creating Systemd Shutdown Timer..." - echo "===================================" + echo "" + echo "4. Creating Systemd Shutdown Timer..." + echo "===================================" - local timer_file="/etc/systemd/system/day-specific-shutdown.timer" + local timer_file="/etc/systemd/system/day-specific-shutdown.timer" - # Calculate earliest shutdown hour (minimum of MON_WED and THU_SUN) - local earliest_hour=$SCHEDULE_MON_WED_HOUR - if [[ $SCHEDULE_THU_SUN_HOUR -lt $earliest_hour ]]; then - earliest_hour=$SCHEDULE_THU_SUN_HOUR - fi + # Calculate earliest shutdown hour (minimum of MON_WED and THU_SUN) + local earliest_hour=$SCHEDULE_MON_WED_HOUR + if [[ $SCHEDULE_THU_SUN_HOUR -lt $earliest_hour ]]; then + earliest_hour=$SCHEDULE_THU_SUN_HOUR + fi - # Generate timer entries dynamically from earliest_hour to MORNING_END_HOUR - # This ensures timer fires at all possible shutdown times - { - cat <"$timer_file" + } > "$timer_file" - echo "✓ Created systemd timer: $timer_file" - echo " Timer covers: ${earliest_hour}:00 to 0${SCHEDULE_MORNING_END_HOUR}:00" + echo "✓ Created systemd timer: $timer_file" + echo " Timer covers: ${earliest_hour}:00 to 0${SCHEDULE_MORNING_END_HOUR}:00" } # Function to create management script create_management_script() { - echo "" - echo "5. Creating Management Script..." - echo "==============================" + echo "" + echo "5. Creating Management Script..." + echo "==============================" - local script_file="/usr/local/bin/day-specific-shutdown-manager.sh" + local script_file="/usr/local/bin/day-specific-shutdown-manager.sh" - cat >"$script_file" <<'EOF' + cat > "$script_file" << 'EOF' #!/bin/bash # Day-Specific Auto-Shutdown Manager # Provides easy management of the day-specific shutdown feature @@ -810,19 +810,19 @@ 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 create smart shutdown check script create_shutdown_check_script() { - echo "" - echo "6. Creating Smart Shutdown Check Script..." - echo "========================================" + echo "" + echo "6. Creating Smart Shutdown Check Script..." + echo "========================================" - local check_script="/usr/local/bin/day-specific-shutdown-check.sh" + local check_script="/usr/local/bin/day-specific-shutdown-check.sh" - cat >"$check_script" <<'EOF' + cat > "$check_script" << 'EOF' #!/bin/bash # Smart day-specific shutdown check script # Reads shutdown windows from /etc/shutdown-schedule.conf @@ -902,42 +902,42 @@ else fi EOF - chmod +x "$check_script" - echo "✓ Created smart shutdown check script: $check_script" + chmod +x "$check_script" + echo "✓ Created smart shutdown check script: $check_script" } # Function to enable the timer enable_timer() { - echo "" - echo "5. Enabling Shutdown Timer..." - echo "============================" + echo "" + echo "5. Enabling Shutdown Timer..." + echo "============================" - # Reload systemd daemon - systemctl daemon-reload - echo "✓ Reloaded systemd daemon" + # Reload systemd daemon + systemctl daemon-reload + echo "✓ Reloaded systemd daemon" - # Enable the timer - systemctl enable day-specific-shutdown.timer - echo "✓ Enabled day-specific-shutdown timer" + # Enable the timer + systemctl enable day-specific-shutdown.timer + echo "✓ Enabled day-specific-shutdown timer" - # Start the timer - systemctl start day-specific-shutdown.timer - echo "✓ Started day-specific-shutdown timer" + # Start the timer + systemctl start day-specific-shutdown.timer + echo "✓ Started day-specific-shutdown timer" } # Function to install the monitor service install_monitor_service() { - echo "" - echo "7. Installing Shutdown Timer Monitor Service..." - echo "==============================================" + echo "" + echo "7. Installing Shutdown Timer Monitor Service..." + echo "==============================================" - local monitor_script="/usr/local/bin/shutdown-timer-monitor.sh" - local monitor_service="/etc/systemd/system/shutdown-timer-monitor.service" - local monitor_timer="/etc/systemd/system/shutdown-timer-monitor-watchdog.timer" - local monitor_watchdog_service="/etc/systemd/system/shutdown-timer-monitor-watchdog.service" + local monitor_script="/usr/local/bin/shutdown-timer-monitor.sh" + local monitor_service="/etc/systemd/system/shutdown-timer-monitor.service" + local monitor_timer="/etc/systemd/system/shutdown-timer-monitor-watchdog.timer" + local monitor_watchdog_service="/etc/systemd/system/shutdown-timer-monitor-watchdog.service" - # Create the monitor script - cat >"$monitor_script" <<'EOF' + # Create the monitor script + cat > "$monitor_script" << 'EOF' #!/bin/bash # Shutdown timer monitor script # Watches the day-specific-shutdown timer and re-enables it if disabled @@ -1015,11 +1015,11 @@ while true; do done EOF - chmod +x "$monitor_script" - echo "✓ Created monitor script: $monitor_script" + chmod +x "$monitor_script" + echo "✓ Created monitor script: $monitor_script" - # Create the monitor service with RefuseManualStop to prevent manual stopping - cat >"$monitor_service" <<'EOF' + # Create the monitor service with RefuseManualStop to prevent manual stopping + cat > "$monitor_service" << 'EOF' [Unit] Description=Shutdown Timer Monitor and Auto-Restore Service After=network-online.target day-specific-shutdown.timer @@ -1048,10 +1048,10 @@ CPUQuota=10% WantedBy=multi-user.target EOF - echo "✓ Created monitor service: $monitor_service" + echo "✓ Created monitor service: $monitor_service" - # Create a watchdog timer that ensures the monitor stays running - cat >"$monitor_watchdog_service" <<'EOF' + # Create a watchdog timer that ensures the monitor stays running + cat > "$monitor_watchdog_service" << 'EOF' [Unit] Description=Watchdog for Shutdown Timer Monitor After=multi-user.target @@ -1062,9 +1062,9 @@ ExecStart=/bin/bash -c 'systemctl is-active shutdown-timer-monitor.service || sy ExecStart=/bin/bash -c 'systemctl is-active day-specific-shutdown.timer || systemctl start day-specific-shutdown.timer' EOF - echo "✓ Created watchdog service: $monitor_watchdog_service" + echo "✓ Created watchdog service: $monitor_watchdog_service" - cat >"$monitor_timer" <<'EOF' + cat > "$monitor_timer" << 'EOF' [Unit] Description=Watchdog Timer for Shutdown Timer Monitor After=multi-user.target @@ -1078,281 +1078,281 @@ Persistent=true WantedBy=timers.target EOF - echo "✓ Created watchdog timer: $monitor_timer" + echo "✓ Created watchdog timer: $monitor_timer" - # Reload and enable everything - systemctl daemon-reload - systemctl enable shutdown-timer-monitor.service - systemctl enable shutdown-timer-monitor-watchdog.timer - systemctl start shutdown-timer-monitor.service - systemctl start shutdown-timer-monitor-watchdog.timer - echo "✓ Enabled and started shutdown-timer-monitor.service" - echo "✓ Enabled and started shutdown-timer-monitor-watchdog.timer" + # Reload and enable everything + systemctl daemon-reload + systemctl enable shutdown-timer-monitor.service + systemctl enable shutdown-timer-monitor-watchdog.timer + systemctl start shutdown-timer-monitor.service + systemctl start shutdown-timer-monitor-watchdog.timer + echo "✓ Enabled and started shutdown-timer-monitor.service" + echo "✓ Enabled and started shutdown-timer-monitor-watchdog.timer" } # Function to test the setup test_setup() { - echo "" - echo "8. Testing Setup..." - echo "==================" + echo "" + echo "8. Testing Setup..." + echo "==================" - echo "Service files:" - if [[ -f "/etc/systemd/system/day-specific-shutdown.service" ]]; then - echo "✓ Service file exists" - else - echo "✗ Service file missing" - fi + echo "Service files:" + if [[ -f "/etc/systemd/system/day-specific-shutdown.service" ]]; then + echo "✓ Service file exists" + else + echo "✗ Service file missing" + fi - if [[ -f "/etc/systemd/system/day-specific-shutdown.timer" ]]; then - echo "✓ Timer file exists" - else - echo "✗ Timer file missing" - fi + if [[ -f "/etc/systemd/system/day-specific-shutdown.timer" ]]; then + echo "✓ Timer file exists" + else + echo "✗ Timer file missing" + fi - if [[ -f "/etc/systemd/system/shutdown-timer-monitor.service" ]]; then - echo "✓ Monitor service file exists" - else - echo "✗ Monitor service file missing" - fi + if [[ -f "/etc/systemd/system/shutdown-timer-monitor.service" ]]; then + echo "✓ Monitor service file exists" + else + echo "✗ Monitor service file missing" + fi - echo "" - echo "Timer status:" - if systemctl is-enabled day-specific-shutdown.timer &>/dev/null; then - echo "✓ Timer is enabled" - else - echo "✗ Timer is not enabled" - fi + echo "" + echo "Timer status:" + if systemctl is-enabled day-specific-shutdown.timer &> /dev/null; then + echo "✓ Timer is enabled" + else + echo "✗ Timer is not enabled" + fi - if systemctl is-active day-specific-shutdown.timer &>/dev/null; then - echo "✓ Timer is active" - else - echo "✗ Timer is not active" - fi + if systemctl is-active day-specific-shutdown.timer &> /dev/null; then + echo "✓ Timer is active" + else + echo "✗ Timer is not active" + fi - echo "" - echo "Monitor status:" - if systemctl is-enabled shutdown-timer-monitor.service &>/dev/null; then - echo "✓ Monitor is enabled" - else - echo "✗ Monitor is not enabled" - fi + echo "" + echo "Monitor status:" + if systemctl is-enabled shutdown-timer-monitor.service &> /dev/null; then + echo "✓ Monitor is enabled" + else + echo "✗ Monitor is not enabled" + fi - if systemctl is-active shutdown-timer-monitor.service &>/dev/null; then - echo "✓ Monitor is active" - else - echo "✗ Monitor is not active" - fi + if systemctl is-active shutdown-timer-monitor.service &> /dev/null; then + echo "✓ Monitor is active" + else + echo "✗ Monitor is not active" + fi - echo "" - echo "Watchdog timer status:" - if systemctl is-enabled shutdown-timer-monitor-watchdog.timer &>/dev/null; then - echo "✓ Watchdog timer is enabled" - else - echo "✗ Watchdog timer is not enabled" - fi + echo "" + echo "Watchdog timer status:" + if systemctl is-enabled shutdown-timer-monitor-watchdog.timer &> /dev/null; then + echo "✓ Watchdog timer is enabled" + else + echo "✗ Watchdog timer is not enabled" + fi - if systemctl is-active shutdown-timer-monitor-watchdog.timer &>/dev/null; then - echo "✓ Watchdog timer is active" - else - echo "✗ Watchdog timer is not active" - fi + if systemctl is-active shutdown-timer-monitor-watchdog.timer &> /dev/null; then + echo "✓ Watchdog timer is active" + else + echo "✗ Watchdog timer is not active" + fi - echo "" - echo "Config file protection status:" - local config_file="/etc/shutdown-schedule.conf" - local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" + echo "" + echo "Config file protection status:" + local config_file="/etc/shutdown-schedule.conf" + local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" - if [[ -f "$config_file" ]]; then - echo "✓ Config file exists" - if lsattr "$config_file" 2>/dev/null | grep -q '^....i'; then - echo "✓ Config file is immutable" - else - echo "✗ Config file is NOT immutable" - fi - else - echo "✗ Config file missing" - fi + if [[ -f $config_file ]]; then + echo "✓ Config file exists" + if lsattr "$config_file" 2> /dev/null | grep -q '^....i'; then + echo "✓ Config file is immutable" + else + echo "✗ Config file is NOT immutable" + fi + else + echo "✗ Config file missing" + fi - if [[ -f "$canonical_file" ]]; then - echo "✓ Canonical copy exists" - else - echo "✗ Canonical copy missing" - fi + if [[ -f $canonical_file ]]; then + echo "✓ Canonical copy exists" + else + echo "✗ Canonical copy missing" + fi - if systemctl is-enabled shutdown-schedule-guard.path &>/dev/null; then - echo "✓ Config guard path watcher is enabled" - else - echo "✗ Config guard path watcher is not enabled" - fi + if systemctl is-enabled shutdown-schedule-guard.path &> /dev/null; then + echo "✓ Config guard path watcher is enabled" + else + echo "✗ Config guard path watcher is not enabled" + fi - if systemctl is-active shutdown-schedule-guard.path &>/dev/null; then - echo "✓ Config guard path watcher is active" - else - echo "✗ Config guard path watcher is not active" - fi + if systemctl is-active shutdown-schedule-guard.path &> /dev/null; then + echo "✓ Config guard path watcher is active" + else + echo "✗ Config guard path watcher is not active" + fi - if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then - echo "✓ Unlock script exists" - else - echo "✗ Unlock script missing" - fi + if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then + echo "✓ Unlock script exists" + else + echo "✗ Unlock script missing" + fi - echo "" - echo "Next scheduled checks:" - systemctl list-timers day-specific-shutdown.timer --no-pager 2>/dev/null | head -5 | grep day-specific-shutdown || echo "Timer information not available" + echo "" + echo "Next scheduled checks:" + systemctl list-timers day-specific-shutdown.timer --no-pager 2> /dev/null | head -5 | grep day-specific-shutdown || echo "Timer information not available" } # Display the shutdown schedule (used in multiple places) print_shutdown_schedule() { - # Convert 24h to 12h format for display - local mon_wed_12h thu_sun_12h morning_12h - if [[ $SCHEDULE_MON_WED_HOUR -gt 12 ]]; then - mon_wed_12h="$((SCHEDULE_MON_WED_HOUR - 12)):00 PM" - else - mon_wed_12h="${SCHEDULE_MON_WED_HOUR}:00 AM" - fi - if [[ $SCHEDULE_THU_SUN_HOUR -gt 12 ]]; then - thu_sun_12h="$((SCHEDULE_THU_SUN_HOUR - 12)):00 PM" - else - thu_sun_12h="${SCHEDULE_THU_SUN_HOUR}:00 AM" - fi - morning_12h="${SCHEDULE_MORNING_END_HOUR}:00 AM" + # Convert 24h to 12h format for display + local mon_wed_12h thu_sun_12h morning_12h + if [[ $SCHEDULE_MON_WED_HOUR -gt 12 ]]; then + mon_wed_12h="$((SCHEDULE_MON_WED_HOUR - 12)):00 PM" + else + mon_wed_12h="${SCHEDULE_MON_WED_HOUR}:00 AM" + fi + if [[ $SCHEDULE_THU_SUN_HOUR -gt 12 ]]; then + thu_sun_12h="$((SCHEDULE_THU_SUN_HOUR - 12)):00 PM" + else + thu_sun_12h="${SCHEDULE_THU_SUN_HOUR}:00 AM" + fi + morning_12h="${SCHEDULE_MORNING_END_HOUR}:00 AM" - echo "Shutdown Schedule:" - echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00 (${mon_wed_12h} to ${morning_12h})" - echo " Thursday-Sunday: ${SCHEDULE_THU_SUN_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00 (${thu_sun_12h} to ${morning_12h})" + echo "Shutdown Schedule:" + echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00 (${mon_wed_12h} to ${morning_12h})" + echo " Thursday-Sunday: ${SCHEDULE_THU_SUN_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00 (${thu_sun_12h} to ${morning_12h})" } # Function to show final instructions show_instructions() { - echo "" - echo "=================================================" - echo "Day-Specific Auto-Shutdown Setup Complete" - echo "=================================================" - echo "Summary:" - echo "✓ Systemd service created (/etc/systemd/system/day-specific-shutdown.service)" - echo "✓ Systemd timer created (/etc/systemd/system/day-specific-shutdown.timer)" - echo "✓ Management script created (/usr/local/bin/day-specific-shutdown-manager.sh)" - echo "✓ Smart check script created (/usr/local/bin/day-specific-shutdown-check.sh)" - echo "✓ Timer enabled and started" - echo "✓ Monitor service installed (protects timer from being disabled)" - echo "✓ Watchdog timer installed (restarts monitor if stopped)" - echo "✓ Config file protected (immutable + path watcher + canonical copy)" - echo "" - print_shutdown_schedule - echo "" - echo "Management commands:" - echo " sudo day-specific-shutdown-manager.sh status - Check status" - echo " sudo day-specific-shutdown-manager.sh logs - View shutdown logs" - echo "" - echo "To modify shutdown hours (with psychological friction):" - echo " sudo /usr/local/sbin/unlock-shutdown-schedule" - echo "" - echo "How it works:" - echo "• Timer checks every 30 minutes during potential shutdown windows" - echo "• Smart logic determines shutdown eligibility based on day and time" - echo "• Monitor service watches the timer and re-enables it if disabled" - echo "• Watchdog timer restarts the monitor every 60 seconds if stopped" - echo "• Monitor has RefuseManualStop=true to prevent easy stopping" - echo "• Config file is protected by:" - echo " - Immutable attribute (chattr +i)" - echo " - Canonical copy at /usr/local/share/locked-shutdown-schedule.conf" - echo " - Path watcher that auto-restores if you modify the file" - echo "• There is NO disable option - this is intentional for digital wellbeing" - echo "" - echo "WARNING: This will automatically shutdown your PC during designated hours." - echo "Make sure to save your work before the shutdown windows!" - echo "" + echo "" + echo "=================================================" + echo "Day-Specific Auto-Shutdown Setup Complete" + echo "=================================================" + echo "Summary:" + echo "✓ Systemd service created (/etc/systemd/system/day-specific-shutdown.service)" + echo "✓ Systemd timer created (/etc/systemd/system/day-specific-shutdown.timer)" + echo "✓ Management script created (/usr/local/bin/day-specific-shutdown-manager.sh)" + echo "✓ Smart check script created (/usr/local/bin/day-specific-shutdown-check.sh)" + echo "✓ Timer enabled and started" + echo "✓ Monitor service installed (protects timer from being disabled)" + echo "✓ Watchdog timer installed (restarts monitor if stopped)" + echo "✓ Config file protected (immutable + path watcher + canonical copy)" + echo "" + print_shutdown_schedule + echo "" + echo "Management commands:" + echo " sudo day-specific-shutdown-manager.sh status - Check status" + echo " sudo day-specific-shutdown-manager.sh logs - View shutdown logs" + echo "" + echo "To modify shutdown hours (with psychological friction):" + echo " sudo /usr/local/sbin/unlock-shutdown-schedule" + echo "" + echo "How it works:" + echo "• Timer checks every 30 minutes during potential shutdown windows" + echo "• Smart logic determines shutdown eligibility based on day and time" + echo "• Monitor service watches the timer and re-enables it if disabled" + echo "• Watchdog timer restarts the monitor every 60 seconds if stopped" + echo "• Monitor has RefuseManualStop=true to prevent easy stopping" + echo "• Config file is protected by:" + echo " - Immutable attribute (chattr +i)" + echo " - Canonical copy at /usr/local/share/locked-shutdown-schedule.conf" + echo " - Path watcher that auto-restores if you modify the file" + echo "• There is NO disable option - this is intentional for digital wellbeing" + echo "" + echo "WARNING: This will automatically shutdown your PC during designated hours." + echo "Make sure to save your work before the shutdown windows!" + echo "" } # Function to prompt for confirmation confirm_setup() { - echo "" - echo "WARNING: Day-Specific Auto-Shutdown Confirmation" - echo "===============================================" - echo "This will set up your PC to automatically shutdown during specific time windows." - echo "" - print_shutdown_schedule - echo "" - echo "Important considerations:" - echo "- Any unsaved work will be lost during shutdown windows" - echo "- Running processes will be terminated" - echo "- Downloads/uploads in progress will be interrupted" - echo "- You'll need to manually power on your PC each day" - echo "- Timer checks every 30 minutes during potential shutdown windows" - echo "- There is NO disable option - this is protected by a monitor service" - echo "" - read -r -p "Do you want to proceed? (y/N): " confirm + echo "" + echo "WARNING: Day-Specific Auto-Shutdown Confirmation" + echo "===============================================" + echo "This will set up your PC to automatically shutdown during specific time windows." + echo "" + print_shutdown_schedule + echo "" + echo "Important considerations:" + echo "- Any unsaved work will be lost during shutdown windows" + echo "- Running processes will be terminated" + echo "- Downloads/uploads in progress will be interrupted" + echo "- You'll need to manually power on your PC each day" + echo "- Timer checks every 30 minutes during potential shutdown windows" + echo "- There is NO disable option - this is protected by a monitor service" + echo "" + 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 + case "$confirm" in + [yY] | [yY][eE][sS]) + echo "Proceeding with setup..." + return 0 + ;; + *) + echo "Setup cancelled." + exit 0 + ;; + esac } # Main execution flow for enable enable_midnight_shutdown() { - echo "Day-Specific Auto-Shutdown Setup for Arch Linux" - echo "===============================================" - echo "Current Date: $(date)" - echo "User: $ACTUAL_USER" - echo "Target user: $ACTUAL_USER" - echo "User home: $USER_HOME" + echo "Day-Specific Auto-Shutdown Setup for Arch Linux" + echo "===============================================" + echo "Current Date: $(date)" + echo "User: $ACTUAL_USER" + echo "Target user: $ACTUAL_USER" + echo "User home: $USER_HOME" - # Check if trying to cheat by making schedule more lenient - check_schedule_protection + # Check if trying to cheat by making schedule more lenient + check_schedule_protection - # Confirm setup - confirm_setup + # Confirm setup + confirm_setup - # Create config file (shared with i3blocks countdown script) - create_shutdown_config + # Create config file (shared with i3blocks countdown script) + create_shutdown_config - # Create config guard (path watcher, enforcement, unlock script) - create_config_guard + # Create config guard (path watcher, enforcement, unlock script) + create_config_guard - # Create systemd files - create_shutdown_service - create_shutdown_timer - create_management_script - create_shutdown_check_script + # Create systemd files + create_shutdown_service + create_shutdown_timer + create_management_script + create_shutdown_check_script - # Enable and start timer - enable_timer + # Enable and start timer + enable_timer - # Install monitor service (protects timer from being disabled) - install_monitor_service + # Install monitor service (protects timer from being disabled) + install_monitor_service - # Test setup - test_setup + # Test setup + test_setup - # Show instructions - show_instructions + # Show instructions + show_instructions } # Parse command line arguments case "${1:-enable}" in -"enable") - check_sudo "$@" - enable_midnight_shutdown - ;; -"status") - check_sudo "$@" - show_current_status - ;; -"help" | "-h" | "--help") - show_usage - ;; -*) - echo "Error: Unknown command '$1'" - echo "" - show_usage - exit 1 - ;; + "enable") + check_sudo "$@" + enable_midnight_shutdown + ;; + "status") + check_sudo "$@" + show_current_status + ;; + "help" | "-h" | "--help") + show_usage + ;; + *) + echo "Error: Unknown command '$1'" + echo "" + show_usage + exit 1 + ;; esac diff --git a/scripts/digital_wellbeing/setup_pc_startup_monitor.sh b/scripts/digital_wellbeing/setup_pc_startup_monitor.sh index 2e479b7..a3bd7e4 100755 --- a/scripts/digital_wellbeing/setup_pc_startup_monitor.sh +++ b/scripts/digital_wellbeing/setup_pc_startup_monitor.sh @@ -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 diff --git a/scripts/digital_wellbeing/youtube-music-wrapper.sh b/scripts/digital_wellbeing/youtube-music-wrapper.sh index 46ce656..0d38ad3 100644 --- a/scripts/digital_wellbeing/youtube-music-wrapper.sh +++ b/scripts/digital_wellbeing/youtube-music-wrapper.sh @@ -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 diff --git a/scripts/features/install_unreal_mcp.sh b/scripts/features/install_unreal_mcp.sh index 91ddaeb..1c5fbb7 100755 --- a/scripts/features/install_unreal_mcp.sh +++ b/scripts/features/install_unreal_mcp.sh @@ -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 </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" < "$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" < "$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 </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 <"$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 <"$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 <"$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 <&2 + echo -e "${BLUE}[INFO]${NC} $1" >&2 } log_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 + echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 } log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" >&2 + echo -e "${YELLOW}[WARNING]${NC} $1" >&2 } log_error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 + echo -e "${RED}[ERROR]${NC} $1" >&2 } die() { - log_error "$1" - exit 1 + log_error "$1" + exit 1 } check_root() { - if [[ $EUID -ne 0 ]]; then - die "This script must be run as root. Use: sudo $0" - fi + if [[ $EUID -ne 0 ]]; then + die "This script must be run as root. Use: sudo $0" + fi } save_config() { - cat >"$CONFIG_FILE" < "$CONFIG_FILE" << EOF # Raspberry Pi Setup - Auto-generated config # This file is gitignored and stores discovered settings @@ -81,22 +81,22 @@ PI_TIMEZONE="${PI_TIMEZONE}" # Generated passwords (KEEP THIS FILE SECURE!) PI_PASSWORD="${PI_PASSWORD}" EOF - chmod 600 "$CONFIG_FILE" - log_info "Configuration saved to $CONFIG_FILE" + chmod 600 "$CONFIG_FILE" + log_info "Configuration saved to $CONFIG_FILE" } generate_password() { - local length="${1:-16}" - local chars - chars=$(dd if=/dev/urandom bs=256 count=1 2>/dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") - echo "$chars" + local length="${1:-16}" + local chars + chars=$(dd if=/dev/urandom bs=256 count=1 2> /dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") + echo "$chars" } auto_generate_pi_password() { - if [[ -z $PI_PASSWORD ]]; then - PI_PASSWORD=$(generate_password 16) - log_info "Auto-generated Pi password (will be saved to config file)" - fi + if [[ -z $PI_PASSWORD ]]; then + PI_PASSWORD=$(generate_password 16) + log_info "Auto-generated Pi password (will be saved to config file)" + fi } # ============================================================================= @@ -104,166 +104,166 @@ auto_generate_pi_password() { # ============================================================================= ensure_dependencies() { - local missing_packages=() + local missing_packages=() - if ! command -v nmap &>/dev/null; then - missing_packages+=("nmap") - fi + if ! command -v nmap &> /dev/null; then + missing_packages+=("nmap") + fi - if ! command -v sshpass &>/dev/null; then - missing_packages+=("sshpass") - fi + if ! command -v sshpass &> /dev/null; then + missing_packages+=("sshpass") + fi - if [[ ${#missing_packages[@]} -gt 0 ]]; then - log_info "Installing missing packages: ${missing_packages[*]}" + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log_info "Installing missing packages: ${missing_packages[*]}" - if command -v pacman &>/dev/null; then - sudo pacman -S --noconfirm "${missing_packages[@]}" - elif command -v apt-get &>/dev/null; then - sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" - elif command -v dnf &>/dev/null; then - sudo dnf install -y "${missing_packages[@]}" - else - die "Could not detect package manager. Please install manually: ${missing_packages[*]}" - fi + if command -v pacman &> /dev/null; then + sudo pacman -S --noconfirm "${missing_packages[@]}" + elif command -v apt-get &> /dev/null; then + sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" + elif command -v dnf &> /dev/null; then + sudo dnf install -y "${missing_packages[@]}" + else + die "Could not detect package manager. Please install manually: ${missing_packages[*]}" + fi - log_success "Dependencies installed" - fi + log_success "Dependencies installed" + fi } discover_remote_laptop() { - log_info "Auto-discovering remote laptop on local network..." + log_info "Auto-discovering remote laptop on local network..." - ensure_dependencies + ensure_dependencies - local my_ip - my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) - local gateway - gateway=$(ip route | grep default | awk '{print $3}' | head -1) - local network="${gateway%.*}.0/24" + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" - log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" - log_info "Scanning network for SSH-enabled devices (using nmap)..." + log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" + log_info "Scanning network for SSH-enabled devices (using nmap)..." - local ssh_hosts - nmap -sn -T4 "$network" &>/dev/null || true - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) + local ssh_hosts + nmap -sn -T4 "$network" &> /dev/null || true + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2> /dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) - if [[ -z $ssh_hosts ]]; then - die "No SSH-enabled devices found on network" - fi + if [[ -z $ssh_hosts ]]; then + die "No SSH-enabled devices found on network" + fi - local host_count - host_count=$(echo "$ssh_hosts" | wc -l) - log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" + local host_count + host_count=$(echo "$ssh_hosts" | wc -l) + log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" - local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") - local users=() - for u in "${common_users[@]}"; do - local is_dup=0 - for existing in "${users[@]}"; do - if [[ $u == "$existing" ]]; then - is_dup=1 - break - fi - done - if [[ $is_dup -eq 0 ]]; then - users+=("$u") - fi - done + local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") + local users=() + for u in "${common_users[@]}"; do + local is_dup=0 + for existing in "${users[@]}"; do + if [[ $u == "$existing" ]]; then + is_dup=1 + break + fi + done + if [[ $is_dup -eq 0 ]]; then + users+=("$u") + fi + done - log_info "Will try usernames: ${users[*]}" + log_info "Will try usernames: ${users[*]}" - local found_laptop="" - local found_user="" - local idx=0 + local found_laptop="" + local found_user="" + local idx=0 - for ip in $ssh_hosts; do - idx=$((idx + 1)) + for ip in $ssh_hosts; do + idx=$((idx + 1)) - if [[ $ip == "$gateway" ]]; then - log_info "[$idx/$host_count] Skipping $ip (gateway)" - continue - fi + if [[ $ip == "$gateway" ]]; then + log_info "[$idx/$host_count] Skipping $ip (gateway)" + continue + fi - log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." + log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." - for try_user in "${users[@]}"; do - if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then - log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" - found_user="$try_user" + for try_user in "${users[@]}"; do + if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2> /dev/null | grep -q "ok"; then + log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" + found_user="$try_user" - log_info "[$idx/$host_count] $ip - Checking for SD card..." - local has_sd - has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2>/dev/null || true) + log_info "[$idx/$host_count] $ip - Checking for SD card..." + local has_sd + has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2> /dev/null || true) - if [[ -n $has_sd ]]; then - log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" - found_laptop="$ip" - break 2 - else - log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." - if [[ -z $found_laptop ]]; then - found_laptop="$ip" - fi - fi - break - fi - done - done + if [[ -n $has_sd ]]; then + log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" + found_laptop="$ip" + break 2 + else + log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." + if [[ -z $found_laptop ]]; then + found_laptop="$ip" + fi + fi + break + fi + done + done - if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then - log_warning "No device with passwordless SSH found using common usernames." + if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then + log_warning "No device with passwordless SSH found using common usernames." - found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) + found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) - if [[ -z $found_laptop ]]; then - die "Could not find any suitable SSH-enabled device" - fi + if [[ -z $found_laptop ]]; then + die "Could not find any suitable SSH-enabled device" + fi - log_info "Found SSH host at $found_laptop but need credentials." - read -r -p "Enter username for $found_laptop: " found_user + log_info "Found SSH host at $found_laptop but need credentials." + read -r -p "Enter username for $found_laptop: " found_user - if [[ -z $found_user ]]; then - die "No username provided" - fi - fi + if [[ -z $found_user ]]; then + die "No username provided" + fi + fi - REMOTE_LAPTOP_IP="$found_laptop" - REMOTE_LAPTOP_USER="$found_user" - log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + REMOTE_LAPTOP_IP="$found_laptop" + REMOTE_LAPTOP_USER="$found_user" + log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" - save_config + save_config } setup_ssh_key_to_remote() { - local remote_host="$1" - local remote_user="$2" + local remote_host="$1" + local remote_user="$2" - if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then - log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" - return 0 - fi + if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2> /dev/null; then + log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" + return 0 + fi - log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." + log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." - if [[ ! -f ~/.ssh/id_rsa.pub ]] && [[ ! -f ~/.ssh/id_ed25519.pub ]]; then - log_info "Generating SSH key..." - ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -q - fi + if [[ ! -f ~/.ssh/id_rsa.pub ]] && [[ ! -f ~/.ssh/id_ed25519.pub ]]; then + log_info "Generating SSH key..." + ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -q + fi - log_info "Copying SSH key to remote host (you may be prompted for password)..." + log_info "Copying SSH key to remote host (you may be prompted for password)..." - if command -v ssh-copy-id &>/dev/null; then - ssh-copy-id -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" - else - local pub_key - pub_key=$(cat ~/.ssh/id_ed25519.pub 2>/dev/null || cat ~/.ssh/id_rsa.pub) - ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" "mkdir -p ~/.ssh && echo '$pub_key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" - fi + if command -v ssh-copy-id &> /dev/null; then + ssh-copy-id -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" + else + local pub_key + pub_key=$(cat ~/.ssh/id_ed25519.pub 2> /dev/null || cat ~/.ssh/id_rsa.pub) + ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" "mkdir -p ~/.ssh && echo '$pub_key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" + fi - log_success "SSH key authentication configured" + log_success "SSH key authentication configured" } # ============================================================================= @@ -271,61 +271,61 @@ setup_ssh_key_to_remote() { # ============================================================================= download_raspberry_pi_os() { - local download_dir="/tmp/rpi-image" - local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz" - local image_file="$download_dir/raspios.img.xz" - local extracted_image="$download_dir/raspios.img" - local expected_size=459000608 + local download_dir="/tmp/rpi-image" + local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz" + local image_file="$download_dir/raspios.img.xz" + local extracted_image="$download_dir/raspios.img" + local expected_size=459000608 - mkdir -p "$download_dir" + mkdir -p "$download_dir" - if [[ -f $extracted_image ]]; then - log_info "Using existing image at $extracted_image" - echo "$extracted_image" - return - fi + if [[ -f $extracted_image ]]; then + log_info "Using existing image at $extracted_image" + echo "$extracted_image" + return + fi - if [[ -f $image_file ]]; then - local actual_size - actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) - if [[ $actual_size -lt $expected_size ]]; then - log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..." - rm -f "$image_file" - else - log_info "Image archive already downloaded" - fi - fi + if [[ -f $image_file ]]; then + local actual_size + actual_size=$(stat -c%s "$image_file" 2> /dev/null || stat -f%z "$image_file" 2> /dev/null || echo 0) + if [[ $actual_size -lt $expected_size ]]; then + log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..." + rm -f "$image_file" + else + log_info "Image archive already downloaded" + fi + fi - if [[ ! -f $image_file ]]; then - log_info "Downloading Raspberry Pi OS Lite (64-bit)..." - log_info "This may take a while depending on your internet connection..." + if [[ ! -f $image_file ]]; then + log_info "Downloading Raspberry Pi OS Lite (64-bit)..." + log_info "This may take a while depending on your internet connection..." - if command -v aria2c &>/dev/null; then - aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2 - elif command -v wget &>/dev/null; then - wget --continue --show-progress -O "$image_file" "$image_url" >&2 - elif command -v curl &>/dev/null; then - curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2 - else - die "No download tool available. Install wget, curl, or aria2c" - fi + if command -v aria2c &> /dev/null; then + aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2 + elif command -v wget &> /dev/null; then + wget --continue --show-progress -O "$image_file" "$image_url" >&2 + elif command -v curl &> /dev/null; then + curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2 + else + die "No download tool available. Install wget, curl, or aria2c" + fi - local actual_size - actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) - if [[ $actual_size -lt $expected_size ]]; then - die "Download incomplete: got $actual_size bytes, expected $expected_size" - fi - log_success "Download complete: $actual_size bytes" - fi + local actual_size + actual_size=$(stat -c%s "$image_file" 2> /dev/null || stat -f%z "$image_file" 2> /dev/null || echo 0) + if [[ $actual_size -lt $expected_size ]]; then + die "Download incomplete: got $actual_size bytes, expected $expected_size" + fi + log_success "Download complete: $actual_size bytes" + fi - log_info "Extracting image..." - xz -dk "$image_file" + log_info "Extracting image..." + xz -dk "$image_file" - if [[ ! -f $extracted_image ]]; then - die "Failed to extract image" - fi + if [[ ! -f $extracted_image ]]; then + die "Failed to extract image" + fi - echo "$extracted_image" + echo "$extracted_image" } # ============================================================================= @@ -333,124 +333,124 @@ download_raspberry_pi_os() { # ============================================================================= phase_flash_local() { - check_root + check_root - log_info "=== Flash Raspberry Pi OS to SD Card (Local) ===" + log_info "=== Flash Raspberry Pi OS to SD Card (Local) ===" - # Detect SD card - log_info "Detecting removable storage devices..." - local devices - devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}') + # Detect SD card + log_info "Detecting removable storage devices..." + local devices + devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}') - if [[ -z $devices ]]; then - log_warning "No removable devices detected automatically." - lsblk -d -o NAME,SIZE,TYPE,RM,TRAN - read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE - else - echo "Detected removable devices:" - echo "$devices" - read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE - fi + if [[ -z $devices ]]; then + log_warning "No removable devices detected automatically." + lsblk -d -o NAME,SIZE,TYPE,RM,TRAN + read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE + else + echo "Detected removable devices:" + echo "$devices" + read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE + fi - if [[ ! -b $SD_CARD_DEVICE ]]; then - die "Device $SD_CARD_DEVICE does not exist or is not a block device" - fi + if [[ ! -b $SD_CARD_DEVICE ]]; then + die "Device $SD_CARD_DEVICE does not exist or is not a block device" + fi - local root_device - root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//') - if [[ $SD_CARD_DEVICE == "$root_device" ]]; then - die "Cannot flash to the system drive!" - fi + local root_device + root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//') + if [[ $SD_CARD_DEVICE == "$root_device" ]]; then + die "Cannot flash to the system drive!" + fi - auto_generate_pi_password + auto_generate_pi_password - local encrypted_password - encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) + local encrypted_password + encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) - save_config + save_config - local image_path - image_path=$(download_raspberry_pi_os) + local image_path + image_path=$(download_raspberry_pi_os) - log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE" - read -r -p "Are you sure you want to continue? (yes/no): " confirm + log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE" + read -r -p "Are you sure you want to continue? (yes/no): " confirm - if [[ $confirm != "yes" ]]; then - die "Aborted by user" - fi + if [[ $confirm != "yes" ]]; then + die "Aborted by user" + fi - log_info "Unmounting partitions on $SD_CARD_DEVICE..." - for partition in "${SD_CARD_DEVICE}"*; do - if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then - umount "$partition" 2>/dev/null || true - fi - done + log_info "Unmounting partitions on $SD_CARD_DEVICE..." + for partition in "${SD_CARD_DEVICE}"*; do + if mountpoint -q "$partition" 2> /dev/null || mount | grep -q "$partition"; then + umount "$partition" 2> /dev/null || true + fi + done - log_info "Flashing image to SD card..." - dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync - sync - log_success "Image flashed successfully!" + log_info "Flashing image to SD card..." + dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync + sync + log_success "Image flashed successfully!" - # Configure headless boot - log_info "Configuring headless boot..." - sleep 2 - partprobe "$SD_CARD_DEVICE" 2>/dev/null || true - sleep 2 + # Configure headless boot + log_info "Configuring headless boot..." + sleep 2 + partprobe "$SD_CARD_DEVICE" 2> /dev/null || true + sleep 2 - local boot_partition - if [[ -b "${SD_CARD_DEVICE}1" ]]; then - boot_partition="${SD_CARD_DEVICE}1" - elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then - boot_partition="${SD_CARD_DEVICE}p1" - else - die "Could not find boot partition" - fi + local boot_partition + if [[ -b "${SD_CARD_DEVICE}1" ]]; then + boot_partition="${SD_CARD_DEVICE}1" + elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then + boot_partition="${SD_CARD_DEVICE}p1" + else + die "Could not find boot partition" + fi - local boot_mount="/tmp/rpi-boot" - mkdir -p "$boot_mount" - mount "$boot_partition" "$boot_mount" + local boot_mount="/tmp/rpi-boot" + mkdir -p "$boot_mount" + mount "$boot_partition" "$boot_mount" - touch "$boot_mount/ssh" - log_success "SSH enabled" + touch "$boot_mount/ssh" + log_success "SSH enabled" - echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" - log_success "User '$PI_USER' configured" + echo "${PI_USER}:${encrypted_password}" > "$boot_mount/userconf.txt" + log_success "User '$PI_USER' configured" - local root_partition - if [[ -b "${SD_CARD_DEVICE}2" ]]; then - root_partition="${SD_CARD_DEVICE}2" - elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then - root_partition="${SD_CARD_DEVICE}p2" - fi + local root_partition + if [[ -b "${SD_CARD_DEVICE}2" ]]; then + root_partition="${SD_CARD_DEVICE}2" + elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then + root_partition="${SD_CARD_DEVICE}p2" + fi - if [[ -n $root_partition ]]; then - local root_mount="/tmp/rpi-root" - mkdir -p "$root_mount" - mount "$root_partition" "$root_mount" + if [[ -n $root_partition ]]; then + local root_mount="/tmp/rpi-root" + mkdir -p "$root_mount" + mount "$root_partition" "$root_mount" - echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" - sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" + echo "$PI_HOSTNAME" > "$root_mount/etc/hostname" + sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" - log_success "Hostname set to '$PI_HOSTNAME'" + log_success "Hostname set to '$PI_HOSTNAME'" - umount "$root_mount" - fi + umount "$root_mount" + fi - umount "$boot_mount" - sync + umount "$boot_mount" + sync - log_success "SD card configured for headless boot!" - log_success "Flash complete!" - echo - log_info "Pi credentials:" - log_info " User: $PI_USER" - log_info " Password: $PI_PASSWORD" - log_info " Hostname: $PI_HOSTNAME" - echo - log_info "Next steps:" - log_info "1. Remove SD card and insert into Raspberry Pi" - log_info "2. Connect the Pi to power and network" - log_info "3. Wait 2-3 minutes for first boot" + log_success "SD card configured for headless boot!" + log_success "Flash complete!" + echo + log_info "Pi credentials:" + log_info " User: $PI_USER" + log_info " Password: $PI_PASSWORD" + log_info " Hostname: $PI_HOSTNAME" + echo + log_info "Next steps:" + log_info "1. Remove SD card and insert into Raspberry Pi" + log_info "2. Connect the Pi to power and network" + log_info "3. Wait 2-3 minutes for first boot" } # ============================================================================= @@ -458,147 +458,147 @@ phase_flash_local() { # ============================================================================= phase_flash_remote() { - log_info "=== Flash Raspberry Pi OS to SD Card on Remote Laptop ===" + log_info "=== Flash Raspberry Pi OS to SD Card on Remote Laptop ===" - discover_remote_laptop + discover_remote_laptop - setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER" + setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER" - local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" - log_info "Checking for SD card on remote laptop..." - echo "Block devices on ${remote}:" - ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true - echo + log_info "Checking for SD card on remote laptop..." + echo "Block devices on ${remote}:" + ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true + echo - log_info "Auto-detecting SD card on remote laptop..." - local sd_device - sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2>/dev/null || true) + log_info "Auto-detecting SD card on remote laptop..." + local sd_device + sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2> /dev/null || true) - if [[ -z $sd_device ]]; then - die "No SD card detected on remote laptop. Please insert an SD card and try again." - fi + if [[ -z $sd_device ]]; then + die "No SD card detected on remote laptop. Please insert an SD card and try again." + fi - local sd_info - # shellcheck disable=SC2029 # Intentional client-side expansion - sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true) + local sd_info + # shellcheck disable=SC2029 # Intentional client-side expansion + sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true) - log_success "Auto-detected SD card: $sd_device ($sd_info)" - SD_CARD_DEVICE="$sd_device" + log_success "Auto-detected SD card: $sd_device ($sd_info)" + SD_CARD_DEVICE="$sd_device" - # shellcheck disable=SC2029 # Intentional client-side expansion - if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2>/dev/null; then - die "Device $SD_CARD_DEVICE does not exist on remote laptop" - fi + # shellcheck disable=SC2029 # Intentional client-side expansion + if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2> /dev/null; then + die "Device $SD_CARD_DEVICE does not exist on remote laptop" + fi - auto_generate_pi_password - log_success "Pi user '$PI_USER' password: $PI_PASSWORD" + auto_generate_pi_password + log_success "Pi user '$PI_USER' password: $PI_PASSWORD" - local encrypted_password - encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) + local encrypted_password + encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) - save_config + save_config - log_info "Copying script to remote laptop..." - scp "$0" "${remote}:/tmp/raspberry_pi_flash_sd.sh" + log_info "Copying script to remote laptop..." + scp "$0" "${remote}:/tmp/raspberry_pi_flash_sd.sh" - log_info "Executing flash on remote laptop..." - log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!" - log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)" - sleep 5 + log_info "Executing flash on remote laptop..." + log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!" + log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)" + sleep 5 - ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/raspberry_pi_flash_sd.sh execute-remote '$encrypted_password'" + ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/raspberry_pi_flash_sd.sh execute-remote '$encrypted_password'" - log_success "Flash complete!" - echo - log_info "Pi credentials:" - log_info " User: $PI_USER" - log_info " Password: $PI_PASSWORD" - log_info " Hostname: $PI_HOSTNAME" - echo - log_info "Next steps:" - log_info "1. Remove SD card from the laptop and insert into Raspberry Pi" - log_info "2. Connect the Pi to power and network" - log_info "3. Wait 2-3 minutes for first boot" + log_success "Flash complete!" + echo + log_info "Pi credentials:" + log_info " User: $PI_USER" + log_info " Password: $PI_PASSWORD" + log_info " Hostname: $PI_HOSTNAME" + echo + log_info "Next steps:" + log_info "1. Remove SD card from the laptop and insert into Raspberry Pi" + log_info "2. Connect the Pi to power and network" + log_info "3. Wait 2-3 minutes for first boot" } # Called on the remote laptop by phase_flash_remote phase_execute_remote() { - check_root + check_root - local encrypted_password="${1:-}" + local encrypted_password="${1:-}" - log_info "=== Executing Flash on Remote Laptop ===" + log_info "=== Executing Flash on Remote Laptop ===" - if [[ -z $SD_CARD_DEVICE ]]; then - die "SD_CARD_DEVICE not set" - fi + if [[ -z $SD_CARD_DEVICE ]]; then + die "SD_CARD_DEVICE not set" + fi - local image_path - image_path=$(download_raspberry_pi_os) + local image_path + image_path=$(download_raspberry_pi_os) - log_info "Unmounting partitions on $SD_CARD_DEVICE..." - for partition in "${SD_CARD_DEVICE}"*; do - if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then - umount "$partition" 2>/dev/null || true - fi - done + log_info "Unmounting partitions on $SD_CARD_DEVICE..." + for partition in "${SD_CARD_DEVICE}"*; do + if mountpoint -q "$partition" 2> /dev/null || mount | grep -q "$partition"; then + umount "$partition" 2> /dev/null || true + fi + done - log_info "Flashing image to SD card..." - dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync - sync - log_success "Image flashed successfully!" + log_info "Flashing image to SD card..." + dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync + sync + log_success "Image flashed successfully!" - log_info "Configuring headless boot..." - sleep 2 - partprobe "$SD_CARD_DEVICE" 2>/dev/null || true - sleep 2 + log_info "Configuring headless boot..." + sleep 2 + partprobe "$SD_CARD_DEVICE" 2> /dev/null || true + sleep 2 - local boot_partition - if [[ -b "${SD_CARD_DEVICE}1" ]]; then - boot_partition="${SD_CARD_DEVICE}1" - elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then - boot_partition="${SD_CARD_DEVICE}p1" - else - die "Could not find boot partition" - fi + local boot_partition + if [[ -b "${SD_CARD_DEVICE}1" ]]; then + boot_partition="${SD_CARD_DEVICE}1" + elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then + boot_partition="${SD_CARD_DEVICE}p1" + else + die "Could not find boot partition" + fi - local boot_mount="/tmp/rpi-boot" - mkdir -p "$boot_mount" - mount "$boot_partition" "$boot_mount" + local boot_mount="/tmp/rpi-boot" + mkdir -p "$boot_mount" + mount "$boot_partition" "$boot_mount" - touch "$boot_mount/ssh" - log_success "SSH enabled" + touch "$boot_mount/ssh" + log_success "SSH enabled" - if [[ -n $encrypted_password ]]; then - echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" - log_success "User '$PI_USER' configured" - fi + if [[ -n $encrypted_password ]]; then + echo "${PI_USER}:${encrypted_password}" > "$boot_mount/userconf.txt" + log_success "User '$PI_USER' configured" + fi - local root_partition - if [[ -b "${SD_CARD_DEVICE}2" ]]; then - root_partition="${SD_CARD_DEVICE}2" - elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then - root_partition="${SD_CARD_DEVICE}p2" - fi + local root_partition + if [[ -b "${SD_CARD_DEVICE}2" ]]; then + root_partition="${SD_CARD_DEVICE}2" + elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then + root_partition="${SD_CARD_DEVICE}p2" + fi - if [[ -n $root_partition ]]; then - local root_mount="/tmp/rpi-root" - mkdir -p "$root_mount" - mount "$root_partition" "$root_mount" + if [[ -n $root_partition ]]; then + local root_mount="/tmp/rpi-root" + mkdir -p "$root_mount" + mount "$root_partition" "$root_mount" - echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" - sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" + echo "$PI_HOSTNAME" > "$root_mount/etc/hostname" + sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" - log_success "Hostname set to '$PI_HOSTNAME'" + log_success "Hostname set to '$PI_HOSTNAME'" - umount "$root_mount" - fi + umount "$root_mount" + fi - umount "$boot_mount" - sync + umount "$boot_mount" + sync - log_success "SD card configured for headless boot!" + log_success "SD card configured for headless boot!" } # ============================================================================= @@ -606,7 +606,7 @@ phase_execute_remote() { # ============================================================================= show_help() { - cat <<'EOF' + cat << 'EOF' Raspberry Pi SD Card Flash Script Usage: ./raspberry_pi_flash_sd.sh @@ -635,27 +635,27 @@ EOF } main() { - local command="${1:-help}" + local command="${1:-help}" - case "$command" in - local) - phase_flash_local - ;; - remote) - phase_flash_remote - ;; - execute-remote) - phase_execute_remote "${2:-}" - ;; - help | --help | -h) - show_help - ;; - *) - log_error "Unknown command: $command" - show_help - exit 1 - ;; - esac + case "$command" in + local) + phase_flash_local + ;; + remote) + phase_flash_remote + ;; + execute-remote) + phase_execute_remote "${2:-}" + ;; + help | --help | -h) + show_help + ;; + *) + log_error "Unknown command: $command" + show_help + exit 1 + ;; + esac } main "$@" diff --git a/scripts/features/raspberry_pi_nextcloud.sh b/scripts/features/raspberry_pi_nextcloud.sh index e16f990..4c18b8a 100755 --- a/scripts/features/raspberry_pi_nextcloud.sh +++ b/scripts/features/raspberry_pi_nextcloud.sh @@ -15,8 +15,8 @@ CONFIG_FILE="${SCRIPT_DIR}/.raspberry_pi.conf" # Load configuration from gitignored config file if it exists if [[ -f $CONFIG_FILE ]]; then - # shellcheck source=/dev/null - source "$CONFIG_FILE" + # shellcheck source=/dev/null + source "$CONFIG_FILE" fi # Configuration @@ -44,34 +44,34 @@ BLUE='\033[0;34m' NC='\033[0m' log_info() { - echo -e "${BLUE}[INFO]${NC} $1" >&2 + echo -e "${BLUE}[INFO]${NC} $1" >&2 } log_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 + echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 } log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" >&2 + echo -e "${YELLOW}[WARNING]${NC} $1" >&2 } log_error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 + echo -e "${RED}[ERROR]${NC} $1" >&2 } die() { - log_error "$1" - exit 1 + log_error "$1" + exit 1 } check_root() { - if [[ $EUID -ne 0 ]]; then - die "This script must be run as root. Use: sudo $0" - fi + if [[ $EUID -ne 0 ]]; then + die "This script must be run as root. Use: sudo $0" + fi } save_config() { - cat >"$CONFIG_FILE" < "$CONFIG_FILE" << EOF # Raspberry Pi Nextcloud Setup - Auto-generated config # This file is gitignored and stores discovered settings @@ -90,46 +90,46 @@ DUCKDNS_DOMAIN="${DUCKDNS_DOMAIN:-}" DUCKDNS_TOKEN="${DUCKDNS_TOKEN:-}" LETSENCRYPT_EMAIL="${LETSENCRYPT_EMAIL:-}" EOF - chmod 600 "$CONFIG_FILE" - log_info "Configuration saved to $CONFIG_FILE" + chmod 600 "$CONFIG_FILE" + log_info "Configuration saved to $CONFIG_FILE" } generate_password() { - local length="${1:-16}" - local chars - chars=$(dd if=/dev/urandom bs=256 count=1 2>/dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") - echo "$chars" + local length="${1:-16}" + local chars + chars=$(dd if=/dev/urandom bs=256 count=1 2> /dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") + echo "$chars" } auto_generate_nextcloud_password() { - if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then - NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20) - log_info "Auto-generated Nextcloud admin password (will be saved to config file)" - fi + if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then + NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20) + log_info "Auto-generated Nextcloud admin password (will be saved to config file)" + fi } wait_for_apt_lock() { - local max_wait=600 - local waited=0 + local max_wait=600 + local waited=0 - while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do - if [[ $waited -eq 0 ]]; then - log_info "Waiting for other apt/dpkg processes to finish..." - pgrep -a 'apt|dpkg' | head -5 >&2 || true - fi - sleep 5 - waited=$((waited + 5)) - if [[ $waited -ge $max_wait ]]; then - die "Timeout waiting for apt lock after ${max_wait}s" - fi - if [[ $((waited % 30)) -eq 0 ]]; then - log_info "Still waiting... (${waited}s elapsed)" - fi - done + while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock > /dev/null 2>&1; do + if [[ $waited -eq 0 ]]; then + log_info "Waiting for other apt/dpkg processes to finish..." + pgrep -a 'apt|dpkg' | head -5 >&2 || true + fi + sleep 5 + waited=$((waited + 5)) + if [[ $waited -ge $max_wait ]]; then + die "Timeout waiting for apt lock after ${max_wait}s" + fi + if [[ $((waited % 30)) -eq 0 ]]; then + log_info "Still waiting... (${waited}s elapsed)" + fi + done - if [[ $waited -gt 0 ]]; then - log_success "Apt lock acquired after ${waited}s" - fi + if [[ $waited -gt 0 ]]; then + log_success "Apt lock acquired after ${waited}s" + fi } # ============================================================================= @@ -137,88 +137,88 @@ wait_for_apt_lock() { # ============================================================================= ensure_dependencies() { - local missing_packages=() + local missing_packages=() - if ! command -v nmap &>/dev/null; then - missing_packages+=("nmap") - fi + if ! command -v nmap &> /dev/null; then + missing_packages+=("nmap") + fi - if ! command -v sshpass &>/dev/null; then - missing_packages+=("sshpass") - fi + if ! command -v sshpass &> /dev/null; then + missing_packages+=("sshpass") + fi - if [[ ${#missing_packages[@]} -gt 0 ]]; then - log_info "Installing missing packages: ${missing_packages[*]}" + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log_info "Installing missing packages: ${missing_packages[*]}" - if command -v pacman &>/dev/null; then - sudo pacman -S --noconfirm "${missing_packages[@]}" - elif command -v apt-get &>/dev/null; then - sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" - elif command -v dnf &>/dev/null; then - sudo dnf install -y "${missing_packages[@]}" - else - die "Could not detect package manager. Please install manually: ${missing_packages[*]}" - fi - fi + if command -v pacman &> /dev/null; then + sudo pacman -S --noconfirm "${missing_packages[@]}" + elif command -v apt-get &> /dev/null; then + sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" + elif command -v dnf &> /dev/null; then + sudo dnf install -y "${missing_packages[@]}" + else + die "Could not detect package manager. Please install manually: ${missing_packages[*]}" + fi + fi } discover_raspberry_pi() { - log_info "Auto-discovering Raspberry Pi on local network..." + log_info "Auto-discovering Raspberry Pi on local network..." - ensure_dependencies + ensure_dependencies - local my_ip - my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) - local gateway - gateway=$(ip route | grep default | awk '{print $3}' | head -1) - local network="${gateway%.*}.0/24" + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" - log_info "Local IP: $my_ip, Network: $network" - log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." + log_info "Local IP: $my_ip, Network: $network" + log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." - local pi_ip="" + local pi_ip="" - # Try resolving hostname directly - pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true - if [[ -z $pi_ip ]]; then - pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true - fi + # Try resolving hostname directly + pi_ip=$(getent hosts "$PI_HOSTNAME" 2> /dev/null | awk '{print $1}' | head -1) || true + if [[ -z $pi_ip ]]; then + pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2> /dev/null | awk '{print $1}' | head -1) || true + fi - if [[ -n $pi_ip ]]; then - log_success "Found Pi by hostname: $pi_ip" - echo "$pi_ip" - return - fi + if [[ -n $pi_ip ]]; then + log_success "Found Pi by hostname: $pi_ip" + echo "$pi_ip" + return + fi - log_info "Hostname resolution failed, scanning network..." - nmap -sn -T4 "$network" &>/dev/null || true + log_info "Hostname resolution failed, scanning network..." + nmap -sn -T4 "$network" &> /dev/null || true - local ssh_hosts - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) || true + local ssh_hosts + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2> /dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) || true - if [[ -z $ssh_hosts ]]; then - die "No SSH-enabled devices found. Is the Pi connected and booted?" - fi + if [[ -z $ssh_hosts ]]; then + die "No SSH-enabled devices found. Is the Pi connected and booted?" + fi - log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" + log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" - for ip in $ssh_hosts; do - log_info "Trying $ip with user '$PI_USER'..." + for ip in $ssh_hosts; do + log_info "Trying $ip with user '$PI_USER'..." - if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2>/dev/null | grep -qi "$PI_HOSTNAME"; then - log_success "Found Raspberry Pi at $ip" - echo "$ip" - return - fi + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2> /dev/null | grep -qi "$PI_HOSTNAME"; then + log_success "Found Raspberry Pi at $ip" + echo "$ip" + return + fi - if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then - log_success "Found device responding to Pi credentials at $ip" - echo "$ip" - return - fi - done + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2> /dev/null | grep -q "ok"; then + log_success "Found device responding to Pi credentials at $ip" + echo "$ip" + return + fi + done - die "Could not find Raspberry Pi on network." + die "Could not find Raspberry Pi on network." } # ============================================================================= @@ -226,31 +226,31 @@ discover_raspberry_pi() { # ============================================================================= phase_configure_system() { - check_root + check_root - log_info "=== Configuring Raspberry Pi System ===" + log_info "=== Configuring Raspberry Pi System ===" - wait_for_apt_lock + wait_for_apt_lock - log_info "Fixing any broken packages..." - DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true + log_info "Fixing any broken packages..." + DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true - log_info "Updating system packages..." - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y + log_info "Updating system packages..." + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y - log_info "Setting timezone to $PI_TIMEZONE..." - timedatectl set-timezone "$PI_TIMEZONE" + log_info "Setting timezone to $PI_TIMEZONE..." + timedatectl set-timezone "$PI_TIMEZONE" - log_info "Configuring locale..." - sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen - locale-gen - update-locale LANG="$PI_LOCALE" + log_info "Configuring locale..." + sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen + locale-gen + update-locale LANG="$PI_LOCALE" - log_info "Hardening SSH configuration..." - cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup + log_info "Hardening SSH configuration..." + cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup - cat >>/etc/ssh/sshd_config.d/hardening.conf <<'EOF' + cat >> /etc/ssh/sshd_config.d/hardening.conf << 'EOF' # Security hardening PermitRootLogin no PasswordAuthentication yes @@ -261,29 +261,29 @@ ClientAliveInterval 300 ClientAliveCountMax 2 EOF - systemctl restart sshd + systemctl restart sshd - log_info "Installing useful packages..." - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - vim \ - htop \ - curl \ - wget \ - git \ - ufw \ - fail2ban \ - unattended-upgrades + log_info "Installing useful packages..." + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + vim \ + htop \ + curl \ + wget \ + git \ + ufw \ + fail2ban \ + unattended-upgrades - log_info "Configuring firewall..." - ufw default deny incoming - ufw default allow outgoing - ufw allow ssh - ufw allow 80/tcp - ufw allow 443/tcp - ufw --force enable + log_info "Configuring firewall..." + ufw default deny incoming + ufw default allow outgoing + ufw allow ssh + ufw allow 80/tcp + ufw allow 443/tcp + ufw --force enable - log_info "Configuring fail2ban..." - cat >/etc/fail2ban/jail.local <<'EOF' + log_info "Configuring fail2ban..." + cat > /etc/fail2ban/jail.local << 'EOF' [DEFAULT] bantime = 1h findtime = 10m @@ -297,11 +297,11 @@ logpath = /var/log/auth.log maxretry = 3 EOF - systemctl enable fail2ban - systemctl restart fail2ban + systemctl enable fail2ban + systemctl restart fail2ban - log_info "Enabling automatic security updates..." - cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' + log_info "Enabling automatic security updates..." + cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF' Unattended-Upgrade::Origins-Pattern { "origin=Debian,codename=${distro_codename},label=Debian-Security"; "origin=Raspbian,codename=${distro_codename},label=Raspbian"; @@ -310,13 +310,13 @@ Unattended-Upgrade::AutoFixInterruptedDpkg "true"; Unattended-Upgrade::Remove-Unused-Dependencies "true"; EOF - cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' + cat > /etc/apt/apt.conf.d/20auto-upgrades << 'EOF' APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; APT::Periodic::AutocleanInterval "7"; EOF - log_success "System configuration complete!" + log_success "System configuration complete!" } # ============================================================================= @@ -324,76 +324,76 @@ EOF # ============================================================================= phase_install_nextcloud() { - check_root + check_root - log_info "=== Installing Nextcloud ===" + log_info "=== Installing Nextcloud ===" - wait_for_apt_lock + wait_for_apt_lock - log_info "Installing Apache, PHP, MariaDB, and dependencies..." - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - apache2 \ - mariadb-server \ - php \ - php-gd \ - php-json \ - php-mysql \ - php-curl \ - php-mbstring \ - php-intl \ - php-imagick \ - php-xml \ - php-zip \ - php-bz2 \ - php-bcmath \ - php-gmp \ - php-apcu \ - php-redis \ - php-ldap \ - libapache2-mod-php \ - redis-server \ - certbot \ - python3-certbot-apache \ - imagemagick \ - libmagickcore-6.q16-6-extra + log_info "Installing Apache, PHP, MariaDB, and dependencies..." + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + apache2 \ + mariadb-server \ + php \ + php-gd \ + php-json \ + php-mysql \ + php-curl \ + php-mbstring \ + php-intl \ + php-imagick \ + php-xml \ + php-zip \ + php-bz2 \ + php-bcmath \ + php-gmp \ + php-apcu \ + php-redis \ + php-ldap \ + libapache2-mod-php \ + redis-server \ + certbot \ + python3-certbot-apache \ + imagemagick \ + libmagickcore-6.q16-6-extra - log_success "Packages installed" + log_success "Packages installed" - # Configure MariaDB - log_info "Configuring MariaDB..." + # Configure MariaDB + log_info "Configuring MariaDB..." - local db_password - db_password=$(generate_password 32) + local db_password + db_password=$(generate_password 32) - mysql -u root </root/.nextcloud_db_password - chmod 600 /root/.nextcloud_db_password - log_success "MariaDB configured" + echo "$db_password" > /root/.nextcloud_db_password + chmod 600 /root/.nextcloud_db_password + log_success "MariaDB configured" - # Download Nextcloud - log_info "Downloading Nextcloud..." + # Download Nextcloud + log_info "Downloading Nextcloud..." - cd /tmp - if [[ ! -f nextcloud.zip ]]; then - wget -q --show-progress "https://download.nextcloud.com/server/releases/latest.zip" -O nextcloud.zip >&2 - fi + cd /tmp + if [[ ! -f nextcloud.zip ]]; then + wget -q --show-progress "https://download.nextcloud.com/server/releases/latest.zip" -O nextcloud.zip >&2 + fi - rm -rf /var/www/nextcloud - unzip -q nextcloud.zip -d /var/www/ - chown -R www-data:www-data /var/www/nextcloud + rm -rf /var/www/nextcloud + unzip -q nextcloud.zip -d /var/www/ + chown -R www-data:www-data /var/www/nextcloud - log_success "Nextcloud downloaded and extracted" + log_success "Nextcloud downloaded and extracted" - # Configure Apache - log_info "Configuring Apache..." + # Configure Apache + log_info "Configuring Apache..." - cat >/etc/apache2/sites-available/nextcloud.conf <<'EOF' + cat > /etc/apache2/sites-available/nextcloud.conf << 'EOF' ServerAdmin admin@localhost DocumentRoot /var/www/nextcloud @@ -413,36 +413,36 @@ EOF EOF - a2enmod rewrite - a2enmod headers - a2enmod env - a2enmod dir - a2enmod mime - a2enmod ssl - a2dissite 000-default - a2ensite nextcloud + a2enmod rewrite + a2enmod headers + a2enmod env + a2enmod dir + a2enmod mime + a2enmod ssl + a2dissite 000-default + a2ensite nextcloud - systemctl restart apache2 + systemctl restart apache2 - log_success "Apache configured" + log_success "Apache configured" - # Configure PHP - log_info "Configuring PHP..." + # Configure PHP + log_info "Configuring PHP..." - local php_version - php_version=$(php -v | head -1 | grep -oP '\d+\.\d+') + local php_version + php_version=$(php -v | head -1 | grep -oP '\d+\.\d+') - local php_ini="/etc/php/${php_version}/apache2/php.ini" + local php_ini="/etc/php/${php_version}/apache2/php.ini" - sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" - sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" - sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" - sed -i 's/max_execution_time = .*/max_execution_time = 3600/' "$php_ini" - sed -i 's/max_input_time = .*/max_input_time = 3600/' "$php_ini" - sed -i 's/;date.timezone =.*/date.timezone = Europe\/Warsaw/' "$php_ini" + sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" + sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" + sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" + sed -i 's/max_execution_time = .*/max_execution_time = 3600/' "$php_ini" + sed -i 's/max_input_time = .*/max_input_time = 3600/' "$php_ini" + sed -i 's/;date.timezone =.*/date.timezone = Europe\/Warsaw/' "$php_ini" - if ! grep -q "opcache.interned_strings_buffer" "$php_ini"; then - cat >>"$php_ini" <<'EOF' + if ! grep -q "opcache.interned_strings_buffer" "$php_ini"; then + cat >> "$php_ini" << 'EOF' ; Nextcloud optimizations opcache.enable=1 @@ -455,90 +455,90 @@ opcache.revalidate_freq=1 ; APCu configuration apc.enable_cli=1 EOF - fi + fi - systemctl restart apache2 + systemctl restart apache2 - log_success "PHP configured" + log_success "PHP configured" - # Configure Redis - log_info "Configuring Redis..." + # Configure Redis + log_info "Configuring Redis..." - systemctl enable redis-server - systemctl start redis-server + systemctl enable redis-server + systemctl start redis-server - log_success "Redis configured" + log_success "Redis configured" - # Install Nextcloud - log_info "Installing Nextcloud..." + # Install Nextcloud + log_info "Installing Nextcloud..." - auto_generate_nextcloud_password + auto_generate_nextcloud_password - local pi_ip - pi_ip=$(hostname -I | awk '{print $1}') + local pi_ip + pi_ip=$(hostname -I | awk '{print $1}') - cd /var/www/nextcloud - sudo -u www-data php occ maintenance:install \ - --database "mysql" \ - --database-name "nextcloud" \ - --database-user "nextcloud" \ - --database-pass "$db_password" \ - --admin-user "$NEXTCLOUD_ADMIN_USER" \ - --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ - --data-dir "$NEXTCLOUD_DATA_DIR" + cd /var/www/nextcloud + sudo -u www-data php occ maintenance:install \ + --database "mysql" \ + --database-name "nextcloud" \ + --database-user "nextcloud" \ + --database-pass "$db_password" \ + --admin-user "$NEXTCLOUD_ADMIN_USER" \ + --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ + --data-dir "$NEXTCLOUD_DATA_DIR" - # Configure trusted domains - sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_ip" - sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" - sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" + # Configure trusted domains + sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_ip" + sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" + sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" - # Configure caching - sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' - sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' - sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' - sudo -u www-data php occ config:system:set redis host --value='localhost' - sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer + # Configure caching + sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' + sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set redis host --value='localhost' + sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer - # Set default phone region - sudo -u www-data php occ config:system:set default_phone_region --value='PL' + # Set default phone region + sudo -u www-data php occ config:system:set default_phone_region --value='PL' - # Set maintenance window - sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer + # Set maintenance window + sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer - log_success "Nextcloud installed" + log_success "Nextcloud installed" - # Setup background jobs - log_info "Setting up Nextcloud background jobs..." + # Setup background jobs + log_info "Setting up Nextcloud background jobs..." - sudo -u www-data php occ background:cron + sudo -u www-data php occ background:cron - # Add cron job - ( - crontab -u www-data -l 2>/dev/null || true - echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" - ) | sort -u | crontab -u www-data - + # Add cron job + ( + crontab -u www-data -l 2> /dev/null || true + echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" + ) | sort -u | crontab -u www-data - - log_success "Cron jobs configured" + log_success "Cron jobs configured" - # Verify installation - log_info "Verifying Nextcloud installation..." + # Verify installation + log_info "Verifying Nextcloud installation..." - if sudo -u www-data php occ status | grep -q "installed: true"; then - log_success "Nextcloud is responding!" - sudo -u www-data php occ status - else - log_warning "Nextcloud may not be fully configured" - fi + if sudo -u www-data php occ status | grep -q "installed: true"; then + log_success "Nextcloud is responding!" + sudo -u www-data php occ status + else + log_warning "Nextcloud may not be fully configured" + fi - save_config + save_config - log_success "========================================" - log_success "Nextcloud installation complete!" - log_success "========================================" - log_info "Access Nextcloud at: http://$pi_ip" - log_info "Admin user: $NEXTCLOUD_ADMIN_USER" - log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" - log_info "Database password saved at: /root/.nextcloud_db_password" + log_success "========================================" + log_success "Nextcloud installation complete!" + log_success "========================================" + log_info "Access Nextcloud at: http://$pi_ip" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_info "Database password saved at: /root/.nextcloud_db_password" } # ============================================================================= @@ -546,74 +546,74 @@ EOF # ============================================================================= phase_fix_issues() { - check_root + check_root - log_info "=== Fixing Nextcloud Issues ===" + log_info "=== Fixing Nextcloud Issues ===" - cd /var/www/nextcloud + cd /var/www/nextcloud - # 1. Fix background jobs (cron not running properly) - log_info "Fixing background jobs..." + # 1. Fix background jobs (cron not running properly) + log_info "Fixing background jobs..." - # Ensure cron is set as background job method - sudo -u www-data php occ background:cron + # Ensure cron is set as background job method + sudo -u www-data php occ background:cron - # Ensure cron job exists and is correct - ( - crontab -u www-data -l 2>/dev/null | grep -v "cron.php" - echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" - ) | crontab -u www-data - + # Ensure cron job exists and is correct + ( + crontab -u www-data -l 2> /dev/null | grep -v "cron.php" + echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" + ) | crontab -u www-data - - # Run cron manually now to reset the timer - log_info "Running cron job manually..." - sudo -u www-data php /var/www/nextcloud/cron.php + # Run cron manually now to reset the timer + log_info "Running cron job manually..." + sudo -u www-data php /var/www/nextcloud/cron.php - log_success "Background jobs configured" + log_success "Background jobs configured" - # 2. Setup HTTPS with proper CA-signed certificate - log_info "Setting up HTTPS with trusted CA..." + # 2. Setup HTTPS with proper CA-signed certificate + log_info "Setting up HTTPS with trusted CA..." - local pi_ip - pi_ip=$(hostname -I | awk '{print $1}') + local pi_ip + pi_ip=$(hostname -I | awk '{print $1}') - local ssl_dir="/etc/ssl/nextcloud" - mkdir -p "$ssl_dir" - chmod 700 "$ssl_dir" + local ssl_dir="/etc/ssl/nextcloud" + mkdir -p "$ssl_dir" + chmod 700 "$ssl_dir" - # Generate CA if it doesn't exist - if [[ ! -f "$ssl_dir/ca.crt" ]]; then - log_info "Creating Certificate Authority (CA)..." + # Generate CA if it doesn't exist + if [[ ! -f "$ssl_dir/ca.crt" ]]; then + log_info "Creating Certificate Authority (CA)..." - # Generate CA private key - openssl genrsa -out "$ssl_dir/ca.key" 4096 - chmod 600 "$ssl_dir/ca.key" + # Generate CA private key + openssl genrsa -out "$ssl_dir/ca.key" 4096 + chmod 600 "$ssl_dir/ca.key" - # Generate CA certificate (valid for 10 years) - openssl req -x509 -new -nodes -key "$ssl_dir/ca.key" \ - -sha256 -days 3650 \ - -out "$ssl_dir/ca.crt" \ - -subj "/C=PL/ST=Home/L=Local/O=Nextcloud Home CA/OU=Certificate Authority/CN=Nextcloud Home CA" + # Generate CA certificate (valid for 10 years) + openssl req -x509 -new -nodes -key "$ssl_dir/ca.key" \ + -sha256 -days 3650 \ + -out "$ssl_dir/ca.crt" \ + -subj "/C=PL/ST=Home/L=Local/O=Nextcloud Home CA/OU=Certificate Authority/CN=Nextcloud Home CA" - log_success "CA created: $ssl_dir/ca.crt" - fi + log_success "CA created: $ssl_dir/ca.crt" + fi - # Generate server certificate signed by our CA - local regenerate="${1:-}" - if [[ ! -f "$ssl_dir/server.crt" ]] || [[ $regenerate == "--regenerate" ]]; then - log_info "Generating server certificate signed by CA..." + # Generate server certificate signed by our CA + local regenerate="${1:-}" + if [[ ! -f "$ssl_dir/server.crt" ]] || [[ $regenerate == "--regenerate" ]]; then + log_info "Generating server certificate signed by CA..." - # Generate server private key - openssl genrsa -out "$ssl_dir/server.key" 2048 - chmod 600 "$ssl_dir/server.key" + # Generate server private key + openssl genrsa -out "$ssl_dir/server.key" 2048 + chmod 600 "$ssl_dir/server.key" - # Create certificate signing request (CSR) - openssl req -new -key "$ssl_dir/server.key" \ - -out "$ssl_dir/server.csr" \ - -subj "/C=PL/ST=Home/L=Local/O=Nextcloud/OU=Server/CN=$PI_HOSTNAME" + # Create certificate signing request (CSR) + openssl req -new -key "$ssl_dir/server.key" \ + -out "$ssl_dir/server.csr" \ + -subj "/C=PL/ST=Home/L=Local/O=Nextcloud/OU=Server/CN=$PI_HOSTNAME" - # Create extension file for SAN (Subject Alternative Names) - # This allows the certificate to be valid for hostname, IP, and .local - cat >"$ssl_dir/server.ext" < "$ssl_dir/server.ext" << EXTEOF authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment @@ -627,30 +627,30 @@ IP.1 = $pi_ip IP.2 = 127.0.0.1 EXTEOF - # Sign the certificate with our CA (valid for 2 years) - openssl x509 -req -in "$ssl_dir/server.csr" \ - -CA "$ssl_dir/ca.crt" \ - -CAkey "$ssl_dir/ca.key" \ - -CAcreateserial \ - -out "$ssl_dir/server.crt" \ - -days 730 \ - -sha256 \ - -extfile "$ssl_dir/server.ext" + # Sign the certificate with our CA (valid for 2 years) + openssl x509 -req -in "$ssl_dir/server.csr" \ + -CA "$ssl_dir/ca.crt" \ + -CAkey "$ssl_dir/ca.key" \ + -CAcreateserial \ + -out "$ssl_dir/server.crt" \ + -days 730 \ + -sha256 \ + -extfile "$ssl_dir/server.ext" - rm -f "$ssl_dir/server.csr" "$ssl_dir/server.ext" + rm -f "$ssl_dir/server.csr" "$ssl_dir/server.ext" - log_success "Server certificate created and signed by CA" - fi + log_success "Server certificate created and signed by CA" + fi - # Copy CA to web-accessible location for easy download - mkdir -p /var/www/nextcloud/ca - cp "$ssl_dir/ca.crt" /var/www/nextcloud/ca/nextcloud-ca.crt - chown -R www-data:www-data /var/www/nextcloud/ca + # Copy CA to web-accessible location for easy download + mkdir -p /var/www/nextcloud/ca + cp "$ssl_dir/ca.crt" /var/www/nextcloud/ca/nextcloud-ca.crt + chown -R www-data:www-data /var/www/nextcloud/ca - log_info "CA certificate available at: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" + log_info "CA certificate available at: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" - # Create HTTPS Apache config - cat >/etc/apache2/sites-available/nextcloud-ssl.conf < /etc/apache2/sites-available/nextcloud-ssl.conf << EOF ServerAdmin admin@localhost DocumentRoot /var/www/nextcloud @@ -690,81 +690,81 @@ EXTEOF EOF - a2enmod ssl - a2enmod headers - a2ensite nextcloud-ssl + a2enmod ssl + a2enmod headers + a2ensite nextcloud-ssl - # Update Nextcloud config for HTTPS - sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$PI_HOSTNAME" - sudo -u www-data php occ config:system:set overwriteprotocol --value="https" + # Update Nextcloud config for HTTPS + sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$PI_HOSTNAME" + sudo -u www-data php occ config:system:set overwriteprotocol --value="https" - systemctl restart apache2 + systemctl restart apache2 - log_success "HTTPS configured with CA-signed certificate" + log_success "HTTPS configured with CA-signed certificate" - # 3. Run mimetype migrations - log_info "Running mimetype migrations..." - sudo -u www-data php occ maintenance:repair --include-expensive - log_success "Mimetype migrations complete" + # 3. Run mimetype migrations + log_info "Running mimetype migrations..." + sudo -u www-data php occ maintenance:repair --include-expensive + log_success "Mimetype migrations complete" - # 4. Add missing database indices - log_info "Adding missing database indices..." - sudo -u www-data php occ db:add-missing-indices - log_success "Database indices added" + # 4. Add missing database indices + log_info "Adding missing database indices..." + sudo -u www-data php occ db:add-missing-indices + log_success "Database indices added" - # 5. Install ImageMagick SVG support - log_info "Installing ImageMagick SVG support..." - DEBIAN_FRONTEND=noninteractive apt-get install -y libmagickcore-6.q16-6-extra + # 5. Install ImageMagick SVG support + log_info "Installing ImageMagick SVG support..." + DEBIAN_FRONTEND=noninteractive apt-get install -y libmagickcore-6.q16-6-extra - # Enable SVG in ImageMagick policy - local policy_file="/etc/ImageMagick-6/policy.xml" - if [[ -f $policy_file ]]; then - # Remove SVG restrictions if present - sed -i 's///' "$policy_file" - # If no SVG policy exists, add one allowing it - if ! grep -q 'pattern="SVG"' "$policy_file"; then - sed -i '//a\ ' "$policy_file" - fi - fi + # Enable SVG in ImageMagick policy + local policy_file="/etc/ImageMagick-6/policy.xml" + if [[ -f $policy_file ]]; then + # Remove SVG restrictions if present + sed -i 's///' "$policy_file" + # If no SVG policy exists, add one allowing it + if ! grep -q 'pattern="SVG"' "$policy_file"; then + sed -i '//a\ ' "$policy_file" + fi + fi - systemctl restart apache2 - log_success "ImageMagick SVG support configured" + systemctl restart apache2 + log_success "ImageMagick SVG support configured" - # 6. Set up basic SMTP (placeholder - user needs to configure actual mail server) - log_info "Note: Email server not configured - please configure in Nextcloud admin settings" + # 6. Set up basic SMTP (placeholder - user needs to configure actual mail server) + log_info "Note: Email server not configured - please configure in Nextcloud admin settings" - # 7. Clear any remaining warnings - log_info "Clearing Nextcloud caches..." - sudo -u www-data php occ maintenance:repair - sudo -u www-data php occ files:scan --all + # 7. Clear any remaining warnings + log_info "Clearing Nextcloud caches..." + sudo -u www-data php occ maintenance:repair + sudo -u www-data php occ files:scan --all - # 8. Verify all fixes - log_info "Verifying fixes..." + # 8. Verify all fixes + log_info "Verifying fixes..." - # Run cron again to update last run time - sudo -u www-data php /var/www/nextcloud/cron.php + # Run cron again to update last run time + sudo -u www-data php /var/www/nextcloud/cron.php - log_success "========================================" - log_success "Nextcloud issues fixed!" - log_success "========================================" - echo - log_info "Summary of changes:" - log_info " ✓ Background jobs (cron) configured and running" - log_info " ✓ HTTPS enabled with CA-signed certificate" - log_info " ✓ Strict-Transport-Security header added" - log_info " ✓ Mimetype migrations completed" - log_info " ✓ Missing database indices added" - log_info " ✓ ImageMagick SVG support installed" - echo - log_info "Current certificate: self-signed CA (requires manual install on devices)" - log_info " - Run: $0 install-ca (on your laptop)" - log_info " - Or download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" - echo - log_info "For auto-trusted HTTPS on ALL devices (recommended):" - log_info " 1. Get free domain at https://www.duckdns.org/" - log_info " 2. Run: $0 setup-ssl" - echo - log_info "Access Nextcloud at: https://$PI_HOSTNAME" + log_success "========================================" + log_success "Nextcloud issues fixed!" + log_success "========================================" + echo + log_info "Summary of changes:" + log_info " ✓ Background jobs (cron) configured and running" + log_info " ✓ HTTPS enabled with CA-signed certificate" + log_info " ✓ Strict-Transport-Security header added" + log_info " ✓ Mimetype migrations completed" + log_info " ✓ Missing database indices added" + log_info " ✓ ImageMagick SVG support installed" + echo + log_info "Current certificate: self-signed CA (requires manual install on devices)" + log_info " - Run: $0 install-ca (on your laptop)" + log_info " - Or download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" + echo + log_info "For auto-trusted HTTPS on ALL devices (recommended):" + log_info " 1. Get free domain at https://www.duckdns.org/" + log_info " 2. Run: $0 setup-ssl" + echo + log_info "Access Nextcloud at: https://$PI_HOSTNAME" } # ============================================================================= @@ -772,113 +772,113 @@ EOF # ============================================================================= phase_setup_ssl() { - check_root + check_root - log_info "=== Setting up Let's Encrypt SSL with DuckDNS ===" + log_info "=== Setting up Let's Encrypt SSL with DuckDNS ===" - # Check if DuckDNS is configured - if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then - echo - log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." - log_info "1. Go to https://www.duckdns.org/ and sign in with Google/GitHub/etc." - log_info "2. Create a subdomain (e.g., 'myhomecloud' for myhomecloud.duckdns.org)" - log_info "3. Copy your token from the DuckDNS page" - echo + # Check if DuckDNS is configured + if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then + echo + log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." + log_info "1. Go to https://www.duckdns.org/ and sign in with Google/GitHub/etc." + log_info "2. Create a subdomain (e.g., 'myhomecloud' for myhomecloud.duckdns.org)" + log_info "3. Copy your token from the DuckDNS page" + echo - read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN - read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN - read -r -p "Enter your email (for Let's Encrypt notifications): " LETSENCRYPT_EMAIL + read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN + read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN + read -r -p "Enter your email (for Let's Encrypt notifications): " LETSENCRYPT_EMAIL - if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]] || [[ -z $LETSENCRYPT_EMAIL ]]; then - die "All fields are required" - fi - fi + if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]] || [[ -z $LETSENCRYPT_EMAIL ]]; then + die "All fields are required" + fi + fi - local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" - local pi_local_ip - pi_local_ip=$(hostname -I | awk '{print $1}') + local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" + local pi_local_ip + pi_local_ip=$(hostname -I | awk '{print $1}') - # Get public IP for DuckDNS (Let's Encrypt needs external access) - local public_ip - public_ip=$(curl -s https://api.ipify.org) || public_ip=$(curl -s https://ifconfig.me) || true + # Get public IP for DuckDNS (Let's Encrypt needs external access) + local public_ip + public_ip=$(curl -s https://api.ipify.org) || public_ip=$(curl -s https://ifconfig.me) || true - log_info "Domain: $full_domain" - log_info "Pi local IP: $pi_local_ip" - log_info "Public IP: $public_ip" + log_info "Domain: $full_domain" + log_info "Pi local IP: $pi_local_ip" + log_info "Public IP: $public_ip" - echo - log_warning "=== IMPORTANT: Port Forwarding Required ===" - log_warning "For Let's Encrypt to work, you MUST forward ports on your router:" - log_warning " - Forward port 80 (HTTP) to $pi_local_ip" - log_warning " - Forward port 443 (HTTPS) to $pi_local_ip" - log_warning "" - log_warning "Go to your router admin page (usually http://192.168.1.1)" - log_warning "and set up port forwarding before continuing." - echo - read -r -p "Have you set up port forwarding? (yes/no): " port_forward_done + echo + log_warning "=== IMPORTANT: Port Forwarding Required ===" + log_warning "For Let's Encrypt to work, you MUST forward ports on your router:" + log_warning " - Forward port 80 (HTTP) to $pi_local_ip" + log_warning " - Forward port 443 (HTTPS) to $pi_local_ip" + log_warning "" + log_warning "Go to your router admin page (usually http://192.168.1.1)" + log_warning "and set up port forwarding before continuing." + echo + read -r -p "Have you set up port forwarding? (yes/no): " port_forward_done - if [[ $port_forward_done != "yes" ]]; then - log_info "Please set up port forwarding and run this command again." - log_info "Without port forwarding, Let's Encrypt cannot verify your domain." - exit 0 - fi + if [[ $port_forward_done != "yes" ]]; then + log_info "Please set up port forwarding and run this command again." + log_info "Without port forwarding, Let's Encrypt cannot verify your domain." + exit 0 + fi - # Update DuckDNS to point to PUBLIC IP (not local IP) - log_info "Updating DuckDNS to point to public IP $public_ip..." - local duckdns_response - # When ip= is empty, DuckDNS auto-detects the public IP - duckdns_response=$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=") + # Update DuckDNS to point to PUBLIC IP (not local IP) + log_info "Updating DuckDNS to point to public IP $public_ip..." + local duckdns_response + # When ip= is empty, DuckDNS auto-detects the public IP + duckdns_response=$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=") - if [[ $duckdns_response != "OK" ]]; then - die "Failed to update DuckDNS: $duckdns_response" - fi - log_success "DuckDNS updated to public IP" + if [[ $duckdns_response != "OK" ]]; then + die "Failed to update DuckDNS: $duckdns_response" + fi + log_success "DuckDNS updated to public IP" - # Set up automatic DuckDNS updates (cron) - auto-detect public IP - log_info "Setting up automatic DuckDNS IP updates..." - mkdir -p /opt/duckdns - cat >/opt/duckdns/duck.sh < /opt/duckdns/duck.sh << DUCKEOF #!/bin/bash echo url="https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=" | curl -k -o /opt/duckdns/duck.log -K - DUCKEOF - chmod 700 /opt/duckdns/duck.sh + chmod 700 /opt/duckdns/duck.sh - # Add cron job for DuckDNS update every 5 minutes - (crontab -l 2>/dev/null || true) | grep -v "duckdns" | { - cat - echo "*/5 * * * * /opt/duckdns/duck.sh >/dev/null 2>&1" - } | crontab - + # Add cron job for DuckDNS update every 5 minutes + (crontab -l 2> /dev/null || true) | grep -v "duckdns" | { + cat + echo "*/5 * * * * /opt/duckdns/duck.sh >/dev/null 2>&1" + } | crontab - - log_success "DuckDNS auto-update configured" + log_success "DuckDNS auto-update configured" - # Wait for DNS propagation - log_info "Waiting for DNS propagation (this may take a minute)..." - local dns_ip="" - local attempts=0 - while [[ $dns_ip != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do - sleep 5 - dns_ip=$(dig +short "$full_domain" 2>/dev/null | tail -1) || true - attempts=$((attempts + 1)) - log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)" - done + # Wait for DNS propagation + log_info "Waiting for DNS propagation (this may take a minute)..." + local dns_ip="" + local attempts=0 + while [[ $dns_ip != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do + sleep 5 + dns_ip=$(dig +short "$full_domain" 2> /dev/null | tail -1) || true + attempts=$((attempts + 1)) + log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)" + done - if [[ $dns_ip != "$public_ip" ]]; then - log_warning "DNS may not have propagated yet. Continuing anyway..." - else - log_success "DNS verified: $full_domain -> $public_ip" - fi + if [[ $dns_ip != "$public_ip" ]]; then + log_warning "DNS may not have propagated yet. Continuing anyway..." + else + log_success "DNS verified: $full_domain -> $public_ip" + fi - # Install certbot if not present - if ! command -v certbot &>/dev/null; then - log_info "Installing certbot..." - DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-apache - fi + # Install certbot if not present + if ! command -v certbot &> /dev/null; then + log_info "Installing certbot..." + DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-apache + fi - # Get Let's Encrypt certificate - log_info "Obtaining Let's Encrypt certificate..." + # Get Let's Encrypt certificate + log_info "Obtaining Let's Encrypt certificate..." - # First update Apache config with the new domain - cat >/etc/apache2/sites-available/nextcloud-ssl.conf < /etc/apache2/sites-available/nextcloud-ssl.conf << EOF ServerAdmin ${LETSENCRYPT_EMAIL} DocumentRoot /var/www/nextcloud @@ -916,93 +916,93 @@ DUCKEOF EOF - systemctl reload apache2 + systemctl reload apache2 - # Run certbot - certbot --apache -d "$full_domain" --non-interactive --agree-tos --email "$LETSENCRYPT_EMAIL" --redirect + # Run certbot + certbot --apache -d "$full_domain" --non-interactive --agree-tos --email "$LETSENCRYPT_EMAIL" --redirect - log_success "Let's Encrypt certificate obtained!" + log_success "Let's Encrypt certificate obtained!" - # Update Nextcloud trusted domains - log_info "Updating Nextcloud configuration..." - cd /var/www/nextcloud - sudo -u www-data php occ config:system:set trusted_domains 0 --value="$full_domain" - sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$full_domain" - sudo -u www-data php occ config:system:set overwriteprotocol --value="https" - sudo -u www-data php occ config:system:set overwritehost --value="$full_domain" + # Update Nextcloud trusted domains + log_info "Updating Nextcloud configuration..." + cd /var/www/nextcloud + sudo -u www-data php occ config:system:set trusted_domains 0 --value="$full_domain" + sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$full_domain" + sudo -u www-data php occ config:system:set overwriteprotocol --value="https" + sudo -u www-data php occ config:system:set overwritehost --value="$full_domain" - # Keep local access working - sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_local_ip" - sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" - sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" + # Keep local access working + sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_local_ip" + sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" + sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" - log_success "Nextcloud configured for $full_domain" + log_success "Nextcloud configured for $full_domain" - # Set up auto-renewal - log_info "Setting up automatic certificate renewal..." - systemctl enable certbot.timer - systemctl start certbot.timer + # Set up auto-renewal + log_info "Setting up automatic certificate renewal..." + systemctl enable certbot.timer + systemctl start certbot.timer - log_success "========================================" - log_success "Let's Encrypt SSL configured!" - log_success "========================================" - echo - log_info "Your Nextcloud is now accessible at:" - log_info " https://$full_domain (from anywhere on the internet)" - log_info " https://$pi_local_ip (from your local network)" - echo - log_info "This certificate is trusted by ALL browsers and devices automatically!" - log_info "No manual certificate installation required." - echo - log_info "Certificate auto-renewal is enabled." - log_info "DuckDNS IP auto-update is enabled." + log_success "========================================" + log_success "Let's Encrypt SSL configured!" + log_success "========================================" + echo + log_info "Your Nextcloud is now accessible at:" + log_info " https://$full_domain (from anywhere on the internet)" + log_info " https://$pi_local_ip (from your local network)" + echo + log_info "This certificate is trusted by ALL browsers and devices automatically!" + log_info "No manual certificate installation required." + echo + log_info "Certificate auto-renewal is enabled." + log_info "DuckDNS IP auto-update is enabled." } phase_setup_ssl_remote() { - log_info "=== Setting up Let's Encrypt SSL via SSH ===" + log_info "=== Setting up Let's Encrypt SSL via SSH ===" - if [[ -z $PI_PASSWORD ]]; then - die "PI_PASSWORD not set. Run install-remote first." - fi + if [[ -z $PI_PASSWORD ]]; then + die "PI_PASSWORD not set. Run install-remote first." + fi - local pi_ip - pi_ip=$(discover_raspberry_pi) + local pi_ip + pi_ip=$(discover_raspberry_pi) - if [[ -z $pi_ip ]]; then - die "Failed to discover Raspberry Pi" - fi + if [[ -z $pi_ip ]]; then + die "Failed to discover Raspberry Pi" + fi - # Get DuckDNS credentials if not set - if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then - echo - log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." - log_info "1. Go to https://www.duckdns.org/ and sign in" - log_info "2. Create a subdomain (e.g., 'myhomecloud')" - log_info "3. Copy your token" - echo + # Get DuckDNS credentials if not set + if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then + echo + log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." + log_info "1. Go to https://www.duckdns.org/ and sign in" + log_info "2. Create a subdomain (e.g., 'myhomecloud')" + log_info "3. Copy your token" + echo - read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN - read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN - read -r -p "Enter your email (for Let's Encrypt): " LETSENCRYPT_EMAIL - fi + read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN + read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN + read -r -p "Enter your email (for Let's Encrypt): " LETSENCRYPT_EMAIL + fi - save_config + save_config - log_info "Copying script to Pi..." - sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" + log_info "Copying script to Pi..." + sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" - log_info "Running SSL setup on Pi..." - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S DUCKDNS_DOMAIN='$DUCKDNS_DOMAIN' DUCKDNS_TOKEN='$DUCKDNS_TOKEN' LETSENCRYPT_EMAIL='$LETSENCRYPT_EMAIL' bash /tmp/raspberry_pi_nextcloud.sh setup-ssl" + log_info "Running SSL setup on Pi..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S DUCKDNS_DOMAIN='$DUCKDNS_DOMAIN' DUCKDNS_TOKEN='$DUCKDNS_TOKEN' LETSENCRYPT_EMAIL='$LETSENCRYPT_EMAIL' bash /tmp/raspberry_pi_nextcloud.sh setup-ssl" - local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" + local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" - log_success "========================================" - log_success "SSL setup complete!" - log_success "========================================" - echo - log_info "Access your Nextcloud at: https://$full_domain" - log_info "This works on ALL devices without certificate warnings!" + log_success "========================================" + log_success "SSL setup complete!" + log_success "========================================" + echo + log_info "Access your Nextcloud at: https://$full_domain" + log_info "This works on ALL devices without certificate warnings!" } # ============================================================================= @@ -1010,57 +1010,57 @@ phase_setup_ssl_remote() { # ============================================================================= phase_install_remote() { - log_info "=== Installing Nextcloud via SSH ===" + log_info "=== Installing Nextcloud via SSH ===" - if [[ -z $PI_PASSWORD ]]; then - die "PI_PASSWORD not set. Did you run flash script first?" - fi + if [[ -z $PI_PASSWORD ]]; then + die "PI_PASSWORD not set. Did you run flash script first?" + fi - local pi_ip - pi_ip=$(discover_raspberry_pi) + local pi_ip + pi_ip=$(discover_raspberry_pi) - if [[ -z $pi_ip ]]; then - die "Failed to discover Raspberry Pi" - fi + if [[ -z $pi_ip ]]; then + die "Failed to discover Raspberry Pi" + fi - log_info "Using Raspberry Pi at: $pi_ip" + log_info "Using Raspberry Pi at: $pi_ip" - # Remove old host key if present - ssh-keygen -R "$pi_ip" 2>/dev/null || true + # Remove old host key if present + ssh-keygen -R "$pi_ip" 2> /dev/null || true - log_info "Copying script to Pi..." - sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" + log_info "Copying script to Pi..." + sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" - log_info "Running system configuration on Pi..." - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh configure" + log_info "Running system configuration on Pi..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh configure" - log_info "Installing Nextcloud on Pi..." - auto_generate_nextcloud_password - save_config + log_info "Installing Nextcloud on Pi..." + auto_generate_nextcloud_password + save_config - log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" - log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" + log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/raspberry_pi_nextcloud.sh install-local" + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/raspberry_pi_nextcloud.sh install-local" - log_info "Fixing Nextcloud issues..." - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh fix" + log_info "Fixing Nextcloud issues..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh fix" - log_success "========================================" - log_success "Remote Nextcloud installation complete!" - log_success "========================================" - echo - log_info "=== Access Information ===" - log_info "Nextcloud URL: https://$pi_ip" - log_info "Admin user: $NEXTCLOUD_ADMIN_USER" - log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" - log_info "All credentials saved in: $CONFIG_FILE" - echo - log_info "=== Trust the certificate ===" - log_info "Run: $0 install-ca" + log_success "========================================" + log_success "Remote Nextcloud installation complete!" + log_success "========================================" + echo + log_info "=== Access Information ===" + log_info "Nextcloud URL: https://$pi_ip" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_info "All credentials saved in: $CONFIG_FILE" + echo + log_info "=== Trust the certificate ===" + log_info "Run: $0 install-ca" } # ============================================================================= @@ -1068,127 +1068,127 @@ phase_install_remote() { # ============================================================================= phase_install_ca() { - log_info "=== Installing Nextcloud CA Certificate ===" + log_info "=== Installing Nextcloud CA Certificate ===" - if [[ -z $PI_PASSWORD ]]; then - die "PI_PASSWORD not set. Run this after running install-remote or flash." - fi + if [[ -z $PI_PASSWORD ]]; then + die "PI_PASSWORD not set. Run this after running install-remote or flash." + fi - local pi_ip - pi_ip=$(discover_raspberry_pi) + local pi_ip + pi_ip=$(discover_raspberry_pi) - if [[ -z $pi_ip ]]; then - die "Failed to discover Raspberry Pi" - fi + if [[ -z $pi_ip ]]; then + die "Failed to discover Raspberry Pi" + fi - log_info "Downloading CA certificate from Pi..." + log_info "Downloading CA certificate from Pi..." - local ca_file="/tmp/nextcloud-ca.crt" + local ca_file="/tmp/nextcloud-ca.crt" - # Use SSH with sudo to cat the file (since it's in a protected directory) - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no \ - "${PI_USER}@${pi_ip}" "echo '$PI_PASSWORD' | sudo -S cat /etc/ssl/nextcloud/ca.crt" >"$ca_file" 2>/dev/null + # Use SSH with sudo to cat the file (since it's in a protected directory) + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no \ + "${PI_USER}@${pi_ip}" "echo '$PI_PASSWORD' | sudo -S cat /etc/ssl/nextcloud/ca.crt" > "$ca_file" 2> /dev/null - if [[ ! -f $ca_file ]] || [[ ! -s $ca_file ]]; then - die "Failed to download CA certificate" - fi + if [[ ! -f $ca_file ]] || [[ ! -s $ca_file ]]; then + die "Failed to download CA certificate" + fi - log_success "CA certificate downloaded to: $ca_file" + log_success "CA certificate downloaded to: $ca_file" - # Detect OS and install appropriately - if [[ -f /etc/arch-release ]]; then - log_info "Detected Arch Linux - installing CA..." - sudo cp "$ca_file" /etc/ca-certificates/trust-source/anchors/nextcloud-ca.crt - sudo trust extract-compat - log_success "CA installed in system trust store" + # Detect OS and install appropriately + if [[ -f /etc/arch-release ]]; then + log_info "Detected Arch Linux - installing CA..." + sudo cp "$ca_file" /etc/ca-certificates/trust-source/anchors/nextcloud-ca.crt + sudo trust extract-compat + log_success "CA installed in system trust store" - elif [[ -f /etc/debian_version ]]; then - log_info "Detected Debian/Ubuntu - installing CA..." - sudo cp "$ca_file" /usr/local/share/ca-certificates/nextcloud-ca.crt - sudo update-ca-certificates - log_success "CA installed in system trust store" + elif [[ -f /etc/debian_version ]]; then + log_info "Detected Debian/Ubuntu - installing CA..." + sudo cp "$ca_file" /usr/local/share/ca-certificates/nextcloud-ca.crt + sudo update-ca-certificates + log_success "CA installed in system trust store" - elif [[ -f /etc/redhat-release ]]; then - log_info "Detected RHEL/Fedora - installing CA..." - sudo cp "$ca_file" /etc/pki/ca-trust/source/anchors/nextcloud-ca.crt - sudo update-ca-trust - log_success "CA installed in system trust store" + elif [[ -f /etc/redhat-release ]]; then + log_info "Detected RHEL/Fedora - installing CA..." + sudo cp "$ca_file" /etc/pki/ca-trust/source/anchors/nextcloud-ca.crt + sudo update-ca-trust + log_success "CA installed in system trust store" - elif [[ "$(uname)" == "Darwin" ]]; then - log_info "Detected macOS - installing CA..." - sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$ca_file" - log_success "CA installed in system keychain" + elif [[ "$(uname)" == "Darwin" ]]; then + log_info "Detected macOS - installing CA..." + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$ca_file" + log_success "CA installed in system keychain" - else - log_warning "Unknown OS - please install CA manually from: $ca_file" - fi + else + log_warning "Unknown OS - please install CA manually from: $ca_file" + fi - # Install in browser certificate stores - log_info "Installing CA in browser certificate stores..." + # Install in browser certificate stores + log_info "Installing CA in browser certificate stores..." - # Chrome/Chromium (uses NSS) - if [[ -d ~/.pki/nssdb ]] || command -v certutil &>/dev/null; then - mkdir -p ~/.pki/nssdb - if ! certutil -d sql:~/.pki/nssdb -L 2>/dev/null | grep -q "Nextcloud"; then - # Initialize NSS db if needed - certutil -d sql:~/.pki/nssdb -N --empty-password 2>/dev/null || true - if certutil -d sql:~/.pki/nssdb -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null; then - log_success "CA installed in Chrome/Chromium" - else - log_warning "Could not install in Chrome/Chromium NSS db" - fi - else - log_info "CA already installed in Chrome/Chromium" - fi - fi + # Chrome/Chromium (uses NSS) + if [[ -d ~/.pki/nssdb ]] || command -v certutil &> /dev/null; then + mkdir -p ~/.pki/nssdb + if ! certutil -d sql:~/.pki/nssdb -L 2> /dev/null | grep -q "Nextcloud"; then + # Initialize NSS db if needed + certutil -d sql:~/.pki/nssdb -N --empty-password 2> /dev/null || true + if certutil -d sql:~/.pki/nssdb -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2> /dev/null; then + log_success "CA installed in Chrome/Chromium" + else + log_warning "Could not install in Chrome/Chromium NSS db" + fi + else + log_info "CA already installed in Chrome/Chromium" + fi + fi - # Firefox (has its own profile NSS databases) - if [[ -d ~/.mozilla/firefox ]]; then - local installed=0 - for profile_dir in ~/.mozilla/firefox/*.default* ~/.mozilla/firefox/*.esr*; do - if [[ -d $profile_dir ]]; then - if ! certutil -d sql:"$profile_dir" -L 2>/dev/null | grep -q "Nextcloud"; then - certutil -d sql:"$profile_dir" -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null && - installed=1 - else - installed=1 - fi - fi - done - if [[ $installed -eq 1 ]]; then - log_success "CA installed in Firefox" - else - log_warning "Could not install in Firefox - you may need to import manually" - fi - fi + # Firefox (has its own profile NSS databases) + if [[ -d ~/.mozilla/firefox ]]; then + local installed=0 + for profile_dir in ~/.mozilla/firefox/*.default* ~/.mozilla/firefox/*.esr*; do + if [[ -d $profile_dir ]]; then + if ! certutil -d sql:"$profile_dir" -L 2> /dev/null | grep -q "Nextcloud"; then + certutil -d sql:"$profile_dir" -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2> /dev/null && + installed=1 + else + installed=1 + fi + fi + done + if [[ $installed -eq 1 ]]; then + log_success "CA installed in Firefox" + else + log_warning "Could not install in Firefox - you may need to import manually" + fi + fi - # Add hostname to /etc/hosts if not present - if ! grep -q "$PI_HOSTNAME" /etc/hosts 2>/dev/null; then - log_info "Adding $PI_HOSTNAME to /etc/hosts..." - echo "$pi_ip $PI_HOSTNAME ${PI_HOSTNAME}.local" | sudo tee -a /etc/hosts >/dev/null - log_success "Added $PI_HOSTNAME to /etc/hosts" - else - log_info "$PI_HOSTNAME already in /etc/hosts" - fi + # Add hostname to /etc/hosts if not present + if ! grep -q "$PI_HOSTNAME" /etc/hosts 2> /dev/null; then + log_info "Adding $PI_HOSTNAME to /etc/hosts..." + echo "$pi_ip $PI_HOSTNAME ${PI_HOSTNAME}.local" | sudo tee -a /etc/hosts > /dev/null + log_success "Added $PI_HOSTNAME to /etc/hosts" + else + log_info "$PI_HOSTNAME already in /etc/hosts" + fi - # Verify - log_info "Verifying HTTPS connection..." - if curl -s --max-time 5 "https://$PI_HOSTNAME/status.php" 2>/dev/null | grep -q "installed"; then - log_success "HTTPS connection verified - no certificate warnings!" - else - log_warning "Could not verify HTTPS - you may need to restart your browser" - fi + # Verify + log_info "Verifying HTTPS connection..." + if curl -s --max-time 5 "https://$PI_HOSTNAME/status.php" 2> /dev/null | grep -q "installed"; then + log_success "HTTPS connection verified - no certificate warnings!" + else + log_warning "Could not verify HTTPS - you may need to restart your browser" + fi - log_success "========================================" - log_success "CA Certificate installed!" - log_success "========================================" - echo - log_info "Access Nextcloud at: https://$PI_HOSTNAME" - log_info "Your browser should now trust the certificate without warnings." - echo - log_info "For other devices (phones, tablets, other computers):" - log_info " Download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" - log_info " Then install the certificate in your device's trust store." + log_success "========================================" + log_success "CA Certificate installed!" + log_success "========================================" + echo + log_info "Access Nextcloud at: https://$PI_HOSTNAME" + log_info "Your browser should now trust the certificate without warnings." + echo + log_info "For other devices (phones, tablets, other computers):" + log_info " Download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" + log_info " Then install the certificate in your device's trust store." } # ============================================================================= @@ -1196,7 +1196,7 @@ phase_install_ca() { # ============================================================================= show_help() { - cat <<'EOF' + cat << 'EOF' Nextcloud Installation Script for Raspberry Pi Usage: ./raspberry_pi_nextcloud.sh @@ -1237,39 +1237,39 @@ EOF } main() { - local command="${1:-help}" + local command="${1:-help}" - case "$command" in - install-remote) - phase_install_remote - ;; - setup-ssl-remote) - phase_setup_ssl_remote - ;; - setup-ssl) - phase_setup_ssl - ;; - install-ca) - phase_install_ca - ;; - configure) - phase_configure_system - ;; - install-local | install) - phase_install_nextcloud - ;; - fix) - phase_fix_issues - ;; - help | --help | -h) - show_help - ;; - *) - log_error "Unknown command: $command" - show_help - exit 1 - ;; - esac + case "$command" in + install-remote) + phase_install_remote + ;; + setup-ssl-remote) + phase_setup_ssl_remote + ;; + setup-ssl) + phase_setup_ssl + ;; + install-ca) + phase_install_ca + ;; + configure) + phase_configure_system + ;; + install-local | install) + phase_install_nextcloud + ;; + fix) + phase_fix_issues + ;; + help | --help | -h) + show_help + ;; + *) + log_error "Unknown command: $command" + show_help + exit 1 + ;; + esac } main "$@" diff --git a/scripts/features/setup_activitywatch.sh b/scripts/features/setup_activitywatch.sh index 6e91332..0a8aa5d 100755 --- a/scripts/features/setup_activitywatch.sh +++ b/scripts/features/setup_activitywatch.sh @@ -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" < "$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 diff --git a/scripts/features/setup_nextcloud_raspberry.sh b/scripts/features/setup_nextcloud_raspberry.sh index d14cc27..1367076 100644 --- a/scripts/features/setup_nextcloud_raspberry.sh +++ b/scripts/features/setup_nextcloud_raspberry.sh @@ -20,8 +20,8 @@ CONFIG_FILE="${SCRIPT_DIR}/.nextcloud_raspberry.conf" # Load configuration from gitignored config file if it exists if [[ -f $CONFIG_FILE ]]; then - # shellcheck source=/dev/null - source "$CONFIG_FILE" + # shellcheck source=/dev/null + source "$CONFIG_FILE" fi # Configuration - Customize these values (or set in config file) @@ -48,35 +48,35 @@ NC='\033[0m' # No Color # All log functions output to stderr so they don't interfere with function return values log_info() { - echo -e "${BLUE}[INFO]${NC} $1" >&2 + echo -e "${BLUE}[INFO]${NC} $1" >&2 } log_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 + echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 } log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" >&2 + echo -e "${YELLOW}[WARNING]${NC} $1" >&2 } log_error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 + echo -e "${RED}[ERROR]${NC} $1" >&2 } die() { - log_error "$1" - exit 1 + log_error "$1" + exit 1 } check_root() { - if [[ $EUID -ne 0 ]]; then - die "This script must be run as root. Use: sudo $0" - fi + if [[ $EUID -ne 0 ]]; then + die "This script must be run as root. Use: sudo $0" + fi } save_config() { - # Save discovered/used configuration to gitignored config file - cat >"$CONFIG_FILE" < "$CONFIG_FILE" << EOF # Nextcloud Raspberry Pi Setup - Auto-generated config # This file is gitignored and stores discovered settings @@ -93,57 +93,57 @@ NEXTCLOUD_ADMIN_USER="${NEXTCLOUD_ADMIN_USER}" PI_PASSWORD="${PI_PASSWORD}" NEXTCLOUD_ADMIN_PASSWORD="${NEXTCLOUD_ADMIN_PASSWORD}" EOF - chmod 600 "$CONFIG_FILE" - log_info "Configuration saved to $CONFIG_FILE" + chmod 600 "$CONFIG_FILE" + log_info "Configuration saved to $CONFIG_FILE" } generate_password() { - # Generate a secure random password (16 chars, alphanumeric + some symbols) - local length="${1:-16}" - # Use /dev/urandom for randomness, base64 encode, take first N chars - # Using dd to avoid SIGPIPE with pipefail - local chars - chars=$(dd if=/dev/urandom bs=256 count=1 2>/dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") - echo "$chars" + # Generate a secure random password (16 chars, alphanumeric + some symbols) + local length="${1:-16}" + # Use /dev/urandom for randomness, base64 encode, take first N chars + # Using dd to avoid SIGPIPE with pipefail + local chars + chars=$(dd if=/dev/urandom bs=256 count=1 2> /dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") + echo "$chars" } auto_generate_pi_password() { - if [[ -z $PI_PASSWORD ]]; then - PI_PASSWORD=$(generate_password 16) - log_info "Auto-generated Pi password (will be saved to config file)" - fi + if [[ -z $PI_PASSWORD ]]; then + PI_PASSWORD=$(generate_password 16) + log_info "Auto-generated Pi password (will be saved to config file)" + fi } auto_generate_nextcloud_password() { - if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then - NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20) - log_info "Auto-generated Nextcloud admin password (will be saved to config file)" - fi + if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then + NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20) + log_info "Auto-generated Nextcloud admin password (will be saved to config file)" + fi } prompt_password() { - local prompt="$1" - local var_name="$2" - local password="" - local password_confirm="" + local prompt="$1" + local var_name="$2" + local password="" + local password_confirm="" - while true; do - read -r -s -p "$prompt: " password - echo - read -r -s -p "Confirm password: " password_confirm - echo + while true; do + read -r -s -p "$prompt: " password + echo + read -r -s -p "Confirm password: " password_confirm + echo - if [[ $password == "$password_confirm" ]]; then - if [[ -z $password ]]; then - log_warning "Password cannot be empty. Please try again." - continue - fi - eval "$var_name='$password'" - break - else - log_warning "Passwords do not match. Please try again." - fi - done + if [[ $password == "$password_confirm" ]]; then + if [[ -z $password ]]; then + log_warning "Password cannot be empty. Please try again." + continue + fi + eval "$var_name='$password'" + break + else + log_warning "Passwords do not match. Please try again." + fi + done } # ============================================================================= @@ -151,161 +151,161 @@ prompt_password() { # ============================================================================= detect_sd_card() { - log_info "Detecting removable storage devices..." + log_info "Detecting removable storage devices..." - # List block devices that are removable - local devices - devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}') + # List block devices that are removable + local devices + devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}') - if [[ -z $devices ]]; then - log_warning "No removable devices detected automatically." - log_info "Available block devices:" - lsblk -d -o NAME,SIZE,TYPE,RM,TRAN - echo - read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE - else - echo "Detected removable devices:" - echo "$devices" - echo - read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE - fi + if [[ -z $devices ]]; then + log_warning "No removable devices detected automatically." + log_info "Available block devices:" + lsblk -d -o NAME,SIZE,TYPE,RM,TRAN + echo + read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE + else + echo "Detected removable devices:" + echo "$devices" + echo + read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE + fi - # Validate device exists - if [[ ! -b $SD_CARD_DEVICE ]]; then - die "Device $SD_CARD_DEVICE does not exist or is not a block device" - fi + # Validate device exists + if [[ ! -b $SD_CARD_DEVICE ]]; then + die "Device $SD_CARD_DEVICE does not exist or is not a block device" + fi - # Safety check - don't flash system drive - local root_device - root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//') - if [[ $SD_CARD_DEVICE == "$root_device" ]]; then - die "Cannot flash to the system drive!" - fi + # Safety check - don't flash system drive + local root_device + root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//') + if [[ $SD_CARD_DEVICE == "$root_device" ]]; then + die "Cannot flash to the system drive!" + fi } download_raspberry_pi_os() { - local download_dir="/tmp/rpi-image" - local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz" - local image_file="$download_dir/raspios.img.xz" - local extracted_image="$download_dir/raspios.img" - local expected_size=459000608 # Size in bytes from content-length + local download_dir="/tmp/rpi-image" + local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz" + local image_file="$download_dir/raspios.img.xz" + local extracted_image="$download_dir/raspios.img" + local expected_size=459000608 # Size in bytes from content-length - mkdir -p "$download_dir" + mkdir -p "$download_dir" - if [[ -f $extracted_image ]]; then - log_info "Using existing image at $extracted_image" - echo "$extracted_image" - return - fi + if [[ -f $extracted_image ]]; then + log_info "Using existing image at $extracted_image" + echo "$extracted_image" + return + fi - # Check if download exists and is complete - if [[ -f $image_file ]]; then - local actual_size - actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) - if [[ $actual_size -lt $expected_size ]]; then - log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..." - rm -f "$image_file" - else - log_info "Image archive already downloaded" - fi - fi + # Check if download exists and is complete + if [[ -f $image_file ]]; then + local actual_size + actual_size=$(stat -c%s "$image_file" 2> /dev/null || stat -f%z "$image_file" 2> /dev/null || echo 0) + if [[ $actual_size -lt $expected_size ]]; then + log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..." + rm -f "$image_file" + else + log_info "Image archive already downloaded" + fi + fi - if [[ ! -f $image_file ]]; then - log_info "Downloading Raspberry Pi OS Lite (64-bit)..." - log_info "This may take a while depending on your internet connection..." + if [[ ! -f $image_file ]]; then + log_info "Downloading Raspberry Pi OS Lite (64-bit)..." + log_info "This may take a while depending on your internet connection..." - # Try to use aria2c for faster download, fall back to wget/curl - # Redirect all output to stderr so it doesn't interfere with function return value - if command -v aria2c &>/dev/null; then - aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2 - elif command -v wget &>/dev/null; then - wget --continue --show-progress -O "$image_file" "$image_url" >&2 - elif command -v curl &>/dev/null; then - curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2 - else - die "No download tool available. Install wget, curl, or aria2c" - fi + # Try to use aria2c for faster download, fall back to wget/curl + # Redirect all output to stderr so it doesn't interfere with function return value + if command -v aria2c &> /dev/null; then + aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2 + elif command -v wget &> /dev/null; then + wget --continue --show-progress -O "$image_file" "$image_url" >&2 + elif command -v curl &> /dev/null; then + curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2 + else + die "No download tool available. Install wget, curl, or aria2c" + fi - # Verify download size - local actual_size - actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) - if [[ $actual_size -lt $expected_size ]]; then - die "Download incomplete: got $actual_size bytes, expected $expected_size" - fi - log_success "Download complete: $actual_size bytes" - fi + # Verify download size + local actual_size + actual_size=$(stat -c%s "$image_file" 2> /dev/null || stat -f%z "$image_file" 2> /dev/null || echo 0) + if [[ $actual_size -lt $expected_size ]]; then + die "Download incomplete: got $actual_size bytes, expected $expected_size" + fi + log_success "Download complete: $actual_size bytes" + fi - log_info "Extracting image..." - xz -dk "$image_file" + log_info "Extracting image..." + xz -dk "$image_file" - if [[ ! -f $extracted_image ]]; then - die "Failed to extract image" - fi + if [[ ! -f $extracted_image ]]; then + die "Failed to extract image" + fi - echo "$extracted_image" + echo "$extracted_image" } flash_sd_card() { - local image_path="$1" + local image_path="$1" - log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE" - read -r -p "Are you sure you want to continue? (yes/no): " confirm + log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE" + read -r -p "Are you sure you want to continue? (yes/no): " confirm - if [[ $confirm != "yes" ]]; then - die "Aborted by user" - fi + if [[ $confirm != "yes" ]]; then + die "Aborted by user" + fi - # Unmount any mounted partitions - log_info "Unmounting partitions on $SD_CARD_DEVICE..." - for partition in "${SD_CARD_DEVICE}"*; do - if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then - umount "$partition" 2>/dev/null || true - fi - done + # Unmount any mounted partitions + log_info "Unmounting partitions on $SD_CARD_DEVICE..." + for partition in "${SD_CARD_DEVICE}"*; do + if mountpoint -q "$partition" 2> /dev/null || mount | grep -q "$partition"; then + umount "$partition" 2> /dev/null || true + fi + done - log_info "Flashing image to SD card..." - log_info "This will take several minutes..." + log_info "Flashing image to SD card..." + log_info "This will take several minutes..." - dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync + dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync - sync - log_success "Image flashed successfully!" + sync + log_success "Image flashed successfully!" } configure_headless_boot() { - log_info "Configuring headless boot (SSH and WiFi)..." + log_info "Configuring headless boot (SSH and WiFi)..." - # Wait for partitions to be available - sleep 2 - partprobe "$SD_CARD_DEVICE" 2>/dev/null || true - sleep 2 + # Wait for partitions to be available + sleep 2 + partprobe "$SD_CARD_DEVICE" 2> /dev/null || true + sleep 2 - # Mount boot partition - local boot_partition - if [[ -b "${SD_CARD_DEVICE}1" ]]; then - boot_partition="${SD_CARD_DEVICE}1" - elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then - boot_partition="${SD_CARD_DEVICE}p1" - else - die "Could not find boot partition" - fi + # Mount boot partition + local boot_partition + if [[ -b "${SD_CARD_DEVICE}1" ]]; then + boot_partition="${SD_CARD_DEVICE}1" + elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then + boot_partition="${SD_CARD_DEVICE}p1" + else + die "Could not find boot partition" + fi - local boot_mount="/tmp/rpi-boot" - mkdir -p "$boot_mount" - mount "$boot_partition" "$boot_mount" + local boot_mount="/tmp/rpi-boot" + mkdir -p "$boot_mount" + mount "$boot_partition" "$boot_mount" - # Enable SSH - touch "$boot_mount/ssh" - log_success "SSH enabled" + # Enable SSH + touch "$boot_mount/ssh" + log_success "SSH enabled" - # Configure WiFi (optional) - read -r -p "Do you want to configure WiFi? (y/n): " configure_wifi - if [[ $configure_wifi == "y" ]]; then - read -r -p "WiFi SSID: " wifi_ssid - read -r -s -p "WiFi Password: " wifi_password - echo + # Configure WiFi (optional) + read -r -p "Do you want to configure WiFi? (y/n): " configure_wifi + if [[ $configure_wifi == "y" ]]; then + read -r -p "WiFi SSID: " wifi_ssid + read -r -s -p "WiFi Password: " wifi_password + echo - cat >"$boot_mount/wpa_supplicant.conf" < "$boot_mount/wpa_supplicant.conf" << EOF country=US ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 @@ -316,66 +316,66 @@ network={ key_mgmt=WPA-PSK } EOF - log_success "WiFi configured" - fi + log_success "WiFi configured" + fi - # Create userconf.txt for first user (Raspberry Pi OS Bookworm+) - if [[ -z $PI_PASSWORD ]]; then - prompt_password "Enter password for Pi user '$PI_USER'" PI_PASSWORD - fi + # Create userconf.txt for first user (Raspberry Pi OS Bookworm+) + if [[ -z $PI_PASSWORD ]]; then + prompt_password "Enter password for Pi user '$PI_USER'" PI_PASSWORD + fi - local encrypted_password - encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) - echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" - log_success "User '$PI_USER' configured" + local encrypted_password + encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) + echo "${PI_USER}:${encrypted_password}" > "$boot_mount/userconf.txt" + log_success "User '$PI_USER' configured" - # Set hostname - local root_partition - if [[ -b "${SD_CARD_DEVICE}2" ]]; then - root_partition="${SD_CARD_DEVICE}2" - elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then - root_partition="${SD_CARD_DEVICE}p2" - fi + # Set hostname + local root_partition + if [[ -b "${SD_CARD_DEVICE}2" ]]; then + root_partition="${SD_CARD_DEVICE}2" + elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then + root_partition="${SD_CARD_DEVICE}p2" + fi - if [[ -n $root_partition ]]; then - local root_mount="/tmp/rpi-root" - mkdir -p "$root_mount" - mount "$root_partition" "$root_mount" + if [[ -n $root_partition ]]; then + local root_mount="/tmp/rpi-root" + mkdir -p "$root_mount" + mount "$root_partition" "$root_mount" - echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" - sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" + echo "$PI_HOSTNAME" > "$root_mount/etc/hostname" + sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" - log_success "Hostname set to '$PI_HOSTNAME'" + log_success "Hostname set to '$PI_HOSTNAME'" - umount "$root_mount" - fi + umount "$root_mount" + fi - umount "$boot_mount" + umount "$boot_mount" - log_success "SD card configured for headless boot!" - log_info "Insert the SD card into your Raspberry Pi and power it on." - log_info "Find the Pi's IP address from your router or use: nmap -sn 192.168.1.0/24" + log_success "SD card configured for headless boot!" + log_info "Insert the SD card into your Raspberry Pi and power it on." + log_info "Find the Pi's IP address from your router or use: nmap -sn 192.168.1.0/24" } phase_flash() { - check_root + check_root - log_info "=== Phase 1: Flash Raspberry Pi OS to SD Card (Local) ===" + log_info "=== Phase 1: Flash Raspberry Pi OS to SD Card (Local) ===" - detect_sd_card - local image_path - image_path=$(download_raspberry_pi_os) - flash_sd_card "$image_path" - configure_headless_boot + detect_sd_card + local image_path + image_path=$(download_raspberry_pi_os) + flash_sd_card "$image_path" + configure_headless_boot - log_success "Phase 1 complete!" - echo - log_info "Next steps:" - log_info "1. Insert the SD card into your Raspberry Pi 5" - log_info "2. Connect the Pi to power and network" - log_info "3. Wait 2-3 minutes for first boot" - log_info "4. Find the Pi's IP address and SSH: ssh ${PI_USER}@" - log_info "5. Copy this script to the Pi and run: sudo ./setup_nextcloud_raspberry.sh configure" + log_success "Phase 1 complete!" + echo + log_info "Next steps:" + log_info "1. Insert the SD card into your Raspberry Pi 5" + log_info "2. Connect the Pi to power and network" + log_info "3. Wait 2-3 minutes for first boot" + log_info "4. Find the Pi's IP address and SSH: ssh ${PI_USER}@" + log_info "5. Copy this script to the Pi and run: sudo ./setup_nextcloud_raspberry.sh configure" } # ============================================================================= @@ -383,356 +383,356 @@ phase_flash() { # ============================================================================= setup_ssh_key_to_remote() { - local remote_host="$1" - local remote_user="$2" + local remote_host="$1" + local remote_user="$2" - # Check if we already have passwordless access - if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then - log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" - return 0 - fi + # Check if we already have passwordless access + if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2> /dev/null; then + log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" + return 0 + fi - log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." + log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." - # Check if SSH key exists, if not create one - if [[ ! -f "$HOME/.ssh/id_ed25519" ]] && [[ ! -f "$HOME/.ssh/id_rsa" ]]; then - log_info "No SSH key found, generating one..." - ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "" -C "$(whoami)@$(hostname)" - log_success "SSH key generated" - fi + # Check if SSH key exists, if not create one + if [[ ! -f "$HOME/.ssh/id_ed25519" ]] && [[ ! -f "$HOME/.ssh/id_rsa" ]]; then + log_info "No SSH key found, generating one..." + ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "" -C "$(whoami)@$(hostname)" + log_success "SSH key generated" + fi - # Copy SSH key to remote host using sshpass if password provided, otherwise prompt - log_info "Copying SSH key to remote laptop (you will be prompted for password)..." - ssh-copy-id -o StrictHostKeyChecking=accept-new "${remote_user}@${remote_host}" + # Copy SSH key to remote host using sshpass if password provided, otherwise prompt + log_info "Copying SSH key to remote laptop (you will be prompted for password)..." + ssh-copy-id -o StrictHostKeyChecking=accept-new "${remote_user}@${remote_host}" - # Verify it works - if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then - log_success "SSH key authentication configured successfully" - return 0 - else - die "Failed to set up SSH key authentication" - fi + # Verify it works + if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2> /dev/null; then + log_success "SSH key authentication configured successfully" + return 0 + else + die "Failed to set up SSH key authentication" + fi } ensure_dependencies() { - log_info "Ensuring required tools are installed..." + log_info "Ensuring required tools are installed..." - local missing_packages=() + local missing_packages=() - # Check for nmap (fast network scanning) - if ! command -v nmap &>/dev/null; then - missing_packages+=("nmap") - fi + # Check for nmap (fast network scanning) + if ! command -v nmap &> /dev/null; then + missing_packages+=("nmap") + fi - # Check for sshpass (for initial SSH key setup) - if ! command -v sshpass &>/dev/null; then - missing_packages+=("sshpass") - fi + # Check for sshpass (for initial SSH key setup) + if ! command -v sshpass &> /dev/null; then + missing_packages+=("sshpass") + fi - if [[ ${#missing_packages[@]} -gt 0 ]]; then - log_info "Installing missing packages: ${missing_packages[*]}" + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log_info "Installing missing packages: ${missing_packages[*]}" - # Detect package manager and install - if command -v pacman &>/dev/null; then - sudo pacman -S --noconfirm "${missing_packages[@]}" - elif command -v apt-get &>/dev/null; then - sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" - elif command -v dnf &>/dev/null; then - sudo dnf install -y "${missing_packages[@]}" - elif command -v yum &>/dev/null; then - sudo yum install -y "${missing_packages[@]}" - else - die "Could not detect package manager. Please install manually: ${missing_packages[*]}" - fi + # Detect package manager and install + if command -v pacman &> /dev/null; then + sudo pacman -S --noconfirm "${missing_packages[@]}" + elif command -v apt-get &> /dev/null; then + sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" + elif command -v dnf &> /dev/null; then + sudo dnf install -y "${missing_packages[@]}" + elif command -v yum &> /dev/null; then + sudo yum install -y "${missing_packages[@]}" + else + die "Could not detect package manager. Please install manually: ${missing_packages[*]}" + fi - log_success "Dependencies installed" - fi + log_success "Dependencies installed" + fi } discover_remote_laptop() { - log_info "Auto-discovering remote laptop on local network..." + log_info "Auto-discovering remote laptop on local network..." - # Ensure we have the tools we need - ensure_dependencies + # Ensure we have the tools we need + ensure_dependencies - # Get local IP to exclude ourselves (works on both Linux variants) - local my_ip - my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + # Get local IP to exclude ourselves (works on both Linux variants) + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) - # Get gateway - local gateway - gateway=$(ip route | grep default | awk '{print $3}' | head -1) - local network="${gateway%.*}.0/24" + # Get gateway + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" - log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" + log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" - # Use nmap for fast parallel SSH port scanning - log_info "Scanning network for SSH-enabled devices (using nmap)..." - local ssh_hosts - # First do a ping sweep to wake up hosts, then scan SSH port - nmap -sn -T4 "$network" &>/dev/null || true - # Extract IPs from nmap output - grep for report lines then extract IP - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) + # Use nmap for fast parallel SSH port scanning + log_info "Scanning network for SSH-enabled devices (using nmap)..." + local ssh_hosts + # First do a ping sweep to wake up hosts, then scan SSH port + nmap -sn -T4 "$network" &> /dev/null || true + # Extract IPs from nmap output - grep for report lines then extract IP + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2> /dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) - if [[ -z $ssh_hosts ]]; then - die "No SSH-enabled devices found on network" - fi + if [[ -z $ssh_hosts ]]; then + die "No SSH-enabled devices found on network" + fi - local host_count - host_count=$(echo "$ssh_hosts" | wc -l) - log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" + local host_count + host_count=$(echo "$ssh_hosts" | wc -l) + log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" - # Common usernames to try (in order of preference) - local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") - # Remove duplicates while preserving order - local users=() - for u in "${common_users[@]}"; do - local is_dup=0 - for existing in "${users[@]}"; do - if [[ $u == "$existing" ]]; then - is_dup=1 - break - fi - done - if [[ $is_dup -eq 0 ]]; then - users+=("$u") - fi - done + # Common usernames to try (in order of preference) + local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") + # Remove duplicates while preserving order + local users=() + for u in "${common_users[@]}"; do + local is_dup=0 + for existing in "${users[@]}"; do + if [[ $u == "$existing" ]]; then + is_dup=1 + break + fi + done + if [[ $is_dup -eq 0 ]]; then + users+=("$u") + fi + done - log_info "Will try usernames: ${users[*]}" + log_info "Will try usernames: ${users[*]}" - # Find a device with passwordless SSH and SD card - local found_laptop="" - local found_user="" - local idx=0 + # Find a device with passwordless SSH and SD card + local found_laptop="" + local found_user="" + local idx=0 - for ip in $ssh_hosts; do - idx=$((idx + 1)) + for ip in $ssh_hosts; do + idx=$((idx + 1)) - # Skip gateway - if [[ $ip == "$gateway" ]]; then - log_info "[$idx/$host_count] Skipping $ip (gateway)" - continue - fi + # Skip gateway + if [[ $ip == "$gateway" ]]; then + log_info "[$idx/$host_count] Skipping $ip (gateway)" + continue + fi - log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." + log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." - # Try each username - for try_user in "${users[@]}"; do - if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then - log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" - found_user="$try_user" + # Try each username + for try_user in "${users[@]}"; do + if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2> /dev/null | grep -q "ok"; then + log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" + found_user="$try_user" - # Check if there's a removable device (SD card) - log_info "[$idx/$host_count] $ip - Checking for SD card..." - local has_sd - has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2>/dev/null || true) + # Check if there's a removable device (SD card) + log_info "[$idx/$host_count] $ip - Checking for SD card..." + local has_sd + has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2> /dev/null || true) - if [[ -n $has_sd ]]; then - log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" - found_laptop="$ip" - break 2 # Break out of both loops - else - log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." - if [[ -z $found_laptop ]]; then - found_laptop="$ip" - fi - fi - break # Found working user, move to next IP if no SD card - fi - done + if [[ -n $has_sd ]]; then + log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" + found_laptop="$ip" + break 2 # Break out of both loops + else + log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." + if [[ -z $found_laptop ]]; then + found_laptop="$ip" + fi + fi + break # Found working user, move to next IP if no SD card + fi + done - if [[ -z $found_user ]]; then - log_info "[$idx/$host_count] $ip - No SSH key access with any common username" - fi - done + if [[ -z $found_user ]]; then + log_info "[$idx/$host_count] $ip - No SSH key access with any common username" + fi + done - # If no passwordless access found, prompt user for username - if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then - log_warning "No device with passwordless SSH found using common usernames." + # If no passwordless access found, prompt user for username + if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then + log_warning "No device with passwordless SSH found using common usernames." - # Pick first available SSH host - found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) + # Pick first available SSH host + found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) - if [[ -z $found_laptop ]]; then - die "Could not find any suitable SSH-enabled device" - fi + if [[ -z $found_laptop ]]; then + die "Could not find any suitable SSH-enabled device" + fi - log_info "Found SSH host at $found_laptop but need credentials." - read -r -p "Enter username for $found_laptop: " found_user + log_info "Found SSH host at $found_laptop but need credentials." + read -r -p "Enter username for $found_laptop: " found_user - if [[ -z $found_user ]]; then - die "No username provided" - fi - fi + if [[ -z $found_user ]]; then + die "No username provided" + fi + fi - REMOTE_LAPTOP_IP="$found_laptop" - REMOTE_LAPTOP_USER="$found_user" - log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + REMOTE_LAPTOP_IP="$found_laptop" + REMOTE_LAPTOP_USER="$found_user" + log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" - # Save to config file for future use - save_config + # Save to config file for future use + save_config } phase_flash_remote() { - log_info "=== Phase 1B: Flash Raspberry Pi OS to SD Card on Remote Laptop ===" + log_info "=== Phase 1B: Flash Raspberry Pi OS to SD Card on Remote Laptop ===" - # Discover and select remote laptop - discover_remote_laptop + # Discover and select remote laptop + discover_remote_laptop - # Set up SSH key authentication - setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER" + # Set up SSH key authentication + setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER" - local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" - # Check for SD card on remote laptop - log_info "Checking for SD card on remote laptop..." - echo "Block devices on ${remote}:" - ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true - echo + # Check for SD card on remote laptop + log_info "Checking for SD card on remote laptop..." + echo "Block devices on ${remote}:" + ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true + echo - # Auto-detect SD card on remote laptop - log_info "Auto-detecting SD card on remote laptop..." - local sd_device - sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2>/dev/null || true) + # Auto-detect SD card on remote laptop + log_info "Auto-detecting SD card on remote laptop..." + local sd_device + sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2> /dev/null || true) - if [[ -z $sd_device ]]; then - die "No SD card detected on remote laptop. Please insert an SD card and try again." - fi + if [[ -z $sd_device ]]; then + die "No SD card detected on remote laptop. Please insert an SD card and try again." + fi - # Get size for confirmation - local sd_info - # shellcheck disable=SC2029 # Intentional client-side expansion - sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true) + # Get size for confirmation + local sd_info + # shellcheck disable=SC2029 # Intentional client-side expansion + sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true) - log_success "Auto-detected SD card: $sd_device ($sd_info)" - SD_CARD_DEVICE="$sd_device" + log_success "Auto-detected SD card: $sd_device ($sd_info)" + SD_CARD_DEVICE="$sd_device" - # Verify device exists on remote - # shellcheck disable=SC2029 # Intentional client-side expansion - if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2>/dev/null; then - die "Device $SD_CARD_DEVICE does not exist on remote laptop" - fi + # Verify device exists on remote + # shellcheck disable=SC2029 # Intentional client-side expansion + if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2> /dev/null; then + die "Device $SD_CARD_DEVICE does not exist on remote laptop" + fi - # Auto-generate Pi password if not set - auto_generate_pi_password - log_success "Pi user '$PI_USER' password: $PI_PASSWORD" + # Auto-generate Pi password if not set + auto_generate_pi_password + log_success "Pi user '$PI_USER' password: $PI_PASSWORD" - # Generate encrypted password locally - local encrypted_password - encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) + # Generate encrypted password locally + local encrypted_password + encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) - # Save config now so password is stored - save_config + # Save config now so password is stored + save_config - # Copy this script to remote laptop - log_info "Copying script to remote laptop..." - scp "$0" "${remote}:/tmp/setup_nextcloud_raspberry.sh" + # Copy this script to remote laptop + log_info "Copying script to remote laptop..." + scp "$0" "${remote}:/tmp/setup_nextcloud_raspberry.sh" - # Execute flash on remote laptop - log_info "Executing flash on remote laptop..." - log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!" - log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)" - sleep 5 + # Execute flash on remote laptop + log_info "Executing flash on remote laptop..." + log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!" + log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)" + sleep 5 - # Run the flash process on remote laptop - # We pass the pre-encrypted password to avoid interactive prompts - # Using -tt to force TTY allocation even without local tty - ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/setup_nextcloud_raspberry.sh flash-remote-execute '$encrypted_password'" + # Run the flash process on remote laptop + # We pass the pre-encrypted password to avoid interactive prompts + # Using -tt to force TTY allocation even without local tty + ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/setup_nextcloud_raspberry.sh flash-remote-execute '$encrypted_password'" - log_success "Phase 1B complete!" - echo - log_info "Next steps:" - log_info "1. Remove SD card from the laptop and insert into Raspberry Pi 5" - log_info "2. Connect the Pi to power and network" - log_info "3. Wait 2-3 minutes for first boot" - log_info "4. Run: ./setup_nextcloud_raspberry.sh configure (on Pi) or all-remote" + log_success "Phase 1B complete!" + echo + log_info "Next steps:" + log_info "1. Remove SD card from the laptop and insert into Raspberry Pi 5" + log_info "2. Connect the Pi to power and network" + log_info "3. Wait 2-3 minutes for first boot" + log_info "4. Run: ./setup_nextcloud_raspberry.sh configure (on Pi) or all-remote" } # This is called on the remote laptop by phase_flash_remote phase_flash_remote_execute() { - check_root + check_root - local encrypted_password="${1:-}" + local encrypted_password="${1:-}" - log_info "=== Executing Flash on Remote Laptop ===" + log_info "=== Executing Flash on Remote Laptop ===" - if [[ -z $SD_CARD_DEVICE ]]; then - die "SD_CARD_DEVICE not set" - fi + if [[ -z $SD_CARD_DEVICE ]]; then + die "SD_CARD_DEVICE not set" + fi - # Download and flash - local image_path - image_path=$(download_raspberry_pi_os) + # Download and flash + local image_path + image_path=$(download_raspberry_pi_os) - # Unmount any mounted partitions - log_info "Unmounting partitions on $SD_CARD_DEVICE..." - for partition in "${SD_CARD_DEVICE}"*; do - if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then - umount "$partition" 2>/dev/null || true - fi - done + # Unmount any mounted partitions + log_info "Unmounting partitions on $SD_CARD_DEVICE..." + for partition in "${SD_CARD_DEVICE}"*; do + if mountpoint -q "$partition" 2> /dev/null || mount | grep -q "$partition"; then + umount "$partition" 2> /dev/null || true + fi + done - log_info "Flashing image to SD card..." - dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync - sync - log_success "Image flashed successfully!" + log_info "Flashing image to SD card..." + dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync + sync + log_success "Image flashed successfully!" - # Configure headless boot - log_info "Configuring headless boot..." - sleep 2 - partprobe "$SD_CARD_DEVICE" 2>/dev/null || true - sleep 2 + # Configure headless boot + log_info "Configuring headless boot..." + sleep 2 + partprobe "$SD_CARD_DEVICE" 2> /dev/null || true + sleep 2 - # Mount boot partition - local boot_partition - if [[ -b "${SD_CARD_DEVICE}1" ]]; then - boot_partition="${SD_CARD_DEVICE}1" - elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then - boot_partition="${SD_CARD_DEVICE}p1" - else - die "Could not find boot partition" - fi + # Mount boot partition + local boot_partition + if [[ -b "${SD_CARD_DEVICE}1" ]]; then + boot_partition="${SD_CARD_DEVICE}1" + elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then + boot_partition="${SD_CARD_DEVICE}p1" + else + die "Could not find boot partition" + fi - local boot_mount="/tmp/rpi-boot" - mkdir -p "$boot_mount" - mount "$boot_partition" "$boot_mount" + local boot_mount="/tmp/rpi-boot" + mkdir -p "$boot_mount" + mount "$boot_partition" "$boot_mount" - # Enable SSH - touch "$boot_mount/ssh" - log_success "SSH enabled" + # Enable SSH + touch "$boot_mount/ssh" + log_success "SSH enabled" - # Create userconf.txt for first user - if [[ -n $encrypted_password ]]; then - echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" - log_success "User '$PI_USER' configured" - fi + # Create userconf.txt for first user + if [[ -n $encrypted_password ]]; then + echo "${PI_USER}:${encrypted_password}" > "$boot_mount/userconf.txt" + log_success "User '$PI_USER' configured" + fi - # Set hostname on root partition - local root_partition - if [[ -b "${SD_CARD_DEVICE}2" ]]; then - root_partition="${SD_CARD_DEVICE}2" - elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then - root_partition="${SD_CARD_DEVICE}p2" - fi + # Set hostname on root partition + local root_partition + if [[ -b "${SD_CARD_DEVICE}2" ]]; then + root_partition="${SD_CARD_DEVICE}2" + elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then + root_partition="${SD_CARD_DEVICE}p2" + fi - if [[ -n $root_partition ]]; then - local root_mount="/tmp/rpi-root" - mkdir -p "$root_mount" - mount "$root_partition" "$root_mount" + if [[ -n $root_partition ]]; then + local root_mount="/tmp/rpi-root" + mkdir -p "$root_mount" + mount "$root_partition" "$root_mount" - echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" - sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" + echo "$PI_HOSTNAME" > "$root_mount/etc/hostname" + sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" - log_success "Hostname set to '$PI_HOSTNAME'" + log_success "Hostname set to '$PI_HOSTNAME'" - umount "$root_mount" - fi + umount "$root_mount" + fi - umount "$boot_mount" - sync + umount "$boot_mount" + sync - log_success "SD card configured for headless boot!" + log_success "SD card configured for headless boot!" } # ============================================================================= @@ -740,66 +740,66 @@ phase_flash_remote_execute() { # ============================================================================= wait_for_apt_lock() { - # Wait for any existing apt/dpkg processes to finish - local max_wait=600 # 10 minutes max - local waited=0 + # Wait for any existing apt/dpkg processes to finish + local max_wait=600 # 10 minutes max + local waited=0 - while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do - if [[ $waited -eq 0 ]]; then - log_info "Waiting for other apt/dpkg processes to finish..." - log_info "Current apt processes:" - pgrep -a 'apt|dpkg' | head -5 >&2 || true - fi - sleep 5 - waited=$((waited + 5)) - if [[ $waited -ge $max_wait ]]; then - die "Timeout waiting for apt lock after ${max_wait}s" - fi - if [[ $((waited % 30)) -eq 0 ]]; then - log_info "Still waiting... (${waited}s elapsed)" - fi - done + while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock > /dev/null 2>&1; do + if [[ $waited -eq 0 ]]; then + log_info "Waiting for other apt/dpkg processes to finish..." + log_info "Current apt processes:" + pgrep -a 'apt|dpkg' | head -5 >&2 || true + fi + sleep 5 + waited=$((waited + 5)) + if [[ $waited -ge $max_wait ]]; then + die "Timeout waiting for apt lock after ${max_wait}s" + fi + if [[ $((waited % 30)) -eq 0 ]]; then + log_info "Still waiting... (${waited}s elapsed)" + fi + done - if [[ $waited -gt 0 ]]; then - log_success "Apt lock acquired after ${waited}s" - fi + if [[ $waited -gt 0 ]]; then + log_success "Apt lock acquired after ${waited}s" + fi } phase_configure() { - check_root + check_root - log_info "=== Phase 2: Configure Raspberry Pi for Remote Access ===" + log_info "=== Phase 2: Configure Raspberry Pi for Remote Access ===" - # Wait for any existing apt processes - wait_for_apt_lock + # Wait for any existing apt processes + wait_for_apt_lock - # Fix any broken packages first - log_info "Fixing any broken packages..." - DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true + # Fix any broken packages first + log_info "Fixing any broken packages..." + DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true - # Update system - use non-interactive mode and auto-accept config changes - log_info "Updating system packages..." - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y + # Update system - use non-interactive mode and auto-accept config changes + log_info "Updating system packages..." + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y - # Set timezone - log_info "Setting timezone to $PI_TIMEZONE..." - timedatectl set-timezone "$PI_TIMEZONE" + # Set timezone + log_info "Setting timezone to $PI_TIMEZONE..." + timedatectl set-timezone "$PI_TIMEZONE" - # Set locale - log_info "Configuring locale..." - sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen - locale-gen - update-locale LANG="$PI_LOCALE" + # Set locale + log_info "Configuring locale..." + sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen + locale-gen + update-locale LANG="$PI_LOCALE" - # Configure SSH for security - log_info "Hardening SSH configuration..." + # Configure SSH for security + log_info "Hardening SSH configuration..." - # Backup original config - cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup + # Backup original config + cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup - # Apply security settings - cat >>/etc/ssh/sshd_config.d/hardening.conf <<'EOF' + # Apply security settings + cat >> /etc/ssh/sshd_config.d/hardening.conf << 'EOF' # Security hardening PermitRootLogin no PasswordAuthentication yes @@ -810,33 +810,33 @@ ClientAliveInterval 300 ClientAliveCountMax 2 EOF - # Restart SSH - systemctl restart sshd + # Restart SSH + systemctl restart sshd - # Install useful packages - log_info "Installing useful packages..." - apt-get install -y \ - vim \ - htop \ - curl \ - wget \ - git \ - ufw \ - fail2ban \ - unattended-upgrades + # Install useful packages + log_info "Installing useful packages..." + apt-get install -y \ + vim \ + htop \ + curl \ + wget \ + git \ + ufw \ + fail2ban \ + unattended-upgrades - # Configure firewall - log_info "Configuring firewall..." - ufw default deny incoming - ufw default allow outgoing - ufw allow ssh - ufw allow 80/tcp # HTTP - ufw allow 443/tcp # HTTPS - ufw --force enable + # Configure firewall + log_info "Configuring firewall..." + ufw default deny incoming + ufw default allow outgoing + ufw allow ssh + ufw allow 80/tcp # HTTP + ufw allow 443/tcp # HTTPS + ufw --force enable - # Configure fail2ban - log_info "Configuring fail2ban..." - cat >/etc/fail2ban/jail.local <<'EOF' + # Configure fail2ban + log_info "Configuring fail2ban..." + cat > /etc/fail2ban/jail.local << 'EOF' [DEFAULT] bantime = 1h findtime = 10m @@ -850,12 +850,12 @@ logpath = /var/log/auth.log maxretry = 3 EOF - systemctl enable fail2ban - systemctl restart fail2ban + systemctl enable fail2ban + systemctl restart fail2ban - # Enable automatic security updates - log_info "Enabling automatic security updates..." - cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' + # Enable automatic security updates + log_info "Enabling automatic security updates..." + cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF' Unattended-Upgrade::Origins-Pattern { "origin=Debian,codename=${distro_codename},label=Debian-Security"; "origin=Raspbian,codename=${distro_codename},label=Raspbian"; @@ -864,18 +864,18 @@ Unattended-Upgrade::AutoFixInterruptedDpkg "true"; Unattended-Upgrade::Remove-Unused-Dependencies "true"; EOF - systemctl enable unattended-upgrades + systemctl enable unattended-upgrades - # Display system info - log_info "System information:" - echo "Hostname: $(hostname)" - echo "IP Address: $(hostname -I | awk '{print $1}')" - echo "Kernel: $(uname -r)" - echo "Architecture: $(uname -m)" + # Display system info + log_info "System information:" + echo "Hostname: $(hostname)" + echo "IP Address: $(hostname -I | awk '{print $1}')" + echo "Kernel: $(uname -r)" + echo "Architecture: $(uname -m)" - log_success "Phase 2 complete!" - echo - log_info "Next step: Run 'sudo ./setup_nextcloud_raspberry.sh nextcloud' to install Nextcloud" + log_success "Phase 2 complete!" + echo + log_info "Next step: Run 'sudo ./setup_nextcloud_raspberry.sh nextcloud' to install Nextcloud" } # ============================================================================= @@ -883,109 +883,109 @@ EOF # ============================================================================= install_nextcloud_dependencies() { - log_info "Installing Nextcloud dependencies..." + log_info "Installing Nextcloud dependencies..." - apt-get update - apt-get install -y \ - apache2 \ - mariadb-server \ - libapache2-mod-php \ - php \ - php-gd \ - php-mysql \ - php-curl \ - php-mbstring \ - php-intl \ - php-gmp \ - php-bcmath \ - php-xml \ - php-zip \ - php-imagick \ - php-apcu \ - php-redis \ - redis-server \ - unzip \ - certbot \ - python3-certbot-apache + apt-get update + apt-get install -y \ + apache2 \ + mariadb-server \ + libapache2-mod-php \ + php \ + php-gd \ + php-mysql \ + php-curl \ + php-mbstring \ + php-intl \ + php-gmp \ + php-bcmath \ + php-xml \ + php-zip \ + php-imagick \ + php-apcu \ + php-redis \ + redis-server \ + unzip \ + certbot \ + python3-certbot-apache - log_success "Dependencies installed" + log_success "Dependencies installed" } configure_mariadb() { - log_info "Configuring MariaDB..." + log_info "Configuring MariaDB..." - # Generate random password for Nextcloud DB user - local db_password - db_password=$(openssl rand -base64 24) + # Generate random password for Nextcloud DB user + local db_password + db_password=$(openssl rand -base64 24) - # Start and enable MariaDB - systemctl start mariadb - systemctl enable mariadb + # Start and enable MariaDB + systemctl start mariadb + systemctl enable mariadb - # Secure MariaDB installation - mysql -e "DELETE FROM mysql.user WHERE User='';" - mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" - mysql -e "DROP DATABASE IF EXISTS test;" - mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" - mysql -e "FLUSH PRIVILEGES;" + # Secure MariaDB installation + mysql -e "DELETE FROM mysql.user WHERE User='';" + mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" + mysql -e "DROP DATABASE IF EXISTS test;" + mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" + mysql -e "FLUSH PRIVILEGES;" - # Create Nextcloud database and user - mysql -e "CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" - mysql -e "CREATE USER IF NOT EXISTS 'nextcloud'@'localhost' IDENTIFIED BY '$db_password';" - mysql -e "GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';" - mysql -e "FLUSH PRIVILEGES;" + # Create Nextcloud database and user + mysql -e "CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" + mysql -e "CREATE USER IF NOT EXISTS 'nextcloud'@'localhost' IDENTIFIED BY '$db_password';" + mysql -e "GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';" + mysql -e "FLUSH PRIVILEGES;" - # Save password for later use - echo "$db_password" >/root/.nextcloud_db_password - chmod 600 /root/.nextcloud_db_password + # Save password for later use + echo "$db_password" > /root/.nextcloud_db_password + chmod 600 /root/.nextcloud_db_password - log_success "MariaDB configured" - echo "$db_password" + log_success "MariaDB configured" + echo "$db_password" } download_nextcloud() { - log_info "Downloading Nextcloud..." + log_info "Downloading Nextcloud..." - local nc_version="30.0.2" - local nc_url="https://download.nextcloud.com/server/releases/nextcloud-${nc_version}.zip" - local download_dir="/tmp" - local nc_zip="$download_dir/nextcloud.zip" + local nc_version="30.0.2" + local nc_url="https://download.nextcloud.com/server/releases/nextcloud-${nc_version}.zip" + local download_dir="/tmp" + local nc_zip="$download_dir/nextcloud.zip" - if [[ -f $nc_zip ]]; then - log_info "Nextcloud archive already downloaded" - else - wget -O "$nc_zip" "$nc_url" - fi + if [[ -f $nc_zip ]]; then + log_info "Nextcloud archive already downloaded" + else + wget -O "$nc_zip" "$nc_url" + fi - # Remove existing installation if present - rm -rf /var/www/nextcloud + # Remove existing installation if present + rm -rf /var/www/nextcloud - # Extract - unzip -q "$nc_zip" -d /var/www/ + # Extract + unzip -q "$nc_zip" -d /var/www/ - # Set permissions - chown -R www-data:www-data /var/www/nextcloud + # Set permissions + chown -R www-data:www-data /var/www/nextcloud - log_success "Nextcloud downloaded and extracted" + log_success "Nextcloud downloaded and extracted" } configure_apache() { - log_info "Configuring Apache..." + log_info "Configuring Apache..." - # Enable required modules - a2enmod rewrite - a2enmod headers - a2enmod env - a2enmod dir - a2enmod mime - a2enmod ssl + # Enable required modules + a2enmod rewrite + a2enmod headers + a2enmod env + a2enmod dir + a2enmod mime + a2enmod ssl - # Get server IP for configuration - local server_ip - server_ip=$(hostname -I | awk '{print $1}') + # Get server IP for configuration + local server_ip + server_ip=$(hostname -I | awk '{print $1}') - # Create Apache virtual host - cat >/etc/apache2/sites-available/nextcloud.conf < /etc/apache2/sites-available/nextcloud.conf << EOF ServerName $server_ip DocumentRoot /var/www/nextcloud @@ -1005,37 +1005,37 @@ configure_apache() { EOF - # Enable site and disable default - a2dissite 000-default.conf - a2ensite nextcloud.conf + # Enable site and disable default + a2dissite 000-default.conf + a2ensite nextcloud.conf - # Restart Apache - systemctl restart apache2 + # Restart Apache + systemctl restart apache2 - log_success "Apache configured" + log_success "Apache configured" } configure_php() { - log_info "Configuring PHP..." + log_info "Configuring PHP..." - # Find PHP version - local php_version - php_version=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') - local php_ini="/etc/php/${php_version}/apache2/php.ini" + # Find PHP version + local php_version + php_version=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') + local php_ini="/etc/php/${php_version}/apache2/php.ini" - # Backup original - cp "$php_ini" "${php_ini}.backup" + # Backup original + cp "$php_ini" "${php_ini}.backup" - # Apply Nextcloud recommended settings - sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" - sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" - sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" - sed -i 's/max_execution_time = .*/max_execution_time = 360/' "$php_ini" - sed -i 's/max_input_time = .*/max_input_time = 360/' "$php_ini" - sed -i 's/;date.timezone.*/date.timezone = Europe\/Warsaw/' "$php_ini" + # Apply Nextcloud recommended settings + sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" + sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" + sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" + sed -i 's/max_execution_time = .*/max_execution_time = 360/' "$php_ini" + sed -i 's/max_input_time = .*/max_input_time = 360/' "$php_ini" + sed -i 's/;date.timezone.*/date.timezone = Europe\/Warsaw/' "$php_ini" - # Configure OPcache - cat >>"$php_ini" <<'EOF' + # Configure OPcache + cat >> "$php_ini" << 'EOF' ; Nextcloud OPcache settings opcache.enable=1 @@ -1046,140 +1046,140 @@ opcache.save_comments=1 opcache.revalidate_freq=1 EOF - # Configure APCu - echo "apc.enable_cli=1" >>"/etc/php/${php_version}/mods-available/apcu.ini" + # Configure APCu + echo "apc.enable_cli=1" >> "/etc/php/${php_version}/mods-available/apcu.ini" - systemctl restart apache2 + systemctl restart apache2 - log_success "PHP configured" + log_success "PHP configured" } configure_redis() { - log_info "Configuring Redis..." + log_info "Configuring Redis..." - systemctl enable redis-server - systemctl start redis-server + systemctl enable redis-server + systemctl start redis-server - log_success "Redis configured" + log_success "Redis configured" } install_nextcloud() { - log_info "Installing Nextcloud..." + log_info "Installing Nextcloud..." - local db_password - db_password=$(cat /root/.nextcloud_db_password) + local db_password + db_password=$(cat /root/.nextcloud_db_password) - if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then - prompt_password "Enter Nextcloud admin password" NEXTCLOUD_ADMIN_PASSWORD - fi + if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then + prompt_password "Enter Nextcloud admin password" NEXTCLOUD_ADMIN_PASSWORD + fi - # Create data directory - mkdir -p "$NEXTCLOUD_DATA_DIR" - chown -R www-data:www-data "$NEXTCLOUD_DATA_DIR" + # Create data directory + mkdir -p "$NEXTCLOUD_DATA_DIR" + chown -R www-data:www-data "$NEXTCLOUD_DATA_DIR" - # Get server IP - local server_ip - server_ip=$(hostname -I | awk '{print $1}') + # Get server IP + local server_ip + server_ip=$(hostname -I | awk '{print $1}') - # Run Nextcloud installer - cd /var/www/nextcloud - sudo -u www-data php occ maintenance:install \ - --database "mysql" \ - --database-name "nextcloud" \ - --database-user "nextcloud" \ - --database-pass "$db_password" \ - --admin-user "$NEXTCLOUD_ADMIN_USER" \ - --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ - --data-dir "$NEXTCLOUD_DATA_DIR" + # Run Nextcloud installer + cd /var/www/nextcloud + sudo -u www-data php occ maintenance:install \ + --database "mysql" \ + --database-name "nextcloud" \ + --database-user "nextcloud" \ + --database-pass "$db_password" \ + --admin-user "$NEXTCLOUD_ADMIN_USER" \ + --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ + --data-dir "$NEXTCLOUD_DATA_DIR" - # Add trusted domain - sudo -u www-data php occ config:system:set trusted_domains 1 --value="$server_ip" - sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" - sudo -u www-data php occ config:system:set trusted_domains 3 --value="$PI_HOSTNAME.local" + # Add trusted domain + sudo -u www-data php occ config:system:set trusted_domains 1 --value="$server_ip" + sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" + sudo -u www-data php occ config:system:set trusted_domains 3 --value="$PI_HOSTNAME.local" - # Configure Redis caching - sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' - sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' - sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' - sudo -u www-data php occ config:system:set redis host --value='localhost' - sudo -u www-data php occ config:system:set redis port --value='6379' --type=integer + # Configure Redis caching + sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' + sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set redis host --value='localhost' + sudo -u www-data php occ config:system:set redis port --value='6379' --type=integer - # Set default phone region - sudo -u www-data php occ config:system:set default_phone_region --value='PL' + # Set default phone region + sudo -u www-data php occ config:system:set default_phone_region --value='PL' - # Enable maintenance window - sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer + # Enable maintenance window + sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer - log_success "Nextcloud installed" + log_success "Nextcloud installed" } setup_nextcloud_cron() { - log_info "Setting up Nextcloud background jobs..." + log_info "Setting up Nextcloud background jobs..." - # Add cron job for background tasks - crontab -u www-data -l 2>/dev/null || echo "" | crontab -u www-data - - ( - crontab -u www-data -l 2>/dev/null | grep -v 'nextcloud/cron.php' - echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" - ) | crontab -u www-data - + # Add cron job for background tasks + crontab -u www-data -l 2> /dev/null || echo "" | crontab -u www-data - + ( + crontab -u www-data -l 2> /dev/null | grep -v 'nextcloud/cron.php' + echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" + ) | crontab -u www-data - - # Switch to cron background job mode - cd /var/www/nextcloud - sudo -u www-data php occ background:cron + # Switch to cron background job mode + cd /var/www/nextcloud + sudo -u www-data php occ background:cron - log_success "Cron jobs configured" + log_success "Cron jobs configured" } verify_nextcloud() { - log_info "Verifying Nextcloud installation..." + log_info "Verifying Nextcloud installation..." - local server_ip - server_ip=$(hostname -I | awk '{print $1}') + local server_ip + server_ip=$(hostname -I | awk '{print $1}') - # Check if Nextcloud is responding - if curl -s -o /dev/null -w "%{http_code}" "http://${server_ip}/status.php" | grep -q "200"; then - log_success "Nextcloud is responding!" - else - log_warning "Nextcloud may not be fully ready. Check manually." - fi + # Check if Nextcloud is responding + if curl -s -o /dev/null -w "%{http_code}" "http://${server_ip}/status.php" | grep -q "200"; then + log_success "Nextcloud is responding!" + else + log_warning "Nextcloud may not be fully ready. Check manually." + fi - # Run Nextcloud check - cd /var/www/nextcloud - sudo -u www-data php occ status + # Run Nextcloud check + cd /var/www/nextcloud + sudo -u www-data php occ status - echo - log_success "========================================" - log_success "Nextcloud installation complete!" - log_success "========================================" - echo - log_info "Access Nextcloud at: http://${server_ip}" - log_info "Admin user: $NEXTCLOUD_ADMIN_USER" - log_info "Database password saved at: /root/.nextcloud_db_password" - echo - log_info "Recommended next steps:" - log_info "1. Set up a domain name pointing to your Pi" - log_info "2. Configure SSL with: sudo certbot --apache" - log_info "3. Install Nextcloud apps via the web interface" - log_info "4. Configure external storage if needed" + echo + log_success "========================================" + log_success "Nextcloud installation complete!" + log_success "========================================" + echo + log_info "Access Nextcloud at: http://${server_ip}" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Database password saved at: /root/.nextcloud_db_password" + echo + log_info "Recommended next steps:" + log_info "1. Set up a domain name pointing to your Pi" + log_info "2. Configure SSL with: sudo certbot --apache" + log_info "3. Install Nextcloud apps via the web interface" + log_info "4. Configure external storage if needed" } phase_nextcloud() { - check_root + check_root - log_info "=== Phase 3: Install Nextcloud ===" + log_info "=== Phase 3: Install Nextcloud ===" - install_nextcloud_dependencies - local db_password - db_password=$(configure_mariadb) - download_nextcloud - configure_apache - configure_php - configure_redis - install_nextcloud - setup_nextcloud_cron - verify_nextcloud + install_nextcloud_dependencies + local db_password + db_password=$(configure_mariadb) + download_nextcloud + configure_apache + configure_php + configure_redis + install_nextcloud + setup_nextcloud_cron + verify_nextcloud - log_success "Phase 3 complete!" + log_success "Phase 3 complete!" } # ============================================================================= @@ -1187,118 +1187,118 @@ phase_nextcloud() { # ============================================================================= discover_raspberry_pi() { - log_info "Auto-discovering Raspberry Pi on local network..." + log_info "Auto-discovering Raspberry Pi on local network..." - ensure_dependencies + ensure_dependencies - # Get local network info - local my_ip - my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) - local gateway - gateway=$(ip route | grep default | awk '{print $3}' | head -1) - local network="${gateway%.*}.0/24" + # Get local network info + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" - log_info "Local IP: $my_ip, Network: $network" - log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." + log_info "Local IP: $my_ip, Network: $network" + log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." - # First try to find by hostname - local pi_ip="" + # First try to find by hostname + local pi_ip="" - # Try resolving hostname directly - pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true - if [[ -z $pi_ip ]]; then - pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true - fi + # Try resolving hostname directly + pi_ip=$(getent hosts "$PI_HOSTNAME" 2> /dev/null | awk '{print $1}' | head -1) || true + if [[ -z $pi_ip ]]; then + pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2> /dev/null | awk '{print $1}' | head -1) || true + fi - if [[ -n $pi_ip ]]; then - log_success "Found Pi by hostname: $pi_ip" - echo "$pi_ip" - return - fi + if [[ -n $pi_ip ]]; then + log_success "Found Pi by hostname: $pi_ip" + echo "$pi_ip" + return + fi - # Ping sweep to wake up hosts - log_info "Hostname resolution failed, scanning network..." - nmap -sn -T4 "$network" &>/dev/null || true + # Ping sweep to wake up hosts + log_info "Hostname resolution failed, scanning network..." + nmap -sn -T4 "$network" &> /dev/null || true - # Scan for SSH-enabled devices (excluding our IP and known laptop) - local ssh_hosts - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | grep -vw "$REMOTE_LAPTOP_IP" 2>/dev/null | sort -u) || true + # Scan for SSH-enabled devices (excluding our IP and known laptop) + local ssh_hosts + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2> /dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | grep -vw "$REMOTE_LAPTOP_IP" 2> /dev/null | sort -u) || true - if [[ -z $ssh_hosts ]]; then - die "No new SSH-enabled devices found. Is the Pi connected and booted?" - fi + if [[ -z $ssh_hosts ]]; then + die "No new SSH-enabled devices found. Is the Pi connected and booted?" + fi - log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" + log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" - # Try to connect with our Pi credentials - for ip in $ssh_hosts; do - log_info "Trying $ip with user '$PI_USER'..." + # Try to connect with our Pi credentials + for ip in $ssh_hosts; do + log_info "Trying $ip with user '$PI_USER'..." - # Try with password - if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2>/dev/null | grep -qi "$PI_HOSTNAME"; then - log_success "Found Raspberry Pi at $ip" - echo "$ip" - return - fi + # Try with password + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2> /dev/null | grep -qi "$PI_HOSTNAME"; then + log_success "Found Raspberry Pi at $ip" + echo "$ip" + return + fi - # Even if hostname doesn't match, check if it's a fresh Pi responding to our credentials - if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then - log_success "Found device responding to Pi credentials at $ip" - echo "$ip" - return - fi - done + # Even if hostname doesn't match, check if it's a fresh Pi responding to our credentials + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2> /dev/null | grep -q "ok"; then + log_success "Found device responding to Pi credentials at $ip" + echo "$ip" + return + fi + done - die "Could not find Raspberry Pi on network. Make sure it's connected and has finished booting." + die "Could not find Raspberry Pi on network. Make sure it's connected and has finished booting." } phase_all_remote() { - log_info "=== All-Remote: Configure and Install Nextcloud via SSH ===" + log_info "=== All-Remote: Configure and Install Nextcloud via SSH ===" - # Auto-discover Pi IP - local pi_ip - pi_ip=$(discover_raspberry_pi) + # Auto-discover Pi IP + local pi_ip + pi_ip=$(discover_raspberry_pi) - if [[ -z $pi_ip ]]; then - die "Failed to discover Raspberry Pi" - fi + if [[ -z $pi_ip ]]; then + die "Failed to discover Raspberry Pi" + fi - log_info "Using Raspberry Pi at: $pi_ip" + log_info "Using Raspberry Pi at: $pi_ip" - # PI_PASSWORD should already be set from config file - if [[ -z $PI_PASSWORD ]]; then - die "PI_PASSWORD not set. Did you run flash-remote first?" - fi + # PI_PASSWORD should already be set from config file + if [[ -z $PI_PASSWORD ]]; then + die "PI_PASSWORD not set. Did you run flash-remote first?" + fi - # Copy this script to Pi - log_info "Copying script to Pi..." - sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/setup_nextcloud.sh" + # Copy this script to Pi + log_info "Copying script to Pi..." + sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/setup_nextcloud.sh" - # Run configuration phase - log_info "Running configuration phase on Pi..." - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S bash /tmp/setup_nextcloud.sh configure" + # Run configuration phase + log_info "Running configuration phase on Pi..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S bash /tmp/setup_nextcloud.sh configure" - # Run Nextcloud installation phase - log_info "Running Nextcloud installation on Pi..." + # Run Nextcloud installation phase + log_info "Running Nextcloud installation on Pi..." - # Auto-generate Nextcloud admin password if not set - auto_generate_nextcloud_password - save_config + # Auto-generate Nextcloud admin password if not set + auto_generate_nextcloud_password + save_config - log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" - log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" + log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/setup_nextcloud.sh nextcloud" + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/setup_nextcloud.sh nextcloud" - log_success "All-Remote phase complete!" - echo - log_info "=== Access Information ===" - log_info "Nextcloud URL: http://$pi_ip/nextcloud" - log_info "Admin user: $NEXTCLOUD_ADMIN_USER" - log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" - log_info "All credentials saved in: $CONFIG_FILE" + log_success "All-Remote phase complete!" + echo + log_info "=== Access Information ===" + log_info "Nextcloud URL: http://$pi_ip/nextcloud" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_info "All credentials saved in: $CONFIG_FILE" } # ============================================================================= @@ -1306,7 +1306,7 @@ phase_all_remote() { # ============================================================================= show_help() { - cat <<'EOF' + cat << 'EOF' Nextcloud on Raspberry Pi 5 Setup Script Usage: ./setup_nextcloud_raspberry.sh @@ -1348,36 +1348,36 @@ EOF } main() { - local command="${1:-help}" + local command="${1:-help}" - case "$command" in - flash) - phase_flash - ;; - flash-remote) - phase_flash_remote - ;; - flash-remote-execute) - phase_flash_remote_execute "${2:-}" - ;; - configure) - phase_configure - ;; - nextcloud) - phase_nextcloud - ;; - all-remote) - phase_all_remote - ;; - help | --help | -h) - show_help - ;; - *) - log_error "Unknown command: $command" - show_help - exit 1 - ;; - esac + case "$command" in + flash) + phase_flash + ;; + flash-remote) + phase_flash_remote + ;; + flash-remote-execute) + phase_flash_remote_execute "${2:-}" + ;; + configure) + phase_configure + ;; + nextcloud) + phase_nextcloud + ;; + all-remote) + phase_all_remote + ;; + help | --help | -h) + show_help + ;; + *) + log_error "Unknown command: $command" + show_help + exit 1 + ;; + esac } main "$@" diff --git a/scripts/fixes/fix_anki.sh b/scripts/fixes/fix_anki.sh index fa3cc49..94dfb81 100755 --- a/scripts/fixes/fix_anki.sh +++ b/scripts/fixes/fix_anki.sh @@ -26,7 +26,7 @@ NC='\033[0m' # No Color CHECK_ONLY=false usage() { - cat </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 "$@" diff --git a/scripts/fixes/fix_systemctl.sh b/scripts/fixes/fix_systemctl.sh index b663d42..a5a430d 100755 --- a/scripts/fixes/fix_systemctl.sh +++ b/scripts/fixes/fix_systemctl.sh @@ -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" < "$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 diff --git a/scripts/fixes/fix_thorium.sh b/scripts/fixes/fix_thorium.sh index 41183e8..2815a66 100755 --- a/scripts/fixes/fix_thorium.sh +++ b/scripts/fixes/fix_thorium.sh @@ -32,7 +32,7 @@ YELLOW='\033[1;33m' NC='\033[0m' # No Color usage() { - cat </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 "$@" diff --git a/scripts/fixes/fix_virtualbox.sh b/scripts/fixes/fix_virtualbox.sh index dad8ad8..536b489 100644 --- a/scripts/fixes/fix_virtualbox.sh +++ b/scripts/fixes/fix_virtualbox.sh @@ -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 "$@" diff --git a/scripts/fixes/fix_yay_aur_database.sh b/scripts/fixes/fix_yay_aur_database.sh index 8f44017..ba45842 100755 --- a/scripts/fixes/fix_yay_aur_database.sh +++ b/scripts/fixes/fix_yay_aur_database.sh @@ -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 diff --git a/scripts/fixes/nvidia_troubleshoot.sh b/scripts/fixes/nvidia_troubleshoot.sh index e4e72f8..89df339 100755 --- a/scripts/fixes/nvidia_troubleshoot.sh +++ b/scripts/fixes/nvidia_troubleshoot.sh @@ -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" < "$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" < "$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" < "$user_home/run-with-pyroveil.sh" << EOF #!/bin/bash # Helper script to run games with Pyroveil # Usage: ./run-with-pyroveil.sh @@ -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 "" diff --git a/scripts/lib/android.sh b/scripts/lib/android.sh index f068cba..e7c1f3e 100644 --- a/scripts/lib/android.sh +++ b/scripts/lib/android.sh @@ -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 } diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index 1443025..a946a52 100644 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -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}" } diff --git a/scripts/meta/shell_check.sh b/scripts/meta/shell_check.sh index 34007ca..8196c37 100755 --- a/scripts/meta/shell_check.sh +++ b/scripts/meta/shell_check.sh @@ -31,7 +31,7 @@ LIST_ONLY="false" VERBOSE="false" usage() { - cat </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 diff --git a/scripts/misc/testsAndMisc-bash/fix_thorium_unity.sh b/scripts/misc/testsAndMisc-bash/fix_thorium_unity.sh index 6849d70..c66f42e 100644 --- a/scripts/misc/testsAndMisc-bash/fix_thorium_unity.sh +++ b/scripts/misc/testsAndMisc-bash/fix_thorium_unity.sh @@ -28,7 +28,7 @@ SET_DEFAULT=false DO_RESTART=false usage() { - cat </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. diff --git a/scripts/misc/testsAndMisc-bash/fix_unity.sh b/scripts/misc/testsAndMisc-bash/fix_unity.sh index 1d9b5b8..c6208cb 100755 --- a/scripts/misc/testsAndMisc-bash/fix_unity.sh +++ b/scripts/misc/testsAndMisc-bash/fix_unity.sh @@ -30,7 +30,7 @@ SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" source "$SCRIPT_DIR/../../lib/common.sh" usage() { - cat </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" < "$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:-}" - log_info "Current handler (unity): ${cur2:-}" - 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:-}" + log_info "Current handler (unity): ${cur2:-}" + 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 "$@" diff --git a/scripts/misc/testsAndMisc-bash/get_rnnoise_model.sh b/scripts/misc/testsAndMisc-bash/get_rnnoise_model.sh index 4fda3b2..bf03a32 100755 --- a/scripts/misc/testsAndMisc-bash/get_rnnoise_model.sh +++ b/scripts/misc/testsAndMisc-bash/get_rnnoise_model.sh @@ -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 diff --git a/scripts/misc/testsAndMisc-bash/install_ffmpeg_with_arnndn.sh b/scripts/misc/testsAndMisc-bash/install_ffmpeg_with_arnndn.sh index c17278f..9ebc86a 100755 --- a/scripts/misc/testsAndMisc-bash/install_ffmpeg_with_arnndn.sh +++ b/scripts/misc/testsAndMisc-bash/install_ffmpeg_with_arnndn.sh @@ -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 "$@" diff --git a/scripts/misc/testsAndMisc-bash/install_unity_mcp.sh b/scripts/misc/testsAndMisc-bash/install_unity_mcp.sh index 77c37ea..d786c91 100755 --- a/scripts/misc/testsAndMisc-bash/install_unity_mcp.sh +++ b/scripts/misc/testsAndMisc-bash/install_unity_mcp.sh @@ -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 "$@" diff --git a/scripts/misc/testsAndMisc-bash/libre_translate.sh b/scripts/misc/testsAndMisc-bash/libre_translate.sh index 707d6b5..916bf15 100755 --- a/scripts/misc/testsAndMisc-bash/libre_translate.sh +++ b/scripts/misc/testsAndMisc-bash/libre_translate.sh @@ -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 </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: " - 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: " + 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 "$@" diff --git a/scripts/misc/testsAndMisc-bash/transcribe.sh b/scripts/misc/testsAndMisc-bash/transcribe.sh index 42e0465..26b401f 100755 --- a/scripts/misc/testsAndMisc-bash/transcribe.sh +++ b/scripts/misc/testsAndMisc-bash/transcribe.sh @@ -15,7 +15,7 @@ PY_HELPERS="$TOOLS_DIR/transcribe_helpers.py" VENV_DIR="$PROJECT_DIR/.venv" usage() { - cat </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 "$@" diff --git a/scripts/setup_periodic_system.sh b/scripts/setup_periodic_system.sh index 91a19f4..414531c 100755 --- a/scripts/setup_periodic_system.sh +++ b/scripts/setup_periodic_system.sh @@ -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 diff --git a/scripts/setup_thorium_startup.sh b/scripts/setup_thorium_startup.sh index 217ccf7..607f86b 100755 --- a/scripts/setup_thorium_startup.sh +++ b/scripts/setup_thorium_startup.sh @@ -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" < "$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 < /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" < "$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 < /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 diff --git a/scripts/system-maintenance/bin/hosts-file-monitor.sh b/scripts/system-maintenance/bin/hosts-file-monitor.sh index bd2f85f..2caa60a 100755 --- a/scripts/system-maintenance/bin/hosts-file-monitor.sh +++ b/scripts/system-maintenance/bin/hosts-file-monitor.sh @@ -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 diff --git a/scripts/system-maintenance/bin/shutdown-timer-monitor.sh b/scripts/system-maintenance/bin/shutdown-timer-monitor.sh index d09b25a..56f90d4 100644 --- a/scripts/system-maintenance/bin/shutdown-timer-monitor.sh +++ b/scripts/system-maintenance/bin/shutdown-timer-monitor.sh @@ -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) diff --git a/scripts/utils/analyze_repo.sh b/scripts/utils/analyze_repo.sh index 48a3628..9dc5f82 100755 --- a/scripts/utils/analyze_repo.sh +++ b/scripts/utils/analyze_repo.sh @@ -16,47 +16,47 @@ WORK_DIR="" RESPECT_GITIGNORE=true for arg in "$@"; do - case "$arg" in - --no-ignore) - RESPECT_GITIGNORE=false - ;; - *) - if [ -z "$INPUT" ]; then - INPUT="$arg" - elif [ -z "$WORK_DIR" ]; then - WORK_DIR="$arg" - fi - ;; - esac + case "$arg" in + --no-ignore) + RESPECT_GITIGNORE=false + ;; + *) + if [ -z "$INPUT" ]; then + INPUT="$arg" + elif [ -z "$WORK_DIR" ]; then + WORK_DIR="$arg" + fi + ;; + esac done INPUT="${INPUT:-https://github.com/torvalds/linux}" WORK_DIR="${WORK_DIR:-/tmp/repo_analysis}" -TOP_N=50 # Number of top results to show +TOP_N=50 # Number of top results to show # Directories to exclude (unless --no-ignore is used) EXCLUDE_DIRS="node_modules|\.git|vendor|\.venv|venv|__pycache__|\.cache|build|dist|\.next|\.nuxt|target|\.tox|\.eggs" # Detect if input is a URL or local path is_url() { - [[ "$1" =~ ^https?:// ]] || [[ "$1" =~ ^git@ ]] || [[ "$1" =~ ^ssh:// ]] + [[ $1 =~ ^https?:// ]] || [[ $1 =~ ^git@ ]] || [[ $1 =~ ^ssh:// ]] } IS_LOCAL=false if is_url "$INPUT"; then - REPO_URL="$INPUT" - REPO_NAME=$(basename "$REPO_URL" .git) - REPO_DIR="$WORK_DIR/$REPO_NAME" + REPO_URL="$INPUT" + REPO_NAME=$(basename "$REPO_URL" .git) + REPO_DIR="$WORK_DIR/$REPO_NAME" else - # Local path - resolve to absolute path - IS_LOCAL=true - if [ -d "$INPUT" ]; then - REPO_DIR=$(cd "$INPUT" && pwd) - REPO_NAME=$(basename "$REPO_DIR") - else - echo "Error: '$INPUT' is not a valid directory or URL" - exit 1 - fi + # Local path - resolve to absolute path + IS_LOCAL=true + if [ -d "$INPUT" ]; then + REPO_DIR=$(cd "$INPUT" && pwd) + REPO_NAME=$(basename "$REPO_DIR") + else + echo "Error: '$INPUT' is not a valid directory or URL" + exit 1 + fi fi RESULTS_DIR="$WORK_DIR/results_${REPO_NAME}" @@ -69,191 +69,191 @@ YELLOW='\033[1;33m' NC='\033[0m' # No Color print_header() { - echo "" - echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" - echo -e "${GREEN} $1${NC}" - echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" - echo "" + echo "" + echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN} $1${NC}" + echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" + echo "" } print_subheader() { - echo "" - echo -e "${YELLOW}--- $1 ---${NC}" - echo "" + echo "" + echo -e "${YELLOW}--- $1 ---${NC}" + echo "" } # Check if we're in a git repository is_git_repo() { - git rev-parse --is-inside-work-tree &>/dev/null + git rev-parse --is-inside-work-tree &> /dev/null } # Helper function to find files while respecting exclusions # Usage: find_files "*.c" or find_files "*.py" "*.pyx" find_files() { - local patterns=("$@") - - if [ "$RESPECT_GITIGNORE" = true ]; then - if is_git_repo; then - # Use git ls-files which respects .gitignore automatically - # This includes tracked files and untracked files not in .gitignore - local git_patterns=() - for pat in "${patterns[@]}"; do - git_patterns+=("$pat") - done - # Get tracked files + untracked (but not ignored) files - { - git ls-files -- "${git_patterns[@]}" 2>/dev/null - git ls-files --others --exclude-standard -- "${git_patterns[@]}" 2>/dev/null - } | sort -u - else - # Not a git repo - fall back to manual exclusion - local find_args=() - for i in "${!patterns[@]}"; do - if [ $i -eq 0 ]; then - find_args+=(-name "${patterns[$i]}") - else - find_args+=(-o -name "${patterns[$i]}") - fi - done - find . -type f \( "${find_args[@]}" \) 2>/dev/null | grep -Ev "/($EXCLUDE_DIRS)/" - fi + local patterns=("$@") + + if [ "$RESPECT_GITIGNORE" = true ]; then + if is_git_repo; then + # Use git ls-files which respects .gitignore automatically + # This includes tracked files and untracked files not in .gitignore + local git_patterns=() + for pat in "${patterns[@]}"; do + git_patterns+=("$pat") + done + # Get tracked files + untracked (but not ignored) files + { + git ls-files -- "${git_patterns[@]}" 2> /dev/null + git ls-files --others --exclude-standard -- "${git_patterns[@]}" 2> /dev/null + } | sort -u else - # No filtering - find all files - local find_args=() - for i in "${!patterns[@]}"; do - if [ $i -eq 0 ]; then - find_args+=(-name "${patterns[$i]}") - else - find_args+=(-o -name "${patterns[$i]}") - fi - done - find . -type f \( "${find_args[@]}" \) 2>/dev/null + # Not a git repo - fall back to manual exclusion + local find_args=() + for i in "${!patterns[@]}"; do + if [ $i -eq 0 ]; then + find_args+=(-name "${patterns[$i]}") + else + find_args+=(-o -name "${patterns[$i]}") + fi + done + find . -type f \( "${find_args[@]}" \) 2> /dev/null | grep -Ev "/($EXCLUDE_DIRS)/" fi + else + # No filtering - find all files + local find_args=() + for i in "${!patterns[@]}"; do + if [ $i -eq 0 ]; then + find_args+=(-name "${patterns[$i]}") + else + find_args+=(-o -name "${patterns[$i]}") + fi + done + find . -type f \( "${find_args[@]}" \) 2> /dev/null + fi } # Count files matching pattern (respecting exclusions) count_files() { - find_files "$@" | wc -l + find_files "$@" | wc -l } #============================================================================== # STEP 0: Install Missing Tools #============================================================================== install_missing_tools() { - local MISSING_TOOLS=() - local MISSING_AUR=() - - # Check for required tools - command -v git &> /dev/null || MISSING_TOOLS+=("git") - command -v ctags &> /dev/null || MISSING_TOOLS+=("ctags") - command -v cscope &> /dev/null || MISSING_TOOLS+=("cscope") - command -v clang &> /dev/null || MISSING_TOOLS+=("clang") - command -v ugrep &> /dev/null || MISSING_TOOLS+=("ugrep") - - # Check for AUR tools - command -v tokei &> /dev/null || MISSING_AUR+=("tokei") - command -v scc &> /dev/null || MISSING_AUR+=("scc") - - # Check for Rust 'counts' tool (install via cargo if missing) - if ! command -v counts &> /dev/null; then + local MISSING_TOOLS=() + local MISSING_AUR=() + + # Check for required tools + command -v git &> /dev/null || MISSING_TOOLS+=("git") + command -v ctags &> /dev/null || MISSING_TOOLS+=("ctags") + command -v cscope &> /dev/null || MISSING_TOOLS+=("cscope") + command -v clang &> /dev/null || MISSING_TOOLS+=("clang") + command -v ugrep &> /dev/null || MISSING_TOOLS+=("ugrep") + + # Check for AUR tools + command -v tokei &> /dev/null || MISSING_AUR+=("tokei") + command -v scc &> /dev/null || MISSING_AUR+=("scc") + + # Check for Rust 'counts' tool (install via cargo if missing) + if ! command -v counts &> /dev/null; then + if command -v cargo &> /dev/null; then + echo "Installing 'counts' via cargo (fast word counter)..." + cargo install counts 2> /dev/null || echo "Warning: counts install failed, will use Python fallback" + fi + fi + + # If nothing is missing, return + if [ ${#MISSING_TOOLS[@]} -eq 0 ] && [ ${#MISSING_AUR[@]} -eq 0 ]; then + echo -e "${GREEN}All required tools are installed.${NC}" + return 0 + fi + + echo -e "${YELLOW}Missing tools detected. Installing...${NC}" + + # Detect package manager + if command -v pacman &> /dev/null; then + # Arch Linux + if [ ${#MISSING_TOOLS[@]} -gt 0 ]; then + echo "Installing from official repos: ${MISSING_TOOLS[*]}" + sudo pacman -S --needed --noconfirm "${MISSING_TOOLS[@]}" + fi + + if [ ${#MISSING_AUR[@]} -gt 0 ]; then + # Find or install AUR helper + if command -v yay &> /dev/null; then + AUR_HELPER="yay" + elif command -v paru &> /dev/null; then + AUR_HELPER="paru" + else + echo "No AUR helper found. Installing yay..." + sudo pacman -S --needed --noconfirm base-devel git + TEMP_DIR=$(mktemp -d) + git clone https://aur.archlinux.org/yay.git "$TEMP_DIR/yay" + (cd "$TEMP_DIR/yay" && makepkg -si --noconfirm) + rm -rf "$TEMP_DIR" + AUR_HELPER="yay" + fi + + echo "Installing from AUR: ${MISSING_AUR[*]}" + $AUR_HELPER -S --needed --noconfirm "${MISSING_AUR[@]}" + fi + + elif command -v apt-get &> /dev/null; then + # Debian/Ubuntu + echo "Installing tools via apt..." + sudo apt-get update + + # Map tool names to package names + APT_PACKAGES=() + for tool in "${MISSING_TOOLS[@]}"; do + case $tool in + ctags) APT_PACKAGES+=("universal-ctags") ;; + ugrep) APT_PACKAGES+=("ugrep") ;; + *) APT_PACKAGES+=("$tool") ;; + esac + done + + [ ${#APT_PACKAGES[@]} -gt 0 ] && sudo apt-get install -y "${APT_PACKAGES[@]}" + + # Install tokei/scc via cargo or snap + for aur_tool in "${MISSING_AUR[@]}"; do + if command -v cargo &> /dev/null; then + echo "Installing $aur_tool via cargo..." + cargo install "$aur_tool" + elif command -v snap &> /dev/null; then + echo "Installing $aur_tool via snap..." + sudo snap install "$aur_tool" + else + echo -e "${YELLOW}Warning: Cannot install $aur_tool. Install cargo or snap first.${NC}" + fi + done + + elif command -v dnf &> /dev/null; then + # Fedora + echo "Installing tools via dnf..." + sudo dnf install -y "${MISSING_TOOLS[@]}" "${MISSING_AUR[@]}" 2> /dev/null || { + # tokei/scc might need cargo + for aur_tool in "${MISSING_AUR[@]}"; do if command -v cargo &> /dev/null; then - echo "Installing 'counts' via cargo (fast word counter)..." - cargo install counts 2>/dev/null || echo "Warning: counts install failed, will use Python fallback" + cargo install "$aur_tool" fi - fi - - # If nothing is missing, return - if [ ${#MISSING_TOOLS[@]} -eq 0 ] && [ ${#MISSING_AUR[@]} -eq 0 ]; then - echo -e "${GREEN}All required tools are installed.${NC}" - return 0 - fi - - echo -e "${YELLOW}Missing tools detected. Installing...${NC}" - - # Detect package manager - if command -v pacman &> /dev/null; then - # Arch Linux - if [ ${#MISSING_TOOLS[@]} -gt 0 ]; then - echo "Installing from official repos: ${MISSING_TOOLS[*]}" - sudo pacman -S --needed --noconfirm "${MISSING_TOOLS[@]}" - fi - - if [ ${#MISSING_AUR[@]} -gt 0 ]; then - # Find or install AUR helper - if command -v yay &> /dev/null; then - AUR_HELPER="yay" - elif command -v paru &> /dev/null; then - AUR_HELPER="paru" - else - echo "No AUR helper found. Installing yay..." - sudo pacman -S --needed --noconfirm base-devel git - TEMP_DIR=$(mktemp -d) - git clone https://aur.archlinux.org/yay.git "$TEMP_DIR/yay" - (cd "$TEMP_DIR/yay" && makepkg -si --noconfirm) - rm -rf "$TEMP_DIR" - AUR_HELPER="yay" - fi - - echo "Installing from AUR: ${MISSING_AUR[*]}" - $AUR_HELPER -S --needed --noconfirm "${MISSING_AUR[@]}" - fi - - elif command -v apt-get &> /dev/null; then - # Debian/Ubuntu - echo "Installing tools via apt..." - sudo apt-get update - - # Map tool names to package names - APT_PACKAGES=() - for tool in "${MISSING_TOOLS[@]}"; do - case $tool in - ctags) APT_PACKAGES+=("universal-ctags") ;; - ugrep) APT_PACKAGES+=("ugrep") ;; - *) APT_PACKAGES+=("$tool") ;; - esac - done - - [ ${#APT_PACKAGES[@]} -gt 0 ] && sudo apt-get install -y "${APT_PACKAGES[@]}" - - # Install tokei/scc via cargo or snap - for aur_tool in "${MISSING_AUR[@]}"; do - if command -v cargo &> /dev/null; then - echo "Installing $aur_tool via cargo..." - cargo install "$aur_tool" - elif command -v snap &> /dev/null; then - echo "Installing $aur_tool via snap..." - sudo snap install "$aur_tool" - else - echo -e "${YELLOW}Warning: Cannot install $aur_tool. Install cargo or snap first.${NC}" - fi - done - - elif command -v dnf &> /dev/null; then - # Fedora - echo "Installing tools via dnf..." - sudo dnf install -y "${MISSING_TOOLS[@]}" "${MISSING_AUR[@]}" 2>/dev/null || { - # tokei/scc might need cargo - for aur_tool in "${MISSING_AUR[@]}"; do - if command -v cargo &> /dev/null; then - cargo install "$aur_tool" - fi - done - } - - elif command -v brew &> /dev/null; then - # macOS with Homebrew - echo "Installing tools via brew..." - ALL_TOOLS=("${MISSING_TOOLS[@]}" "${MISSING_AUR[@]}") - brew install "${ALL_TOOLS[@]}" - - else - echo -e "${RED}Unknown package manager. Please install these tools manually:${NC}" - echo " Official: ${MISSING_TOOLS[*]}" - echo " Additional: ${MISSING_AUR[*]}" - exit 1 - fi - - echo -e "${GREEN}Tool installation complete.${NC}" + done + } + + elif command -v brew &> /dev/null; then + # macOS with Homebrew + echo "Installing tools via brew..." + ALL_TOOLS=("${MISSING_TOOLS[@]}" "${MISSING_AUR[@]}") + brew install "${ALL_TOOLS[@]}" + + else + echo -e "${RED}Unknown package manager. Please install these tools manually:${NC}" + echo " Official: ${MISSING_TOOLS[*]}" + echo " Additional: ${MISSING_AUR[*]}" + exit 1 + fi + + echo -e "${GREEN}Tool installation complete.${NC}" } print_header "STEP 0: Checking/Installing Required Tools" @@ -268,22 +268,22 @@ mkdir -p "$WORK_DIR" "$RESULTS_DIR" print_header "STEP 1: Repository Setup" if [ "$IS_LOCAL" = true ]; then - echo "Using local repository: $REPO_DIR" - if [ ! -d "$REPO_DIR" ]; then - echo "Error: Directory does not exist: $REPO_DIR" - exit 1 - fi + echo "Using local repository: $REPO_DIR" + if [ ! -d "$REPO_DIR" ]; then + echo "Error: Directory does not exist: $REPO_DIR" + exit 1 + fi else - # Remote URL - clone it - if [ -d "$REPO_DIR" ]; then - echo "Repository already exists at $REPO_DIR" - echo "Updating..." - cd "$REPO_DIR" - git pull --depth 1 2>/dev/null || echo "Update skipped (shallow clone)" - else - echo "Cloning $REPO_URL (shallow clone for speed)..." - git clone --depth 1 "$REPO_URL" "$REPO_DIR" - fi + # Remote URL - clone it + if [ -d "$REPO_DIR" ]; then + echo "Repository already exists at $REPO_DIR" + echo "Updating..." + cd "$REPO_DIR" + git pull --depth 1 2> /dev/null || echo "Update skipped (shallow clone)" + else + echo "Cloning $REPO_URL (shallow clone for speed)..." + git clone --depth 1 "$REPO_URL" "$REPO_DIR" + fi fi cd "$REPO_DIR" @@ -291,13 +291,16 @@ echo "Repository: $REPO_NAME" echo "Location: $REPO_DIR" echo "Repository size: $(du -sh . | cut -f1)" if [ "$RESPECT_GITIGNORE" = true ] && is_git_repo; then - # Count files respecting .gitignore - FILE_COUNT=$({ git ls-files 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null; } | sort -u | wc -l) - echo "Files: $FILE_COUNT (respecting .gitignore)" + # Count files respecting .gitignore + FILE_COUNT=$({ + git ls-files 2> /dev/null + git ls-files --others --exclude-standard 2> /dev/null + } | sort -u | wc -l) + echo "Files: $FILE_COUNT (respecting .gitignore)" elif [ "$RESPECT_GITIGNORE" = true ]; then - echo "Files: $(find . -type f 2>/dev/null | grep -Ev "/($EXCLUDE_DIRS)/" | wc -l) (excluding common dirs)" + echo "Files: $(find . -type f 2> /dev/null | grep -Ev "/($EXCLUDE_DIRS)/" | wc -l) (excluding common dirs)" else - echo "Files: $(find . -type f | wc -l)" + echo "Files: $(find . -type f | wc -l)" fi #============================================================================== @@ -317,7 +320,7 @@ echo "Running scc..." scc . | tee "$RESULTS_DIR/scc_stats.txt" print_subheader "Top 10 Most Complex Files" -scc --by-file --sort complexity . 2>/dev/null | head -20 | tee "$RESULTS_DIR/scc_complexity.txt" +scc --by-file --sort complexity . 2> /dev/null | head -20 | tee "$RESULTS_DIR/scc_complexity.txt" #============================================================================== # STEP 4: Fast Keyword Analysis (Code vs Comments) - Multi-Language @@ -327,18 +330,18 @@ print_header "STEP 4: Fast Keyword Analysis (Code vs Comments)" # Helper function for fast word counting # Uses 'counts' (Rust) if available, falls back to Python Counter fast_count() { - local top_n="${1:-50}" - if command -v counts &> /dev/null; then - counts 2>/dev/null | head -$((top_n + 1)) | tail -$top_n - else - python3 -c " + local top_n="${1:-50}" + if command -v counts &> /dev/null; then + counts 2> /dev/null | head -$((top_n + 1)) | tail -$top_n + else + python3 -c " import sys from collections import Counter c = Counter(line.rstrip() for line in sys.stdin) for word, count in c.most_common($top_n): print(f'{count} {word}') " - fi + fi } #------------------------------------------------------------------------------ @@ -347,13 +350,13 @@ for word, count in c.most_common($top_n): print_subheader "Detecting languages in repository..." if [ "$RESPECT_GITIGNORE" = true ]; then - if is_git_repo; then - echo -e "${YELLOW}Note: Respecting .gitignore (excludes node_modules, build outputs, etc.)${NC}" - else - echo -e "${YELLOW}Note: Excluding common directories (node_modules, .git, vendor, etc.)${NC}" - fi - echo " Use --no-ignore to include everything." - echo "" + if is_git_repo; then + echo -e "${YELLOW}Note: Respecting .gitignore (excludes node_modules, build outputs, etc.)${NC}" + else + echo -e "${YELLOW}Note: Excluding common directories (node_modules, .git, vendor, etc.)${NC}" + fi + echo " Use --no-ignore to include everything." + echo "" fi # Count files by extension to detect primary languages (using helper) @@ -372,8 +375,8 @@ LANG_FILES[shell]=$(count_files "*.sh" "*.bash") echo "Files found by language:" for lang in c cpp h python javascript typescript java go rust ruby shell; do - count=${LANG_FILES[$lang]} - [ "$count" -gt 0 ] && echo " $lang: $count files" + count=${LANG_FILES[$lang]} + [ "$count" -gt 0 ] && echo " $lang: $count files" done # Determine which language families are present @@ -386,14 +389,14 @@ HAS_GO=false HAS_RUST=false HAS_JAVA=false -(( ${LANG_FILES[c]} + ${LANG_FILES[cpp]} + ${LANG_FILES[h]} > 0 )) && HAS_C_FAMILY=true -(( ${LANG_FILES[python]} > 0 )) && HAS_PYTHON=true -(( ${LANG_FILES[javascript]} + ${LANG_FILES[typescript]} > 0 )) && HAS_JS_FAMILY=true -(( ${LANG_FILES[shell]} > 0 )) && HAS_SHELL=true -(( ${LANG_FILES[ruby]} > 0 )) && HAS_RUBY=true -(( ${LANG_FILES[go]} > 0 )) && HAS_GO=true -(( ${LANG_FILES[rust]} > 0 )) && HAS_RUST=true -(( ${LANG_FILES[java]} > 0 )) && HAS_JAVA=true +((${LANG_FILES[c]} + ${LANG_FILES[cpp]} + ${LANG_FILES[h]} > 0)) && HAS_C_FAMILY=true +((${LANG_FILES[python]} > 0)) && HAS_PYTHON=true +((${LANG_FILES[javascript]} + ${LANG_FILES[typescript]} > 0)) && HAS_JS_FAMILY=true +((${LANG_FILES[shell]} > 0)) && HAS_SHELL=true +((${LANG_FILES[ruby]} > 0)) && HAS_RUBY=true +((${LANG_FILES[go]} > 0)) && HAS_GO=true +((${LANG_FILES[rust]} > 0)) && HAS_RUST=true +((${LANG_FILES[java]} > 0)) && HAS_JAVA=true #------------------------------------------------------------------------------ # Language-specific keyword definitions @@ -431,101 +434,101 @@ declare -A LANG_CODE_FILES # Process C/C++ files if $HAS_C_FAMILY; then - echo "Processing C/C++ files..." - LANG_CODE_FILES[c_cpp]=$(mktemp /tmp/code_c_cpp.XXXXXX.tmp) - find_files "*.c" "*.cpp" "*.cc" "*.cxx" "*.h" "*.hpp" | head -15000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[c_cpp]}" - - # Extract and strip C-style comments - perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "${LANG_CODE_FILES[c_cpp]}" >> "$COMMENTS_TEMP" - perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "${LANG_CODE_FILES[c_cpp]}" > "${LANG_CODE_FILES[c_cpp]}.clean" - mv "${LANG_CODE_FILES[c_cpp]}.clean" "${LANG_CODE_FILES[c_cpp]}" + echo "Processing C/C++ files..." + LANG_CODE_FILES[c_cpp]=$(mktemp /tmp/code_c_cpp.XXXXXX.tmp) + find_files "*.c" "*.cpp" "*.cc" "*.cxx" "*.h" "*.hpp" | head -15000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[c_cpp]}" + + # Extract and strip C-style comments + perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "${LANG_CODE_FILES[c_cpp]}" >> "$COMMENTS_TEMP" + perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "${LANG_CODE_FILES[c_cpp]}" > "${LANG_CODE_FILES[c_cpp]}.clean" + mv "${LANG_CODE_FILES[c_cpp]}.clean" "${LANG_CODE_FILES[c_cpp]}" fi # Process JavaScript files (separate from TypeScript) if $HAS_JS_FAMILY; then - echo "Processing JavaScript files..." - LANG_CODE_FILES[javascript]=$(mktemp /tmp/code_js.XXXXXX.tmp) - find_files "*.js" "*.jsx" | head -15000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[javascript]}" - - echo "Processing TypeScript files..." - LANG_CODE_FILES[typescript]=$(mktemp /tmp/code_ts.XXXXXX.tmp) - find_files "*.ts" "*.tsx" | head -15000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[typescript]}" - - # Extract and strip comments from both - for lang_file in "${LANG_CODE_FILES[javascript]}" "${LANG_CODE_FILES[typescript]}"; do - [ ! -s "$lang_file" ] && continue - perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "$lang_file" >> "$COMMENTS_TEMP" - perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "$lang_file" > "${lang_file}.clean" - mv "${lang_file}.clean" "$lang_file" - done + echo "Processing JavaScript files..." + LANG_CODE_FILES[javascript]=$(mktemp /tmp/code_js.XXXXXX.tmp) + find_files "*.js" "*.jsx" | head -15000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[javascript]}" + + echo "Processing TypeScript files..." + LANG_CODE_FILES[typescript]=$(mktemp /tmp/code_ts.XXXXXX.tmp) + find_files "*.ts" "*.tsx" | head -15000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[typescript]}" + + # Extract and strip comments from both + for lang_file in "${LANG_CODE_FILES[javascript]}" "${LANG_CODE_FILES[typescript]}"; do + [ ! -s "$lang_file" ] && continue + perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "$lang_file" >> "$COMMENTS_TEMP" + perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "$lang_file" > "${lang_file}.clean" + mv "${lang_file}.clean" "$lang_file" + done fi # Process Python files if $HAS_PYTHON; then - echo "Processing Python files..." - LANG_CODE_FILES[python]=$(mktemp /tmp/code_python.XXXXXX.tmp) - find_files "*.py" | head -15000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[python]}" - - perl -ne 'if (/^\s*#(.*)/) { print "$1\n"; } elsif (/#(.*)$/) { print "$1\n"; }' "${LANG_CODE_FILES[python]}" >> "$COMMENTS_TEMP" - perl -0777 -ne 'while (/"""(.+?)"""/gs) { print "$1\n"; } while (/'"'"''"'"''"'"'(.+?)'"'"''"'"''"'"'/gs) { print "$1\n"; }' "${LANG_CODE_FILES[python]}" >> "$COMMENTS_TEMP" - perl -pe 's/#.*$//' "${LANG_CODE_FILES[python]}" | perl -0777 -pe 's/""".*?"""//gs; s/'"'"''"'"''"'"'.*?'"'"''"'"''"'"'//gs' > "${LANG_CODE_FILES[python]}.clean" - mv "${LANG_CODE_FILES[python]}.clean" "${LANG_CODE_FILES[python]}" + echo "Processing Python files..." + LANG_CODE_FILES[python]=$(mktemp /tmp/code_python.XXXXXX.tmp) + find_files "*.py" | head -15000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[python]}" + + perl -ne 'if (/^\s*#(.*)/) { print "$1\n"; } elsif (/#(.*)$/) { print "$1\n"; }' "${LANG_CODE_FILES[python]}" >> "$COMMENTS_TEMP" + perl -0777 -ne 'while (/"""(.+?)"""/gs) { print "$1\n"; } while (/'"'"''"'"''"'"'(.+?)'"'"''"'"''"'"'/gs) { print "$1\n"; }' "${LANG_CODE_FILES[python]}" >> "$COMMENTS_TEMP" + perl -pe 's/#.*$//' "${LANG_CODE_FILES[python]}" | perl -0777 -pe 's/""".*?"""//gs; s/'"'"''"'"''"'"'.*?'"'"''"'"''"'"'//gs' > "${LANG_CODE_FILES[python]}.clean" + mv "${LANG_CODE_FILES[python]}.clean" "${LANG_CODE_FILES[python]}" fi # Process Go files if $HAS_GO; then - echo "Processing Go files..." - LANG_CODE_FILES[go]=$(mktemp /tmp/code_go.XXXXXX.tmp) - find_files "*.go" | head -15000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[go]}" - - perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "${LANG_CODE_FILES[go]}" >> "$COMMENTS_TEMP" - perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "${LANG_CODE_FILES[go]}" > "${LANG_CODE_FILES[go]}.clean" - mv "${LANG_CODE_FILES[go]}.clean" "${LANG_CODE_FILES[go]}" + echo "Processing Go files..." + LANG_CODE_FILES[go]=$(mktemp /tmp/code_go.XXXXXX.tmp) + find_files "*.go" | head -15000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[go]}" + + perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "${LANG_CODE_FILES[go]}" >> "$COMMENTS_TEMP" + perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "${LANG_CODE_FILES[go]}" > "${LANG_CODE_FILES[go]}.clean" + mv "${LANG_CODE_FILES[go]}.clean" "${LANG_CODE_FILES[go]}" fi # Process Rust files if $HAS_RUST; then - echo "Processing Rust files..." - LANG_CODE_FILES[rust]=$(mktemp /tmp/code_rust.XXXXXX.tmp) - find_files "*.rs" | head -15000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[rust]}" - - perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "${LANG_CODE_FILES[rust]}" >> "$COMMENTS_TEMP" - perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "${LANG_CODE_FILES[rust]}" > "${LANG_CODE_FILES[rust]}.clean" - mv "${LANG_CODE_FILES[rust]}.clean" "${LANG_CODE_FILES[rust]}" + echo "Processing Rust files..." + LANG_CODE_FILES[rust]=$(mktemp /tmp/code_rust.XXXXXX.tmp) + find_files "*.rs" | head -15000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[rust]}" + + perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "${LANG_CODE_FILES[rust]}" >> "$COMMENTS_TEMP" + perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "${LANG_CODE_FILES[rust]}" > "${LANG_CODE_FILES[rust]}.clean" + mv "${LANG_CODE_FILES[rust]}.clean" "${LANG_CODE_FILES[rust]}" fi # Process Ruby files if $HAS_RUBY; then - echo "Processing Ruby files..." - LANG_CODE_FILES[ruby]=$(mktemp /tmp/code_ruby.XXXXXX.tmp) - find_files "*.rb" | head -5000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[ruby]}" - - perl -ne 'if (/#(.*)$/) { print "$1\n"; }' "${LANG_CODE_FILES[ruby]}" >> "$COMMENTS_TEMP" - perl -0777 -ne 'while (/=begin(.+?)=end/gs) { print "$1\n"; }' "${LANG_CODE_FILES[ruby]}" >> "$COMMENTS_TEMP" - perl -pe 's/#.*$//' "${LANG_CODE_FILES[ruby]}" | perl -0777 -pe 's/=begin.*?=end//gs' > "${LANG_CODE_FILES[ruby]}.clean" - mv "${LANG_CODE_FILES[ruby]}.clean" "${LANG_CODE_FILES[ruby]}" + echo "Processing Ruby files..." + LANG_CODE_FILES[ruby]=$(mktemp /tmp/code_ruby.XXXXXX.tmp) + find_files "*.rb" | head -5000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[ruby]}" + + perl -ne 'if (/#(.*)$/) { print "$1\n"; }' "${LANG_CODE_FILES[ruby]}" >> "$COMMENTS_TEMP" + perl -0777 -ne 'while (/=begin(.+?)=end/gs) { print "$1\n"; }' "${LANG_CODE_FILES[ruby]}" >> "$COMMENTS_TEMP" + perl -pe 's/#.*$//' "${LANG_CODE_FILES[ruby]}" | perl -0777 -pe 's/=begin.*?=end//gs' > "${LANG_CODE_FILES[ruby]}.clean" + mv "${LANG_CODE_FILES[ruby]}.clean" "${LANG_CODE_FILES[ruby]}" fi # Process Shell files if $HAS_SHELL; then - echo "Processing Shell files..." - LANG_CODE_FILES[shell]=$(mktemp /tmp/code_shell.XXXXXX.tmp) - find_files "*.sh" "*.bash" | head -5000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[shell]}" - - perl -ne 'if (/^\s*#(.*)/ && !/^#!/) { print "$1\n"; } elsif (/#(.*)$/) { print "$1\n"; }' "${LANG_CODE_FILES[shell]}" >> "$COMMENTS_TEMP" - perl -pe 's/#.*$//' "${LANG_CODE_FILES[shell]}" > "${LANG_CODE_FILES[shell]}.clean" - mv "${LANG_CODE_FILES[shell]}.clean" "${LANG_CODE_FILES[shell]}" + echo "Processing Shell files..." + LANG_CODE_FILES[shell]=$(mktemp /tmp/code_shell.XXXXXX.tmp) + find_files "*.sh" "*.bash" | head -5000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[shell]}" + + perl -ne 'if (/^\s*#(.*)/ && !/^#!/) { print "$1\n"; } elsif (/#(.*)$/) { print "$1\n"; }' "${LANG_CODE_FILES[shell]}" >> "$COMMENTS_TEMP" + perl -pe 's/#.*$//' "${LANG_CODE_FILES[shell]}" > "${LANG_CODE_FILES[shell]}.clean" + mv "${LANG_CODE_FILES[shell]}.clean" "${LANG_CODE_FILES[shell]}" fi # Process Java files if $HAS_JAVA; then - echo "Processing Java files..." - LANG_CODE_FILES[java]=$(mktemp /tmp/code_java.XXXXXX.tmp) - find_files "*.java" | head -15000 | xargs cat 2>/dev/null > "${LANG_CODE_FILES[java]}" - - perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "${LANG_CODE_FILES[java]}" >> "$COMMENTS_TEMP" - perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "${LANG_CODE_FILES[java]}" > "${LANG_CODE_FILES[java]}.clean" - mv "${LANG_CODE_FILES[java]}.clean" "${LANG_CODE_FILES[java]}" + echo "Processing Java files..." + LANG_CODE_FILES[java]=$(mktemp /tmp/code_java.XXXXXX.tmp) + find_files "*.java" | head -15000 | xargs cat 2> /dev/null > "${LANG_CODE_FILES[java]}" + + perl -0777 -ne 'while (/\/\*(.+?)\*\//gs) { print "$1\n"; } while (/\/\/([^\n]*)/g) { print "$1\n"; }' "${LANG_CODE_FILES[java]}" >> "$COMMENTS_TEMP" + perl -0777 -pe 's|/\*.*?\*/||gs; s|//[^\n]*||g;' "${LANG_CODE_FILES[java]}" > "${LANG_CODE_FILES[java]}.clean" + mv "${LANG_CODE_FILES[java]}.clean" "${LANG_CODE_FILES[java]}" fi COMMENT_LINES=$(wc -l < "$COMMENTS_TEMP") @@ -552,17 +555,17 @@ LANG_KEYWORDS[java]="$KEYWORDS_JAVA" # Analyze each language separately for lang in "${!LANG_CODE_FILES[@]}"; do - code_file="${LANG_CODE_FILES[$lang]}" - keywords="${LANG_KEYWORDS[$lang]}" - output_file="$RESULTS_DIR/per_language/keywords_${lang}.txt" - - if [ -f "$code_file" ] && [ -s "$code_file" ] && [ -n "$keywords" ]; then - echo "" - echo -e "${YELLOW}=== $lang Keywords ===${NC}" - ugrep -o "\b($keywords)\b" "$code_file" 2>/dev/null \ - | fast_count 50 \ - | tee "$output_file" - fi + code_file="${LANG_CODE_FILES[$lang]}" + keywords="${LANG_KEYWORDS[$lang]}" + output_file="$RESULTS_DIR/per_language/keywords_${lang}.txt" + + if [ -f "$code_file" ] && [ -s "$code_file" ] && [ -n "$keywords" ]; then + echo "" + echo -e "${YELLOW}=== $lang Keywords ===${NC}" + ugrep -o "\b($keywords)\b" "$code_file" 2> /dev/null | + fast_count 50 | + tee "$output_file" + fi done #------------------------------------------------------------------------------ @@ -571,18 +574,18 @@ done print_subheader "Per-Language Function Calls" for lang in "${!LANG_CODE_FILES[@]}"; do - code_file="${LANG_CODE_FILES[$lang]}" - output_file="$RESULTS_DIR/per_language/functions_${lang}.txt" - - if [ -f "$code_file" ] && [ -s "$code_file" ]; then - echo "" - echo -e "${YELLOW}=== $lang Functions ===${NC}" - ugrep -o '\b[a-zA-Z_][a-zA-Z0-9_]*\s*\(' "$code_file" 2>/dev/null \ - | sed 's/\s*(//' \ - | grep -vE '^(if|for|while|switch|catch|elif)$' \ - | fast_count 30 \ - | tee "$output_file" - fi + code_file="${LANG_CODE_FILES[$lang]}" + output_file="$RESULTS_DIR/per_language/functions_${lang}.txt" + + if [ -f "$code_file" ] && [ -s "$code_file" ]; then + echo "" + echo -e "${YELLOW}=== $lang Functions ===${NC}" + ugrep -o '\b[a-zA-Z_][a-zA-Z0-9_]*\s*\(' "$code_file" 2> /dev/null | + sed 's/\s*(//' | + grep -vE '^(if|for|while|switch|catch|elif)$' | + fast_count 30 | + tee "$output_file" + fi done #------------------------------------------------------------------------------ @@ -592,85 +595,85 @@ print_subheader "Per-Language Imports/Includes" # C/C++ includes if [ -n "${LANG_CODE_FILES[c_cpp]}" ] && [ -s "${LANG_CODE_FILES[c_cpp]}" ]; then - echo -e "${YELLOW}=== C/C++ Includes ===${NC}" - ugrep -o '#include\s*[<"][^>"]+[>"]' "${LANG_CODE_FILES[c_cpp]}" 2>/dev/null \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_c_cpp.txt" + echo -e "${YELLOW}=== C/C++ Includes ===${NC}" + ugrep -o '#include\s*[<"][^>"]+[>"]' "${LANG_CODE_FILES[c_cpp]}" 2> /dev/null | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_c_cpp.txt" fi # Python imports if [ -n "${LANG_CODE_FILES[python]}" ] && [ -s "${LANG_CODE_FILES[python]}" ]; then - echo "" - echo -e "${YELLOW}=== Python Imports ===${NC}" - ugrep -o '^\s*(from\s+\S+\s+import\s+\S+|import\s+\S+)' "${LANG_CODE_FILES[python]}" 2>/dev/null \ - | sed 's/^\s*//' \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_python.txt" + echo "" + echo -e "${YELLOW}=== Python Imports ===${NC}" + ugrep -o '^\s*(from\s+\S+\s+import\s+\S+|import\s+\S+)' "${LANG_CODE_FILES[python]}" 2> /dev/null | + sed 's/^\s*//' | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_python.txt" fi # JavaScript imports if [ -n "${LANG_CODE_FILES[javascript]}" ] && [ -s "${LANG_CODE_FILES[javascript]}" ]; then - echo "" - echo -e "${YELLOW}=== JavaScript Imports ===${NC}" - ugrep -o "(import\s+.*\s+from\s+['\"][^'\"]+['\"]|require\s*\(['\"][^'\"]+['\"]\))" "${LANG_CODE_FILES[javascript]}" 2>/dev/null \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_javascript.txt" + echo "" + echo -e "${YELLOW}=== JavaScript Imports ===${NC}" + ugrep -o "(import\s+.*\s+from\s+['\"][^'\"]+['\"]|require\s*\(['\"][^'\"]+['\"]\))" "${LANG_CODE_FILES[javascript]}" 2> /dev/null | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_javascript.txt" fi # TypeScript imports if [ -n "${LANG_CODE_FILES[typescript]}" ] && [ -s "${LANG_CODE_FILES[typescript]}" ]; then - echo "" - echo -e "${YELLOW}=== TypeScript Imports ===${NC}" - ugrep -o "(import\s+.*\s+from\s+['\"][^'\"]+['\"]|require\s*\(['\"][^'\"]+['\"]\))" "${LANG_CODE_FILES[typescript]}" 2>/dev/null \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_typescript.txt" + echo "" + echo -e "${YELLOW}=== TypeScript Imports ===${NC}" + ugrep -o "(import\s+.*\s+from\s+['\"][^'\"]+['\"]|require\s*\(['\"][^'\"]+['\"]\))" "${LANG_CODE_FILES[typescript]}" 2> /dev/null | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_typescript.txt" fi # Go imports if [ -n "${LANG_CODE_FILES[go]}" ] && [ -s "${LANG_CODE_FILES[go]}" ]; then - echo "" - echo -e "${YELLOW}=== Go Imports ===${NC}" - ugrep -o '"[^"]+/[^"]+"' "${LANG_CODE_FILES[go]}" 2>/dev/null \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_go.txt" + echo "" + echo -e "${YELLOW}=== Go Imports ===${NC}" + ugrep -o '"[^"]+/[^"]+"' "${LANG_CODE_FILES[go]}" 2> /dev/null | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_go.txt" fi # Rust use statements if [ -n "${LANG_CODE_FILES[rust]}" ] && [ -s "${LANG_CODE_FILES[rust]}" ]; then - echo "" - echo -e "${YELLOW}=== Rust Use Statements ===${NC}" - ugrep -o '^\s*use\s+[^;]+' "${LANG_CODE_FILES[rust]}" 2>/dev/null \ - | sed 's/^\s*//' \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_rust.txt" + echo "" + echo -e "${YELLOW}=== Rust Use Statements ===${NC}" + ugrep -o '^\s*use\s+[^;]+' "${LANG_CODE_FILES[rust]}" 2> /dev/null | + sed 's/^\s*//' | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_rust.txt" fi # Java imports if [ -n "${LANG_CODE_FILES[java]}" ] && [ -s "${LANG_CODE_FILES[java]}" ]; then - echo "" - echo -e "${YELLOW}=== Java Imports ===${NC}" - ugrep -o '^\s*import\s+[^;]+' "${LANG_CODE_FILES[java]}" 2>/dev/null \ - | sed 's/^\s*//' \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_java.txt" + echo "" + echo -e "${YELLOW}=== Java Imports ===${NC}" + ugrep -o '^\s*import\s+[^;]+' "${LANG_CODE_FILES[java]}" 2> /dev/null | + sed 's/^\s*//' | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_java.txt" fi # Ruby requires if [ -n "${LANG_CODE_FILES[ruby]}" ] && [ -s "${LANG_CODE_FILES[ruby]}" ]; then - echo "" - echo -e "${YELLOW}=== Ruby Requires ===${NC}" - ugrep -o "(require\s+['\"][^'\"]+['\"]|require_relative\s+['\"][^'\"]+['\"])" "${LANG_CODE_FILES[ruby]}" 2>/dev/null \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_ruby.txt" + echo "" + echo -e "${YELLOW}=== Ruby Requires ===${NC}" + ugrep -o "(require\s+['\"][^'\"]+['\"]|require_relative\s+['\"][^'\"]+['\"])" "${LANG_CODE_FILES[ruby]}" 2> /dev/null | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_ruby.txt" fi # Shell sources if [ -n "${LANG_CODE_FILES[shell]}" ] && [ -s "${LANG_CODE_FILES[shell]}" ]; then - echo "" - echo -e "${YELLOW}=== Shell Sources ===${NC}" - ugrep -o '(source\s+[^\s]+|\.\s+[^\s]+)' "${LANG_CODE_FILES[shell]}" 2>/dev/null \ - | fast_count 30 \ - | tee "$RESULTS_DIR/per_language/imports_shell.txt" + echo "" + echo -e "${YELLOW}=== Shell Sources ===${NC}" + ugrep -o '(source\s+[^\s]+|\.\s+[^\s]+)' "${LANG_CODE_FILES[shell]}" 2> /dev/null | + fast_count 30 | + tee "$RESULTS_DIR/per_language/imports_shell.txt" fi #------------------------------------------------------------------------------ @@ -681,77 +684,76 @@ print_subheader "Combined Code Identifiers (all languages)" # Create combined CODE_TEMP CODE_TEMP=$(mktemp) for lang_file in "${LANG_CODE_FILES[@]}"; do - [ -f "$lang_file" ] && cat "$lang_file" >> "$CODE_TEMP" + [ -f "$lang_file" ] && cat "$lang_file" >> "$CODE_TEMP" done -ugrep -o '\b[a-zA-Z_][a-zA-Z0-9_]*\b' "$CODE_TEMP" 2>/dev/null \ - | fast_count $TOP_N \ - | tee "$RESULTS_DIR/code_identifiers.txt" +ugrep -o '\b[a-zA-Z_][a-zA-Z0-9_]*\b' "$CODE_TEMP" 2> /dev/null | + fast_count $TOP_N | + tee "$RESULTS_DIR/code_identifiers.txt" print_subheader "Most Used Words in COMMENTS" -ugrep -o '\b[a-zA-Z_][a-zA-Z0-9_]*\b' "$COMMENTS_TEMP" 2>/dev/null \ - | fast_count $TOP_N \ - | tee "$RESULTS_DIR/comment_words.txt" +ugrep -o '\b[a-zA-Z_][a-zA-Z0-9_]*\b' "$COMMENTS_TEMP" 2> /dev/null | + fast_count $TOP_N | + tee "$RESULTS_DIR/comment_words.txt" # Create combined files from per-language analysis (for backward compatibility) { - echo "# Combined keywords from all languages" - echo "# Format: count keyword (from per_language/keywords_*.txt)" - cat "$RESULTS_DIR/per_language"/keywords_*.txt 2>/dev/null | grep -v '^$' | sort -t' ' -k1 -nr | head -100 + echo "# Combined keywords from all languages" + echo "# Format: count keyword (from per_language/keywords_*.txt)" + cat "$RESULTS_DIR/per_language"/keywords_*.txt 2> /dev/null | grep -v '^$' | sort -t' ' -k1 -nr | head -100 } > "$RESULTS_DIR/grep_keywords.txt" { - echo "# Combined functions from all languages" - echo "# See per_language/functions_*.txt for language-specific breakdown" - cat "$RESULTS_DIR/per_language"/functions_*.txt 2>/dev/null | grep -v '^$' | sort -t' ' -k1 -nr | head -100 + echo "# Combined functions from all languages" + echo "# See per_language/functions_*.txt for language-specific breakdown" + cat "$RESULTS_DIR/per_language"/functions_*.txt 2> /dev/null | grep -v '^$' | sort -t' ' -k1 -nr | head -100 } > "$RESULTS_DIR/grep_function_calls.txt" { - echo "# Combined imports from all languages" - echo "# See per_language/imports_*.txt for language-specific breakdown" - cat "$RESULTS_DIR/per_language"/imports_*.txt 2>/dev/null | grep -v '^$' | sort -t' ' -k1 -nr | head -100 + echo "# Combined imports from all languages" + echo "# See per_language/imports_*.txt for language-specific breakdown" + cat "$RESULTS_DIR/per_language"/imports_*.txt 2> /dev/null | grep -v '^$' | sort -t' ' -k1 -nr | head -100 } > "$RESULTS_DIR/grep_imports.txt" # List what per-language files were created echo "" echo "Per-language analysis files created:" -ls -la "$RESULTS_DIR/per_language/" 2>/dev/null | grep -v '^total' | awk '{print " " $NF}' - +ls -la "$RESULTS_DIR/per_language/" 2> /dev/null | grep -v '^total' | awk '{print " " $NF}' print_subheader "Generating tags (this may take a while)..." # Generate tags for different kinds -ctags -R --languages=C,C++ --c-kinds=+fp --fields=+lK -f "$RESULTS_DIR/tags" . 2>/dev/null || true +ctags -R --languages=C,C++ --c-kinds=+fp --fields=+lK -f "$RESULTS_DIR/tags" . 2> /dev/null || true if [ -f "$RESULTS_DIR/tags" ]; then - TOTAL_TAGS=$(grep -ac '^[^!]' "$RESULTS_DIR/tags" 2>/dev/null || echo "0") - echo "Total symbols found: $TOTAL_TAGS" - - print_subheader "Most Common Symbol Names" - # Fast: use cut + counts instead of awk + sort | uniq - # -a flag treats tags file as text (may contain binary-like patterns) - grep -a '^[^!]' "$RESULTS_DIR/tags" | cut -f1 | fast_count $TOP_N \ - | tee "$RESULTS_DIR/ctags_symbols.txt" - - print_subheader "Symbol Types Distribution" - # Fast: extract single-letter kind code after ;" and count - grep -aoP ';"\t\K[a-z]' "$RESULTS_DIR/tags" 2>/dev/null | fast_count 20 | while read count kind; do - case $kind in - f) echo "$count functions" ;; - v) echo "$count variables" ;; - s) echo "$count structs" ;; - t) echo "$count typedefs" ;; - e) echo "$count enum values" ;; - g) echo "$count enums" ;; - m) echo "$count struct/union members" ;; - d) echo "$count macro definitions" ;; - p) echo "$count function prototypes" ;; - u) echo "$count unions" ;; - c) echo "$count classes" ;; - n) echo "$count namespaces" ;; - *) echo "$count kind=$kind" ;; - esac - done | tee "$RESULTS_DIR/ctags_kinds.txt" + TOTAL_TAGS=$(grep -ac '^[^!]' "$RESULTS_DIR/tags" 2> /dev/null || echo "0") + echo "Total symbols found: $TOTAL_TAGS" + + print_subheader "Most Common Symbol Names" + # Fast: use cut + counts instead of awk + sort | uniq + # -a flag treats tags file as text (may contain binary-like patterns) + grep -a '^[^!]' "$RESULTS_DIR/tags" | cut -f1 | fast_count $TOP_N | + tee "$RESULTS_DIR/ctags_symbols.txt" + + print_subheader "Symbol Types Distribution" + # Fast: extract single-letter kind code after ;" and count + grep -aoP ';"\t\K[a-z]' "$RESULTS_DIR/tags" 2> /dev/null | fast_count 20 | while read count kind; do + case $kind in + f) echo "$count functions" ;; + v) echo "$count variables" ;; + s) echo "$count structs" ;; + t) echo "$count typedefs" ;; + e) echo "$count enum values" ;; + g) echo "$count enums" ;; + m) echo "$count struct/union members" ;; + d) echo "$count macro definitions" ;; + p) echo "$count function prototypes" ;; + u) echo "$count unions" ;; + c) echo "$count classes" ;; + n) echo "$count namespaces" ;; + *) echo "$count kind=$kind" ;; + esac + done | tee "$RESULTS_DIR/ctags_kinds.txt" fi #============================================================================== @@ -763,28 +765,31 @@ print_subheader "Building cscope database..." # Find all C source files (respecting .gitignore if available) if [ "$RESPECT_GITIGNORE" = true ] && is_git_repo; then - { git ls-files -- '*.c' '*.h' 2>/dev/null; git ls-files --others --exclude-standard -- '*.c' '*.h' 2>/dev/null; } | sort -u > "$RESULTS_DIR/cscope.files" + { + git ls-files -- '*.c' '*.h' 2> /dev/null + git ls-files --others --exclude-standard -- '*.c' '*.h' 2> /dev/null + } | sort -u > "$RESULTS_DIR/cscope.files" elif [ "$RESPECT_GITIGNORE" = true ]; then - find . \( -name "*.c" -o -name "*.h" \) -type f 2>/dev/null | grep -Ev "/($EXCLUDE_DIRS)/" > "$RESULTS_DIR/cscope.files" + find . \( -name "*.c" -o -name "*.h" \) -type f 2> /dev/null | grep -Ev "/($EXCLUDE_DIRS)/" > "$RESULTS_DIR/cscope.files" else - find . \( -name "*.c" -o -name "*.h" \) -type f > "$RESULTS_DIR/cscope.files" 2>/dev/null + find . \( -name "*.c" -o -name "*.h" \) -type f > "$RESULTS_DIR/cscope.files" 2> /dev/null fi FILE_COUNT=$(wc -l < "$RESULTS_DIR/cscope.files") echo "Found $FILE_COUNT source files" # Build cscope database (can take a while for large repos) echo "Building database (this may take several minutes for Linux kernel)..." -cscope -b -q -i "$RESULTS_DIR/cscope.files" -f "$RESULTS_DIR/cscope.out" 2>/dev/null || true +cscope -b -q -i "$RESULTS_DIR/cscope.files" -f "$RESULTS_DIR/cscope.out" 2> /dev/null || true if [ -f "$RESULTS_DIR/cscope.out" ]; then - echo "Database built successfully" - echo "Database size: $(du -sh "$RESULTS_DIR/cscope.out" | cut -f1)" - - print_subheader "Example: Finding callers of 'printk' function" - cscope -d -f "$RESULTS_DIR/cscope.out" -L -3 printk 2>/dev/null | head -20 || echo "No results" - - print_subheader "Example: Finding definition of 'struct file'" - cscope -d -f "$RESULTS_DIR/cscope.out" -L -1 "struct file" 2>/dev/null | head -10 || echo "No results" + echo "Database built successfully" + echo "Database size: $(du -sh "$RESULTS_DIR/cscope.out" | cut -f1)" + + print_subheader "Example: Finding callers of 'printk' function" + cscope -d -f "$RESULTS_DIR/cscope.out" -L -3 printk 2> /dev/null | head -20 || echo "No results" + + print_subheader "Example: Finding definition of 'struct file'" + cscope -d -f "$RESULTS_DIR/cscope.out" -L -1 "struct file" 2> /dev/null | head -10 || echo "No results" fi #============================================================================== @@ -796,24 +801,24 @@ print_subheader "Analyzing a sample file with clang AST dump" # Find a simple C file to analyze (respecting .gitignore) if [ "$RESPECT_GITIGNORE" = true ] && is_git_repo; then - SAMPLE_FILE=$(git ls-files -- '*.c' 2>/dev/null | head -20 | while read -r f; do - [ -f "$f" ] && [ "$(stat -c%s "$f" 2>/dev/null || echo 999999)" -lt 51200 ] && echo "$f" - done | head -1) + SAMPLE_FILE=$(git ls-files -- '*.c' 2> /dev/null | head -20 | while read -r f; do + [ -f "$f" ] && [ "$(stat -c%s "$f" 2> /dev/null || echo 999999)" -lt 51200 ] && echo "$f" + done | head -1) elif [ "$RESPECT_GITIGNORE" = true ]; then - SAMPLE_FILE=$(find . -name "*.c" -size -50k -type f 2>/dev/null | grep -Ev "/($EXCLUDE_DIRS)/" | head -1) + SAMPLE_FILE=$(find . -name "*.c" -size -50k -type f 2> /dev/null | grep -Ev "/($EXCLUDE_DIRS)/" | head -1) else - SAMPLE_FILE=$(find . -name "*.c" -size -50k 2>/dev/null | head -1) + SAMPLE_FILE=$(find . -name "*.c" -size -50k 2> /dev/null | head -1) fi if [ -n "$SAMPLE_FILE" ]; then - echo "Sample file: $SAMPLE_FILE" - echo "" - echo "Function declarations in this file:" - clang -Xclang -ast-dump -fsyntax-only "$SAMPLE_FILE" 2>/dev/null \ - | grep -E "FunctionDecl.*<.*>" \ - | head -20 \ - | sed 's/.*FunctionDecl.*<[^>]*> / /' \ - | tee "$RESULTS_DIR/clang_sample_functions.txt" || echo "Analysis failed (missing headers)" + echo "Sample file: $SAMPLE_FILE" + echo "" + echo "Function declarations in this file:" + clang -Xclang -ast-dump -fsyntax-only "$SAMPLE_FILE" 2> /dev/null | + grep -E "FunctionDecl.*<.*>" | + head -20 | + sed 's/.*FunctionDecl.*<[^>]*> / /' | + tee "$RESULTS_DIR/clang_sample_functions.txt" || echo "Analysis failed (missing headers)" fi print_subheader "Note: Full clang analysis requires compile_commands.json" @@ -836,26 +841,26 @@ echo -e "${GREEN}Quick Summary:${NC}" echo "" if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then - echo "Top 5 Language Keywords (in code):" - head -5 "$RESULTS_DIR/grep_keywords.txt" | awk '{printf " %s: %s times\n", $2, $1}' + echo "Top 5 Language Keywords (in code):" + head -5 "$RESULTS_DIR/grep_keywords.txt" | awk '{printf " %s: %s times\n", $2, $1}' fi echo "" if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then - echo "Top 5 Function/Method Calls (in code):" - head -5 "$RESULTS_DIR/grep_function_calls.txt" | awk '{printf " %s(): %s times\n", $2, $1}' + echo "Top 5 Function/Method Calls (in code):" + head -5 "$RESULTS_DIR/grep_function_calls.txt" | awk '{printf " %s(): %s times\n", $2, $1}' fi echo "" if [ -f "$RESULTS_DIR/comment_words.txt" ]; then - echo "Top 5 Words in Comments:" - head -5 "$RESULTS_DIR/comment_words.txt" | awk '{printf " %s: %s times\n", $2, $1}' + echo "Top 5 Words in Comments:" + head -5 "$RESULTS_DIR/comment_words.txt" | awk '{printf " %s: %s times\n", $2, $1}' fi echo "" if [ -f "$RESULTS_DIR/grep_imports.txt" ]; then - echo "Top 5 Imports/Includes:" - head -5 "$RESULTS_DIR/grep_imports.txt" | awk '{count=$1; $1=""; printf " %s: %s times\n", substr($0,2), count}' + echo "Top 5 Imports/Includes:" + head -5 "$RESULTS_DIR/grep_imports.txt" | awk '{count=$1; $1=""; printf " %s: %s times\n", substr($0,2), count}' fi echo "" diff --git a/scripts/utils/android_guardian/post-fs-data.sh b/scripts/utils/android_guardian/post-fs-data.sh index 1e47410..f172b7b 100755 --- a/scripts/utils/android_guardian/post-fs-data.sh +++ b/scripts/utils/android_guardian/post-fs-data.sh @@ -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" diff --git a/scripts/utils/android_guardian/service.sh b/scripts/utils/android_guardian/service.sh index eab2044..8729326 100755 --- a/scripts/utils/android_guardian/service.sh +++ b/scripts/utils/android_guardian/service.sh @@ -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: $!)" diff --git a/scripts/utils/android_guardian/uninstall.sh b/scripts/utils/android_guardian/uninstall.sh index ae76f6e..b229f21 100755 --- a/scripts/utils/android_guardian/uninstall.sh +++ b/scripts/utils/android_guardian/uninstall.sh @@ -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 diff --git a/scripts/utils/convert_video.sh b/scripts/utils/convert_video.sh index a7d1692..ea4a7fb 100644 --- a/scripts/utils/convert_video.sh +++ b/scripts/utils/convert_video.sh @@ -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 </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 "$@" diff --git a/scripts/utils/download_exercism_bulk.sh b/scripts/utils/download_exercism_bulk.sh index fb904ed..3ee3112 100755 --- a/scripts/utils/download_exercism_bulk.sh +++ b/scripts/utils/download_exercism_bulk.sh @@ -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 diff --git a/scripts/utils/find_keepassxc.sh b/scripts/utils/find_keepassxc.sh index f80dcba..192c0de 100755 --- a/scripts/utils/find_keepassxc.sh +++ b/scripts/utils/find_keepassxc.sh @@ -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" diff --git a/scripts/utils/generate_study_materials.sh b/scripts/utils/generate_study_materials.sh index 77dce3b..63b3c62 100755 --- a/scripts/utils/generate_study_materials.sh +++ b/scripts/utils/generate_study_materials.sh @@ -14,24 +14,24 @@ set -e #============================================================================== RESULTS_DIR="${1:-.}" TOP_N=30 -LANGUAGES="auto" # Will detect from results +LANGUAGES="auto" # Will detect from results # Parse arguments shift || true while [[ $# -gt 0 ]]; do - case "$1" in - --top) - TOP_N="$2" - shift 2 - ;; - --languages) - LANGUAGES="$2" - shift 2 - ;; - *) - shift - ;; - esac + case "$1" in + --top) + TOP_N="$2" + shift 2 + ;; + --languages) + LANGUAGES="$2" + shift 2 + ;; + *) + shift + ;; + esac done # Output files @@ -46,7 +46,7 @@ USE_OFFLINE_DOCS=false # Check if offline docs are available if [ -d "$OFFLINE_DOCS_DIR" ] && [ -x "$LOOKUP_SCRIPT" ]; then - USE_OFFLINE_DOCS=true + USE_OFFLINE_DOCS=true fi # Colors @@ -60,33 +60,33 @@ NC='\033[0m' # Offline Documentation Lookup (preferred if available) #============================================================================== lookup_offline() { - local term="$1" - local lang="$2" - local import_line="$3" # Optional: full import line for context - - if ! $USE_OFFLINE_DOCS; then - return 1 - fi - - local result - if [ -n "$import_line" ]; then - # Use import-aware lookup - get the line with the file path - result=$("$LOOKUP_SCRIPT" --import "$import_line" "$lang" 2>/dev/null | grep "^/" | head -1) - else - result=$("$LOOKUP_SCRIPT" "$term" "$lang" 2>/dev/null | grep "^File:" | head -1 | sed 's/^File: //') - fi - - if [ -n "$result" ]; then - # Extract file path (before the | separator) - local file_path - file_path=$(echo "$result" | cut -d'|' -f1) - if [ -n "$file_path" ]; then - echo "$file_path" - return 0 - fi - fi - + local term="$1" + local lang="$2" + local import_line="$3" # Optional: full import line for context + + if ! $USE_OFFLINE_DOCS; then return 1 + fi + + local result + if [ -n "$import_line" ]; then + # Use import-aware lookup - get the line with the file path + result=$("$LOOKUP_SCRIPT" --import "$import_line" "$lang" 2> /dev/null | grep "^/" | head -1) + else + result=$("$LOOKUP_SCRIPT" "$term" "$lang" 2> /dev/null | grep "^File:" | head -1 | sed 's/^File: //') + fi + + if [ -n "$result" ]; then + # Extract file path (before the | separator) + local file_path + file_path=$(echo "$result" | cut -d'|' -f1) + if [ -n "$file_path" ]; then + echo "$file_path" + return 0 + fi + fi + + return 1 } #============================================================================== @@ -95,345 +95,345 @@ lookup_offline() { # Python documentation python_doc_url() { - local term="$1" - local type="$2" # keyword, builtin, module - - case "$term" in - # Keywords - if|else|elif|for|while|try|except|finally|with|as|import|from|def|class|return|yield|raise|pass|break|continue|and|or|not|in|is|lambda|global|nonlocal|assert|del|True|False|None|async|await) - echo "https://docs.python.org/3/reference/compound_stmts.html" - ;; - # Built-in functions - print|len|range|type|str|int|float|list|dict|set|tuple|bool|open|input|format|sorted|reversed|enumerate|zip|map|filter|any|all|sum|min|max|abs|round|isinstance|issubclass|hasattr|getattr|setattr|delattr|callable|iter|next|super|property|staticmethod|classmethod|vars|dir|help|id|hash|repr|ascii|bin|hex|oct|chr|ord|eval|exec|compile) - echo "https://docs.python.org/3/library/functions.html#$term" - ;; - # Common modules - os|sys|re|json|datetime|collections|itertools|functools|pathlib|subprocess|threading|multiprocessing|asyncio|typing|dataclasses|unittest|pytest|logging|argparse|configparser) - echo "https://docs.python.org/3/library/$term.html" - ;; - # Testing - MagicMock|Mock|patch|PropertyMock) - echo "https://docs.python.org/3/library/unittest.mock.html" - ;; - *) - echo "https://docs.python.org/3/search.html?q=$term" - ;; - esac + local term="$1" + local type="$2" # keyword, builtin, module + + case "$term" in + # Keywords + if | else | elif | for | while | try | except | finally | with | as | import | from | def | class | return | yield | raise | pass | break | continue | and | or | not | in | is | lambda | global | nonlocal | assert | del | True | False | None | async | await) + echo "https://docs.python.org/3/reference/compound_stmts.html" + ;; + # Built-in functions + print | len | range | type | str | int | float | list | dict | set | tuple | bool | open | input | format | sorted | reversed | enumerate | zip | map | filter | any | all | sum | min | max | abs | round | isinstance | issubclass | hasattr | getattr | setattr | delattr | callable | iter | next | super | property | staticmethod | classmethod | vars | dir | help | id | hash | repr | ascii | bin | hex | oct | chr | ord | eval | exec | compile) + echo "https://docs.python.org/3/library/functions.html#$term" + ;; + # Common modules + os | sys | re | json | datetime | collections | itertools | functools | pathlib | subprocess | threading | multiprocessing | asyncio | typing | dataclasses | unittest | pytest | logging | argparse | configparser) + echo "https://docs.python.org/3/library/$term.html" + ;; + # Testing + MagicMock | Mock | patch | PropertyMock) + echo "https://docs.python.org/3/library/unittest.mock.html" + ;; + *) + echo "https://docs.python.org/3/search.html?q=$term" + ;; + esac } # JavaScript/TypeScript documentation (MDN) js_doc_url() { - local term="$1" - - case "$term" in - # Keywords & statements - if|else|for|while|do|switch|case|break|continue|return|throw|try|catch|finally|function|class|const|let|var|new|this|super|import|export|default|async|await|yield|typeof|instanceof|in|of|delete|void) - echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements" - ;; - # Global objects - Array|Object|String|Number|Boolean|Symbol|Map|Set|WeakMap|WeakSet|Date|RegExp|Error|Promise|Proxy|Reflect|JSON|Math|Intl) - echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/$term" - ;; - # Array methods - map|filter|reduce|forEach|find|findIndex|some|every|includes|indexOf|slice|splice|concat|join|push|pop|shift|unshift|sort|reverse|flat|flatMap) - echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/$term" - ;; - # String methods - split|replace|match|search|substring|substr|toLowerCase|toUpperCase|trim|padStart|padEnd|startsWith|endsWith|charAt|charCodeAt) - echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/$term" - ;; - # Promise methods - then|resolve|reject|all|race|allSettled|any) - echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/$term" - ;; - # Common Web APIs - fetch|console|document|window|localStorage|sessionStorage|setTimeout|setInterval|addEventListener|querySelector|querySelectorAll) - echo "https://developer.mozilla.org/en-US/docs/Web/API" - ;; - *) - echo "https://developer.mozilla.org/en-US/search?q=$term" - ;; - esac + local term="$1" + + case "$term" in + # Keywords & statements + if | else | for | while | do | switch | case | break | continue | return | throw | try | catch | finally | function | class | const | let | var | new | this | super | import | export | default | async | await | yield | typeof | instanceof | in | of | delete | void) + echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements" + ;; + # Global objects + Array | Object | String | Number | Boolean | Symbol | Map | Set | WeakMap | WeakSet | Date | RegExp | Error | Promise | Proxy | Reflect | JSON | Math | Intl) + echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/$term" + ;; + # Array methods + map | filter | reduce | forEach | find | findIndex | some | every | includes | indexOf | slice | splice | concat | join | push | pop | shift | unshift | sort | reverse | flat | flatMap) + echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/$term" + ;; + # String methods + split | replace | match | search | substring | substr | toLowerCase | toUpperCase | trim | padStart | padEnd | startsWith | endsWith | charAt | charCodeAt) + echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/$term" + ;; + # Promise methods + then | resolve | reject | all | race | allSettled | any) + echo "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/$term" + ;; + # Common Web APIs + fetch | console | document | window | localStorage | sessionStorage | setTimeout | setInterval | addEventListener | querySelector | querySelectorAll) + echo "https://developer.mozilla.org/en-US/docs/Web/API" + ;; + *) + echo "https://developer.mozilla.org/en-US/search?q=$term" + ;; + esac } # TypeScript-specific documentation ts_doc_url() { - local term="$1" - - case "$term" in - interface|type|enum|namespace|declare|readonly|abstract|implements|extends|keyof|typeof|infer|as|is|asserts|satisfies|override) - echo "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html" - ;; - Partial|Required|Readonly|Record|Pick|Omit|Exclude|Extract|NonNullable|ReturnType|Parameters|InstanceType|Awaited) - echo "https://www.typescriptlang.org/docs/handbook/utility-types.html" - ;; - *) - # Fall back to JS docs for runtime features - js_doc_url "$term" - ;; - esac + local term="$1" + + case "$term" in + interface | type | enum | namespace | declare | readonly | abstract | implements | extends | keyof | typeof | infer | as | is | asserts | satisfies | override) + echo "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html" + ;; + Partial | Required | Readonly | Record | Pick | Omit | Exclude | Extract | NonNullable | ReturnType | Parameters | InstanceType | Awaited) + echo "https://www.typescriptlang.org/docs/handbook/utility-types.html" + ;; + *) + # Fall back to JS docs for runtime features + js_doc_url "$term" + ;; + esac } # C documentation c_doc_url() { - local term="$1" - - case "$term" in - # Keywords - if|else|for|while|do|switch|case|break|continue|return|goto|sizeof|typedef|struct|union|enum|const|static|extern|register|volatile|inline|restrict|_Bool|_Complex|_Imaginary|_Alignas|_Alignof|_Atomic|_Generic|_Noreturn|_Static_assert|_Thread_local) - echo "https://en.cppreference.com/w/c/keyword/$term" - ;; - # Standard library headers - stdio|stdlib|string|math|time|ctype|stdint|stdbool|stddef|limits|float|errno|assert|signal|setjmp|stdarg|locale) - echo "https://en.cppreference.com/w/c/header/${term}.h" - ;; - # Common functions - printf|fprintf|sprintf|snprintf|scanf|fscanf|sscanf|fopen|fclose|fread|fwrite|fgets|fputs|fseek|ftell|rewind|fflush) - echo "https://en.cppreference.com/w/c/io" - ;; - malloc|calloc|realloc|free|memcpy|memmove|memset|memcmp) - echo "https://en.cppreference.com/w/c/memory" - ;; - strlen|strcpy|strncpy|strcat|strncat|strcmp|strncmp|strchr|strrchr|strstr|strtok) - echo "https://en.cppreference.com/w/c/string/byte" - ;; - *) - echo "https://en.cppreference.com/mwiki/index.php?search=$term" - ;; - esac + local term="$1" + + case "$term" in + # Keywords + if | else | for | while | do | switch | case | break | continue | return | goto | sizeof | typedef | struct | union | enum | const | static | extern | register | volatile | inline | restrict | _Bool | _Complex | _Imaginary | _Alignas | _Alignof | _Atomic | _Generic | _Noreturn | _Static_assert | _Thread_local) + echo "https://en.cppreference.com/w/c/keyword/$term" + ;; + # Standard library headers + stdio | stdlib | string | math | time | ctype | stdint | stdbool | stddef | limits | float | errno | assert | signal | setjmp | stdarg | locale) + echo "https://en.cppreference.com/w/c/header/${term}.h" + ;; + # Common functions + printf | fprintf | sprintf | snprintf | scanf | fscanf | sscanf | fopen | fclose | fread | fwrite | fgets | fputs | fseek | ftell | rewind | fflush) + echo "https://en.cppreference.com/w/c/io" + ;; + malloc | calloc | realloc | free | memcpy | memmove | memset | memcmp) + echo "https://en.cppreference.com/w/c/memory" + ;; + strlen | strcpy | strncpy | strcat | strncat | strcmp | strncmp | strchr | strrchr | strstr | strtok) + echo "https://en.cppreference.com/w/c/string/byte" + ;; + *) + echo "https://en.cppreference.com/mwiki/index.php?search=$term" + ;; + esac } # C++ documentation cpp_doc_url() { - local term="$1" - - case "$term" in - # C++ specific keywords - class|public|private|protected|virtual|override|final|explicit|mutable|constexpr|consteval|constinit|concept|requires|co_await|co_yield|co_return|nullptr|noexcept|decltype|auto|template|typename|namespace|using|new|delete|throw|try|catch|static_cast|dynamic_cast|const_cast|reinterpret_cast) - echo "https://en.cppreference.com/w/cpp/keyword/$term" - ;; - # STL containers - vector|list|deque|array|forward_list|set|map|unordered_set|unordered_map|multiset|multimap|stack|queue|priority_queue) - echo "https://en.cppreference.com/w/cpp/container/$term" - ;; - # STL algorithms - sort|find|copy|move|transform|accumulate|count|remove|unique|reverse|rotate|shuffle|partition|merge|binary_search|lower_bound|upper_bound) - echo "https://en.cppreference.com/w/cpp/algorithm/$term" - ;; - # Smart pointers - unique_ptr|shared_ptr|weak_ptr|make_unique|make_shared) - echo "https://en.cppreference.com/w/cpp/memory/$term" - ;; - # Common classes - string|string_view|optional|variant|any|tuple|pair|function|bind|thread|mutex|future|promise|chrono) - echo "https://en.cppreference.com/w/cpp/utility" - ;; - *) - # Try C docs as fallback - c_doc_url "$term" - ;; - esac + local term="$1" + + case "$term" in + # C++ specific keywords + class | public | private | protected | virtual | override | final | explicit | mutable | constexpr | consteval | constinit | concept | requires | co_await | co_yield | co_return | nullptr | noexcept | decltype | auto | template | typename | namespace | using | new | delete | throw | try | catch | static_cast | dynamic_cast | const_cast | reinterpret_cast) + echo "https://en.cppreference.com/w/cpp/keyword/$term" + ;; + # STL containers + vector | list | deque | array | forward_list | set | map | unordered_set | unordered_map | multiset | multimap | stack | queue | priority_queue) + echo "https://en.cppreference.com/w/cpp/container/$term" + ;; + # STL algorithms + sort | find | copy | move | transform | accumulate | count | remove | unique | reverse | rotate | shuffle | partition | merge | binary_search | lower_bound | upper_bound) + echo "https://en.cppreference.com/w/cpp/algorithm/$term" + ;; + # Smart pointers + unique_ptr | shared_ptr | weak_ptr | make_unique | make_shared) + echo "https://en.cppreference.com/w/cpp/memory/$term" + ;; + # Common classes + string | string_view | optional | variant | any | tuple | pair | function | bind | thread | mutex | future | promise | chrono) + echo "https://en.cppreference.com/w/cpp/utility" + ;; + *) + # Try C docs as fallback + c_doc_url "$term" + ;; + esac } # Rust documentation rust_doc_url() { - local term="$1" - - case "$term" in - # Keywords - fn|let|mut|const|static|if|else|match|loop|while|for|in|break|continue|return|struct|enum|impl|trait|type|where|pub|mod|use|crate|self|super|async|await|move|ref|dyn|unsafe|extern) - echo "https://doc.rust-lang.org/std/keyword.$term.html" - ;; - # Common types - Option|Result|Vec|String|Box|Rc|Arc|Cell|RefCell|Mutex|RwLock|HashMap|HashSet|BTreeMap|BTreeSet) - echo "https://doc.rust-lang.org/std/$term" - ;; - # Traits - Clone|Copy|Debug|Default|Eq|PartialEq|Ord|PartialOrd|Hash|Display|From|Into|AsRef|AsMut|Deref|DerefMut|Iterator|IntoIterator|Send|Sync) - echo "https://doc.rust-lang.org/std/$term" - ;; - # Macros - println|print|format|vec|panic|assert|assert_eq|assert_ne|debug_assert|todo|unimplemented|unreachable) - echo "https://doc.rust-lang.org/std/macro.$term.html" - ;; - *) - echo "https://doc.rust-lang.org/std/?search=$term" - ;; - esac + local term="$1" + + case "$term" in + # Keywords + fn | let | mut | const | static | if | else | match | loop | while | for | in | break | continue | return | struct | enum | impl | trait | type | where | pub | mod | use | crate | self | super | async | await | move | ref | dyn | unsafe | extern) + echo "https://doc.rust-lang.org/std/keyword.$term.html" + ;; + # Common types + Option | Result | Vec | String | Box | Rc | Arc | Cell | RefCell | Mutex | RwLock | HashMap | HashSet | BTreeMap | BTreeSet) + echo "https://doc.rust-lang.org/std/$term" + ;; + # Traits + Clone | Copy | Debug | Default | Eq | PartialEq | Ord | PartialOrd | Hash | Display | From | Into | AsRef | AsMut | Deref | DerefMut | Iterator | IntoIterator | Send | Sync) + echo "https://doc.rust-lang.org/std/$term" + ;; + # Macros + println | print | format | vec | panic | assert | assert_eq | assert_ne | debug_assert | todo | unimplemented | unreachable) + echo "https://doc.rust-lang.org/std/macro.$term.html" + ;; + *) + echo "https://doc.rust-lang.org/std/?search=$term" + ;; + esac } # Go documentation go_doc_url() { - local term="$1" - - case "$term" in - # Keywords - func|var|const|type|struct|interface|map|chan|go|select|defer|if|else|for|range|switch|case|default|break|continue|return|goto|fallthrough|package|import) - echo "https://go.dev/ref/spec" - ;; - # Built-in functions - make|new|len|cap|append|copy|delete|close|panic|recover|print|println|complex|real|imag) - echo "https://pkg.go.dev/builtin#$term" - ;; - # Common packages - fmt|os|io|net|http|json|time|strings|strconv|errors|context|sync|testing|reflect|regexp|sort|math|crypto|encoding|bufio|bytes|path|filepath) - echo "https://pkg.go.dev/$term" - ;; - *) - echo "https://pkg.go.dev/search?q=$term" - ;; - esac + local term="$1" + + case "$term" in + # Keywords + func | var | const | type | struct | interface | map | chan | go | select | defer | if | else | for | range | switch | case | default | break | continue | return | goto | fallthrough | package | import) + echo "https://go.dev/ref/spec" + ;; + # Built-in functions + make | new | len | cap | append | copy | delete | close | panic | recover | print | println | complex | real | imag) + echo "https://pkg.go.dev/builtin#$term" + ;; + # Common packages + fmt | os | io | net | http | json | time | strings | strconv | errors | context | sync | testing | reflect | regexp | sort | math | crypto | encoding | bufio | bytes | path | filepath) + echo "https://pkg.go.dev/$term" + ;; + *) + echo "https://pkg.go.dev/search?q=$term" + ;; + esac } # Ruby documentation ruby_doc_url() { - local term="$1" - - case "$term" in - # Keywords - if|else|elsif|unless|case|when|while|until|for|do|end|begin|rescue|ensure|raise|return|break|next|redo|retry|yield|def|class|module|self|super|nil|true|false|and|or|not|in|then|alias|defined|__FILE__|__LINE__|__ENCODING__) - echo "https://ruby-doc.org/docs/keywords/1.9/" - ;; - # Core classes - String|Array|Hash|Integer|Float|Symbol|Range|Regexp|Time|Date|File|Dir|IO|Proc|Lambda|Method|Thread|Mutex|Fiber) - echo "https://ruby-doc.org/core/classes/$term.html" - ;; - # Enumerable methods - each|map|select|reject|find|reduce|inject|collect|detect|sort|sort_by|group_by|partition|any|all|none|one|count|first|last|take|drop) - echo "https://ruby-doc.org/core/Enumerable.html" - ;; - *) - echo "https://ruby-doc.org/search.html?q=$term" - ;; - esac + local term="$1" + + case "$term" in + # Keywords + if | else | elsif | unless | case | when | while | until | for | do | end | begin | rescue | ensure | raise | return | break | next | redo | retry | yield | def | class | module | self | super | nil | true | false | and | or | not | in | then | alias | defined | __FILE__ | __LINE__ | __ENCODING__) + echo "https://ruby-doc.org/docs/keywords/1.9/" + ;; + # Core classes + String | Array | Hash | Integer | Float | Symbol | Range | Regexp | Time | Date | File | Dir | IO | Proc | Lambda | Method | Thread | Mutex | Fiber) + echo "https://ruby-doc.org/core/classes/$term.html" + ;; + # Enumerable methods + each | map | select | reject | find | reduce | inject | collect | detect | sort | sort_by | group_by | partition | any | all | none | one | count | first | last | take | drop) + echo "https://ruby-doc.org/core/Enumerable.html" + ;; + *) + echo "https://ruby-doc.org/search.html?q=$term" + ;; + esac } # Java documentation java_doc_url() { - local term="$1" - - case "$term" in - # Keywords - if|else|for|while|do|switch|case|break|continue|return|throw|try|catch|finally|class|interface|enum|extends|implements|new|this|super|static|final|abstract|public|private|protected|void|null|true|false|instanceof|synchronized|volatile|transient|native|strictfp|assert|default|package|import) - echo "https://docs.oracle.com/javase/tutorial/java/nutsandbolts/" - ;; - # Common classes - String|Integer|Long|Double|Float|Boolean|Character|Object|Class|System|Math|Arrays|Collections|List|ArrayList|LinkedList|Map|HashMap|TreeMap|Set|HashSet|TreeSet|Queue|Stack|Optional|Stream) - echo "https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/$term.html" - ;; - *) - echo "https://docs.oracle.com/en/java/javase/17/docs/api/search.html?q=$term" - ;; - esac + local term="$1" + + case "$term" in + # Keywords + if | else | for | while | do | switch | case | break | continue | return | throw | try | catch | finally | class | interface | enum | extends | implements | new | this | super | static | final | abstract | public | private | protected | void | null | true | false | instanceof | synchronized | volatile | transient | native | strictfp | assert | default | package | import) + echo "https://docs.oracle.com/javase/tutorial/java/nutsandbolts/" + ;; + # Common classes + String | Integer | Long | Double | Float | Boolean | Character | Object | Class | System | Math | Arrays | Collections | List | ArrayList | LinkedList | Map | HashMap | TreeMap | Set | HashSet | TreeSet | Queue | Stack | Optional | Stream) + echo "https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/$term.html" + ;; + *) + echo "https://docs.oracle.com/en/java/javase/17/docs/api/search.html?q=$term" + ;; + esac } # Shell documentation shell_doc_url() { - local term="$1" - - case "$term" in - # Built-in commands - if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time|coproc) - echo "https://www.gnu.org/software/bash/manual/bash.html#Conditional-Constructs" - ;; - echo|printf|read|declare|local|export|unset|set|shopt|alias|source|eval|exec|exit|return|break|continue|shift|trap|wait|kill|jobs|bg|fg|disown|suspend|logout|cd|pwd|pushd|popd|dirs|type|which|command|builtin|enable|help|hash|bind|complete|compgen|compopt) - echo "https://www.gnu.org/software/bash/manual/bash.html#Shell-Builtin-Commands" - ;; - # Common external commands - grep|sed|awk|find|xargs|sort|uniq|cut|tr|head|tail|wc|cat|tee|diff|patch|tar|gzip|zip|curl|wget|ssh|scp|rsync|git|make|chmod|chown|chgrp|ln|cp|mv|rm|mkdir|rmdir|touch|ls|stat|file|df|du|free|top|ps|kill|pkill|pgrep|nohup|screen|tmux) - echo "https://man7.org/linux/man-pages/man1/$term.1.html" - ;; - *) - echo "https://www.gnu.org/software/bash/manual/bash.html" - ;; - esac + local term="$1" + + case "$term" in + # Built-in commands + if | then | else | elif | fi | for | while | until | do | done | case | esac | in | function | select | time | coproc) + echo "https://www.gnu.org/software/bash/manual/bash.html#Conditional-Constructs" + ;; + echo | printf | read | declare | local | export | unset | set | shopt | alias | source | eval | exec | exit | return | break | continue | shift | trap | wait | kill | jobs | bg | fg | disown | suspend | logout | cd | pwd | pushd | popd | dirs | type | which | command | builtin | enable | help | hash | bind | complete | compgen | compopt) + echo "https://www.gnu.org/software/bash/manual/bash.html#Shell-Builtin-Commands" + ;; + # Common external commands + grep | sed | awk | find | xargs | sort | uniq | cut | tr | head | tail | wc | cat | tee | diff | patch | tar | gzip | zip | curl | wget | ssh | scp | rsync | git | make | chmod | chown | chgrp | ln | cp | mv | rm | mkdir | rmdir | touch | ls | stat | file | df | du | free | top | ps | kill | pkill | pgrep | nohup | screen | tmux) + echo "https://man7.org/linux/man-pages/man1/$term.1.html" + ;; + *) + echo "https://www.gnu.org/software/bash/manual/bash.html" + ;; + esac } #============================================================================== # Get documentation URL for a term based on detected language #============================================================================== get_doc_url() { - local term="$1" - local lang="$2" - local import_line="$3" # Optional: full import for context - - # Try offline docs first - local offline_result - offline_result=$(lookup_offline "$term" "$lang" "$import_line") + local term="$1" + local lang="$2" + local import_line="$3" # Optional: full import for context + + # Try offline docs first + local offline_result + offline_result=$(lookup_offline "$term" "$lang" "$import_line") + if [ -n "$offline_result" ]; then + echo "$offline_result" + return 0 + fi + + # For TypeScript, also try JavaScript offline docs (most TS keywords are JS) + if [[ $lang == "typescript" || $lang == "ts" || $lang == "tsx" ]]; then + offline_result=$(lookup_offline "$term" "js" "$import_line") if [ -n "$offline_result" ]; then - echo "$offline_result" - return 0 + echo "$offline_result" + return 0 fi - - # For TypeScript, also try JavaScript offline docs (most TS keywords are JS) - if [[ "$lang" == "typescript" || "$lang" == "ts" || "$lang" == "tsx" ]]; then - offline_result=$(lookup_offline "$term" "js" "$import_line") - if [ -n "$offline_result" ]; then - echo "$offline_result" - return 0 - fi - fi - - # Fall back to online URLs - case "$lang" in - python|py) - python_doc_url "$term" - ;; - javascript|js|jsx) - js_doc_url "$term" - ;; - typescript|ts|tsx) - # For TypeScript, try JS doc first (since most keywords are shared) - # Only use TS-specific docs for TS-only features - case "$term" in - interface|type|enum|namespace|declare|readonly|abstract|implements|keyof|infer|as|is|asserts|satisfies|override|Partial|Required|Readonly|Record|Pick|Omit|Exclude|Extract|NonNullable|ReturnType|Parameters|InstanceType|Awaited) - ts_doc_url "$term" - ;; - *) - js_doc_url "$term" - ;; - esac - ;; - c) - c_doc_url "$term" - ;; - cpp|c++|cc|cxx) - cpp_doc_url "$term" - ;; - rust|rs) - rust_doc_url "$term" - ;; - go) - go_doc_url "$term" - ;; - ruby|rb) - ruby_doc_url "$term" - ;; - java) - java_doc_url "$term" - ;; - shell|bash|sh) - shell_doc_url "$term" - ;; + fi + + # Fall back to online URLs + case "$lang" in + python | py) + python_doc_url "$term" + ;; + javascript | js | jsx) + js_doc_url "$term" + ;; + typescript | ts | tsx) + # For TypeScript, try JS doc first (since most keywords are shared) + # Only use TS-specific docs for TS-only features + case "$term" in + interface | type | enum | namespace | declare | readonly | abstract | implements | keyof | infer | as | is | asserts | satisfies | override | Partial | Required | Readonly | Record | Pick | Omit | Exclude | Extract | NonNullable | ReturnType | Parameters | InstanceType | Awaited) + ts_doc_url "$term" + ;; *) - echo "https://devdocs.io/#q=$term" - ;; - esac + js_doc_url "$term" + ;; + esac + ;; + c) + c_doc_url "$term" + ;; + cpp | c++ | cc | cxx) + cpp_doc_url "$term" + ;; + rust | rs) + rust_doc_url "$term" + ;; + go) + go_doc_url "$term" + ;; + ruby | rb) + ruby_doc_url "$term" + ;; + java) + java_doc_url "$term" + ;; + shell | bash | sh) + shell_doc_url "$term" + ;; + *) + echo "https://devdocs.io/#q=$term" + ;; + esac } #============================================================================== # Detect primary language from results #============================================================================== detect_language() { - if [ -f "$RESULTS_DIR/tokei_stats.txt" ]; then - # Parse tokei output to find most used language - grep -E "^\s+(Python|JavaScript|TypeScript|C\+\+|C |Rust|Go|Ruby|Java|Shell)" "$RESULTS_DIR/tokei_stats.txt" 2>/dev/null \ - | head -1 \ - | awk '{print tolower($1)}' \ - | sed 's/c++/cpp/' - else - echo "unknown" - fi + if [ -f "$RESULTS_DIR/tokei_stats.txt" ]; then + # Parse tokei output to find most used language + grep -E "^\s+(Python|JavaScript|TypeScript|C\+\+|C |Rust|Go|Ruby|Java|Shell)" "$RESULTS_DIR/tokei_stats.txt" 2> /dev/null | + head -1 | + awk '{print tolower($1)}' | + sed 's/c++/cpp/' + else + echo "unknown" + fi } #============================================================================== @@ -442,18 +442,18 @@ detect_language() { # Check if results directory exists if [ ! -d "$RESULTS_DIR" ]; then - echo -e "${RED}Error: Results directory not found: $RESULTS_DIR${NC}" - echo "Run analyze_repo.sh first to generate analysis results." - exit 1 + echo -e "${RED}Error: Results directory not found: $RESULTS_DIR${NC}" + echo "Run analyze_repo.sh first to generate analysis results." + exit 1 fi # Detect or use specified language if [ "$LANGUAGES" = "auto" ]; then - PRIMARY_LANG=$(detect_language) - echo -e "${BLUE}Detected primary language: ${GREEN}$PRIMARY_LANG${NC}" + PRIMARY_LANG=$(detect_language) + echo -e "${BLUE}Detected primary language: ${GREEN}$PRIMARY_LANG${NC}" else - PRIMARY_LANG=$(echo "$LANGUAGES" | cut -d',' -f1) - echo -e "${BLUE}Using specified language: ${GREEN}$PRIMARY_LANG${NC}" + PRIMARY_LANG=$(echo "$LANGUAGES" | cut -d',' -f1) + echo -e "${BLUE}Using specified language: ${GREEN}$PRIMARY_LANG${NC}" fi echo "" @@ -484,189 +484,189 @@ EOF PER_LANG_DIR="$RESULTS_DIR/per_language" if [ -d "$PER_LANG_DIR" ]; then - echo -e "${GREEN}Using per-language analysis files${NC}" - - # Map internal lang names to doc function names - lang_to_doc() { - case "$1" in - c_cpp) echo "cpp" ;; - javascript) echo "js" ;; - typescript) echo "ts" ;; - shell) echo "bash" ;; - *) echo "$1" ;; - esac - } - - # Process keywords by language + echo -e "${GREEN}Using per-language analysis files${NC}" + + # Map internal lang names to doc function names + lang_to_doc() { + case "$1" in + c_cpp) echo "cpp" ;; + javascript) echo "js" ;; + typescript) echo "ts" ;; + shell) echo "bash" ;; + *) echo "$1" ;; + esac + } + + # Process keywords by language + echo "## Language Keywords" >> "$DOCS_FILE" + echo "" >> "$DOCS_FILE" + + for keyword_file in "$PER_LANG_DIR"/keywords_*.txt; do + [ ! -f "$keyword_file" ] && continue + [ ! -s "$keyword_file" ] && continue + + # Extract language name from filename + lang=$(basename "$keyword_file" | sed 's/keywords_//; s/\.txt//') + doc_lang=$(lang_to_doc "$lang") + + # Format language name for display + case "$lang" in + c_cpp) display_lang="C/C++" ;; + javascript) display_lang="JavaScript" ;; + typescript) display_lang="TypeScript" ;; + python) display_lang="Python" ;; + rust) display_lang="Rust" ;; + go) display_lang="Go" ;; + ruby) display_lang="Ruby" ;; + java) display_lang="Java" ;; + shell) display_lang="Shell/Bash" ;; + *) display_lang="$lang" ;; + esac + + echo "### $display_lang Keywords" >> "$DOCS_FILE" + echo "" >> "$DOCS_FILE" + echo "| Keyword | Count | Documentation |" >> "$DOCS_FILE" + echo "|---------|-------|---------------|" >> "$DOCS_FILE" + + head -$TOP_N "$keyword_file" | while read -r count term; do + [ -z "$term" ] && continue + [[ $term =~ ^[#] ]] && continue # Skip comment lines + url=$(get_doc_url "$term" "$doc_lang") + echo "| \`$term\` | $count | [docs]($url) |" >> "$DOCS_FILE" + done + echo "" >> "$DOCS_FILE" + done + + # Process functions by language + echo "## Function/Method Calls" >> "$DOCS_FILE" + echo "" >> "$DOCS_FILE" + + for func_file in "$PER_LANG_DIR"/functions_*.txt; do + [ ! -f "$func_file" ] && continue + [ ! -s "$func_file" ] && continue + + lang=$(basename "$func_file" | sed 's/functions_//; s/\.txt//') + doc_lang=$(lang_to_doc "$lang") + + case "$lang" in + c_cpp) display_lang="C/C++" ;; + javascript) display_lang="JavaScript" ;; + typescript) display_lang="TypeScript" ;; + python) display_lang="Python" ;; + rust) display_lang="Rust" ;; + go) display_lang="Go" ;; + ruby) display_lang="Ruby" ;; + java) display_lang="Java" ;; + shell) display_lang="Shell/Bash" ;; + *) display_lang="$lang" ;; + esac + + echo "### $display_lang Functions" >> "$DOCS_FILE" + echo "" >> "$DOCS_FILE" + echo "| Function | Count | Documentation |" >> "$DOCS_FILE" + echo "|----------|-------|---------------|" >> "$DOCS_FILE" + + head -$TOP_N "$func_file" | while read -r count term; do + [ -z "$term" ] && continue + [[ $term =~ ^(if|for|while|switch|catch|elif)$ ]] && continue + url=$(get_doc_url "$term" "$doc_lang") + echo "| \`$term()\` | $count | [docs]($url) |" >> "$DOCS_FILE" + done + echo "" >> "$DOCS_FILE" + done + + # Process imports by language + echo "## Imports/Includes" >> "$DOCS_FILE" + echo "" >> "$DOCS_FILE" + + for import_file in "$PER_LANG_DIR"/imports_*.txt; do + [ ! -f "$import_file" ] && continue + [ ! -s "$import_file" ] && continue + + lang=$(basename "$import_file" | sed 's/imports_//; s/\.txt//') + doc_lang=$(lang_to_doc "$lang") + + case "$lang" in + c_cpp) display_lang="C/C++ (#include)" ;; + javascript) display_lang="JavaScript (import/require)" ;; + typescript) display_lang="TypeScript (import)" ;; + python) display_lang="Python (import/from)" ;; + rust) display_lang="Rust (use)" ;; + go) display_lang="Go (import)" ;; + ruby) display_lang="Ruby (require)" ;; + java) display_lang="Java (import)" ;; + shell) display_lang="Shell (source)" ;; + *) display_lang="$lang" ;; + esac + + echo "### $display_lang" >> "$DOCS_FILE" + echo "" >> "$DOCS_FILE" + echo "| Import | Count | Documentation |" >> "$DOCS_FILE" + echo "|--------|-------|---------------|" >> "$DOCS_FILE" + + head -20 "$import_file" | while read -r count import; do + [ -z "$import" ] && continue + # For offline lookup, pass the full import line for better context + url=$(get_doc_url "" "$doc_lang" "$import") + if [ -z "$url" ] || [[ $url == *"search.html"* ]]; then + # Fallback: extract module and try again + module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//') + url=$(get_doc_url "$module" "$doc_lang") + fi + import_escaped=$(echo "$import" | sed 's/|/\\|/g') + echo "| \`$import_escaped\` | $count | [docs]($url) |" >> "$DOCS_FILE" + done + echo "" >> "$DOCS_FILE" + done + +else + # Fallback to combined files (old behavior) + echo -e "${YELLOW}No per-language files found, using combined analysis${NC}" + + if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then echo "## Language Keywords" >> "$DOCS_FILE" echo "" >> "$DOCS_FILE" - - for keyword_file in "$PER_LANG_DIR"/keywords_*.txt; do - [ ! -f "$keyword_file" ] && continue - [ ! -s "$keyword_file" ] && continue - - # Extract language name from filename - lang=$(basename "$keyword_file" | sed 's/keywords_//; s/\.txt//') - doc_lang=$(lang_to_doc "$lang") - - # Format language name for display - case "$lang" in - c_cpp) display_lang="C/C++" ;; - javascript) display_lang="JavaScript" ;; - typescript) display_lang="TypeScript" ;; - python) display_lang="Python" ;; - rust) display_lang="Rust" ;; - go) display_lang="Go" ;; - ruby) display_lang="Ruby" ;; - java) display_lang="Java" ;; - shell) display_lang="Shell/Bash" ;; - *) display_lang="$lang" ;; - esac - - echo "### $display_lang Keywords" >> "$DOCS_FILE" - echo "" >> "$DOCS_FILE" - echo "| Keyword | Count | Documentation |" >> "$DOCS_FILE" - echo "|---------|-------|---------------|" >> "$DOCS_FILE" - - head -$TOP_N "$keyword_file" | while read -r count term; do - [ -z "$term" ] && continue - [[ "$term" =~ ^[#] ]] && continue # Skip comment lines - url=$(get_doc_url "$term" "$doc_lang") - echo "| \`$term\` | $count | [docs]($url) |" >> "$DOCS_FILE" - done - echo "" >> "$DOCS_FILE" + echo "| Keyword | Count | Documentation |" >> "$DOCS_FILE" + echo "|---------|-------|---------------|" >> "$DOCS_FILE" + + head -$TOP_N "$RESULTS_DIR/grep_keywords.txt" | while read -r count term; do + [ -z "$term" ] && continue + url=$(get_doc_url "$term" "$PRIMARY_LANG") + echo "| \`$term\` | $count | [docs]($url) |" >> "$DOCS_FILE" done - - # Process functions by language + echo "" >> "$DOCS_FILE" + fi + + if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then echo "## Function/Method Calls" >> "$DOCS_FILE" echo "" >> "$DOCS_FILE" - - for func_file in "$PER_LANG_DIR"/functions_*.txt; do - [ ! -f "$func_file" ] && continue - [ ! -s "$func_file" ] && continue - - lang=$(basename "$func_file" | sed 's/functions_//; s/\.txt//') - doc_lang=$(lang_to_doc "$lang") - - case "$lang" in - c_cpp) display_lang="C/C++" ;; - javascript) display_lang="JavaScript" ;; - typescript) display_lang="TypeScript" ;; - python) display_lang="Python" ;; - rust) display_lang="Rust" ;; - go) display_lang="Go" ;; - ruby) display_lang="Ruby" ;; - java) display_lang="Java" ;; - shell) display_lang="Shell/Bash" ;; - *) display_lang="$lang" ;; - esac - - echo "### $display_lang Functions" >> "$DOCS_FILE" - echo "" >> "$DOCS_FILE" - echo "| Function | Count | Documentation |" >> "$DOCS_FILE" - echo "|----------|-------|---------------|" >> "$DOCS_FILE" - - head -$TOP_N "$func_file" | while read -r count term; do - [ -z "$term" ] && continue - [[ "$term" =~ ^(if|for|while|switch|catch|elif)$ ]] && continue - url=$(get_doc_url "$term" "$doc_lang") - echo "| \`$term()\` | $count | [docs]($url) |" >> "$DOCS_FILE" - done - echo "" >> "$DOCS_FILE" + echo "| Function | Count | Documentation |" >> "$DOCS_FILE" + echo "|----------|-------|---------------|" >> "$DOCS_FILE" + + head -$TOP_N "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do + [ -z "$term" ] && continue + [[ $term =~ ^(if|for|while|switch|catch)$ ]] && continue + url=$(get_doc_url "$term" "$PRIMARY_LANG") + echo "| \`$term()\` | $count | [docs]($url) |" >> "$DOCS_FILE" done - - # Process imports by language + echo "" >> "$DOCS_FILE" + fi + + if [ -f "$RESULTS_DIR/grep_imports.txt" ]; then echo "## Imports/Includes" >> "$DOCS_FILE" echo "" >> "$DOCS_FILE" - - for import_file in "$PER_LANG_DIR"/imports_*.txt; do - [ ! -f "$import_file" ] && continue - [ ! -s "$import_file" ] && continue - - lang=$(basename "$import_file" | sed 's/imports_//; s/\.txt//') - doc_lang=$(lang_to_doc "$lang") - - case "$lang" in - c_cpp) display_lang="C/C++ (#include)" ;; - javascript) display_lang="JavaScript (import/require)" ;; - typescript) display_lang="TypeScript (import)" ;; - python) display_lang="Python (import/from)" ;; - rust) display_lang="Rust (use)" ;; - go) display_lang="Go (import)" ;; - ruby) display_lang="Ruby (require)" ;; - java) display_lang="Java (import)" ;; - shell) display_lang="Shell (source)" ;; - *) display_lang="$lang" ;; - esac - - echo "### $display_lang" >> "$DOCS_FILE" - echo "" >> "$DOCS_FILE" - echo "| Import | Count | Documentation |" >> "$DOCS_FILE" - echo "|--------|-------|---------------|" >> "$DOCS_FILE" - - head -20 "$import_file" | while read -r count import; do - [ -z "$import" ] && continue - # For offline lookup, pass the full import line for better context - url=$(get_doc_url "" "$doc_lang" "$import") - if [ -z "$url" ] || [[ "$url" == *"search.html"* ]]; then - # Fallback: extract module and try again - module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//') - url=$(get_doc_url "$module" "$doc_lang") - fi - import_escaped=$(echo "$import" | sed 's/|/\\|/g') - echo "| \`$import_escaped\` | $count | [docs]($url) |" >> "$DOCS_FILE" - done - echo "" >> "$DOCS_FILE" + echo "| Import | Count | Documentation |" >> "$DOCS_FILE" + echo "|--------|-------|---------------|" >> "$DOCS_FILE" + + head -20 "$RESULTS_DIR/grep_imports.txt" | while read -r count import; do + [ -z "$import" ] && continue + module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//') + url=$(get_doc_url "$module" "$PRIMARY_LANG") + import_escaped=$(echo "$import" | sed 's/|/\\|/g') + echo "| \`$import_escaped\` | $count | [docs]($url) |" >> "$DOCS_FILE" done - -else - # Fallback to combined files (old behavior) - echo -e "${YELLOW}No per-language files found, using combined analysis${NC}" - - if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then - echo "## Language Keywords" >> "$DOCS_FILE" - echo "" >> "$DOCS_FILE" - echo "| Keyword | Count | Documentation |" >> "$DOCS_FILE" - echo "|---------|-------|---------------|" >> "$DOCS_FILE" - - head -$TOP_N "$RESULTS_DIR/grep_keywords.txt" | while read -r count term; do - [ -z "$term" ] && continue - url=$(get_doc_url "$term" "$PRIMARY_LANG") - echo "| \`$term\` | $count | [docs]($url) |" >> "$DOCS_FILE" - done - echo "" >> "$DOCS_FILE" - fi - - if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then - echo "## Function/Method Calls" >> "$DOCS_FILE" - echo "" >> "$DOCS_FILE" - echo "| Function | Count | Documentation |" >> "$DOCS_FILE" - echo "|----------|-------|---------------|" >> "$DOCS_FILE" - - head -$TOP_N "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do - [ -z "$term" ] && continue - [[ "$term" =~ ^(if|for|while|switch|catch)$ ]] && continue - url=$(get_doc_url "$term" "$PRIMARY_LANG") - echo "| \`$term()\` | $count | [docs]($url) |" >> "$DOCS_FILE" - done - echo "" >> "$DOCS_FILE" - fi - - if [ -f "$RESULTS_DIR/grep_imports.txt" ]; then - echo "## Imports/Includes" >> "$DOCS_FILE" - echo "" >> "$DOCS_FILE" - echo "| Import | Count | Documentation |" >> "$DOCS_FILE" - echo "|--------|-------|---------------|" >> "$DOCS_FILE" - - head -20 "$RESULTS_DIR/grep_imports.txt" | while read -r count import; do - [ -z "$import" ] && continue - module=$(echo "$import" | sed -E 's/.*[<"]([^">]+)[">].*/\1/' | sed 's|.*/||' | sed 's/\..*$//') - url=$(get_doc_url "$module" "$PRIMARY_LANG") - import_escaped=$(echo "$import" | sed 's/|/\\|/g') - echo "| \`$import_escaped\` | $count | [docs]($url) |" >> "$DOCS_FILE" - done - echo "" >> "$DOCS_FILE" - fi + echo "" >> "$DOCS_FILE" + fi fi echo "" >> "$DOCS_FILE" @@ -693,52 +693,52 @@ EOF # Generate cards for top keywords if [ -f "$RESULTS_DIR/grep_keywords.txt" ]; then - echo "# Keywords" >> "$ANKI_FILE" - head -$TOP_N "$RESULTS_DIR/grep_keywords.txt" | while read -r count term; do - [ -z "$term" ] && continue - url=$(get_doc_url "$term" "$PRIMARY_LANG") - - # Create different card types based on term type - case "$term" in - if|else|elif|elseif|switch|case|match) - echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tConditional control flow - executes code based on boolean conditions. See: $url\t${PRIMARY_LANG}::keywords::control-flow" >> "$ANKI_FILE" - ;; - for|while|loop|do|until) - echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tLoop construct - repeats code execution. See: $url\t${PRIMARY_LANG}::keywords::loops" >> "$ANKI_FILE" - ;; - try|except|catch|finally|raise|throw) - echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tException handling - manages errors and exceptional conditions. See: $url\t${PRIMARY_LANG}::keywords::exceptions" >> "$ANKI_FILE" - ;; - class|struct|interface|trait|impl) - echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tType definition - defines custom data structures. See: $url\t${PRIMARY_LANG}::keywords::types" >> "$ANKI_FILE" - ;; - def|fn|func|function) - echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tFunction definition - declares a reusable block of code. See: $url\t${PRIMARY_LANG}::keywords::functions" >> "$ANKI_FILE" - ;; - import|from|use|require|include) - echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tModule import - brings external code into current scope. See: $url\t${PRIMARY_LANG}::keywords::modules" >> "$ANKI_FILE" - ;; - async|await|yield) - echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tAsynchronous programming - handles concurrent operations. See: $url\t${PRIMARY_LANG}::keywords::async" >> "$ANKI_FILE" - ;; - *) - echo -e "What does the keyword \`$term\` do in $PRIMARY_LANG?\t[FILL: Look up at $url]\t${PRIMARY_LANG}::keywords" >> "$ANKI_FILE" - ;; - esac - done + echo "# Keywords" >> "$ANKI_FILE" + head -$TOP_N "$RESULTS_DIR/grep_keywords.txt" | while read -r count term; do + [ -z "$term" ] && continue + url=$(get_doc_url "$term" "$PRIMARY_LANG") + + # Create different card types based on term type + case "$term" in + if | else | elif | elseif | switch | case | match) + echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tConditional control flow - executes code based on boolean conditions. See: $url\t${PRIMARY_LANG}::keywords::control-flow" >> "$ANKI_FILE" + ;; + for | while | loop | do | until) + echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tLoop construct - repeats code execution. See: $url\t${PRIMARY_LANG}::keywords::loops" >> "$ANKI_FILE" + ;; + try | except | catch | finally | raise | throw) + echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tException handling - manages errors and exceptional conditions. See: $url\t${PRIMARY_LANG}::keywords::exceptions" >> "$ANKI_FILE" + ;; + class | struct | interface | trait | impl) + echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tType definition - defines custom data structures. See: $url\t${PRIMARY_LANG}::keywords::types" >> "$ANKI_FILE" + ;; + def | fn | func | function) + echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tFunction definition - declares a reusable block of code. See: $url\t${PRIMARY_LANG}::keywords::functions" >> "$ANKI_FILE" + ;; + import | from | use | require | include) + echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tModule import - brings external code into current scope. See: $url\t${PRIMARY_LANG}::keywords::modules" >> "$ANKI_FILE" + ;; + async | await | yield) + echo -e "What is the purpose of \`$term\` in $PRIMARY_LANG?\tAsynchronous programming - handles concurrent operations. See: $url\t${PRIMARY_LANG}::keywords::async" >> "$ANKI_FILE" + ;; + *) + echo -e "What does the keyword \`$term\` do in $PRIMARY_LANG?\t[FILL: Look up at $url]\t${PRIMARY_LANG}::keywords" >> "$ANKI_FILE" + ;; + esac + done fi # Generate cards for top functions if [ -f "$RESULTS_DIR/grep_function_calls.txt" ]; then - echo "" >> "$ANKI_FILE" - echo "# Functions" >> "$ANKI_FILE" - head -$TOP_N "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do - [ -z "$term" ] && continue - [[ "$term" =~ ^(if|for|while|switch|catch)$ ]] && continue - url=$(get_doc_url "$term" "$PRIMARY_LANG") - - echo -e "What does \`$term()\` do in $PRIMARY_LANG? (Used $count times)\t[FILL: Look up at $url]\t${PRIMARY_LANG}::functions" >> "$ANKI_FILE" - done + echo "" >> "$ANKI_FILE" + echo "# Functions" >> "$ANKI_FILE" + head -$TOP_N "$RESULTS_DIR/grep_function_calls.txt" | while read -r count term; do + [ -z "$term" ] && continue + [[ $term =~ ^(if|for|while|switch|catch)$ ]] && continue + url=$(get_doc_url "$term" "$PRIMARY_LANG") + + echo -e "What does \`$term()\` do in $PRIMARY_LANG? (Used $count times)\t[FILL: Look up at $url]\t${PRIMARY_LANG}::functions" >> "$ANKI_FILE" + done fi echo -e "${GREEN}Created: $ANKI_FILE${NC}" @@ -750,82 +750,82 @@ echo -e "${YELLOW}Generating LLM prompt...${NC}" # Helper function to get doc link for a term get_llm_doc_link() { - local term="$1" - local lang="$2" - local is_import="$3" # "true" if it's an import line - - # Check if it's an internal/project-specific item - if [[ "$term" =~ ^@/ ]] || [[ "$term" =~ ^\./ ]] || [[ "$term" =~ ^app\. ]] || [[ "$term" =~ ^src/ ]] || [[ "$term" =~ from\ \'@/ ]] || [[ "$term" =~ from\ \'\./ ]]; then - echo "[INTERNAL - SKIP]" - return - fi - - # Try offline lookup - local offline_result - if [ "$is_import" = "true" ]; then - offline_result=$("$LOOKUP_SCRIPT" --import "$term" "$lang" 2>/dev/null | grep "^/" | head -1) - else - offline_result=$("$LOOKUP_SCRIPT" "$term" "$lang" 2>/dev/null | grep "^File:" | head -1 | sed 's/^File: //') - fi - - if [ -n "$offline_result" ]; then - echo "$offline_result" - else - echo "[NO OFFLINE DOC]" - fi + local term="$1" + local lang="$2" + local is_import="$3" # "true" if it's an import line + + # Check if it's an internal/project-specific item + if [[ $term =~ ^@/ ]] || [[ $term =~ ^\./ ]] || [[ $term =~ ^app\. ]] || [[ $term =~ ^src/ ]] || [[ $term =~ from\ \'@/ ]] || [[ $term =~ from\ \'\./ ]]; then + echo "[INTERNAL - SKIP]" + return + fi + + # Try offline lookup + local offline_result + if [ "$is_import" = "true" ]; then + offline_result=$("$LOOKUP_SCRIPT" --import "$term" "$lang" 2> /dev/null | grep "^/" | head -1) + else + offline_result=$("$LOOKUP_SCRIPT" "$term" "$lang" 2> /dev/null | grep "^File:" | head -1 | sed 's/^File: //') + fi + + if [ -n "$offline_result" ]; then + echo "$offline_result" + else + echo "[NO OFFLINE DOC]" + fi } # Generate keywords with doc links generate_keywords_with_docs() { - local keywords_file="$RESULTS_DIR/grep_keywords.txt" - [ ! -f "$keywords_file" ] && echo "No keywords found" && return - - head -$TOP_N "$keywords_file" | grep -v '^#' | while read -r line; do - local count=$(echo "$line" | awk '{print $1}') - local keyword=$(echo "$line" | awk '{print $2}') - [ -z "$keyword" ] && continue - local doc_link=$(get_llm_doc_link "$keyword" "$PRIMARY_LANG" "false") - echo "$count $keyword → $doc_link" - done + local keywords_file="$RESULTS_DIR/grep_keywords.txt" + [ ! -f "$keywords_file" ] && echo "No keywords found" && return + + head -$TOP_N "$keywords_file" | grep -v '^#' | while read -r line; do + local count=$(echo "$line" | awk '{print $1}') + local keyword=$(echo "$line" | awk '{print $2}') + [ -z "$keyword" ] && continue + local doc_link=$(get_llm_doc_link "$keyword" "$PRIMARY_LANG" "false") + echo "$count $keyword → $doc_link" + done } # Generate functions with doc links generate_functions_with_docs() { - local functions_file="$RESULTS_DIR/grep_function_calls.txt" - [ ! -f "$functions_file" ] && echo "No functions found" && return - - head -$TOP_N "$functions_file" | grep -v '^#' | while read -r line; do - local count=$(echo "$line" | awk '{print $1}') - local func=$(echo "$line" | awk '{print $2}') - - # Skip single-letter functions (minified code) or empty - if [ -z "$func" ] || [ ${#func} -le 1 ]; then - continue - fi - - local doc_link=$(get_llm_doc_link "$func" "$PRIMARY_LANG" "false") - echo "$count $func() → $doc_link" - done + local functions_file="$RESULTS_DIR/grep_function_calls.txt" + [ ! -f "$functions_file" ] && echo "No functions found" && return + + head -$TOP_N "$functions_file" | grep -v '^#' | while read -r line; do + local count=$(echo "$line" | awk '{print $1}') + local func=$(echo "$line" | awk '{print $2}') + + # Skip single-letter functions (minified code) or empty + if [ -z "$func" ] || [ ${#func} -le 1 ]; then + continue + fi + + local doc_link=$(get_llm_doc_link "$func" "$PRIMARY_LANG" "false") + echo "$count $func() → $doc_link" + done } # Generate imports with doc links generate_imports_with_docs() { - local imports_file="$RESULTS_DIR/grep_imports.txt" - [ ! -f "$imports_file" ] && echo "No imports found" && return - - head -20 "$imports_file" | grep -v '^#' | while read -r line; do - local count=$(echo "$line" | awk '{print $1}') - local import_stmt=$(echo "$line" | cut -d' ' -f2-) - [ -z "$import_stmt" ] && continue - - # Check if internal import - if [[ "$import_stmt" =~ @/ ]] || [[ "$import_stmt" =~ \'\./ ]] || [[ "$import_stmt" =~ from\ app\. ]] || [[ "$import_stmt" =~ from\ src\. ]]; then - echo "$count $import_stmt → [INTERNAL - SKIP]" - else - local doc_link=$(get_llm_doc_link "$import_stmt" "$PRIMARY_LANG" "true") - echo "$count $import_stmt → $doc_link" - fi - done + local imports_file="$RESULTS_DIR/grep_imports.txt" + [ ! -f "$imports_file" ] && echo "No imports found" && return + + head -20 "$imports_file" | grep -v '^#' | while read -r line; do + local count=$(echo "$line" | awk '{print $1}') + local import_stmt=$(echo "$line" | cut -d' ' -f2-) + [ -z "$import_stmt" ] && continue + + # Check if internal import + if [[ $import_stmt =~ @/ ]] || [[ $import_stmt =~ \'\./ ]] || [[ $import_stmt =~ from\ app\. ]] || [[ $import_stmt =~ from\ src\. ]]; then + echo "$count $import_stmt → [INTERNAL - SKIP]" + else + local doc_link=$(get_llm_doc_link "$import_stmt" "$PRIMARY_LANG" "true") + echo "$count $import_stmt → $doc_link" + fi + done } cat > "$LLM_PROMPT_FILE" << 'PROMPT_HEADER' @@ -962,7 +962,6 @@ PROMPT_FOOTER echo -e "${GREEN}Created: $LLM_PROMPT_FILE${NC}" - #============================================================================== # Summary #============================================================================== diff --git a/scripts/utils/image_to_resolution.sh b/scripts/utils/image_to_resolution.sh index ee0593e..0112e96 100755 --- a/scripts/utils/image_to_resolution.sh +++ b/scripts/utils/image_to_resolution.sh @@ -15,7 +15,7 @@ DEFAULT_RESOLUTION="320x240" # Function to display usage usage() { - cat < [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 " - usage + echo "Error: Missing required argument " + 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 diff --git a/scripts/utils/install_exercism.sh b/scripts/utils/install_exercism.sh index 7f6d7f3..078a108 100755 --- a/scripts/utils/install_exercism.sh +++ b/scripts/utils/install_exercism.sh @@ -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 "$@" diff --git a/scripts/utils/install_offline_docs.sh b/scripts/utils/install_offline_docs.sh index 5d68b4e..2fd6984 100644 --- a/scripts/utils/install_offline_docs.sh +++ b/scripts/utils/install_offline_docs.sh @@ -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 "$@" diff --git a/scripts/utils/install_plagiarism_tools.sh b/scripts/utils/install_plagiarism_tools.sh index 91daf6d..1de0b64 100755 --- a/scripts/utils/install_plagiarism_tools.sh +++ b/scripts/utils/install_plagiarism_tools.sh @@ -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" < "$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 diff --git a/scripts/utils/lookup_docs.sh b/scripts/utils/lookup_docs.sh index b9c715e..80a2df9 100755 --- a/scripts/utils/lookup_docs.sh +++ b/scripts/utils/lookup_docs.sh @@ -20,400 +20,400 @@ INDEX_DIR="$DOCS_DIR/.index" # Colors - only use if stdout is a terminal if [ -t 1 ]; then - RED='\033[0;31m' - GREEN='\033[0;32m' - BLUE='\033[0;34m' - YELLOW='\033[1;33m' - CYAN='\033[0;36m' - NC='\033[0m' + RED='\033[0;31m' + GREEN='\033[0;32m' + BLUE='\033[0;34m' + YELLOW='\033[1;33m' + CYAN='\033[0;36m' + NC='\033[0m' else - RED='' - GREEN='' - BLUE='' - YELLOW='' - CYAN='' - NC='' + RED='' + GREEN='' + BLUE='' + YELLOW='' + CYAN='' + NC='' fi #============================================================================== # Python-specific lookup #============================================================================== lookup_python() { - local term="$1" - local in_module="$2" # Optional: look for term within this module - local doc_dir="$DOCS_DIR/python" - local result="" - local desc="" - - # Normalize term (preserve case for True/False/None) - local term_lower - term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') - - # If looking for a term within a specific module - if [ -n "$in_module" ]; then - local module_lower - module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]') - - if [ -f "$doc_dir/library/${module_lower}.html" ]; then - # Find anchor for the specific item in the module - local anchor - anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2>/dev/null | head -1 | sed 's/id="//;s/"//') - - if [ -n "$anchor" ]; then - result="$doc_dir/library/${module_lower}.html#$anchor" - desc="Python: $in_module.$term" - else - # Just link to the module - result="$doc_dir/library/${module_lower}.html" - desc="Python: $term in module $in_module" - fi - echo "$result|$desc" - return 0 - fi + local term="$1" + local in_module="$2" # Optional: look for term within this module + local doc_dir="$DOCS_DIR/python" + local result="" + local desc="" + + # Normalize term (preserve case for True/False/None) + local term_lower + term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') + + # If looking for a term within a specific module + if [ -n "$in_module" ]; then + local module_lower + module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]') + + if [ -f "$doc_dir/library/${module_lower}.html" ]; then + # Find anchor for the specific item in the module + local anchor + anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2> /dev/null | head -1 | sed 's/id="//;s/"//') + + if [ -n "$anchor" ]; then + result="$doc_dir/library/${module_lower}.html#$anchor" + desc="Python: $in_module.$term" + else + # Just link to the module + result="$doc_dir/library/${module_lower}.html" + desc="Python: $term in module $in_module" + fi + echo "$result|$desc" + return 0 fi - - #-------------------------------------------------------------------------- - # PRIORITY 1: Python keywords - map to exact documentation locations - #-------------------------------------------------------------------------- - - # Compound statements (reference/compound_stmts.html) + fi + + #-------------------------------------------------------------------------- + # PRIORITY 1: Python keywords - map to exact documentation locations + #-------------------------------------------------------------------------- + + # Compound statements (reference/compound_stmts.html) + case "$term_lower" in + if | elif | else) + result="$doc_dir/reference/compound_stmts.html#if" + desc="Python: if statement" + ;; + for) + result="$doc_dir/reference/compound_stmts.html#for" + desc="Python: for statement" + ;; + while) + result="$doc_dir/reference/compound_stmts.html#while" + desc="Python: while statement" + ;; + def) + result="$doc_dir/reference/compound_stmts.html#def" + desc="Python: function definition" + ;; + class) + result="$doc_dir/reference/compound_stmts.html#class" + desc="Python: class definition" + ;; + try | except | finally) + result="$doc_dir/reference/compound_stmts.html#try" + desc="Python: try statement" + ;; + with) + result="$doc_dir/reference/compound_stmts.html#with" + desc="Python: with statement" + ;; + async) + result="$doc_dir/reference/compound_stmts.html#async" + desc="Python: async definition" + ;; + match | case) + result="$doc_dir/reference/compound_stmts.html#match" + desc="Python: match statement" + ;; + esac + + # Simple statements (reference/simple_stmts.html) + if [ -z "$result" ]; then case "$term_lower" in - if|elif|else) - result="$doc_dir/reference/compound_stmts.html#if" - desc="Python: if statement" - ;; - for) - result="$doc_dir/reference/compound_stmts.html#for" - desc="Python: for statement" - ;; - while) - result="$doc_dir/reference/compound_stmts.html#while" - desc="Python: while statement" - ;; - def) - result="$doc_dir/reference/compound_stmts.html#def" - desc="Python: function definition" - ;; - class) - result="$doc_dir/reference/compound_stmts.html#class" - desc="Python: class definition" - ;; - try|except|finally) - result="$doc_dir/reference/compound_stmts.html#try" - desc="Python: try statement" - ;; - with) - result="$doc_dir/reference/compound_stmts.html#with" - desc="Python: with statement" - ;; - async) - result="$doc_dir/reference/compound_stmts.html#async" - desc="Python: async definition" - ;; - match|case) - result="$doc_dir/reference/compound_stmts.html#match" - desc="Python: match statement" - ;; + return) + result="$doc_dir/reference/simple_stmts.html#return" + desc="Python: return statement" + ;; + pass) + result="$doc_dir/reference/simple_stmts.html#pass" + desc="Python: pass statement" + ;; + break) + result="$doc_dir/reference/simple_stmts.html#break" + desc="Python: break statement" + ;; + continue) + result="$doc_dir/reference/simple_stmts.html#continue" + desc="Python: continue statement" + ;; + import | from) + result="$doc_dir/reference/simple_stmts.html#import" + desc="Python: import statement" + ;; + raise) + result="$doc_dir/reference/simple_stmts.html#raise" + desc="Python: raise statement" + ;; + assert) + result="$doc_dir/reference/simple_stmts.html#assert" + desc="Python: assert statement" + ;; + yield) + result="$doc_dir/reference/simple_stmts.html#yield" + desc="Python: yield expression" + ;; + del) + result="$doc_dir/reference/simple_stmts.html#del" + desc="Python: del statement" + ;; + global) + result="$doc_dir/reference/simple_stmts.html#global" + desc="Python: global statement" + ;; + nonlocal) + result="$doc_dir/reference/simple_stmts.html#nonlocal" + desc="Python: nonlocal statement" + ;; + type) + result="$doc_dir/reference/simple_stmts.html#type" + desc="Python: type alias statement" + ;; esac - - # Simple statements (reference/simple_stmts.html) - if [ -z "$result" ]; then - case "$term_lower" in - return) - result="$doc_dir/reference/simple_stmts.html#return" - desc="Python: return statement" - ;; - pass) - result="$doc_dir/reference/simple_stmts.html#pass" - desc="Python: pass statement" - ;; - break) - result="$doc_dir/reference/simple_stmts.html#break" - desc="Python: break statement" - ;; - continue) - result="$doc_dir/reference/simple_stmts.html#continue" - desc="Python: continue statement" - ;; - import|from) - result="$doc_dir/reference/simple_stmts.html#import" - desc="Python: import statement" - ;; - raise) - result="$doc_dir/reference/simple_stmts.html#raise" - desc="Python: raise statement" - ;; - assert) - result="$doc_dir/reference/simple_stmts.html#assert" - desc="Python: assert statement" - ;; - yield) - result="$doc_dir/reference/simple_stmts.html#yield" - desc="Python: yield expression" - ;; - del) - result="$doc_dir/reference/simple_stmts.html#del" - desc="Python: del statement" - ;; - global) - result="$doc_dir/reference/simple_stmts.html#global" - desc="Python: global statement" - ;; - nonlocal) - result="$doc_dir/reference/simple_stmts.html#nonlocal" - desc="Python: nonlocal statement" - ;; - type) - result="$doc_dir/reference/simple_stmts.html#type" - desc="Python: type alias statement" - ;; - esac + fi + + # Expressions/operators (reference/expressions.html) + if [ -z "$result" ]; then + case "$term_lower" in + and) + result="$doc_dir/reference/expressions.html#and" + desc="Python: and operator" + ;; + or) + result="$doc_dir/reference/expressions.html#or" + desc="Python: or operator" + ;; + not) + result="$doc_dir/reference/expressions.html#not" + desc="Python: not operator" + ;; + in) + result="$doc_dir/reference/expressions.html#in" + desc="Python: in operator" + ;; + is) + result="$doc_dir/reference/expressions.html#is" + desc="Python: is operator" + ;; + lambda) + result="$doc_dir/reference/expressions.html#lambda" + desc="Python: lambda expression" + ;; + await) + result="$doc_dir/reference/expressions.html#await" + desc="Python: await expression" + ;; + esac + fi + + # Built-in constants (library/constants.html) - case-sensitive! + if [ -z "$result" ]; then + case "$term" in + True | False) + result="$doc_dir/library/constants.html#$term" + desc="Python: $term constant" + ;; + None) + result="$doc_dir/library/constants.html#None" + desc="Python: None constant" + ;; + Ellipsis) + result="$doc_dir/library/constants.html#Ellipsis" + desc="Python: Ellipsis constant" + ;; + NotImplemented) + result="$doc_dir/library/constants.html#NotImplemented" + desc="Python: NotImplemented constant" + ;; + esac + fi + + # Verify file exists for keyword lookups + if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then + result="" + desc="" + fi + + #-------------------------------------------------------------------------- + # PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.) + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then + result="$doc_dir/library/${term_lower}.html" + desc="Python module: $term" + fi + + #-------------------------------------------------------------------------- + # PRIORITY 3: Built-in functions (library/functions.html) + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then + if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2> /dev/null; then + result="$doc_dir/library/functions.html#$term_lower" + desc="Python built-in function: $term" fi - - # Expressions/operators (reference/expressions.html) - if [ -z "$result" ]; then - case "$term_lower" in - and) - result="$doc_dir/reference/expressions.html#and" - desc="Python: and operator" - ;; - or) - result="$doc_dir/reference/expressions.html#or" - desc="Python: or operator" - ;; - not) - result="$doc_dir/reference/expressions.html#not" - desc="Python: not operator" - ;; - in) - result="$doc_dir/reference/expressions.html#in" - desc="Python: in operator" - ;; - is) - result="$doc_dir/reference/expressions.html#is" - desc="Python: is operator" - ;; - lambda) - result="$doc_dir/reference/expressions.html#lambda" - desc="Python: lambda expression" - ;; - await) - result="$doc_dir/reference/expressions.html#await" - desc="Python: await expression" - ;; - esac + fi + + #-------------------------------------------------------------------------- + # PRIORITY 4: Built-in types (library/stdtypes.html) + #-------------------------------------------------------------------------- + if [ -z "$result" ]; then + case "$term_lower" in + str | string) + result="$doc_dir/library/stdtypes.html#str" + desc="Python: str type" + ;; + int | integer) + result="$doc_dir/library/stdtypes.html#int" + desc="Python: int type" + ;; + float) + result="$doc_dir/library/stdtypes.html#float" + desc="Python: float type" + ;; + list) + result="$doc_dir/library/stdtypes.html#list" + desc="Python: list type" + ;; + dict | dictionary) + result="$doc_dir/library/stdtypes.html#dict" + desc="Python: dict type" + ;; + set) + result="$doc_dir/library/stdtypes.html#set" + desc="Python: set type" + ;; + tuple) + result="$doc_dir/library/stdtypes.html#tuple" + desc="Python: tuple type" + ;; + bool | boolean) + result="$doc_dir/library/stdtypes.html#boolean-values" + desc="Python: bool type" + ;; + bytes) + result="$doc_dir/library/stdtypes.html#bytes" + desc="Python: bytes type" + ;; + esac + fi + + #-------------------------------------------------------------------------- + # PRIORITY 5: Check for class/function in module docs (exact id match) + #-------------------------------------------------------------------------- + if [ -z "$result" ]; then + local found_in + # Look for exact id match first + found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2> /dev/null | head -1) + if [ -n "$found_in" ]; then + result="$found_in#$term" + local module + module=$(basename "$found_in" .html) + desc="Python: $term in module $module" fi - - # Built-in constants (library/constants.html) - case-sensitive! - if [ -z "$result" ]; then - case "$term" in - True|False) - result="$doc_dir/library/constants.html#$term" - desc="Python: $term constant" - ;; - None) - result="$doc_dir/library/constants.html#None" - desc="Python: None constant" - ;; - Ellipsis) - result="$doc_dir/library/constants.html#Ellipsis" - desc="Python: Ellipsis constant" - ;; - NotImplemented) - result="$doc_dir/library/constants.html#NotImplemented" - desc="Python: NotImplemented constant" - ;; - esac - fi - - # Verify file exists for keyword lookups - if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then - result="" - desc="" - fi - - #-------------------------------------------------------------------------- - # PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.) - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then - result="$doc_dir/library/${term_lower}.html" - desc="Python module: $term" - fi - - #-------------------------------------------------------------------------- - # PRIORITY 3: Built-in functions (library/functions.html) - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then - if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2>/dev/null; then - result="$doc_dir/library/functions.html#$term_lower" - desc="Python built-in function: $term" - fi - fi - - #-------------------------------------------------------------------------- - # PRIORITY 4: Built-in types (library/stdtypes.html) - #-------------------------------------------------------------------------- - if [ -z "$result" ]; then - case "$term_lower" in - str|string) - result="$doc_dir/library/stdtypes.html#str" - desc="Python: str type" - ;; - int|integer) - result="$doc_dir/library/stdtypes.html#int" - desc="Python: int type" - ;; - float) - result="$doc_dir/library/stdtypes.html#float" - desc="Python: float type" - ;; - list) - result="$doc_dir/library/stdtypes.html#list" - desc="Python: list type" - ;; - dict|dictionary) - result="$doc_dir/library/stdtypes.html#dict" - desc="Python: dict type" - ;; - set) - result="$doc_dir/library/stdtypes.html#set" - desc="Python: set type" - ;; - tuple) - result="$doc_dir/library/stdtypes.html#tuple" - desc="Python: tuple type" - ;; - bool|boolean) - result="$doc_dir/library/stdtypes.html#boolean-values" - desc="Python: bool type" - ;; - bytes) - result="$doc_dir/library/stdtypes.html#bytes" - desc="Python: bytes type" - ;; - esac - fi - - #-------------------------------------------------------------------------- - # PRIORITY 5: Check for class/function in module docs (exact id match) - #-------------------------------------------------------------------------- - if [ -z "$result" ]; then - local found_in - # Look for exact id match first - found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2>/dev/null | head -1) - if [ -n "$found_in" ]; then - result="$found_in#$term" - local module - module=$(basename "$found_in" .html) - desc="Python: $term in module $module" - fi - fi - - #-------------------------------------------------------------------------- - # PRIORITY 6: Search in index - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then - local index_match - index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2>/dev/null | head -1) - if [ -n "$index_match" ]; then - result=$(echo "$index_match" | cut -d' ' -f2-) - desc="Python: $term (from index)" - fi - fi - - # NO full-text search fallback - it produces garbage results - # If we can't find a specific doc, return nothing (will fall back to online) - - if [ -n "$result" ]; then - echo "$result|$desc" + fi + + #-------------------------------------------------------------------------- + # PRIORITY 6: Search in index + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then + local index_match + index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2> /dev/null | head -1) + if [ -n "$index_match" ]; then + result=$(echo "$index_match" | cut -d' ' -f2-) + desc="Python: $term (from index)" fi + fi + + # NO full-text search fallback - it produces garbage results + # If we can't find a specific doc, return nothing (will fall back to online) + + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== # C/C++ specific lookup #============================================================================== lookup_cpp() { - local term="$1" - local doc_dir="$DOCS_DIR/c_cpp" - local result="" - local desc="" - - # Resolve symlink if present (system package installs to c_cpp/system/) - [ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system" - - # Common C headers - case "$term" in - stdio.h|stdio) - [ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html" - [ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html" - desc="C standard I/O header" - ;; - stdlib.h|stdlib) - [ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html" - [ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html" - desc="C standard library header" - ;; - string.h|cstring) - [ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html" - desc="C string handling header" - ;; - math.h|cmath) - [ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html" - desc="C math header" - ;; - esac - - # C++ STL containers - case "$term" in - vector) - [ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html" - [ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html" - desc="C++ std::vector container" - ;; - map) - [ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html" - desc="C++ std::map container" - ;; - string) - [ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html" - desc="C++ std::string" - ;; - iostream) - [ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html" - desc="C++ iostream header" - ;; - esac - - # C keywords - case "$term" in - if|else|for|while|do|switch|case|break|continue|return|goto) - [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" - [ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html" - desc="C/C++ keyword: $term" - ;; - int|char|float|double|void|long|short|unsigned|signed) - [ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html" - desc="C/C++ type: $term" - ;; - struct|union|enum|typedef) - [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" - desc="C/C++ keyword: $term" - ;; - esac - - # Search in files if not found (use -L to follow symlinks) - if [ -z "$result" ]; then - local found - found=$(find -L "$doc_dir" -name "*${term}*" -type f 2>/dev/null | head -1) - if [ -n "$found" ]; then - result="$found" - desc="C/C++: $term" - fi - fi - - if [ -n "$result" ]; then - echo "$result|$desc" + local term="$1" + local doc_dir="$DOCS_DIR/c_cpp" + local result="" + local desc="" + + # Resolve symlink if present (system package installs to c_cpp/system/) + [ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system" + + # Common C headers + case "$term" in + stdio.h | stdio) + [ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html" + [ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html" + desc="C standard I/O header" + ;; + stdlib.h | stdlib) + [ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html" + [ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html" + desc="C standard library header" + ;; + string.h | cstring) + [ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html" + desc="C string handling header" + ;; + math.h | cmath) + [ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html" + desc="C math header" + ;; + esac + + # C++ STL containers + case "$term" in + vector) + [ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html" + [ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html" + desc="C++ std::vector container" + ;; + map) + [ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html" + desc="C++ std::map container" + ;; + string) + [ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html" + desc="C++ std::string" + ;; + iostream) + [ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html" + desc="C++ iostream header" + ;; + esac + + # C keywords + case "$term" in + if | else | for | while | do | switch | case | break | continue | return | goto) + [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" + [ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html" + desc="C/C++ keyword: $term" + ;; + int | char | float | double | void | long | short | unsigned | signed) + [ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html" + desc="C/C++ type: $term" + ;; + struct | union | enum | typedef) + [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" + desc="C/C++ keyword: $term" + ;; + esac + + # Search in files if not found (use -L to follow symlinks) + if [ -z "$result" ]; then + local found + found=$(find -L "$doc_dir" -name "*${term}*" -type f 2> /dev/null | head -1) + if [ -n "$found" ]; then + result="$found" + desc="C/C++: $term" fi + fi + + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== @@ -421,387 +421,387 @@ lookup_cpp() { # Searches the cloned MDN content repository #============================================================================== lookup_js() { - local term="$1" - local mdn_dir="$DOCS_DIR/mdn-content/files/en-us" - - # Normalize term for searching - local term_lower - term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') - - # Handle common statement aliases (MDN uses if...else, try...catch, etc.) - local statement_aliases=( - "if:if...else" - "else:if...else" - "try:try...catch" - "catch:try...catch" - "finally:try...catch" - "do:do...while" - "while:while" - "for:for" - "switch:switch" - "case:switch" - "default:switch" - ) - - for alias in "${statement_aliases[@]}"; do - local key="${alias%%:*}" - local value="${alias##*:}" - if [ "$term_lower" = "$key" ]; then - local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value" - if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$stmt_dir/index.md|${title:-$term}" - return 0 - fi - fi - done - - # Handle boolean/null literals - case "$term_lower" in - true|false) - local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean" - if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then - echo "$bool_dir/index.md|Boolean ($term)" - return 0 - fi - ;; - null) - local null_dir="$mdn_dir/web/javascript/reference/operators/null" - if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$null_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$null_dir/index.md|${title:-null}" - return 0 - fi - ;; - undefined) - local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined" - if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$undef_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$undef_dir/index.md|${title:-undefined}" - return 0 - fi - ;; - esac - - # Search JavaScript reference directory structure (priority order) - local search_dirs=( - "$mdn_dir/web/javascript/reference/statements" - "$mdn_dir/web/javascript/reference/operators" - "$mdn_dir/web/javascript/reference/global_objects" - "$mdn_dir/web/javascript/reference/functions" - "$mdn_dir/web/javascript/reference/classes" - ) - - for search_dir in "${search_dirs[@]}"; do - if [ -d "$search_dir" ]; then - # Look for exact directory match (MDN uses directories with index.md) - local found_dir - found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2>/dev/null | head -1) - if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found_dir/index.md|${title:-$term}" - return 0 - fi - fi - done - - # Search Web APIs - prioritize *_api directories for common terms - if [ -d "$mdn_dir/web/api" ]; then - # First try _api directory (e.g., fetch_api, console_api) - local api_dir="$mdn_dir/web/api/${term_lower}_api" - if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$api_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$api_dir/index.md|${title:-$term API}" - return 0 - fi - - # Then try exact top-level API interface (e.g., Console, Document, Element) - local found - found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi - - # Try window/ for global functions like alert, confirm, etc. - local window_method="$mdn_dir/web/api/window/${term_lower}" - if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$window_method/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$window_method/index.md|${title:-Window.$term()}" - return 0 - fi - - # Search nested API methods - found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2>/dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi + local term="$1" + local mdn_dir="$DOCS_DIR/mdn-content/files/en-us" + + # Normalize term for searching + local term_lower + term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') + + # Handle common statement aliases (MDN uses if...else, try...catch, etc.) + local statement_aliases=( + "if:if...else" + "else:if...else" + "try:try...catch" + "catch:try...catch" + "finally:try...catch" + "do:do...while" + "while:while" + "for:for" + "switch:switch" + "case:switch" + "default:switch" + ) + + for alias in "${statement_aliases[@]}"; do + local key="${alias%%:*}" + local value="${alias##*:}" + if [ "$term_lower" = "$key" ]; then + local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value" + if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$stmt_dir/index.md|${title:-$term}" + return 0 + fi fi - - # Now try partial matches in Global Objects (e.g., Array.from, Object.keys) - if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then - local found - found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2>/dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi + done + + # Handle boolean/null literals + case "$term_lower" in + true | false) + local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean" + if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then + echo "$bool_dir/index.md|Boolean ($term)" + return 0 + fi + ;; + null) + local null_dir="$mdn_dir/web/javascript/reference/operators/null" + if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$null_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$null_dir/index.md|${title:-null}" + return 0 + fi + ;; + undefined) + local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined" + if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$undef_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$undef_dir/index.md|${title:-undefined}" + return 0 + fi + ;; + esac + + # Search JavaScript reference directory structure (priority order) + local search_dirs=( + "$mdn_dir/web/javascript/reference/statements" + "$mdn_dir/web/javascript/reference/operators" + "$mdn_dir/web/javascript/reference/global_objects" + "$mdn_dir/web/javascript/reference/functions" + "$mdn_dir/web/javascript/reference/classes" + ) + + for search_dir in "${search_dirs[@]}"; do + if [ -d "$search_dir" ]; then + # Look for exact directory match (MDN uses directories with index.md) + local found_dir + found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2> /dev/null | head -1) + if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found_dir/index.md|${title:-$term}" + return 0 + fi fi - - # Glossary as last resort - if [ -d "$mdn_dir/glossary" ]; then - local found - found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi + done + + # Search Web APIs - prioritize *_api directories for common terms + if [ -d "$mdn_dir/web/api" ]; then + # First try _api directory (e.g., fetch_api, console_api) + local api_dir="$mdn_dir/web/api/${term_lower}_api" + if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$api_dir/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$api_dir/index.md|${title:-$term API}" + return 0 fi - - return 1 + + # Then try exact top-level API interface (e.g., Console, Document, Element) + local found + found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2> /dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + + # Try window/ for global functions like alert, confirm, etc. + local window_method="$mdn_dir/web/api/window/${term_lower}" + if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$window_method/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$window_method/index.md|${title:-Window.$term()}" + return 0 + fi + + # Search nested API methods + found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2> /dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi + + # Now try partial matches in Global Objects (e.g., Array.from, Object.keys) + if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then + local found + found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2> /dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi + + # Glossary as last resort + if [ -d "$mdn_dir/glossary" ]; then + local found + found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2> /dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi + + return 1 } #============================================================================== # Rust specific lookup #============================================================================== lookup_rust() { - local term="$1" - local result="" - local desc="" - - if command -v rustup &>/dev/null; then - # Use rustup doc to get path - local rust_doc_path - rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null) - - # Search in std docs - if [ -d "$rust_doc_path/std" ]; then - local found - found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2>/dev/null | head -1) - if [ -n "$found" ]; then - result="$found" - desc="Rust: $term" - fi - fi - fi - - if [ -n "$result" ]; then - echo "$result|$desc" + local term="$1" + local result="" + local desc="" + + if command -v rustup &> /dev/null; then + # Use rustup doc to get path + local rust_doc_path + rust_doc_path=$(rustup doc --path 2> /dev/null | head -1 | xargs dirname 2> /dev/null) + + # Search in std docs + if [ -d "$rust_doc_path/std" ]; then + local found + found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2> /dev/null | head -1) + if [ -n "$found" ]; then + result="$found" + desc="Rust: $term" + fi fi + fi + + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== # Go specific lookup #============================================================================== lookup_go() { - local term="$1" - local result="" - local desc="" - - if command -v go &>/dev/null; then - # Check if it's a stdlib package - if go doc "$term" &>/dev/null; then - result="go doc $term" - desc="Go package: $term (use 'go doc $term' to view)" - fi - fi - - if [ -n "$result" ]; then - echo "$result|$desc" + local term="$1" + local result="" + local desc="" + + if command -v go &> /dev/null; then + # Check if it's a stdlib package + if go doc "$term" &> /dev/null; then + result="go doc $term" + desc="Go package: $term (use 'go doc $term' to view)" fi + fi + + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== # Shell specific lookup #============================================================================== lookup_shell() { - local term="$1" - local doc_dir="$DOCS_DIR/shell" - local result="" - local desc="" - - # Check bash builtins - if [ -f "$doc_dir/bash_builtins.txt" ]; then - if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2>/dev/null; then - result="$doc_dir/bash_builtins.txt" - desc="Bash builtin: $term" - fi + local term="$1" + local doc_dir="$DOCS_DIR/shell" + local result="" + local desc="" + + # Check bash builtins + if [ -f "$doc_dir/bash_builtins.txt" ]; then + if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2> /dev/null; then + result="$doc_dir/bash_builtins.txt" + desc="Bash builtin: $term" fi - - # Check common commands - if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then - if grep -q "^$term" "$doc_dir/common_commands.txt" 2>/dev/null; then - local cmd_desc - cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1) - result="$doc_dir/common_commands.txt" - desc="Shell command: $cmd_desc" - fi + fi + + # Check common commands + if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then + if grep -q "^$term" "$doc_dir/common_commands.txt" 2> /dev/null; then + local cmd_desc + cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1) + result="$doc_dir/common_commands.txt" + desc="Shell command: $cmd_desc" fi - - # Try man page - if [ -z "$result" ]; then - local man_path - man_path=$(man -w "$term" 2>/dev/null) - if [ -n "$man_path" ]; then - result="man $term" - desc="Manual page: $term (use 'man $term' to view)" - fi - fi - - if [ -n "$result" ]; then - echo "$result|$desc" + fi + + # Try man page + if [ -z "$result" ]; then + local man_path + man_path=$(man -w "$term" 2> /dev/null) + if [ -n "$man_path" ]; then + result="man $term" + desc="Manual page: $term (use 'man $term' to view)" fi + fi + + if [ -n "$result" ]; then + echo "$result|$desc" + fi } #============================================================================== # Generic lookup (searches all languages) #============================================================================== lookup_all() { - local term="$1" - - # Try each language - for lang in python cpp js rust go shell; do - local result - result=$(lookup_$lang "$term" 2>/dev/null) - if [ -n "$result" ]; then - echo "$lang: $result" - fi - done + local term="$1" + + # Try each language + for lang in python cpp js rust go shell; do + local result + result=$(lookup_$lang "$term" 2> /dev/null) + if [ -n "$result" ]; then + echo "$lang: $result" + fi + done } #============================================================================== # Parse Python import and lookup the actual imported item #============================================================================== parse_python_import() { - local import_line="$1" - - # Handle "from X import Y" format - if [[ "$import_line" =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then - local module="${BASH_REMATCH[1]}" - local items="${BASH_REMATCH[2]}" - - # Clean up items (remove parentheses, commas, etc.) - items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}') - - # Output: module and first imported item - echo "$module|$items" - return 0 - fi - - # Handle "import X" format - if [[ "$import_line" =~ ^import[[:space:]]+([^[:space:],]+) ]]; then - local module="${BASH_REMATCH[1]}" - echo "$module|" - return 0 - fi - - return 1 + local import_line="$1" + + # Handle "from X import Y" format + if [[ $import_line =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then + local module="${BASH_REMATCH[1]}" + local items="${BASH_REMATCH[2]}" + + # Clean up items (remove parentheses, commas, etc.) + items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}') + + # Output: module and first imported item + echo "$module|$items" + return 0 + fi + + # Handle "import X" format + if [[ $import_line =~ ^import[[:space:]]+([^[:space:],]+) ]]; then + local module="${BASH_REMATCH[1]}" + echo "$module|" + return 0 + fi + + return 1 } #============================================================================== # Smart lookup for imports #============================================================================== lookup_import() { - local import_line="$1" - local lang="$2" - - case "$lang" in - python) - local parsed - parsed=$(parse_python_import "$import_line") - if [ -n "$parsed" ]; then - local module item - module=$(echo "$parsed" | cut -d'|' -f1) - item=$(echo "$parsed" | cut -d'|' -f2) - - # For "from X import Y", look up Y within module X's documentation - if [ -n "$item" ] && [ -n "$module" ]; then - local result - # Pass both item and module to lookup_python - result=$(lookup_python "$item" "$module") - if [ -n "$result" ]; then - echo "$result" - return 0 - fi - fi - - # Fall back to module documentation - lookup_python "$module" - fi - ;; - - c_cpp) - # Extract header name from #include
or #include "header" - local header - header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//') - lookup_cpp "$header" - ;; - - javascript|typescript) - # Extract module from import/require - local module="" - # Match: from "module" or from 'module' - module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+") - if [ -z "$module" ]; then - # Match: require("module") or require('module') - module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+") - fi - [ -n "$module" ] && lookup_js "$module" - ;; - - *) - echo "Unknown language: $lang" - ;; - esac + local import_line="$1" + local lang="$2" + + case "$lang" in + python) + local parsed + parsed=$(parse_python_import "$import_line") + if [ -n "$parsed" ]; then + local module item + module=$(echo "$parsed" | cut -d'|' -f1) + item=$(echo "$parsed" | cut -d'|' -f2) + + # For "from X import Y", look up Y within module X's documentation + if [ -n "$item" ] && [ -n "$module" ]; then + local result + # Pass both item and module to lookup_python + result=$(lookup_python "$item" "$module") + if [ -n "$result" ]; then + echo "$result" + return 0 + fi + fi + + # Fall back to module documentation + lookup_python "$module" + fi + ;; + + c_cpp) + # Extract header name from #include
or #include "header" + local header + header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//') + lookup_cpp "$header" + ;; + + javascript | typescript) + # Extract module from import/require + local module="" + # Match: from "module" or from 'module' + module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+") + if [ -z "$module" ]; then + # Match: require("module") or require('module') + module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+") + fi + [ -n "$module" ] && lookup_js "$module" + ;; + + *) + echo "Unknown language: $lang" + ;; + esac } #============================================================================== # Extract documentation content #============================================================================== extract_doc_content() { - local file="$1" - local term="$2" - local max_lines="${3:-20}" - - if [[ "$file" == *.html ]]; then - # Extract text from HTML, find section about term - if command -v html2text &>/dev/null; then - html2text "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" - elif command -v lynx &>/dev/null; then - lynx -dump -nolist "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" - else - # Basic extraction - sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines" - fi - elif [[ "$file" == *.json ]]; then - # Pretty print JSON section - grep -A5 "\"$term\"" "$file" 2>/dev/null + local file="$1" + local term="$2" + local max_lines="${3:-20}" + + if [[ $file == *.html ]]; then + # Extract text from HTML, find section about term + if command -v html2text &> /dev/null; then + html2text "$file" 2> /dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" + elif command -v lynx &> /dev/null; then + lynx -dump -nolist "$file" 2> /dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" else - # Plain text - grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines" + # Basic extraction + sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines" fi + elif [[ $file == *.json ]]; then + # Pretty print JSON section + grep -A5 "\"$term\"" "$file" 2> /dev/null + else + # Plain text + grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines" + fi } #============================================================================== # Main #============================================================================== usage() { - cat << EOF + cat << EOF Usage: $0 [language] [options] Search offline documentation for a term. @@ -824,119 +824,119 @@ EOF } main() { - if [ $# -eq 0 ]; then + if [ $# -eq 0 ]; then + usage + exit 0 + fi + + local term="" + local lang="" + local action="lookup" + local open_file=false + local extract=false + + while [ $# -gt 0 ]; do + case "$1" in + --open) + open_file=true + shift + ;; + --extract) + extract=true + shift + ;; + --import) + action="import" + shift + term="$1" + shift + ;; + --batch) + action="batch" + shift + term="$1" # This is the file + shift + ;; + --help | -h) usage exit 0 - fi - - local term="" - local lang="" - local action="lookup" - local open_file=false - local extract=false - - while [ $# -gt 0 ]; do - case "$1" in - --open) - open_file=true - shift - ;; - --extract) - extract=true - shift - ;; - --import) - action="import" - shift - term="$1" - shift - ;; - --batch) - action="batch" - shift - term="$1" # This is the file - shift - ;; - --help|-h) - usage - exit 0 - ;; - python|cpp|c_cpp|c|js|javascript|ts|typescript|tsx|jsx|rust|go|shell|bash|all) - lang="$1" - shift - ;; - *) - if [ -z "$term" ]; then - term="$1" - fi - shift - ;; - esac - done - - # Normalize language - case "$lang" in - c) lang="cpp" ;; - javascript|js|typescript|ts|jsx|tsx) lang="js" ;; - bash) lang="shell" ;; - "") lang="all" ;; - esac - - case "$action" in - lookup) - if [ "$lang" = "all" ]; then - lookup_all "$term" - else - result=$(lookup_$lang "$term" 2>/dev/null) - if [ -n "$result" ]; then - local file desc - file=$(echo "$result" | cut -d'|' -f1) - desc=$(echo "$result" | cut -d'|' -f2) - - echo -e "${GREEN}Found:${NC} $desc" - echo -e "${BLUE}File:${NC} $file" - - if $extract; then - echo "" - echo -e "${YELLOW}--- Content ---${NC}" - extract_doc_content "$file" "$term" - fi - - if $open_file && [ -f "$file" ]; then - xdg-open "$file" 2>/dev/null & - fi - else - echo -e "${RED}Not found:${NC} $term in $lang documentation" - fi - fi - ;; - - import) - result=$(lookup_import "$term" "$lang") - if [ -n "$result" ]; then - echo -e "${GREEN}Import lookup:${NC} $term" - echo "$result" - else - echo -e "${RED}Could not parse import:${NC} $term" - fi - ;; - - batch) - if [ ! -f "$term" ]; then - echo "File not found: $term" - exit 1 - fi - - while IFS= read -r line || [ -n "$line" ]; do - [ -z "$line" ] && continue - [[ "$line" =~ ^# ]] && continue - - echo -e "${CYAN}Looking up:${NC} $line" - lookup_import "$line" "$lang" - echo "" - done < "$term" - ;; + ;; + python | cpp | c_cpp | c | js | javascript | ts | typescript | tsx | jsx | rust | go | shell | bash | all) + lang="$1" + shift + ;; + *) + if [ -z "$term" ]; then + term="$1" + fi + shift + ;; esac + done + + # Normalize language + case "$lang" in + c) lang="cpp" ;; + javascript | js | typescript | ts | jsx | tsx) lang="js" ;; + bash) lang="shell" ;; + "") lang="all" ;; + esac + + case "$action" in + lookup) + if [ "$lang" = "all" ]; then + lookup_all "$term" + else + result=$(lookup_$lang "$term" 2> /dev/null) + if [ -n "$result" ]; then + local file desc + file=$(echo "$result" | cut -d'|' -f1) + desc=$(echo "$result" | cut -d'|' -f2) + + echo -e "${GREEN}Found:${NC} $desc" + echo -e "${BLUE}File:${NC} $file" + + if $extract; then + echo "" + echo -e "${YELLOW}--- Content ---${NC}" + extract_doc_content "$file" "$term" + fi + + if $open_file && [ -f "$file" ]; then + xdg-open "$file" 2> /dev/null & + fi + else + echo -e "${RED}Not found:${NC} $term in $lang documentation" + fi + fi + ;; + + import) + result=$(lookup_import "$term" "$lang") + if [ -n "$result" ]; then + echo -e "${GREEN}Import lookup:${NC} $term" + echo "$result" + else + echo -e "${RED}Could not parse import:${NC} $term" + fi + ;; + + batch) + if [ ! -f "$term" ]; then + echo "File not found: $term" + exit 1 + fi + + while IFS= read -r line || [ -n "$line" ]; do + [ -z "$line" ] && continue + [[ $line =~ ^# ]] && continue + + echo -e "${CYAN}Looking up:${NC} $line" + lookup_import "$line" "$lang" + echo "" + done < "$term" + ;; + esac } main "$@" diff --git a/scripts/utils/pdf_to_image.sh b/scripts/utils/pdf_to_image.sh index 4d35464..a0fa68a 100755 --- a/scripts/utils/pdf_to_image.sh +++ b/scripts/utils/pdf_to_image.sh @@ -17,7 +17,7 @@ OUTPUT_FORMAT="jpg" PDF_FILES=() usage() { - cat <&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 "$@" diff --git a/scripts/utils/repo_to_study.sh b/scripts/utils/repo_to_study.sh index 074b29f..85a14ed 100755 --- a/scripts/utils/repo_to_study.sh +++ b/scripts/utils/repo_to_study.sh @@ -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 "$@" diff --git a/scripts/utils/root_bl9000.sh b/scripts/utils/root_bl9000.sh index 88bfe08..56a511f 100755 --- a/scripts/utils/root_bl9000.sh +++ b/scripts/utils/root_bl9000.sh @@ -37,60 +37,60 @@ NC='\033[0m' # No Color timestamp() { date '+%Y-%m-%d %H:%M:%S%z'; } log() { - local msg="$1" - echo -e "${GREEN}[$(timestamp)]${NC} $msg" - if [[ -w "$(dirname "$LOG_FILE")" ]] || [[ ! -e $LOG_FILE && -w /var/log ]]; then - echo "[$(timestamp)] $msg" >>"$LOG_FILE" 2>&1 || true - fi + local msg="$1" + echo -e "${GREEN}[$(timestamp)]${NC} $msg" + if [[ -w "$(dirname "$LOG_FILE")" ]] || [[ ! -e $LOG_FILE && -w /var/log ]]; then + echo "[$(timestamp)] $msg" >> "$LOG_FILE" 2>&1 || true + fi } warn() { - local msg="$1" - echo -e "${YELLOW}[WARN]${NC} $msg" >&2 - if [[ -w "$(dirname "$LOG_FILE")" ]] || [[ ! -e $LOG_FILE && -w /var/log ]]; then - echo "[$(timestamp)] [WARN] $msg" >>"$LOG_FILE" 2>&1 || true - fi + local msg="$1" + echo -e "${YELLOW}[WARN]${NC} $msg" >&2 + if [[ -w "$(dirname "$LOG_FILE")" ]] || [[ ! -e $LOG_FILE && -w /var/log ]]; then + echo "[$(timestamp)] [WARN] $msg" >> "$LOG_FILE" 2>&1 || true + fi } error() { - local msg="$1" - echo -e "${RED}[ERROR]${NC} $msg" >&2 - if [[ -w "$(dirname "$LOG_FILE")" ]] || [[ ! -e $LOG_FILE && -w /var/log ]]; then - echo "[$(timestamp)] [ERROR] $msg" >>"$LOG_FILE" 2>&1 || true - fi + local msg="$1" + echo -e "${RED}[ERROR]${NC} $msg" >&2 + if [[ -w "$(dirname "$LOG_FILE")" ]] || [[ ! -e $LOG_FILE && -w /var/log ]]; then + echo "[$(timestamp)] [ERROR] $msg" >> "$LOG_FILE" 2>&1 || true + fi } die() { - error "$1" - exit 1 + error "$1" + exit 1 } print_header() { - echo - echo -e "${BLUE}========================================${NC}" - echo -e "${BLUE} $1${NC}" - echo -e "${BLUE}========================================${NC}" - echo + echo + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}========================================${NC}" + echo } confirm() { - local prompt="$1" - local reply - read -r -p "$(echo -e "${YELLOW}${prompt}${NC} [y/N]: ")" reply - case "$reply" in - [Yy][Ee][Ss] | [Yy]) return 0 ;; - *) return 1 ;; - esac + local prompt="$1" + local reply + read -r -p "$(echo -e "${YELLOW}${prompt}${NC} [y/N]: ")" reply + case "$reply" in + [Yy][Ee][Ss] | [Yy]) return 0 ;; + *) return 1 ;; + esac } require_non_root() { - if [[ ${EUID:-$(id -u)} -eq 0 ]]; then - die "Do not run this script as root. ADB must run as your regular user to access USB devices properly." - fi + if [[ ${EUID:-$(id -u)} -eq 0 ]]; then + die "Do not run this script as root. ADB must run as your regular user to access USB devices properly." + fi } usage() { - cat </dev/null 2>&1; then - packages+=("android-tools") - missing+=("adb") - fi + # Check for required commands + if ! command -v adb > /dev/null 2>&1; then + packages+=("android-tools") + missing+=("adb") + fi - if ! command -v fastboot >/dev/null 2>&1 && ! pacman -Q android-tools >/dev/null 2>&1; then - packages+=("android-tools") - missing+=("fastboot") - fi + if ! command -v fastboot > /dev/null 2>&1 && ! pacman -Q android-tools > /dev/null 2>&1; then + packages+=("android-tools") + missing+=("fastboot") + fi - if ! command -v unzip >/dev/null 2>&1; then - packages+=("unzip") - missing+=("unzip") - fi + if ! command -v unzip > /dev/null 2>&1; then + packages+=("unzip") + missing+=("unzip") + fi - if ! command -v curl >/dev/null 2>&1; then - packages+=("curl") - missing+=("curl") - fi + if ! command -v curl > /dev/null 2>&1; then + packages+=("curl") + missing+=("curl") + fi - if ! command -v python3 >/dev/null 2>&1; then - packages+=("python") - missing+=("python3") - fi + if ! command -v python3 > /dev/null 2>&1; then + packages+=("python") + missing+=("python3") + fi - if ! command -v git >/dev/null 2>&1; then - packages+=("git") - missing+=("git") - fi + if ! command -v git > /dev/null 2>&1; then + packages+=("git") + missing+=("git") + fi - # Check for libusb and fuse2 (needed for mtkclient) - if ! pacman -Q libusb >/dev/null 2>&1; then - packages+=("libusb") - missing+=("libusb") - fi + # Check for libusb and fuse2 (needed for mtkclient) + if ! pacman -Q libusb > /dev/null 2>&1; then + packages+=("libusb") + missing+=("libusb") + fi - if ! pacman -Q fuse2 >/dev/null 2>&1; then - packages+=("fuse2") - missing+=("fuse2") - fi + if ! pacman -Q fuse2 > /dev/null 2>&1; then + packages+=("fuse2") + missing+=("fuse2") + fi - # Check for python-protobuf (needed for boot image tools) - if ! python3 -c "import google.protobuf" 2>/dev/null; then - packages+=("python-protobuf") - missing+=("python-protobuf") - fi + # Check for python-protobuf (needed for boot image tools) + if ! python3 -c "import google.protobuf" 2> /dev/null; then + packages+=("python-protobuf") + missing+=("python-protobuf") + fi - if [[ ${#missing[@]} -eq 0 ]]; then - log "All dependencies are already installed." - return 0 - fi + if [[ ${#missing[@]} -eq 0 ]]; then + log "All dependencies are already installed." + return 0 + fi - log "Missing dependencies: ${missing[*]}" + log "Missing dependencies: ${missing[*]}" - # Remove duplicates - readarray -t packages < <(printf '%s\n' "${packages[@]}" | sort -u) + # Remove duplicates + readarray -t packages < <(printf '%s\n' "${packages[@]}" | sort -u) - if ! confirm "Install missing packages: ${packages[*]}?"; then - die "Cannot proceed without required dependencies." - fi + if ! confirm "Install missing packages: ${packages[*]}?"; then + die "Cannot proceed without required dependencies." + fi - log "Installing packages: ${packages[*]}" - sudo pacman -S --needed --noconfirm "${packages[@]}" || die "Failed to install dependencies" + log "Installing packages: ${packages[*]}" + sudo pacman -S --needed --noconfirm "${packages[@]}" || die "Failed to install dependencies" - log "Dependencies installed successfully." + log "Dependencies installed successfully." } setup_udev_rules() { - print_header "Setting Up USB Access" + print_header "Setting Up USB Access" - local udev_file="/etc/udev/rules.d/51-android.rules" - local mtk_udev_dir="${WORK_DIR}/mtkclient/mtkclient/Setup/Linux" + local udev_file="/etc/udev/rules.d/51-android.rules" + local mtk_udev_dir="${WORK_DIR}/mtkclient/mtkclient/Setup/Linux" - # Install MTKClient udev rules if mtkclient is present - if [[ -d "${WORK_DIR}/mtkclient" ]]; then - log "Installing MTKClient udev rules..." - if [[ -d $mtk_udev_dir ]]; then - sudo cp "$mtk_udev_dir"/*.rules /etc/udev/rules.d/ 2>/dev/null || warn "Failed to copy MTKClient rules" - fi - fi + # Install MTKClient udev rules if mtkclient is present + if [[ -d "${WORK_DIR}/mtkclient" ]]; then + log "Installing MTKClient udev rules..." + if [[ -d $mtk_udev_dir ]]; then + sudo cp "$mtk_udev_dir"/*.rules /etc/udev/rules.d/ 2> /dev/null || warn "Failed to copy MTKClient rules" + fi + fi - if [[ -f $udev_file ]]; then - log "Android udev rules already exist at $udev_file" - else - if ! confirm "Create udev rules for Android device access?"; then - warn "Skipping udev rules. You may need to run commands with sudo." - return 0 - fi + if [[ -f $udev_file ]]; then + log "Android udev rules already exist at $udev_file" + else + if ! confirm "Create udev rules for Android device access?"; then + warn "Skipping udev rules. You may need to run commands with sudo." + return 0 + fi - log "Creating Android udev rules..." + log "Creating Android udev rules..." - # Create comprehensive udev rules for Android devices - sudo tee "$udev_file" >/dev/null <<'EOF' + # Create comprehensive udev rules for Android devices + sudo tee "$udev_file" > /dev/null << 'EOF' # Android Debug Bridge (ADB) devices # Add your device's vendor ID if not listed @@ -241,876 +241,876 @@ SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0666", GROUP="adbusers" # Generic catch-all for Android devices SUBSYSTEM=="usb", ATTR{idVendor}=="*", ATTR{idProduct}=="*", MODE="0666", GROUP="adbusers", SYMLINK+="android%n" EOF - fi + fi - # Create adbusers group if it doesn't exist - if ! getent group adbusers >/dev/null; then - sudo groupadd -r adbusers - log "Created adbusers group" - fi + # Create adbusers group if it doesn't exist + if ! getent group adbusers > /dev/null; then + sudo groupadd -r adbusers + log "Created adbusers group" + fi - # Add current user to adbusers and plugdev groups - if ! groups "$USER" | grep -q '\badbusers\b'; then - sudo usermod -aG adbusers "$USER" - log "Added $USER to adbusers group" - fi + # Add current user to adbusers and plugdev groups + if ! groups "$USER" | grep -q '\badbusers\b'; then + sudo usermod -aG adbusers "$USER" + log "Added $USER to adbusers group" + fi - if ! getent group plugdev >/dev/null; then - sudo groupadd -r plugdev - fi + if ! getent group plugdev > /dev/null; then + sudo groupadd -r plugdev + fi - if ! groups "$USER" | grep -q '\bplugdev\b'; then - sudo usermod -aG plugdev "$USER" - log "Added $USER to plugdev group" - fi + if ! groups "$USER" | grep -q '\bplugdev\b'; then + sudo usermod -aG plugdev "$USER" + log "Added $USER to plugdev group" + fi - if ! getent group dialout >/dev/null; then - sudo groupadd -r dialout - fi + if ! getent group dialout > /dev/null; then + sudo groupadd -r dialout + fi - if ! groups "$USER" | grep -q '\bdialout\b'; then - sudo usermod -aG dialout "$USER" - log "Added $USER to dialout group" - warn "You need to log out and back in for group membership to take effect." - warn "Alternatively, run: newgrp dialout" - fi + if ! groups "$USER" | grep -q '\bdialout\b'; then + sudo usermod -aG dialout "$USER" + log "Added $USER to dialout group" + warn "You need to log out and back in for group membership to take effect." + warn "Alternatively, run: newgrp dialout" + fi - # Reload udev rules - sudo udevadm control --reload-rules - sudo udevadm trigger + # Reload udev rules + sudo udevadm control --reload-rules + sudo udevadm trigger - log "USB access configured successfully." + log "USB access configured successfully." } backup_device_data() { - print_header "Backing Up Device Data" + print_header "Backing Up Device Data" - local backup_dir - backup_dir="${WORK_DIR}/backup_$(date +%Y%m%d_%H%M%S)" - mkdir -p "$backup_dir" + local backup_dir + backup_dir="${WORK_DIR}/backup_$(date +%Y%m%d_%H%M%S)" + mkdir -p "$backup_dir" - log "Backup directory: $backup_dir" # Check device connection first - if ! adb get-state >/dev/null 2>&1; then - error "Device not connected. Please connect your device first." - return 1 - fi + log "Backup directory: $backup_dir" # Check device connection first + if ! adb get-state > /dev/null 2>&1; then + error "Device not connected. Please connect your device first." + return 1 + fi - log "Starting comprehensive backup process..." + log "Starting comprehensive backup process..." - # 1. Backup internal storage (DCIM, Pictures, Documents, Downloads, etc.) - log "Backing up internal storage (this may take a while)..." - local storage_dirs=("DCIM" "Pictures" "Documents" "Download" "Music" "Movies" "WhatsApp" "Telegram") + # 1. Backup internal storage (DCIM, Pictures, Documents, Downloads, etc.) + log "Backing up internal storage (this may take a while)..." + local storage_dirs=("DCIM" "Pictures" "Documents" "Download" "Music" "Movies" "WhatsApp" "Telegram") - for dir in "${storage_dirs[@]}"; do - if adb shell "[ -d /sdcard/$dir ]" 2>/dev/null; then - log " → Backing up /sdcard/$dir..." - if adb pull "/sdcard/$dir" "$backup_dir/$dir" 2>&1 | grep -v "^$"; then - log " ✓ $dir backed up successfully" - else - warn " ⚠ Could not backup $dir (may be empty or inaccessible)" - fi - fi - done + for dir in "${storage_dirs[@]}"; do + if adb shell "[ -d /sdcard/$dir ]" 2> /dev/null; then + log " → Backing up /sdcard/$dir..." + if adb pull "/sdcard/$dir" "$backup_dir/$dir" 2>&1 | grep -v "^$"; then + log " ✓ $dir backed up successfully" + else + warn " ⚠ Could not backup $dir (may be empty or inaccessible)" + fi + fi + done - # 2. Backup SMS/MMS (if possible) - log "Backing up SMS/MMS database..." - if adb shell "su -c 'cp /data/data/com.android.providers.telephony/databases/mmssms.db /sdcard/mmssms.db'" 2>/dev/null; then - adb pull /sdcard/mmssms.db "$backup_dir/mmssms.db" 2>/dev/null && log " ✓ SMS/MMS backed up" - adb shell "rm /sdcard/mmssms.db" 2>/dev/null || true - else - warn " ⚠ SMS/MMS backup requires root (skipping)" - fi + # 2. Backup SMS/MMS (if possible) + log "Backing up SMS/MMS database..." + if adb shell "su -c 'cp /data/data/com.android.providers.telephony/databases/mmssms.db /sdcard/mmssms.db'" 2> /dev/null; then + adb pull /sdcard/mmssms.db "$backup_dir/mmssms.db" 2> /dev/null && log " ✓ SMS/MMS backed up" + adb shell "rm /sdcard/mmssms.db" 2> /dev/null || true + else + warn " ⚠ SMS/MMS backup requires root (skipping)" + fi - # 3. Backup contacts - log "Backing up contacts..." - if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/contacts2.db /sdcard/contacts2.db'" 2>/dev/null; then - adb pull /sdcard/contacts2.db "$backup_dir/contacts2.db" 2>/dev/null && log " ✓ Contacts backed up" - adb shell "rm /sdcard/contacts2.db" 2>/dev/null || true - else - warn " ⚠ Contacts backup requires root (skipping)" - fi + # 3. Backup contacts + log "Backing up contacts..." + if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/contacts2.db /sdcard/contacts2.db'" 2> /dev/null; then + adb pull /sdcard/contacts2.db "$backup_dir/contacts2.db" 2> /dev/null && log " ✓ Contacts backed up" + adb shell "rm /sdcard/contacts2.db" 2> /dev/null || true + else + warn " ⚠ Contacts backup requires root (skipping)" + fi - # 4. Backup call logs - log "Backing up call logs..." - if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/calllog.db /sdcard/calllog.db'" 2>/dev/null; then - adb pull /sdcard/calllog.db "$backup_dir/calllog.db" 2>/dev/null && log " ✓ Call logs backed up" - adb shell "rm /sdcard/calllog.db" 2>/dev/null || true - else - warn " ⚠ Call logs backup requires root (skipping)" - fi + # 4. Backup call logs + log "Backing up call logs..." + if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/calllog.db /sdcard/calllog.db'" 2> /dev/null; then + adb pull /sdcard/calllog.db "$backup_dir/calllog.db" 2> /dev/null && log " ✓ Call logs backed up" + adb shell "rm /sdcard/calllog.db" 2> /dev/null || true + else + warn " ⚠ Call logs backup requires root (skipping)" + fi - # 5. Backup app list - log "Backing up installed apps list..." - adb shell "pm list packages -f" >"$backup_dir/installed_apps.txt" - log " ✓ App list saved to installed_apps.txt" + # 5. Backup app list + log "Backing up installed apps list..." + adb shell "pm list packages -f" > "$backup_dir/installed_apps.txt" + log " ✓ App list saved to installed_apps.txt" - # 6. Backup APKs for user-installed apps (optional, can be large) - if confirm "Backup APK files for installed apps? (This can take a long time and use lots of space)"; then - log "Backing up user-installed APKs..." - local apk_dir="$backup_dir/apks" - mkdir -p "$apk_dir" + # 6. Backup APKs for user-installed apps (optional, can be large) + if confirm "Backup APK files for installed apps? (This can take a long time and use lots of space)"; then + log "Backing up user-installed APKs..." + local apk_dir="$backup_dir/apks" + mkdir -p "$apk_dir" - # Get user-installed packages - local user_apps - user_apps=$(adb shell "pm list packages -3 -f" | sed 's/package://' | cut -d'=' -f2) + # Get user-installed packages + local user_apps + user_apps=$(adb shell "pm list packages -3 -f" | sed 's/package://' | cut -d'=' -f2) - local count=0 - while IFS= read -r pkg; do - if [[ -n $pkg ]]; then - log " → Backing up $pkg..." - local apk_path - apk_path=$(adb shell "pm path $pkg" | head -n1 | sed 's/package://') - if [[ -n $apk_path ]]; then - adb pull "$apk_path" "$apk_dir/${pkg}.apk" >/dev/null 2>&1 && count=$((count + 1)) - fi - fi - done <<<"$user_apps" + local count=0 + while IFS= read -r pkg; do + if [[ -n $pkg ]]; then + log " → Backing up $pkg..." + local apk_path + apk_path=$(adb shell "pm path $pkg" | head -n1 | sed 's/package://') + if [[ -n $apk_path ]]; then + adb pull "$apk_path" "$apk_dir/${pkg}.apk" > /dev/null 2>&1 && count=$((count + 1)) + fi + fi + done <<< "$user_apps" - log " ✓ Backed up $count APK files" - fi + log " ✓ Backed up $count APK files" + fi - # 7. Full ADB backup (app data, if device supports it) - log "Creating full ADB backup (app data)..." - if confirm "Create full ADB backup? (You'll need to confirm on your device)"; then - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " On your phone: Tap 'Back up my data' when prompted" - echo " You can set a password or leave it blank" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo + # 7. Full ADB backup (app data, if device supports it) + log "Creating full ADB backup (app data)..." + if confirm "Create full ADB backup? (You'll need to confirm on your device)"; then + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " On your phone: Tap 'Back up my data' when prompted" + echo " You can set a password or leave it blank" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo - if adb backup -apk -shared -all -system -f "$backup_dir/full_backup.ab"; then - log " ✓ Full ADB backup completed" - log " Note: Restore with: adb restore full_backup.ab" - else - warn " ⚠ ADB backup failed or was cancelled" - fi - fi + if adb backup -apk -shared -all -system -f "$backup_dir/full_backup.ab"; then + log " ✓ Full ADB backup completed" + log " Note: Restore with: adb restore full_backup.ab" + else + warn " ⚠ ADB backup failed or was cancelled" + fi + fi - # 8. Backup device info - log "Saving device information..." - { - echo "Device Backup Information" - echo "=========================" - echo "Date: $(date)" - echo - echo "Device Model: $(adb shell getprop ro.product.model | tr -d '\r\n')" - echo "Android Version: $(adb shell getprop ro.build.version.release | tr -d '\r\n')" - echo "Build Number: $(adb shell getprop ro.build.display.id | tr -d '\r\n')" - echo "Security Patch: $(adb shell getprop ro.build.version.security_patch | tr -d '\r\n')" - echo "Serial: $(adb shell getprop ro.serialno | tr -d '\r\n')" - echo - echo "Installed Apps:" - adb shell "pm list packages -3" | sed 's/package:/ - /' - } >"$backup_dir/device_info.txt" + # 8. Backup device info + log "Saving device information..." + { + echo "Device Backup Information" + echo "=========================" + echo "Date: $(date)" + echo + echo "Device Model: $(adb shell getprop ro.product.model | tr -d '\r\n')" + echo "Android Version: $(adb shell getprop ro.build.version.release | tr -d '\r\n')" + echo "Build Number: $(adb shell getprop ro.build.display.id | tr -d '\r\n')" + echo "Security Patch: $(adb shell getprop ro.build.version.security_patch | tr -d '\r\n')" + echo "Serial: $(adb shell getprop ro.serialno | tr -d '\r\n')" + echo + echo "Installed Apps:" + adb shell "pm list packages -3" | sed 's/package:/ - /' + } > "$backup_dir/device_info.txt" - log " ✓ Device info saved" + log " ✓ Device info saved" - # Summary - local backup_size - backup_size=$(du -sh "$backup_dir" 2>/dev/null | cut -f1 || echo "unknown") + # Summary + local backup_size + backup_size=$(du -sh "$backup_dir" 2> /dev/null | cut -f1 || echo "unknown") - echo - echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ Backup Completed Successfully! ║${NC}" - echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}" - echo - log "Backup location: $backup_dir" - log "Backup size: $backup_size" - echo - echo "What was backed up:" - echo " ✓ Photos (DCIM)" - echo " ✓ Pictures" - echo " ✓ Documents" - echo " ✓ Downloads" - echo " ✓ Music" - echo " ✓ Movies" - echo " ✓ WhatsApp data (if present)" - echo " ✓ Telegram data (if present)" - echo " ✓ Installed apps list" - echo " ✓ Device information" - if [[ -f "$backup_dir/full_backup.ab" ]]; then - echo " ✓ Full app data backup" - fi - if [[ -d "$backup_dir/apks" ]]; then - echo " ✓ APK files" - fi - echo - log "Keep this backup safe! You'll need it to restore your data after rooting." + echo + echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Backup Completed Successfully! ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}" + echo + log "Backup location: $backup_dir" + log "Backup size: $backup_size" + echo + echo "What was backed up:" + echo " ✓ Photos (DCIM)" + echo " ✓ Pictures" + echo " ✓ Documents" + echo " ✓ Downloads" + echo " ✓ Music" + echo " ✓ Movies" + echo " ✓ WhatsApp data (if present)" + echo " ✓ Telegram data (if present)" + echo " ✓ Installed apps list" + echo " ✓ Device information" + if [[ -f "$backup_dir/full_backup.ab" ]]; then + echo " ✓ Full app data backup" + fi + if [[ -d "$backup_dir/apks" ]]; then + echo " ✓ APK files" + fi + echo + log "Keep this backup safe! You'll need it to restore your data after rooting." - return 0 + return 0 } check_device() { - print_header "Checking Device Connection" + print_header "Checking Device Connection" - log "Starting ADB server..." - adb start-server >/dev/null 2>&1 || true + log "Starting ADB server..." + adb start-server > /dev/null 2>&1 || true - log "Waiting for device..." - if ! adb wait-for-device; then - error "Failed to detect device via ADB." - echo - echo "Troubleshooting steps:" - echo "1. Make sure USB debugging is enabled on your phone" - echo " Settings → About Phone → Tap Build Number 7 times" - echo " Settings → Developer Options → Enable USB Debugging" - echo "2. Connect your phone via USB cable" - echo "3. Accept the 'Allow USB debugging' prompt on your phone" - echo "4. Run: adb devices" - echo - return 1 - fi + log "Waiting for device..." + if ! adb wait-for-device; then + error "Failed to detect device via ADB." + echo + echo "Troubleshooting steps:" + echo "1. Make sure USB debugging is enabled on your phone" + echo " Settings → About Phone → Tap Build Number 7 times" + echo " Settings → Developer Options → Enable USB Debugging" + echo "2. Connect your phone via USB cable" + echo "3. Accept the 'Allow USB debugging' prompt on your phone" + echo "4. Run: adb devices" + echo + return 1 + fi - local device_info - device_info=$(adb devices -l | grep -v "List of devices" | grep -v "^$" | head -n1) + local device_info + device_info=$(adb devices -l | grep -v "List of devices" | grep -v "^$" | head -n1) - if [[ -z $device_info ]]; then - error "No device detected" - return 1 - fi + if [[ -z $device_info ]]; then + error "No device detected" + return 1 + fi - log "Device connected: $device_info" + log "Device connected: $device_info" - # Check device properties - local model - model=$(adb shell getprop ro.product.model 2>/dev/null | tr -d '\r\n' || echo "Unknown") - log "Model: $model" + # Check device properties + local model + model=$(adb shell getprop ro.product.model 2> /dev/null | tr -d '\r\n' || echo "Unknown") + log "Model: $model" - local android_version - android_version=$(adb shell getprop ro.build.version.release 2>/dev/null | tr -d '\r\n' || echo "Unknown") - log "Android version: $android_version" + local android_version + android_version=$(adb shell getprop ro.build.version.release 2> /dev/null | tr -d '\r\n' || echo "Unknown") + log "Android version: $android_version" - local battery_level - battery_level=$(adb shell dumpsys battery | grep level | awk '{print $2}' | tr -d '\r\n' || echo "Unknown") - log "Battery level: ${battery_level}%" + local battery_level + battery_level=$(adb shell dumpsys battery | grep level | awk '{print $2}' | tr -d '\r\n' || echo "Unknown") + log "Battery level: ${battery_level}%" - if [[ $battery_level != "Unknown" && $battery_level -lt 50 ]]; then - warn "Battery level is below 50%. Charge your phone before proceeding." - if ! confirm "Continue anyway?"; then - return 1 - fi - fi + if [[ $battery_level != "Unknown" && $battery_level -lt 50 ]]; then + warn "Battery level is below 50%. Charge your phone before proceeding." + if ! confirm "Continue anyway?"; then + return 1 + fi + fi - # Check if bootloader is unlocked - local unlock_status - unlock_status=$(adb shell getprop ro.boot.verifiedbootstate 2>/dev/null | tr -d '\r\n' || echo "unknown") - if [[ $unlock_status == "orange" || $unlock_status == "red" ]]; then - log "Bootloader unlock status: ${GREEN}UNLOCKED${NC}" - else - warn "Bootloader appears to be LOCKED. You'll need to unlock it to root." - fi + # Check if bootloader is unlocked + local unlock_status + unlock_status=$(adb shell getprop ro.boot.verifiedbootstate 2> /dev/null | tr -d '\r\n' || echo "unknown") + if [[ $unlock_status == "orange" || $unlock_status == "red" ]]; then + log "Bootloader unlock status: ${GREEN}UNLOCKED${NC}" + else + warn "Bootloader appears to be LOCKED. You'll need to unlock it to root." + fi - # Check if OEM unlocking is enabled - local oem_unlock - oem_unlock=$(adb shell getprop sys.oem_unlock_allowed 2>/dev/null | tr -d '\r\n' || echo "unknown") - if [[ $oem_unlock == "1" ]]; then - log "OEM unlocking: ${GREEN}ENABLED${NC}" - else - warn "OEM unlocking is not enabled in Developer Options." - echo "Enable it at: Settings → Developer Options → OEM unlocking" - fi + # Check if OEM unlocking is enabled + local oem_unlock + oem_unlock=$(adb shell getprop sys.oem_unlock_allowed 2> /dev/null | tr -d '\r\n' || echo "unknown") + if [[ $oem_unlock == "1" ]]; then + log "OEM unlocking: ${GREEN}ENABLED${NC}" + else + warn "OEM unlocking is not enabled in Developer Options." + echo "Enable it at: Settings → Developer Options → OEM unlocking" + fi - return 0 + return 0 } unlock_bootloader() { - print_header "Unlocking Bootloader" + print_header "Unlocking Bootloader" - echo - echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}" - echo -e "${RED}║ WARNING ║${NC}" - echo -e "${RED}║ ║${NC}" - echo -e "${RED}║ Unlocking the bootloader will ERASE ALL DATA on your phone! ║${NC}" - echo -e "${RED}║ ║${NC}" - echo -e "${RED}║ This includes: ║${NC}" - echo -e "${RED}║ - All apps and app data ║${NC}" - echo -e "${RED}║ - Photos, videos, and files ║${NC}" - echo -e "${RED}║ - System settings ║${NC}" - echo -e "${RED}║ - Everything else on internal storage ║${NC}" - echo -e "${RED}║ ║${NC}" - echo -e "${RED}║ Make sure you have backed up important data! ║${NC}" - echo -e "${RED}╚═══════════════════════════════════════════════════════════════╝${NC}" - echo + echo + echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${RED}║ WARNING ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ Unlocking the bootloader will ERASE ALL DATA on your phone! ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ This includes: ║${NC}" + echo -e "${RED}║ - All apps and app data ║${NC}" + echo -e "${RED}║ - Photos, videos, and files ║${NC}" + echo -e "${RED}║ - System settings ║${NC}" + echo -e "${RED}║ - Everything else on internal storage ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ Make sure you have backed up important data! ║${NC}" + echo -e "${RED}╚═══════════════════════════════════════════════════════════════╝${NC}" + echo - if ! confirm "Have you backed up all important data and want to proceed?"; then - log "Bootloader unlock cancelled by user." - return 1 - fi + if ! confirm "Have you backed up all important data and want to proceed?"; then + log "Bootloader unlock cancelled by user." + return 1 + fi - if ! confirm "Are you ABSOLUTELY SURE? This cannot be undone!"; then - log "Bootloader unlock cancelled by user." - return 1 - fi + if ! confirm "Are you ABSOLUTELY SURE? This cannot be undone!"; then + log "Bootloader unlock cancelled by user." + return 1 + fi - log "Rebooting device to bootloader..." - adb reboot bootloader || die "Failed to reboot to bootloader" + log "Rebooting device to bootloader..." + adb reboot bootloader || die "Failed to reboot to bootloader" - log "Waiting for fastboot mode..." - sleep 5 + log "Waiting for fastboot mode..." + sleep 5 - if ! fastboot devices | grep -q .; then - error "Device not detected in fastboot mode." - echo - echo "If the device doesn't enter fastboot automatically:" - echo "1. Power off the phone completely" - echo "2. Hold Volume Down + Power buttons simultaneously" - echo "3. Release when you see the bootloader/fastboot screen" - echo "4. Run: fastboot devices" - echo - return 1 - fi + if ! fastboot devices | grep -q .; then + error "Device not detected in fastboot mode." + echo + echo "If the device doesn't enter fastboot automatically:" + echo "1. Power off the phone completely" + echo "2. Hold Volume Down + Power buttons simultaneously" + echo "3. Release when you see the bootloader/fastboot screen" + echo "4. Run: fastboot devices" + echo + return 1 + fi - log "Device in fastboot mode" + log "Device in fastboot mode" - # Check current bootloader status - local bl_status - bl_status=$(fastboot getvar unlocked 2>&1 | grep "unlocked:" | awk '{print $2}' || echo "unknown") - if [[ $bl_status == "yes" ]]; then - log "Bootloader is already unlocked." - fastboot reboot - return 0 - fi + # Check current bootloader status + local bl_status + bl_status=$(fastboot getvar unlocked 2>&1 | grep "unlocked:" | awk '{print $2}' || echo "unknown") + if [[ $bl_status == "yes" ]]; then + log "Bootloader is already unlocked." + fastboot reboot + return 0 + fi - log "Attempting to unlock bootloader..." + log "Attempting to unlock bootloader..." - # Try different unlock commands (varies by device) - if fastboot flashing unlock 2>&1 | grep -qi "okay\|finished"; then - log "Bootloader unlock command sent successfully." - elif fastboot oem unlock 2>&1 | grep -qi "okay\|finished"; then - log "Bootloader unlock command sent successfully." - else - error "Bootloader unlock command may have failed." - echo - echo "On your phone:" - echo "1. Use volume buttons to select 'Unlock the bootloader'" - echo "2. Press power button to confirm" - echo + # Try different unlock commands (varies by device) + if fastboot flashing unlock 2>&1 | grep -qi "okay\|finished"; then + log "Bootloader unlock command sent successfully." + elif fastboot oem unlock 2>&1 | grep -qi "okay\|finished"; then + log "Bootloader unlock command sent successfully." + else + error "Bootloader unlock command may have failed." + echo + echo "On your phone:" + echo "1. Use volume buttons to select 'Unlock the bootloader'" + echo "2. Press power button to confirm" + echo - if ! confirm "Did you complete the unlock on the device?"; then - fastboot reboot - return 1 - fi - fi + if ! confirm "Did you complete the unlock on the device?"; then + fastboot reboot + return 1 + fi + fi - log "Rebooting device..." - fastboot reboot || true + log "Rebooting device..." + fastboot reboot || true - log "Bootloader unlocked successfully!" - log "Device will now boot up and perform factory reset..." - log "Waiting for device to come back online..." + log "Bootloader unlocked successfully!" + log "Device will now boot up and perform factory reset..." + log "Waiting for device to come back online..." - sleep 10 - adb wait-for-device || true + sleep 10 + adb wait-for-device || true - log "Please complete the initial setup on your phone, then re-enable USB debugging." - echo + log "Please complete the initial setup on your phone, then re-enable USB debugging." + echo - return 0 + return 0 } download_magisk() { - print_header "Downloading Magisk" + print_header "Downloading Magisk" - local magisk_apk="$WORK_DIR/magisk.apk" + local magisk_apk="$WORK_DIR/magisk.apk" - if [[ -f $magisk_apk ]]; then - log "Magisk APK already downloaded at $magisk_apk" - return 0 - fi + if [[ -f $magisk_apk ]]; then + log "Magisk APK already downloaded at $magisk_apk" + return 0 + fi - log "Downloading latest Magisk APK..." - if ! curl -L -o "$magisk_apk" "$MAGISK_APK_URL"; then - error "Failed to download Magisk APK" - return 1 - fi + log "Downloading latest Magisk APK..." + if ! curl -L -o "$magisk_apk" "$MAGISK_APK_URL"; then + error "Failed to download Magisk APK" + return 1 + fi - log "Magisk downloaded successfully: $magisk_apk" - return 0 + log "Magisk downloaded successfully: $magisk_apk" + return 0 } install_mtkclient() { - print_header "Installing MTKClient" + print_header "Installing MTKClient" - local mtk_dir="${WORK_DIR}/mtkclient" + local mtk_dir="${WORK_DIR}/mtkclient" - if [[ -d $mtk_dir && -f "$mtk_dir/mtk.py" ]]; then - log "MTKClient already installed at $mtk_dir" - return 0 - fi + if [[ -d $mtk_dir && -f "$mtk_dir/mtk.py" ]]; then + log "MTKClient already installed at $mtk_dir" + return 0 + fi - if ! confirm "Install MTKClient for MediaTek boot image extraction?"; then - return 1 - fi + if ! confirm "Install MTKClient for MediaTek boot image extraction?"; then + return 1 + fi - log "Cloning MTKClient repository..." - if ! git clone https://github.com/bkerler/mtkclient "$mtk_dir"; then - error "Failed to clone MTKClient" - return 1 - fi + log "Cloning MTKClient repository..." + if ! git clone https://github.com/bkerler/mtkclient "$mtk_dir"; then + error "Failed to clone MTKClient" + return 1 + fi - log "Installing MTKClient Python dependencies..." - cd "$mtk_dir" - python3 -m pip install --user -r requirements.txt || warn "Some dependencies may have failed to install" - python3 -m pip install --user . || warn "MTKClient installation may be incomplete" - cd - >/dev/null + log "Installing MTKClient Python dependencies..." + cd "$mtk_dir" + python3 -m pip install --user -r requirements.txt || warn "Some dependencies may have failed to install" + python3 -m pip install --user . || warn "MTKClient installation may be incomplete" + cd - > /dev/null - log "MTKClient installed successfully" - return 0 + log "MTKClient installed successfully" + return 0 } extract_boot_with_mtkclient() { - print_header "Extracting Boot with MTKClient" + print_header "Extracting Boot with MTKClient" - local mtk_dir="${WORK_DIR}/mtkclient" - local boot_img="$WORK_DIR/boot.img" - local boot_a_img="$WORK_DIR/boot_a.img" - local vbmeta_a_img="$WORK_DIR/vbmeta_a.img" + local mtk_dir="${WORK_DIR}/mtkclient" + local boot_img="$WORK_DIR/boot.img" + local boot_a_img="$WORK_DIR/boot_a.img" + local vbmeta_a_img="$WORK_DIR/vbmeta_a.img" - if [[ ! -d $mtk_dir ]]; then - error "MTKClient not installed. Run: $SCRIPT_NAME install-mtk" - return 1 - fi + if [[ ! -d $mtk_dir ]]; then + error "MTKClient not installed. Run: $SCRIPT_NAME install-mtk" + return 1 + fi - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " MTKClient Boot ROM Mode Instructions" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo - echo "BEFORE continuing, you MUST:" - echo - echo " 1. Power off your phone COMPLETELY" - echo " 2. DISCONNECT the USB cable from your phone" - echo " 3. Have the USB cable ready in your hand" - echo - echo "When you press Enter:" - echo - echo " 4. Press and hold BOTH Volume buttons (Up + Down)" - echo " 5. While holding BOTH buttons, connect USB cable" - echo " 6. Keep holding until device is detected (may take 5-10 seconds)" - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " MTKClient Boot ROM Mode Instructions" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + echo "BEFORE continuing, you MUST:" + echo + echo " 1. Power off your phone COMPLETELY" + echo " 2. DISCONNECT the USB cable from your phone" + echo " 3. Have the USB cable ready in your hand" + echo + echo "When you press Enter:" + echo + echo " 4. Press and hold BOTH Volume buttons (Up + Down)" + echo " 5. While holding BOTH buttons, connect USB cable" + echo " 6. Keep holding until device is detected (may take 5-10 seconds)" + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo - if ! confirm "Phone is OFF and USB DISCONNECTED - ready to proceed?"; then - return 1 - fi + if ! confirm "Phone is OFF and USB DISCONNECTED - ready to proceed?"; then + return 1 + fi - log "Starting MTKClient NOW - quickly enter BROM mode!" - log "Hold BOTH volume buttons and connect USB cable..." - echo + log "Starting MTKClient NOW - quickly enter BROM mode!" + log "Hold BOTH volume buttons and connect USB cable..." + echo - cd "$mtk_dir" + cd "$mtk_dir" - # Activate venv and extract boot and vbmeta - if [[ ! -d "$mtk_dir/venv" ]]; then - error "MTKClient virtual environment not found. Run: $SCRIPT_NAME install-mtk" - cd - >/dev/null - return 1 - fi + # Activate venv and extract boot and vbmeta + if [[ ! -d "$mtk_dir/venv" ]]; then + error "MTKClient virtual environment not found. Run: $SCRIPT_NAME install-mtk" + cd - > /dev/null + return 1 + fi - # shellcheck source=/dev/null - source venv/bin/activate - # BL9000 uses A/B partitions, so extract boot_a and vbmeta_a - if python3 mtk.py r boot_a,vbmeta_a "$boot_a_img,$vbmeta_a_img"; then - log "Boot_a and vbmeta_a extracted successfully!" + # shellcheck source=/dev/null + source venv/bin/activate + # BL9000 uses A/B partitions, so extract boot_a and vbmeta_a + if python3 mtk.py r boot_a,vbmeta_a "$boot_a_img,$vbmeta_a_img"; then + log "Boot_a and vbmeta_a extracted successfully!" - # Copy boot_a.img to boot.img for Magisk compatibility - cp "$boot_a_img" "$boot_img" - BOOT_IMG="$boot_img" + # Copy boot_a.img to boot.img for Magisk compatibility + cp "$boot_a_img" "$boot_img" + BOOT_IMG="$boot_img" - log "Boot image ready for patching: $BOOT_IMG" + log "Boot image ready for patching: $BOOT_IMG" - # Reset device - log "Resetting device..." - python3 mtk.py reset || warn "Failed to reset device, please reboot manually" + # Reset device + log "Resetting device..." + python3 mtk.py reset || warn "Failed to reset device, please reboot manually" - # Deactivate venv if function exists - type deactivate &>/dev/null && deactivate + # Deactivate venv if function exists + type deactivate &> /dev/null && deactivate - cd - >/dev/null - return 0 - else - # Deactivate venv if function exists - type deactivate &>/dev/null && deactivate + cd - > /dev/null + return 0 + else + # Deactivate venv if function exists + type deactivate &> /dev/null && deactivate - error "Failed to extract boot image with MTKClient" - cd - >/dev/null - return 1 - fi + error "Failed to extract boot image with MTKClient" + cd - > /dev/null + return 1 + fi } extract_boot_image() { - print_header "Extracting Boot Image" + print_header "Extracting Boot Image" - local boot_img="$WORK_DIR/boot.img" + local boot_img="$WORK_DIR/boot.img" - if [[ -n ${BOOT_IMG:-} && -f $BOOT_IMG ]]; then - log "Using provided boot image: $BOOT_IMG" - cp "$BOOT_IMG" "$boot_img" - BOOT_IMG="$boot_img" - return 0 - fi + if [[ -n ${BOOT_IMG:-} && -f $BOOT_IMG ]]; then + log "Using provided boot image: $BOOT_IMG" + cp "$BOOT_IMG" "$boot_img" + BOOT_IMG="$boot_img" + return 0 + fi - log "Attempting to extract boot image from device..." + log "Attempting to extract boot image from device..." - # Method 1: Try MTKClient first (best for MediaTek devices) - if [[ -d "${WORK_DIR}/mtkclient" ]]; then - log "Trying MTKClient extraction..." - if extract_boot_with_mtkclient; then - return 0 - fi - warn "MTKClient extraction failed, trying ADB methods..." - fi + # Method 1: Try MTKClient first (best for MediaTek devices) + if [[ -d "${WORK_DIR}/mtkclient" ]]; then + log "Trying MTKClient extraction..." + if extract_boot_with_mtkclient; then + return 0 + fi + warn "MTKClient extraction failed, trying ADB methods..." + fi - # Method 2: Try to pull boot partition directly via ADB - local boot_partition - boot_partition=$(adb shell "find /dev/block -name boot | head -n1" 2>/dev/null | tr -d '\r\n' || echo "") + # Method 2: Try to pull boot partition directly via ADB + local boot_partition + boot_partition=$(adb shell "find /dev/block -name boot | head -n1" 2> /dev/null | tr -d '\r\n' || echo "") - if [[ -n $boot_partition ]]; then - log "Found boot partition: $boot_partition" - if adb pull "$boot_partition" "$boot_img" 2>/dev/null; then - log "Boot image extracted successfully" - BOOT_IMG="$boot_img" - return 0 - fi - fi + if [[ -n $boot_partition ]]; then + log "Found boot partition: $boot_partition" + if adb pull "$boot_partition" "$boot_img" 2> /dev/null; then + log "Boot image extracted successfully" + BOOT_IMG="$boot_img" + return 0 + fi + fi - # Method 3: Try to get boot partition via by-name - boot_partition=$(adb shell "ls /dev/block/by-name/boot*" 2>/dev/null | head -n1 | tr -d '\r\n' || echo "") + # Method 3: Try to get boot partition via by-name + boot_partition=$(adb shell "ls /dev/block/by-name/boot*" 2> /dev/null | head -n1 | tr -d '\r\n' || echo "") - if [[ -n $boot_partition ]]; then - log "Found boot partition: $boot_partition" - if adb shell "su -c 'dd if=$boot_partition of=/sdcard/boot.img'" 2>/dev/null && - adb pull /sdcard/boot.img "$boot_img" 2>/dev/null; then - adb shell rm /sdcard/boot.img 2>/dev/null || true - log "Boot image extracted successfully" - BOOT_IMG="$boot_img" - return 0 - fi - fi + if [[ -n $boot_partition ]]; then + log "Found boot partition: $boot_partition" + if adb shell "su -c 'dd if=$boot_partition of=/sdcard/boot.img'" 2> /dev/null && + adb pull /sdcard/boot.img "$boot_img" 2> /dev/null; then + adb shell rm /sdcard/boot.img 2> /dev/null || true + log "Boot image extracted successfully" + BOOT_IMG="$boot_img" + return 0 + fi + fi - error "Failed to extract boot image automatically." - echo - echo "Manual extraction options:" - echo "1. Use MTKClient: $SCRIPT_NAME extract-mtk" - echo "2. Extract boot.img from your device's firmware package" - echo "3. Get boot.img from device manufacturer's official ROM" - echo - echo "Then run: $SCRIPT_NAME root --boot-img /path/to/boot.img" - echo + error "Failed to extract boot image automatically." + echo + echo "Manual extraction options:" + echo "1. Use MTKClient: $SCRIPT_NAME extract-mtk" + echo "2. Extract boot.img from your device's firmware package" + echo "3. Get boot.img from device manufacturer's official ROM" + echo + echo "Then run: $SCRIPT_NAME root --boot-img /path/to/boot.img" + echo - return 1 + return 1 } patch_boot_with_magisk() { - print_header "Patching Boot Image with Magisk" + print_header "Patching Boot Image with Magisk" - if [[ ! -f ${BOOT_IMG:-} ]]; then - die "Boot image not found: ${BOOT_IMG:-none}" - fi + if [[ ! -f ${BOOT_IMG:-} ]]; then + die "Boot image not found: ${BOOT_IMG:-none}" + fi - local magisk_apk="$WORK_DIR/magisk.apk" - if [[ ! -f $magisk_apk ]]; then - die "Magisk APK not found. Run download step first." - fi + local magisk_apk="$WORK_DIR/magisk.apk" + if [[ ! -f $magisk_apk ]]; then + die "Magisk APK not found. Run download step first." + fi - log "Checking if device is connected..." - if ! adb devices | grep -q "device$"; then - die "No device detected. Make sure USB debugging is enabled and device is connected." - fi + log "Checking if device is connected..." + if ! adb devices | grep -q "device$"; then + die "No device detected. Make sure USB debugging is enabled and device is connected." + fi - log "Installing Magisk APK on device..." - if ! adb install -r "$magisk_apk" 2>/dev/null; then - warn "Magisk APK installation failed (may already be installed)" - fi + log "Installing Magisk APK on device..." + if ! adb install -r "$magisk_apk" 2> /dev/null; then + warn "Magisk APK installation failed (may already be installed)" + fi - log "Pushing boot image to device..." - adb push "$BOOT_IMG" /sdcard/Download/boot.img || die "Failed to push boot image" + log "Pushing boot image to device..." + adb push "$BOOT_IMG" /sdcard/Download/boot.img || die "Failed to push boot image" - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " MANUAL STEP REQUIRED" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo - echo "On your phone:" - echo "1. Open the Magisk app" - echo "2. Tap 'Install' next to Magisk" - echo "3. Select 'Select and Patch a File'" - echo "4. Navigate to Downloads and select boot.img" - echo "5. Tap 'Let's Go' and wait for patching to complete" - echo "6. The patched file will be saved as magisk_patched_*.img" - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " MANUAL STEP REQUIRED" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + echo "On your phone:" + echo "1. Open the Magisk app" + echo "2. Tap 'Install' next to Magisk" + echo "3. Select 'Select and Patch a File'" + echo "4. Navigate to Downloads and select boot.img" + echo "5. Tap 'Let's Go' and wait for patching to complete" + echo "6. The patched file will be saved as magisk_patched_*.img" + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo - if ! confirm "Have you completed patching the boot image in Magisk app?"; then - error "Patching cancelled by user" - return 1 - fi + if ! confirm "Have you completed patching the boot image in Magisk app?"; then + error "Patching cancelled by user" + return 1 + fi - log "Pulling patched boot image from device..." + log "Pulling patched boot image from device..." - local patched_img - patched_img=$(adb shell "ls /sdcard/Download/magisk_patched_*.img 2>/dev/null" | tr -d '\r\n' | head -n1 || echo "") + local patched_img + patched_img=$(adb shell "ls /sdcard/Download/magisk_patched_*.img 2>/dev/null" | tr -d '\r\n' | head -n1 || echo "") - if [[ -z $patched_img ]]; then - error "Could not find patched boot image on device." - echo "Please ensure the patching completed successfully in Magisk app." - return 1 - fi + if [[ -z $patched_img ]]; then + error "Could not find patched boot image on device." + echo "Please ensure the patching completed successfully in Magisk app." + return 1 + fi - PATCHED_BOOT_IMG="$WORK_DIR/magisk_patched.img" - if ! adb pull "$patched_img" "$PATCHED_BOOT_IMG"; then - error "Failed to pull patched boot image" - return 1 - fi + PATCHED_BOOT_IMG="$WORK_DIR/magisk_patched.img" + if ! adb pull "$patched_img" "$PATCHED_BOOT_IMG"; then + error "Failed to pull patched boot image" + return 1 + fi - log "Patched boot image saved to: $PATCHED_BOOT_IMG" - return 0 + log "Patched boot image saved to: $PATCHED_BOOT_IMG" + return 0 } flash_patched_boot() { - print_header "Flashing Patched Boot Image" + print_header "Flashing Patched Boot Image" - if [[ ! -f ${PATCHED_BOOT_IMG:-} ]]; then - die "Patched boot image not found: ${PATCHED_BOOT_IMG:-none}" - fi + if [[ ! -f ${PATCHED_BOOT_IMG:-} ]]; then + die "Patched boot image not found: ${PATCHED_BOOT_IMG:-none}" + fi - echo - echo -e "${YELLOW}This will flash the patched boot image to your device.${NC}" - echo "Device uses A/B partitions - will flash to boot_a" - echo + echo + echo -e "${YELLOW}This will flash the patched boot image to your device.${NC}" + echo "Device uses A/B partitions - will flash to boot_a" + echo - if ! confirm "Proceed with flashing?"; then - log "Flashing cancelled by user" - return 1 - fi + if ! confirm "Proceed with flashing?"; then + log "Flashing cancelled by user" + return 1 + fi - log "Rebooting to bootloader..." - adb reboot bootloader || die "Failed to reboot to bootloader" + log "Rebooting to bootloader..." + adb reboot bootloader || die "Failed to reboot to bootloader" - log "Waiting for fastboot mode..." - sleep 5 + log "Waiting for fastboot mode..." + sleep 5 - if ! sudo fastboot devices | grep -q .; then - die "Device not detected in fastboot mode" - fi + if ! sudo fastboot devices | grep -q .; then + die "Device not detected in fastboot mode" + fi - log "Flashing patched boot image to boot_a..." - if ! sudo fastboot flash boot_a "$PATCHED_BOOT_IMG"; then - error "Failed to flash boot image" - return 1 - fi + log "Flashing patched boot image to boot_a..." + if ! sudo fastboot flash boot_a "$PATCHED_BOOT_IMG"; then + error "Failed to flash boot image" + return 1 + fi - log "Flashed successfully!" - log "Rebooting device..." - sudo fastboot reboot + log "Flashed successfully!" + log "Rebooting device..." + sudo fastboot reboot - log "Waiting for device to boot..." - sleep 10 - adb wait-for-device || true + log "Waiting for device to boot..." + sleep 10 + adb wait-for-device || true - echo - echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ Root Process Complete! ║${NC}" - echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}" - echo - echo "Your BL9000 phone should now be rooted with Magisk!" - echo - echo "Next steps:" - echo "1. Open the Magisk app on your phone" - echo "2. Verify that it shows 'Installed' for both Magisk and App" - echo "3. Grant root access to apps as needed" - echo "4. Install Magisk modules if desired" - echo - echo "Note: Some banking and secure apps may not work with root." - echo " Use Magisk's DenyList feature to hide root from specific apps." - echo + echo + echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Root Process Complete! ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}" + echo + echo "Your BL9000 phone should now be rooted with Magisk!" + echo + echo "Next steps:" + echo "1. Open the Magisk app on your phone" + echo "2. Verify that it shows 'Installed' for both Magisk and App" + echo "3. Grant root access to apps as needed" + echo "4. Install Magisk modules if desired" + echo + echo "Note: Some banking and secure apps may not work with root." + echo " Use Magisk's DenyList feature to hide root from specific apps." + echo - return 0 + return 0 } clean_work_dir() { - if [[ -d $WORK_DIR ]]; then - log "Removing working directory: $WORK_DIR" - rm -rf "$WORK_DIR" - log "Cleaned successfully" - else - log "Work directory doesn't exist: $WORK_DIR" - fi + if [[ -d $WORK_DIR ]]; then + log "Removing working directory: $WORK_DIR" + rm -rf "$WORK_DIR" + log "Cleaned successfully" + else + log "Work directory doesn't exist: $WORK_DIR" + fi } run_full_process() { - print_header "BL9000 Full Root Process" + print_header "BL9000 Full Root Process" - log "Starting complete rooting process..." + log "Starting complete rooting process..." - install_dependencies || die "Failed to install dependencies" - setup_udev_rules || true + install_dependencies || die "Failed to install dependencies" + setup_udev_rules || true - echo - if ! confirm "Continue to device check?"; then - die "Process cancelled by user" - fi + echo + if ! confirm "Continue to device check?"; then + die "Process cancelled by user" + fi - check_device || die "Device check failed" + check_device || die "Device check failed" - echo - if ! confirm "Continue to backup device data?"; then - die "Process cancelled by user" - fi + echo + if ! confirm "Continue to backup device data?"; then + die "Process cancelled by user" + fi - backup_device_data || warn "Backup failed or incomplete" + backup_device_data || warn "Backup failed or incomplete" - echo - if ! confirm "Continue to bootloader unlock?"; then - die "Process cancelled by user" - fi + echo + if ! confirm "Continue to bootloader unlock?"; then + die "Process cancelled by user" + fi - unlock_bootloader || die "Bootloader unlock failed" + unlock_bootloader || die "Bootloader unlock failed" - echo - log "Please complete device setup and re-enable USB debugging, then press Enter..." - read -r + echo + log "Please complete device setup and re-enable USB debugging, then press Enter..." + read -r - check_device || die "Device check failed after unlock" + check_device || die "Device check failed after unlock" - download_magisk || die "Failed to download Magisk" - extract_boot_image || die "Failed to extract boot image" - patch_boot_with_magisk || die "Failed to patch boot image" - flash_patched_boot || die "Failed to flash patched boot" + download_magisk || die "Failed to download Magisk" + extract_boot_image || die "Failed to extract boot image" + patch_boot_with_magisk || die "Failed to patch boot image" + flash_patched_boot || die "Failed to flash patched boot" - log "Full root process completed successfully!" + log "Full root process completed successfully!" } run_root_only() { - print_header "BL9000 Root Process (Skip Unlock)" + print_header "BL9000 Root Process (Skip Unlock)" - log "Starting root process (assuming bootloader is already unlocked)..." + log "Starting root process (assuming bootloader is already unlocked)..." - check_device || die "Device check failed" - download_magisk || die "Failed to download Magisk" - extract_boot_image || die "Failed to extract boot image" - patch_boot_with_magisk || die "Failed to patch boot image" - flash_patched_boot || die "Failed to flash patched boot" + check_device || die "Device check failed" + download_magisk || die "Failed to download Magisk" + extract_boot_image || die "Failed to extract boot image" + patch_boot_with_magisk || die "Failed to patch boot image" + flash_patched_boot || die "Failed to flash patched boot" - log "Root process completed successfully!" + log "Root process completed successfully!" } main() { - require_non_root + require_non_root - # Create work directory - mkdir -p "$WORK_DIR" + # Create work directory + mkdir -p "$WORK_DIR" - local command="${1:-help}" - shift || true + local command="${1:-help}" + shift || true - # Handle flash command's image argument before option parsing - if [[ $command == "flash" && -n ${1:-} && $1 != --* ]]; then - PATCHED_BOOT_IMG="$1" - if [[ ! -f $PATCHED_BOOT_IMG ]]; then - die "Patched boot image file not found: $PATCHED_BOOT_IMG" - fi - shift - fi + # Handle flash command's image argument before option parsing + if [[ $command == "flash" && -n ${1:-} && $1 != --* ]]; then + PATCHED_BOOT_IMG="$1" + if [[ ! -f $PATCHED_BOOT_IMG ]]; then + die "Patched boot image file not found: $PATCHED_BOOT_IMG" + fi + shift + fi - # Parse options - while [[ $# -gt 0 ]]; do - case "$1" in - --work-dir) - WORK_DIR="$2" - mkdir -p "$WORK_DIR" - shift 2 - ;; - --boot-img) - BOOT_IMG="$2" - if [[ ! -f $BOOT_IMG ]]; then - die "Boot image file not found: $BOOT_IMG" - fi - shift 2 - ;; - -h | --help) - usage - exit 0 - ;; - *) - error "Unknown option: $1" - usage - exit 1 - ;; - esac - done + # Parse options + while [[ $# -gt 0 ]]; do + case "$1" in + --work-dir) + WORK_DIR="$2" + mkdir -p "$WORK_DIR" + shift 2 + ;; + --boot-img) + BOOT_IMG="$2" + if [[ ! -f $BOOT_IMG ]]; then + die "Boot image file not found: $BOOT_IMG" + fi + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + error "Unknown option: $1" + usage + exit 1 + ;; + esac + done - case "$command" in - install-deps) - install_dependencies - setup_udev_rules - ;; - install-mtk) - install_dependencies - setup_udev_rules - install_mtkclient - ;; - check) - check_device - ;; - backup) - check_device || die "Device check failed" - backup_device_data - ;; - unlock) - check_device || die "Device check failed" - unlock_bootloader - ;; - extract-mtk) - extract_boot_with_mtkclient - ;; - patch) - # Patch the extracted boot image with Magisk - if [[ -f "$WORK_DIR/boot.img" ]]; then - BOOT_IMG="$WORK_DIR/boot.img" - fi - patch_boot_with_magisk - ;; - flash) - # Flash the patched boot image - if [[ -z ${PATCHED_BOOT_IMG:-} ]]; then - if [[ -f "$WORK_DIR/magisk_patched.img" ]]; then - PATCHED_BOOT_IMG="$WORK_DIR/magisk_patched.img" - else - die "No patched boot image specified and none found at $WORK_DIR/magisk_patched.img" - fi - fi - flash_patched_boot - ;; - auto-root) - # Automated rooting: extract -> patch -> flash - extract_boot_with_mtkclient || die "Boot extraction failed" - BOOT_IMG="$WORK_DIR/boot.img" - patch_boot_with_magisk || die "Boot patching failed" - flash_patched_boot || die "Boot flashing failed" - log "Rooting process completed!" - ;; - root) - run_root_only - ;; - full) - run_full_process - ;; - clean) - clean_work_dir - ;; - help | --help | -h) - usage - ;; - *) - error "Unknown command: $command" - usage - exit 1 - ;; - esac + case "$command" in + install-deps) + install_dependencies + setup_udev_rules + ;; + install-mtk) + install_dependencies + setup_udev_rules + install_mtkclient + ;; + check) + check_device + ;; + backup) + check_device || die "Device check failed" + backup_device_data + ;; + unlock) + check_device || die "Device check failed" + unlock_bootloader + ;; + extract-mtk) + extract_boot_with_mtkclient + ;; + patch) + # Patch the extracted boot image with Magisk + if [[ -f "$WORK_DIR/boot.img" ]]; then + BOOT_IMG="$WORK_DIR/boot.img" + fi + patch_boot_with_magisk + ;; + flash) + # Flash the patched boot image + if [[ -z ${PATCHED_BOOT_IMG:-} ]]; then + if [[ -f "$WORK_DIR/magisk_patched.img" ]]; then + PATCHED_BOOT_IMG="$WORK_DIR/magisk_patched.img" + else + die "No patched boot image specified and none found at $WORK_DIR/magisk_patched.img" + fi + fi + flash_patched_boot + ;; + auto-root) + # Automated rooting: extract -> patch -> flash + extract_boot_with_mtkclient || die "Boot extraction failed" + BOOT_IMG="$WORK_DIR/boot.img" + patch_boot_with_magisk || die "Boot patching failed" + flash_patched_boot || die "Boot flashing failed" + log "Rooting process completed!" + ;; + root) + run_root_only + ;; + full) + run_full_process + ;; + clean) + clean_work_dir + ;; + help | --help | -h) + usage + ;; + *) + error "Unknown command: $command" + usage + exit 1 + ;; + esac } main "$@" diff --git a/scripts/utils/setup_android_adblock.sh b/scripts/utils/setup_android_adblock.sh index d954093..e7a2943 100755 --- a/scripts/utils/setup_android_adblock.sh +++ b/scripts/utils/setup_android_adblock.sh @@ -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 "$@" diff --git a/scripts/utils/setup_media_organizer.sh b/scripts/utils/setup_media_organizer.sh index 468d4be..ed06a7f 100755 --- a/scripts/utils/setup_media_organizer.sh +++ b/scripts/utils/setup_media_organizer.sh @@ -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" < "$SERVICE_FILE" << EOF [Unit] Description=Media File Organizer After=graphical-session.target diff --git a/scripts/utils/setup_offline_docs.sh b/scripts/utils/setup_offline_docs.sh index c0622f8..207f65e 100755 --- a/scripts/utils/setup_offline_docs.sh +++ b/scripts/utils/setup_offline_docs.sh @@ -22,27 +22,27 @@ YELLOW='\033[1;33m' NC='\033[0m' print_header() { - echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" - echo -e "${GREEN} $1${NC}" - echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" + echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN} $1${NC}" + echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}" } print_status() { - echo -e "${YELLOW}→${NC} $1" + echo -e "${YELLOW}→${NC} $1" } print_success() { - echo -e "${GREEN}✓${NC} $1" + echo -e "${GREEN}✓${NC} $1" } print_error() { - echo -e "${RED}✗${NC} $1" + echo -e "${RED}✗${NC} $1" } # Create directory structure setup_dirs() { - mkdir -p "$DOCS_DIR"/{python,c_cpp,javascript,typescript,rust,go,ruby,java,shell} - mkdir -p "$INDEX_DIR" + mkdir -p "$DOCS_DIR"/{python,c_cpp,javascript,typescript,rust,go,ruby,java,shell} + mkdir -p "$INDEX_DIR" } #============================================================================== @@ -50,62 +50,62 @@ setup_dirs() { # Source: https://docs.python.org/3/download.html #============================================================================== download_python_docs() { - print_header "Python Documentation" - local dest="$DOCS_DIR/python" - - # Check if already downloaded - if [ -f "$dest/library/index.html" ]; then - print_status "Python docs already present, checking for updates..." - fi - - print_status "Downloading Python 3.12 documentation..." - - # Download HTML documentation (most searchable) - local url="https://www.python.org/ftp/python/doc/3.12.8/python-3.12.8-docs-html.tar.bz2" - local archive="/tmp/python-docs.tar.bz2" - - if curl -L -o "$archive" "$url" 2>/dev/null; then - print_status "Extracting..." - tar -xjf "$archive" -C "$dest" --strip-components=1 - rm -f "$archive" - print_success "Python documentation installed to $dest" - - # Build index - build_python_index - else - print_error "Failed to download Python docs" - print_status "Alternative: Use 'python -m pydoc' for built-in docs" - fi + print_header "Python Documentation" + local dest="$DOCS_DIR/python" + + # Check if already downloaded + if [ -f "$dest/library/index.html" ]; then + print_status "Python docs already present, checking for updates..." + fi + + print_status "Downloading Python 3.12 documentation..." + + # Download HTML documentation (most searchable) + local url="https://www.python.org/ftp/python/doc/3.12.8/python-3.12.8-docs-html.tar.bz2" + local archive="/tmp/python-docs.tar.bz2" + + if curl -L -o "$archive" "$url" 2> /dev/null; then + print_status "Extracting..." + tar -xjf "$archive" -C "$dest" --strip-components=1 + rm -f "$archive" + print_success "Python documentation installed to $dest" + + # Build index + build_python_index + else + print_error "Failed to download Python docs" + print_status "Alternative: Use 'python -m pydoc' for built-in docs" + fi } build_python_index() { - print_status "Building Python documentation index..." - local dest="$DOCS_DIR/python" - local index="$INDEX_DIR/python_index.txt" - - # Create searchable index: term -> file path - { - # Index library modules - find "$dest/library" -name "*.html" -exec basename {} .html \; 2>/dev/null | while read -r mod; do - echo "$mod $dest/library/$mod.html" - done - - # Index built-in functions from functions.html - if [ -f "$dest/library/functions.html" ]; then - grep -oP '(?<=id=")[^"]+' "$dest/library/functions.html" 2>/dev/null | while read -r func; do - echo "$func $dest/library/functions.html#$func" - done - fi - - # Index from general index - if [ -f "$dest/genindex.html" ]; then - grep -oP 'href="([^"]+)"[^>]*>([^<]+)' "$dest/genindex.html" 2>/dev/null | \ - sed -E 's/href="([^"]+)"[^>]*>([^<]+)/\2 \1/' | \ - head -5000 - fi - } | sort -u > "$index" - - print_success "Python index created with $(wc -l < "$index") entries" + print_status "Building Python documentation index..." + local dest="$DOCS_DIR/python" + local index="$INDEX_DIR/python_index.txt" + + # Create searchable index: term -> file path + { + # Index library modules + find "$dest/library" -name "*.html" -exec basename {} .html \; 2> /dev/null | while read -r mod; do + echo "$mod $dest/library/$mod.html" + done + + # Index built-in functions from functions.html + if [ -f "$dest/library/functions.html" ]; then + grep -oP '(?<=id=")[^"]+' "$dest/library/functions.html" 2> /dev/null | while read -r func; do + echo "$func $dest/library/functions.html#$func" + done + fi + + # Index from general index + if [ -f "$dest/genindex.html" ]; then + grep -oP 'href="([^"]+)"[^>]*>([^<]+)' "$dest/genindex.html" 2> /dev/null | + sed -E 's/href="([^"]+)"[^>]*>([^<]+)/\2 \1/' | + head -5000 + fi + } | sort -u > "$index" + + print_success "Python index created with $(wc -l < "$index") entries" } #============================================================================== @@ -114,114 +114,114 @@ build_python_index() { # Fallback: AUR cppreference package or direct download #============================================================================== download_cpp_docs() { - print_header "C/C++ Documentation (cppreference)" - local dest="$DOCS_DIR/c_cpp" - - if [ -f "$dest/en/index.html" ] || [ -d "$dest/reference" ] || [ -L "$dest/system" ]; then - print_status "C/C++ docs already present" - return 0 - fi - - mkdir -p "$dest" - - # Method 1: Use cppman if available (best - fetches and caches on demand) - if command -v cppman &>/dev/null; then - print_status "Found cppman, caching common C++ references..." - cppman -s cppreference.com 2>/dev/null - cppman -c 2>/dev/null # Cache all pages - print_success "cppman configured - use 'cppman ' for lookups" - print_status "Cppman cache at: ~/.cache/cppman/" - ln -sf ~/.cache/cppman "$dest/cppman_cache" 2>/dev/null - build_cpp_index - return 0 - fi - - # Method 2: Check if system package already installed - if [ -d /usr/share/doc/cppreference/en ]; then - print_status "Found system cppreference package" + print_header "C/C++ Documentation (cppreference)" + local dest="$DOCS_DIR/c_cpp" + + if [ -f "$dest/en/index.html" ] || [ -d "$dest/reference" ] || [ -L "$dest/system" ]; then + print_status "C/C++ docs already present" + return 0 + fi + + mkdir -p "$dest" + + # Method 1: Use cppman if available (best - fetches and caches on demand) + if command -v cppman &> /dev/null; then + print_status "Found cppman, caching common C++ references..." + cppman -s cppreference.com 2> /dev/null + cppman -c 2> /dev/null # Cache all pages + print_success "cppman configured - use 'cppman ' for lookups" + print_status "Cppman cache at: ~/.cache/cppman/" + ln -sf ~/.cache/cppman "$dest/cppman_cache" 2> /dev/null + build_cpp_index + return 0 + fi + + # Method 2: Check if system package already installed + if [ -d /usr/share/doc/cppreference/en ]; then + print_status "Found system cppreference package" + ln -sf /usr/share/doc/cppreference "$dest/system" + print_success "C/C++ documentation linked from system package" + build_cpp_index + return 0 + fi + + # Method 3: Try AUR package (Arch Linux) + if command -v yay &> /dev/null; then + print_status "Installing cppreference from AUR..." + if yay -S --noconfirm cppreference 2> /dev/null; then + # Link to installed docs (the package uses /en not /html) + if [ -d /usr/share/doc/cppreference/en ]; then ln -sf /usr/share/doc/cppreference "$dest/system" print_success "C/C++ documentation linked from system package" build_cpp_index return 0 + fi fi - - # Method 3: Try AUR package (Arch Linux) - if command -v yay &>/dev/null; then - print_status "Installing cppreference from AUR..." - if yay -S --noconfirm cppreference 2>/dev/null; then - # Link to installed docs (the package uses /en not /html) - if [ -d /usr/share/doc/cppreference/en ]; then - ln -sf /usr/share/doc/cppreference "$dest/system" - print_success "C/C++ documentation linked from system package" - build_cpp_index - return 0 - fi - fi + fi + + # Method 4: Direct download (try multiple mirrors) + print_status "Downloading cppreference offline archive..." + local archive="/tmp/cppreference.tar.xz" + local urls=( + "https://upload.cppreference.com/mwiki/images/1/16/html_book_20241110.tar.xz" + "https://github.com/nicovank/cppreference-doc/releases/latest/download/html_book.tar.xz" + ) + + for url in "${urls[@]}"; do + print_status "Trying: $url" + if curl -fL -o "$archive" "$url" 2> /dev/null; then + print_status "Extracting (this may take a while)..." + if tar -xJf "$archive" -C "$dest" 2> /dev/null; then + rm -f "$archive" + print_success "C/C++ documentation installed to $dest" + build_cpp_index + return 0 + fi fi - - # Method 4: Direct download (try multiple mirrors) - print_status "Downloading cppreference offline archive..." - local archive="/tmp/cppreference.tar.xz" - local urls=( - "https://upload.cppreference.com/mwiki/images/1/16/html_book_20241110.tar.xz" - "https://github.com/nicovank/cppreference-doc/releases/latest/download/html_book.tar.xz" - ) - - for url in "${urls[@]}"; do - print_status "Trying: $url" - if curl -fL -o "$archive" "$url" 2>/dev/null; then - print_status "Extracting (this may take a while)..." - if tar -xJf "$archive" -C "$dest" 2>/dev/null; then - rm -f "$archive" - print_success "C/C++ documentation installed to $dest" - build_cpp_index - return 0 - fi - fi - done - - print_error "Failed to download cppreference" - print_status "Manual install: yay -S cppreference OR yay -S cppman" - return 1 + done + + print_error "Failed to download cppreference" + print_status "Manual install: yay -S cppreference OR yay -S cppman" + return 1 } build_cpp_index() { - print_status "Building C/C++ documentation index..." - local dest="$DOCS_DIR/c_cpp" - local index="$INDEX_DIR/cpp_index.txt" - - # Resolve symlink if present - local search_dir="$dest" - [ -L "$dest/system" ] && search_dir="$dest/system" - - { - # Find all HTML files and extract identifiers - # Format: term|filepath (using | as separator to handle spaces) - find "$search_dir" -name "*.html" -type f 2>/dev/null | while read -r file; do - # Extract meaningful term from path (e.g., /en/cpp/container/vector.html -> vector) - local term - term=$(basename "$file" .html) - # Skip index files and overly generic names - [[ "$term" == "index" ]] && continue - echo "${term}|${file}" - done - - # Also index by path components for better discoverability - # e.g., cpp/container/vector -> vector - find "$search_dir/en" -name "*.html" -type f 2>/dev/null | while read -r file; do - # Extract path relative to en/ and create searchable term - local relpath - relpath=$(echo "$file" | sed "s|$search_dir/en/||" | sed 's|\.html$||') - # Get the last component as primary term - local term - term=$(basename "$relpath") - [[ "$term" == "index" ]] && continue - # Also add the full path as a searchable term (cpp/vector, c/stdlib/malloc) - echo "${relpath}|${file}" - done - } | sort -u > "$index" - - print_success "C/C++ index created with $(wc -l < "$index") entries" + print_status "Building C/C++ documentation index..." + local dest="$DOCS_DIR/c_cpp" + local index="$INDEX_DIR/cpp_index.txt" + + # Resolve symlink if present + local search_dir="$dest" + [ -L "$dest/system" ] && search_dir="$dest/system" + + { + # Find all HTML files and extract identifiers + # Format: term|filepath (using | as separator to handle spaces) + find "$search_dir" -name "*.html" -type f 2> /dev/null | while read -r file; do + # Extract meaningful term from path (e.g., /en/cpp/container/vector.html -> vector) + local term + term=$(basename "$file" .html) + # Skip index files and overly generic names + [[ $term == "index" ]] && continue + echo "${term}|${file}" + done + + # Also index by path components for better discoverability + # e.g., cpp/container/vector -> vector + find "$search_dir/en" -name "*.html" -type f 2> /dev/null | while read -r file; do + # Extract path relative to en/ and create searchable term + local relpath + relpath=$(echo "$file" | sed "s|$search_dir/en/||" | sed 's|\.html$||') + # Get the last component as primary term + local term + term=$(basename "$relpath") + [[ $term == "index" ]] && continue + # Also add the full path as a searchable term (cpp/vector, c/stdlib/malloc) + echo "${relpath}|${file}" + done + } | sort -u > "$index" + + print_success "C/C++ index created with $(wc -l < "$index") entries" } #============================================================================== @@ -230,237 +230,237 @@ build_cpp_index() { # https://github.com/mdn/content #============================================================================== download_js_docs() { - print_header "JavaScript/MDN Documentation" - local dest="$DOCS_DIR/javascript" - local mdn_repo="$DOCS_DIR/mdn-content" - - # Check if already cloned - if [ -d "$mdn_repo/files/en-us/web/javascript" ]; then - print_status "MDN content already present" - build_js_index - return 0 - fi - - print_status "Cloning MDN content repository (sparse checkout for web docs)..." - print_status "This may take a few minutes on first run..." - - mkdir -p "$mdn_repo" - cd "$mdn_repo" || exit 1 - - # Initialize sparse checkout to only get what we need - if [ ! -d ".git" ]; then - git init - git remote add origin https://github.com/mdn/content.git - git config core.sparseCheckout true - - # Only checkout web-related documentation (JS, HTML, CSS, Web APIs) - cat > .git/info/sparse-checkout << 'SPARSE' + print_header "JavaScript/MDN Documentation" + local dest="$DOCS_DIR/javascript" + local mdn_repo="$DOCS_DIR/mdn-content" + + # Check if already cloned + if [ -d "$mdn_repo/files/en-us/web/javascript" ]; then + print_status "MDN content already present" + build_js_index + return 0 + fi + + print_status "Cloning MDN content repository (sparse checkout for web docs)..." + print_status "This may take a few minutes on first run..." + + mkdir -p "$mdn_repo" + cd "$mdn_repo" || exit 1 + + # Initialize sparse checkout to only get what we need + if [ ! -d ".git" ]; then + git init + git remote add origin https://github.com/mdn/content.git + git config core.sparseCheckout true + + # Only checkout web-related documentation (JS, HTML, CSS, Web APIs) + cat > .git/info/sparse-checkout << 'SPARSE' /files/en-us/web/javascript/ /files/en-us/web/api/ /files/en-us/web/html/ /files/en-us/web/css/ /files/en-us/glossary/ SPARSE - - print_status "Fetching MDN content (JavaScript, HTML, CSS, Web APIs)..." - git fetch --depth 1 origin main - git checkout main - else - print_status "Updating MDN content..." - git pull --depth 1 origin main 2>/dev/null || true - fi - - cd - > /dev/null || exit 1 - - # Create symlink for easier access - mkdir -p "$dest" - ln -sf "$mdn_repo/files/en-us/web/javascript" "$dest/javascript" - ln -sf "$mdn_repo/files/en-us/web/api" "$dest/web-api" - ln -sf "$mdn_repo/files/en-us/web/html" "$dest/html" - ln -sf "$mdn_repo/files/en-us/web/css" "$dest/css" - ln -sf "$mdn_repo/files/en-us/glossary" "$dest/glossary" - - build_js_index - print_success "MDN offline documentation ready" - - local doc_count - doc_count=$(find "$mdn_repo/files" -name "index.md" 2>/dev/null | wc -l) - print_status "Downloaded $doc_count documentation pages" + + print_status "Fetching MDN content (JavaScript, HTML, CSS, Web APIs)..." + git fetch --depth 1 origin main + git checkout main + else + print_status "Updating MDN content..." + git pull --depth 1 origin main 2> /dev/null || true + fi + + cd - > /dev/null || exit 1 + + # Create symlink for easier access + mkdir -p "$dest" + ln -sf "$mdn_repo/files/en-us/web/javascript" "$dest/javascript" + ln -sf "$mdn_repo/files/en-us/web/api" "$dest/web-api" + ln -sf "$mdn_repo/files/en-us/web/html" "$dest/html" + ln -sf "$mdn_repo/files/en-us/web/css" "$dest/css" + ln -sf "$mdn_repo/files/en-us/glossary" "$dest/glossary" + + build_js_index + print_success "MDN offline documentation ready" + + local doc_count + doc_count=$(find "$mdn_repo/files" -name "index.md" 2> /dev/null | wc -l) + print_status "Downloaded $doc_count documentation pages" } build_js_index() { - print_status "Building MDN documentation index..." - local mdn_repo="$DOCS_DIR/mdn-content" - local index="$INDEX_DIR/js_index.txt" - - if [ ! -d "$mdn_repo/files" ]; then - print_error "MDN content not found" - return 1 - fi - - # Build comprehensive index from MDN markdown files - { - # Index JavaScript reference - find "$mdn_repo/files/en-us/web/javascript/reference" -name "index.md" 2>/dev/null | while read -r file; do - local dir - dir=$(dirname "$file") - local term - term=$(basename "$dir") - # Extract title from frontmatter if available - local title - title=$(grep -m1 "^title:" "$file" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "${term}|${file}|${title:-$term}" - done - - # Index Web APIs - find "$mdn_repo/files/en-us/web/api" -name "index.md" 2>/dev/null | while read -r file; do - local dir - dir=$(dirname "$file") - local term - term=$(basename "$dir") - local title - title=$(grep -m1 "^title:" "$file" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "${term}|${file}|${title:-$term}" - done - - # Index HTML elements - find "$mdn_repo/files/en-us/web/html/element" -name "index.md" 2>/dev/null | while read -r file; do - local dir - dir=$(dirname "$file") - local term - term=$(basename "$dir") - echo "${term}|${file}|HTML <${term}> element" - done - - # Index CSS properties - find "$mdn_repo/files/en-us/web/css" -maxdepth 2 -name "index.md" 2>/dev/null | while read -r file; do - local dir - dir=$(dirname "$file") - local term - term=$(basename "$dir") - local title - title=$(grep -m1 "^title:" "$file" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "${term}|${file}|${title:-$term}" - done - - # Index Glossary - find "$mdn_repo/files/en-us/glossary" -name "index.md" 2>/dev/null | while read -r file; do - local dir - dir=$(dirname "$file") - local term - term=$(basename "$dir") - local title - title=$(grep -m1 "^title:" "$file" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "${term}|${file}|${title:-$term}" - done - } | sort -t'|' -k1,1 -u > "$index" - - local count - count=$(wc -l < "$index") - print_success "MDN index created with $count entries" + print_status "Building MDN documentation index..." + local mdn_repo="$DOCS_DIR/mdn-content" + local index="$INDEX_DIR/js_index.txt" + + if [ ! -d "$mdn_repo/files" ]; then + print_error "MDN content not found" + return 1 + fi + + # Build comprehensive index from MDN markdown files + { + # Index JavaScript reference + find "$mdn_repo/files/en-us/web/javascript/reference" -name "index.md" 2> /dev/null | while read -r file; do + local dir + dir=$(dirname "$file") + local term + term=$(basename "$dir") + # Extract title from frontmatter if available + local title + title=$(grep -m1 "^title:" "$file" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "${term}|${file}|${title:-$term}" + done + + # Index Web APIs + find "$mdn_repo/files/en-us/web/api" -name "index.md" 2> /dev/null | while read -r file; do + local dir + dir=$(dirname "$file") + local term + term=$(basename "$dir") + local title + title=$(grep -m1 "^title:" "$file" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "${term}|${file}|${title:-$term}" + done + + # Index HTML elements + find "$mdn_repo/files/en-us/web/html/element" -name "index.md" 2> /dev/null | while read -r file; do + local dir + dir=$(dirname "$file") + local term + term=$(basename "$dir") + echo "${term}|${file}|HTML <${term}> element" + done + + # Index CSS properties + find "$mdn_repo/files/en-us/web/css" -maxdepth 2 -name "index.md" 2> /dev/null | while read -r file; do + local dir + dir=$(dirname "$file") + local term + term=$(basename "$dir") + local title + title=$(grep -m1 "^title:" "$file" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "${term}|${file}|${title:-$term}" + done + + # Index Glossary + find "$mdn_repo/files/en-us/glossary" -name "index.md" 2> /dev/null | while read -r file; do + local dir + dir=$(dirname "$file") + local term + term=$(basename "$dir") + local title + title=$(grep -m1 "^title:" "$file" 2> /dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "${term}|${file}|${title:-$term}" + done + } | sort -t'|' -k1,1 -u > "$index" + + local count + count=$(wc -l < "$index") + print_success "MDN index created with $count entries" } #============================================================================== # Rust Documentation (via rustup) #============================================================================== download_rust_docs() { - print_header "Rust Documentation" - local dest="$DOCS_DIR/rust" - - if command -v rustup &>/dev/null; then - print_status "Rust docs available via 'rustup doc'" - - # Get the rust doc path - local rust_doc_path - rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null) - - if [ -n "$rust_doc_path" ] && [ -d "$rust_doc_path" ]; then - ln -sf "$rust_doc_path" "$dest/std" - print_success "Linked Rust std docs from $rust_doc_path" - build_rust_index - fi - else - print_status "Rust not installed. Install with: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + print_header "Rust Documentation" + local dest="$DOCS_DIR/rust" + + if command -v rustup &> /dev/null; then + print_status "Rust docs available via 'rustup doc'" + + # Get the rust doc path + local rust_doc_path + rust_doc_path=$(rustup doc --path 2> /dev/null | head -1 | xargs dirname 2> /dev/null) + + if [ -n "$rust_doc_path" ] && [ -d "$rust_doc_path" ]; then + ln -sf "$rust_doc_path" "$dest/std" + print_success "Linked Rust std docs from $rust_doc_path" + build_rust_index fi + else + print_status "Rust not installed. Install with: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + fi } build_rust_index() { - print_status "Building Rust documentation index..." - local index="$INDEX_DIR/rust_index.txt" - - if command -v rustup &>/dev/null; then - local rust_doc_path - rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null) - - if [ -d "$rust_doc_path/std" ]; then - find "$rust_doc_path/std" -name "*.html" 2>/dev/null | head -2000 | while read -r file; do - basename "$file" .html - done | sort -u > "$index" - fi + print_status "Building Rust documentation index..." + local index="$INDEX_DIR/rust_index.txt" + + if command -v rustup &> /dev/null; then + local rust_doc_path + rust_doc_path=$(rustup doc --path 2> /dev/null | head -1 | xargs dirname 2> /dev/null) + + if [ -d "$rust_doc_path/std" ]; then + find "$rust_doc_path/std" -name "*.html" 2> /dev/null | head -2000 | while read -r file; do + basename "$file" .html + done | sort -u > "$index" fi - - print_success "Rust index created" + fi + + print_success "Rust index created" } #============================================================================== # Go Documentation #============================================================================== download_go_docs() { - print_header "Go Documentation" - local dest="$DOCS_DIR/go" - - if command -v go &>/dev/null; then - print_status "Go docs available via 'go doc'" - - # Create a reference of standard library packages - mkdir -p "$dest" - go list std 2>/dev/null > "$dest/stdlib_packages.txt" - - print_success "Go stdlib package list created" - build_go_index - else - print_status "Go not installed" - fi + print_header "Go Documentation" + local dest="$DOCS_DIR/go" + + if command -v go &> /dev/null; then + print_status "Go docs available via 'go doc'" + + # Create a reference of standard library packages + mkdir -p "$dest" + go list std 2> /dev/null > "$dest/stdlib_packages.txt" + + print_success "Go stdlib package list created" + build_go_index + else + print_status "Go not installed" + fi } build_go_index() { - print_status "Building Go documentation index..." - local dest="$DOCS_DIR/go" - local index="$INDEX_DIR/go_index.txt" - - if [ -f "$dest/stdlib_packages.txt" ]; then - cp "$dest/stdlib_packages.txt" "$index" - fi - - print_success "Go index created" + print_status "Building Go documentation index..." + local dest="$DOCS_DIR/go" + local index="$INDEX_DIR/go_index.txt" + + if [ -f "$dest/stdlib_packages.txt" ]; then + cp "$dest/stdlib_packages.txt" "$index" + fi + + print_success "Go index created" } #============================================================================== # Shell/Bash Documentation (man pages + built-in help) #============================================================================== download_shell_docs() { - print_header "Shell/Bash Documentation" - local dest="$DOCS_DIR/shell" - mkdir -p "$dest" - - print_status "Extracting bash built-in help..." - - # Extract help for all bash builtins - { - echo "# Bash Built-in Commands Reference" - echo "# Generated from 'help' command" - echo "" - - # Get list of builtins - compgen -b 2>/dev/null | while read -r builtin; do - echo "=== $builtin ===" - help "$builtin" 2>/dev/null || echo "No help available" - echo "" - done - } > "$dest/bash_builtins.txt" - - # Create quick reference for common commands - cat > "$dest/common_commands.txt" << 'SHELLREF' + print_header "Shell/Bash Documentation" + local dest="$DOCS_DIR/shell" + mkdir -p "$dest" + + print_status "Extracting bash built-in help..." + + # Extract help for all bash builtins + { + echo "# Bash Built-in Commands Reference" + echo "# Generated from 'help' command" + echo "" + + # Get list of builtins + compgen -b 2> /dev/null | while read -r builtin; do + echo "=== $builtin ===" + help "$builtin" 2> /dev/null || echo "No help available" + echo "" + done + } > "$dest/bash_builtins.txt" + + # Create quick reference for common commands + cat > "$dest/common_commands.txt" << 'SHELLREF' # Common Shell Commands Quick Reference ## File Operations @@ -555,53 +555,53 @@ eval - Evaluate arguments exec - Execute command SHELLREF - print_success "Shell documentation created" - build_shell_index + print_success "Shell documentation created" + build_shell_index } build_shell_index() { - print_status "Building Shell documentation index..." - local dest="$DOCS_DIR/shell" - local index="$INDEX_DIR/shell_index.txt" - - { - # Bash builtins - compgen -b 2>/dev/null | while read -r cmd; do - echo "$cmd $dest/bash_builtins.txt" - done - - # Common commands from man pages - for cmd in ls cd cp mv rm mkdir cat grep sed awk find sort curl wget tar chmod; do - man_path=$(man -w "$cmd" 2>/dev/null) - [ -n "$man_path" ] && echo "$cmd $man_path" - done - } | sort -u > "$index" - - print_success "Shell index created" + print_status "Building Shell documentation index..." + local dest="$DOCS_DIR/shell" + local index="$INDEX_DIR/shell_index.txt" + + { + # Bash builtins + compgen -b 2> /dev/null | while read -r cmd; do + echo "$cmd $dest/bash_builtins.txt" + done + + # Common commands from man pages + for cmd in ls cd cp mv rm mkdir cat grep sed awk find sort curl wget tar chmod; do + man_path=$(man -w "$cmd" 2> /dev/null) + [ -n "$man_path" ] && echo "$cmd $man_path" + done + } | sort -u > "$index" + + print_success "Shell index created" } #============================================================================== # Zeal Docsets (cross-platform dash alternative) #============================================================================== setup_zeal_docsets() { - print_header "Zeal Docsets (Optional)" - - if ! command -v zeal &>/dev/null; then - print_status "Zeal not installed." - print_status "Install with: pacman -S zeal (or your package manager)" - print_status "Zeal provides a GUI for offline documentation" - return 0 - fi - - print_status "Zeal is installed. You can download docsets from within Zeal." - print_status "Recommended docsets: Python 3, JavaScript, TypeScript, C, C++" + print_header "Zeal Docsets (Optional)" + + if ! command -v zeal &> /dev/null; then + print_status "Zeal not installed." + print_status "Install with: pacman -S zeal (or your package manager)" + print_status "Zeal provides a GUI for offline documentation" + return 0 + fi + + print_status "Zeal is installed. You can download docsets from within Zeal." + print_status "Recommended docsets: Python 3, JavaScript, TypeScript, C, C++" } #============================================================================== # Main #============================================================================== usage() { - cat << EOF + cat << EOF Usage: $0 [OPTIONS] Download and setup offline documentation for programming languages. @@ -628,86 +628,86 @@ EOF } show_status() { - print_header "Offline Documentation Status" - echo "Documentation directory: $DOCS_DIR" - echo "" - - for lang in python c_cpp javascript rust go shell; do - dir="$DOCS_DIR/$lang" - if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then - size=$(du -sh "$dir" 2>/dev/null | cut -f1) - print_success "$lang: installed ($size)" - else - print_error "$lang: not installed" - fi - done - - echo "" - echo "Index files:" - ls -la "$INDEX_DIR"/*.txt 2>/dev/null || echo "No indexes built yet" + print_header "Offline Documentation Status" + echo "Documentation directory: $DOCS_DIR" + echo "" + + for lang in python c_cpp javascript rust go shell; do + dir="$DOCS_DIR/$lang" + if [ -d "$dir" ] && [ "$(ls -A "$dir" 2> /dev/null)" ]; then + size=$(du -sh "$dir" 2> /dev/null | cut -f1) + print_success "$lang: installed ($size)" + else + print_error "$lang: not installed" + fi + done + + echo "" + echo "Index files:" + ls -la "$INDEX_DIR"/*.txt 2> /dev/null || echo "No indexes built yet" } main() { - setup_dirs - - if [ $# -eq 0 ]; then + setup_dirs + + if [ $# -eq 0 ]; then + usage + exit 0 + fi + + while [ $# -gt 0 ]; do + case "$1" in + --all) + download_python_docs + download_cpp_docs + download_js_docs + download_rust_docs + download_go_docs + download_shell_docs + setup_zeal_docsets + ;; + --python) + download_python_docs + ;; + --cpp | --c | --c++) + download_cpp_docs + ;; + --js | --javascript) + download_js_docs + ;; + --rust) + download_rust_docs + ;; + --go) + download_go_docs + ;; + --shell | --bash) + download_shell_docs + ;; + --zeal) + setup_zeal_docsets + ;; + --status) + show_status + ;; + --help | -h) usage exit 0 - fi - - while [ $# -gt 0 ]; do - case "$1" in - --all) - download_python_docs - download_cpp_docs - download_js_docs - download_rust_docs - download_go_docs - download_shell_docs - setup_zeal_docsets - ;; - --python) - download_python_docs - ;; - --cpp|--c|--c++) - download_cpp_docs - ;; - --js|--javascript) - download_js_docs - ;; - --rust) - download_rust_docs - ;; - --go) - download_go_docs - ;; - --shell|--bash) - download_shell_docs - ;; - --zeal) - setup_zeal_docsets - ;; - --status) - show_status - ;; - --help|-h) - usage - exit 0 - ;; - *) - print_error "Unknown option: $1" - usage - exit 1 - ;; - esac - shift - done - - echo "" - print_header "Setup Complete" - echo "Documentation stored in: $DOCS_DIR" - echo "" - echo "Use 'lookup_docs.sh [language]' to search documentation" + ;; + *) + print_error "Unknown option: $1" + usage + exit 1 + ;; + esac + shift + done + + echo "" + print_header "Setup Complete" + echo "Documentation stored in: $DOCS_DIR" + echo "" + echo "Use 'lookup_docs.sh [language]' to search documentation" } main "$@" diff --git a/scripts/utils/sync_keepassxc.sh b/scripts/utils/sync_keepassxc.sh index 16f5670..547a855 100755 --- a/scripts/utils/sync_keepassxc.sh +++ b/scripts/utils/sync_keepassxc.sh @@ -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 diff --git a/scripts/utils/toggle_window_manager.sh b/scripts/utils/toggle_window_manager.sh index 5185c15..21643e3 100755 --- a/scripts/utils/toggle_window_manager.sh +++ b/scripts/utils/toggle_window_manager.sh @@ -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 </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 "$@" diff --git a/scripts/utils/txt_to_image.sh b/scripts/utils/txt_to_image.sh index 5a02f60..f5c289d 100755 --- a/scripts/utils/txt_to_image.sh +++ b/scripts/utils/txt_to_image.sh @@ -16,7 +16,7 @@ DEFAULT_RESOLUTION="320x240" # Function to display usage usage() { - cat < [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 " - usage + echo "Error: Missing required argument " + 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 diff --git a/scripts/utils/update_android_hosts.sh b/scripts/utils/update_android_hosts.sh index a56037a..47f7f6c 100755 --- a/scripts/utils/update_android_hosts.sh +++ b/scripts/utils/update_android_hosts.sh @@ -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 </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 " - echo "Example: $0 block-app com.ubercab.eats" - exit 1 - fi + if [[ -z $package ]]; then + echo "Usage: $0 block-app " + 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 " - exit 1 - fi + if [[ -z $package ]]; then + echo "Usage: $0 unblock-app " + 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