diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e41e5aa..df49390 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,19 +19,20 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Shellcheck - run: sudo apt-get install -y shellcheck + - name: Install Shellcheck & Zsh + run: sudo apt-get install -y shellcheck zsh - name: Lint Installer (Bash) - # SC2086: Double quote variables (ignored for flexibility) - # SC2034: Unused variables (ignored) - # SC1091: Not following sourced files (ignored for /etc/os-release) - run: shellcheck -e SC2086,SC2034,SC1091 install.sh + run: shellcheck install.sh - - name: Verify Zsh Syntax - run: | - sudo apt-get install -y zsh - zsh -n .zshrc + - name: Lint Theme Switcher (Bash) + run: shellcheck theme.sh + + - name: Lint Uninstaller (Bash) + run: shellcheck uninstall.sh + + - name: Verify Zsh Syntax (.zshrc) + run: zsh -n .zshrc # ----------------------------------------------------------------------------- # JOB 2: INTEGRATION TEST (UBUNTU & MACOS) @@ -44,7 +45,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} - + steps: - uses: actions/checkout@v4 @@ -52,11 +53,9 @@ jobs: run: chmod +x install.sh - name: Run Installer (Non-Interactive) - # Pipes 'yes' for prompts. Uses CI_ENV to skip fonts/chsh. - run: | - yes | ./install.sh + run: yes | ./install.sh env: - CI_ENV: true + CI_ENV: true - name: Verify Zshrc Created run: | @@ -65,17 +64,38 @@ jobs: exit 1 fi - # FIX FOR MAC/UBUNTU: - # We must run zsh once to trigger the Zinit git clone inside .zshrc - name: Bootstrap Zinit (Initialize Zsh) - run: zsh -ic "exit" || true + run: | + TERM=xterm-256color zsh -ic "exit" 2>&1 || echo "Note: zsh bootstrap exited non-zero (likely async plugin load — see output above)" - - name: Verify Plugins Installed + - name: Verify Zinit Installed run: | - # Check standard XDG path or legacy path if [ ! -d "${HOME}/.local/share/zinit" ] && [ ! -d "${HOME}/.zinit" ]; then - echo "Error: Zinit directory not found! Plugin installation failed." - exit 1 + echo "Error: Zinit directory not found! Plugin installation failed." + exit 1 + fi + + ZINIT_PLUGINS="${HOME}/.local/share/zinit/plugins" + MISSING=0 + # "dir:binary" pairs — no associative arrays, compatible with bash 3 (macOS) + for spec in \ + "eza-community---eza:eza" \ + "sharkdp---bat:bat" \ + "junegunn---fzf:fzf" \ + "ajeetdsouza---zoxide:zoxide" \ + "starship---starship:starship"; do + dir="${spec%%:*}" + bin="${spec##*:}" + if find "$ZINIT_PLUGINS/$dir" -maxdepth 1 -name "$bin" -type f 2>/dev/null | grep -q .; then + echo "PASS: $bin found in Zinit store" + else + echo "WARN: $bin not found ($ZINIT_PLUGINS/$dir)" + MISSING=$((MISSING + 1)) + fi + done + if [ "$MISSING" -ge 3 ]; then + echo "Error: Too many tools missing from Zinit store" + exit 1 fi # ----------------------------------------------------------------------------- @@ -88,25 +108,23 @@ jobs: strategy: matrix: distro: [archlinux:latest, fedora:latest] - + steps: - uses: actions/checkout@v4 - + - name: Test in Docker run: | docker run --rm -v ${{ github.workspace }}:/workspace -w /workspace ${{ matrix.distro }} /bin/bash -c " - # Set colors for better logging GREEN='\033[0;32m' NC='\033[0m' - + echo -e \"\${GREEN}>>> 1. SETUP: Installing dependencies for ${{ matrix.distro }}...\${NC}\" - + # --- ARCH LINUX --- if [ -f /etc/arch-release ]; then pacman -Sy --noconfirm archlinux-keyring - # Ensure wget is installed to handle the file:// download - pacman -Syu --noconfirm git curl wget unzip sudo which base-devel ca-certificates - + # 'file' required by Zinit ziextract for archive type detection + pacman -Syu --noconfirm git curl wget unzip sudo which base-devel ca-certificates file useradd -m -s /bin/bash tester echo 'tester ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers fi @@ -114,51 +132,60 @@ jobs: # --- FEDORA --- if [ -f /etc/fedora-release ]; then dnf upgrade -y --refresh - dnf install -y git curl wget unzip sudo util-linux-user which findutils dnf-plugins-core ca-certificates - - # Pre-install Eza + # 'file' required by Zinit ziextract — without it every gh-r download + # (bat, fzf, fd, rg, zoxide) fails mid-extract with 'command not found: file' + dnf install -y git curl wget unzip sudo util-linux-user which findutils dnf-plugins-core ca-certificates file dnf install -y eza || (dnf copr enable -y piegames/eza && dnf install -y eza) - useradd -m -s /bin/bash tester echo 'tester ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers fi echo -e \"\${GREEN}>>> 2. PREPARE: Injecting local file path...\${NC}\" - - # 1. Copy the repository files to a temp location so we can modify them freely + cp /workspace/install.sh /tmp/install.sh cp /workspace/.zshrc /tmp/.zshrc - - # 2. USE SED TO REPLACE THE URL - # This changes the URL from 'https://...' to 'file:///tmp/.zshrc' - # Now wget will 'download' the local file instead of going to the internet. sed -i 's|^ZSHRC_URL=.*|ZSHRC_URL=\"file:///tmp/.zshrc\"|' /tmp/install.sh - - # 3. Permissions chmod +x /tmp/install.sh chmod 644 /tmp/.zshrc chown -R tester:tester /tmp echo -e \"\${GREEN}>>> 3. TEST: Running installer as user tester...\${NC}\" - - # Run the modified script in /tmp + su tester -c 'export CI_ENV=true && yes | /tmp/install.sh' - + echo -e \"\${GREEN}>>> 4. VERIFY: Checking results...\${NC}\" - + if [ -f /home/tester/.zshrc ]; then echo 'SUCCESS: .zshrc created' else echo 'FAILURE: .zshrc missing' exit 1 fi - - # Verify Zinit bootstrap - su tester -c 'zsh -ic exit' || true + + su tester -c 'TERM=xterm-256color zsh -ic exit' 2>&1 || echo 'Note: zsh bootstrap exited non-zero' + if [ -d /home/tester/.local/share/zinit ]; then - echo 'SUCCESS: Zinit installed' + echo 'SUCCESS: Zinit installed' else - echo 'FAILURE: Zinit missing' - exit 1 + echo 'FAILURE: Zinit missing' + exit 1 + fi + + echo -e \"\${GREEN}>>> 5. VERIFY: Checking Zinit plugin store...\${NC}\" + ZINIT_PLUGINS=\"/home/tester/.local/share/zinit/plugins\" + MISSING=0 + for spec in \"eza-community---eza:eza\" \"starship---starship:starship\" \"sharkdp---bat:bat\" \"junegunn---fzf:fzf\"; do + dir=\"\${spec%%:*}\" + bin=\"\${spec##*:}\" + if find \"\$ZINIT_PLUGINS/\$dir\" -maxdepth 1 -name \"\$bin\" -type f 2>/dev/null | grep -q .; then + echo \"SUCCESS: \$bin found in Zinit store\" + else + echo \"WARN: \$bin not found (\$ZINIT_PLUGINS/\$dir)\" + MISSING=\$((MISSING + 1)) + fi + done + if [ \"\$MISSING\" -ge 3 ]; then + echo 'FAILURE: Too many tools missing from Zinit store' + exit 1 fi " \ No newline at end of file diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..04e1e47 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,12 @@ +# .shellcheckrc: Shared Shellcheck configuration for Z-Shift +# FIX #12: Centralises ignore rules so CI (ci.yml) and local tests +# (tests/local_test.sh) use identical linting settings without duplication. + +# SC2086: Double-quoting variables — suppressed for flexibility in install_pkg +disable=SC2086 + +# SC2034: Variable appears unused — suppressed for sourced env vars +disable=SC2034 + +# SC1091: Not following sourced files — suppressed for /etc/os-release +disable=SC1091 diff --git a/.zshrc b/.zshrc index 7006fbe..073515c 100644 --- a/.zshrc +++ b/.zshrc @@ -148,6 +148,12 @@ alias zini='zinit' alias zup='zinit self-update && zinit update --parallel && zinit cclear && tldr --update' alias zclean='zinit cclear && zinit delete --clean' +# This avoids a noisy warning if the aliases were never set. + +(( ${+aliases[zi]} )) && unalias zi +(( ${+aliases[zpl]} )) && unalias zpl +(( ${+aliases[zplg]})) && unalias zplg + # --- Eza (The ls replacement) --- if [[ -n "${commands[eza]}" ]]; then alias ls='eza --icons --group-directories-first --git' @@ -200,7 +206,7 @@ zshift-update() { DATE_STAMP=$(date +%Y%m%d_%H%M%S) BACKUP_FILE="$HOME/.zshrc.zshift_${DATE_STAMP}.bak" TEMP_ZSHRC="$(mktemp)" - + UPDATE_URL="${ZSHIFT_CUSTOM_URL:-https://raw.githubusercontent.com/0xdilshan/Z-SHIFT/main/.zshrc}" echo -e "${BLUE}:: Initiating Z-Shift Update...${NC}" @@ -223,6 +229,19 @@ zshift-update() { cp "$HOME/.zshrc" "$BACKUP_FILE" fi + # Warn the user if their current .zshrc differs from the backup, + if [ -f "$BACKUP_FILE" ] && ! diff -q "$HOME/.zshrc" "$BACKUP_FILE" > /dev/null 2>&1; then + echo -e "${YELLOW}!! Warning: Your .zshrc has local modifications.${NC}" + echo -e "${YELLOW} These will be replaced. Move personal config to ~/.zshrc.local to preserve it.${NC}" + echo -ne "${YELLOW} Continue anyway? [y/N]: ${NC}" + read -r REPLY + if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then + echo -e "${BLUE}:: Update cancelled.${NC}" + rm -f "$TEMP_ZSHRC" + return 0 + fi + fi + mv "$TEMP_ZSHRC" "$HOME/.zshrc" echo -e "${GREEN}:: Configuration file updated.${NC}" @@ -241,6 +260,8 @@ zshift-update() { exec zsh } +alias zsu='zshift-update' + # ============================================================================= # 7. BYTE-COMPILATION & LOCAL CUSTOMIZATIONS # ============================================================================= diff --git a/install.sh b/install.sh index ebdb15c..75cb363 100644 --- a/install.sh +++ b/install.sh @@ -5,25 +5,43 @@ # ╚══███╔╝ ██╔════╝██║ ██║██║██╔════╝╚══██╔══╝ # ███╔╝ █████╗███████╗███████║██║█████╗ ██║ # ███╔╝ ╚════╝╚════██║██╔══██║██║██╔══╝ ██║ -# ███████╗ ███████╗██║ ██║██║██║ ██║ -# ╚══════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ +# ███████╗ ███████╗██║ ██║██║██║ ██║ +# ╚══════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ # ============================================================================= # Z-SHIFT: High-Performance Zsh + Starship + Zinit installation script # ============================================================================= -# Exit immediately if a command exits with a non-zero status -set -e +# Removed bare 'set -e' in favour of explicit checks on critical commands +# 'set -e' interacts poorly with subshells and conditional logic. +set -uo pipefail -# --- CONFIGURATION --- -ZSHRC_URL="${ZSHIFT_CUSTOM_URL:-https://raw.githubusercontent.com/0xdilshan/Z-SHIFT/main/.zshrc}" - -# Colors for output +# --- COLORS --- GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' RED='\033[0;31m' -NC='\033[0m' # No Color +NC='\033[0m' + +# --- ROOT CHECK --- +if [ "$EUID" -eq 0 ] && [ "${CI_ENV:-}" != "true" ]; then + echo -e "${RED}Please do not run this script as root or with sudo.${NC}" + echo -e "${YELLOW}The script will prompt for your sudo password when needed.${NC}" + exit 1 +fi + +# --- GLOBAL VARIABLES & CLEANUP TRAP --- +TEMP_DIRS=() + +cleanup() { + for d in "${TEMP_DIRS[@]}"; do + [ -n "$d" ] && [ -d "$d" ] && rm -rf "$d" + done +} +trap cleanup EXIT INT TERM + +# --- CONFIGURATION --- +ZSHRC_URL="${ZSHIFT_CUSTOM_URL:-https://raw.githubusercontent.com/0xdilshan/Z-SHIFT/main/.zshrc}" echo -e "${CYAN}>>> Initiating Z-Shift Environment Deployment...${NC}" @@ -40,33 +58,32 @@ elif [ -f /etc/os-release ]; then OS_TYPE="linux" # shellcheck disable=SC1091 . /etc/os-release - DISTRO=$ID + DISTRO="$ID" fi echo -e "${BLUE}Detected OS: ${OS_TYPE} (${DISTRO})${NC}" -# Helper function to install packages based on distro install_pkg() { local pkgs=("$@") echo -e "${YELLOW}Installing: ${pkgs[*]}...${NC}" case $DISTRO in ubuntu|debian|pop|kali|linuxmint) - sudo apt update -y - sudo apt install -y "${pkgs[@]}" + export DEBIAN_FRONTEND=noninteractive + sudo apt-get update -qq -y > /dev/null + sudo apt-get install -qq -y "${pkgs[@]}" > /dev/null ;; arch|manjaro|endeavouros) - # Use --needed to skip up-to-date packages - sudo pacman -Sy --noconfirm --needed "${pkgs[@]}" + sudo pacman -Sy --quiet --noconfirm --needed "${pkgs[@]}" > /dev/null ;; fedora|rhel|centos) - sudo dnf install -y "${pkgs[@]}" + sudo dnf install -q -y "${pkgs[@]}" > /dev/null ;; opensuse*|suse) - sudo zypper install -y "${pkgs[@]}" + sudo zypper install -q -y "${pkgs[@]}" > /dev/null ;; macos) - brew install "${pkgs[@]}" + brew install -q "${pkgs[@]}" > /dev/null ;; *) echo -e "${RED}Unsupported distribution: $DISTRO${NC}" @@ -80,9 +97,10 @@ install_pkg() { # ============================================================================= if [[ "$OS_TYPE" == "macos" ]]; then if ! command -v brew &> /dev/null; then - echo -e "${YELLOW}Homebrew not found. Installing Homebrew...${NC}" - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - + echo -e "${YELLOW}Homebrew not found. Installing Homebrew (this may take a while)...${NC}" + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" > /dev/null || { + echo -e "${RED}Homebrew installation failed.${NC}"; exit 1 + } if [ -f "/opt/homebrew/bin/brew" ]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [ -f "/usr/local/bin/brew" ]; then @@ -95,29 +113,15 @@ fi # 2. SYSTEM DEPENDENCIES # ============================================================================= echo -e "${YELLOW}Installing base dependencies...${NC}" -COMMON_DEPS="git curl unzip zsh" -if [[ "$OS_TYPE" == "macos" ]]; then - install_pkg git curl wget unzip zsh -else - # Linux specific checks: using 'gnupg' for cross-distro compatibility - install_pkg wget gnupg $COMMON_DEPS -fi - -# ============================================================================= -# 2. SYSTEM DEPENDENCIES -# ============================================================================= -echo -e "${YELLOW}Installing base dependencies...${NC}" -COMMON_DEPS="git curl unzip zsh" +COMMON_DEPS=(git curl unzip zsh) if [[ "$OS_TYPE" == "macos" ]]; then install_pkg git curl wget unzip zsh else - # Linux-specific checks: using 'gnupg' for cross-distro compatibility - install_pkg wget gnupg $COMMON_DEPS + install_pkg wget gnupg "${COMMON_DEPS[@]}" fi - # ============================================================================= # 3. INSTALL STANDALONE TOOLS (Eza) # ============================================================================= @@ -128,23 +132,23 @@ if command -v eza &> /dev/null; then else case "$DISTRO" in ubuntu|debian|pop|kali|linuxmint) + export DEBIAN_FRONTEND=noninteractive sudo mkdir -p /etc/apt/keyrings wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \ - | sudo gpg --dearmor -o /etc/apt/keyrings/gierens.gpg + | sudo gpg --dearmor -o /etc/apt/keyrings/gierens.gpg > /dev/null 2>&1 || true echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main" \ - | sudo tee /etc/apt/sources.list.d/gierens.list + | sudo tee /etc/apt/sources.list.d/gierens.list > /dev/null sudo chmod 644 /etc/apt/keyrings/gierens.gpg \ /etc/apt/sources.list.d/gierens.list - sudo apt update && sudo apt install -y eza + sudo apt-get update -qq -y > /dev/null && sudo apt-get install -qq -y eza > /dev/null ;; fedora) FEDORA_VERSION=$(rpm -E %fedora) - # Fedora 42+ logic (assuming eza isn't in repos yet) if [ "$FEDORA_VERSION" -ge 42 ]; then echo -e "${YELLOW}:: Fedora $FEDORA_VERSION detected. Downloading binary...${NC}" @@ -162,14 +166,15 @@ else FILENAME="eza_${BINARY_ARCH}.tar.gz" URL="https://github.com/eza-community/eza/releases/latest/download/${FILENAME}" - echo -e "${BLUE}Downloading $FILENAME...${NC}" + EZA_TMP=$(mktemp -d) + TEMP_DIRS+=("$EZA_TMP") - # Download and extract in one pipeline to avoid messy temp files - curl -L "$URL" | tar -xz -C /tmp + curl -sSL "$URL" | tar -xz -C "$EZA_TMP" || { + echo -e "${RED}Error: Failed to download/extract eza binary.${NC}"; exit 1 + } - # Move and set permissions - if [ -f "/tmp/eza" ]; then - sudo mv /tmp/eza /usr/local/bin/eza + if [ -f "$EZA_TMP/eza" ]; then + sudo mv "$EZA_TMP/eza" /usr/local/bin/eza sudo chmod +x /usr/local/bin/eza echo -e "${GREEN}:: eza installed successfully to /usr/local/bin/eza${NC}" else @@ -177,9 +182,7 @@ else exit 1 fi else - # Standard repo install for older versions - echo -e "${BLUE}:: Installing eza via DNF...${NC}" - sudo dnf install -y eza + sudo dnf install -q -y eza > /dev/null fi ;; @@ -194,63 +197,46 @@ fi # ============================================================================= echo -e "${YELLOW}Setting up Configuration...${NC}" -# --- Prepare Directories --- -rm -rf ~/.config/eza-themes -git clone --quiet https://github.com/eza-community/eza-themes.git ~/.config/eza-themes -mkdir -p ~/.config/eza -mkdir -p ~/.config +rm -rf "$HOME/.config/eza-themes" +git clone --quiet https://github.com/eza-community/eza-themes.git "$HOME/.config/eza-themes" > /dev/null 2>&1 +mkdir -p "$HOME/.config/eza" +mkdir -p "$HOME/.config" -# --- Install Starship Binary if missing --- -if ! command -v starship &> /dev/null; then +if command -v starship &> /dev/null; then + echo -e "${GREEN}:: Starship is already installed. Skipping.${NC}" +else if [[ "$OS_TYPE" == "macos" ]]; then install_pkg starship else - curl -sS https://starship.rs/install.sh | sh -s -- -y + curl -sS https://starship.rs/install.sh | sh -s -- -y > /dev/null || { + echo -e "${RED}Starship installation failed.${NC}"; exit 1 + } fi fi -# --- Theme Arrays --- STARSHIP_THEMES=( - "gruvbox-rainbow" - "nerd-font-symbols" - "no-nerd-font" - "bracketed-segments" - "plain-text-symbols" - "no-runtime-versions" - "no-empty-icons" - "pure-preset" - "pastel-powerline" - "tokyo-night" - "jetpack" - "catppuccin-powerline" + "gruvbox-rainbow" "nerd-font-symbols" "no-nerd-font" + "bracketed-segments" "plain-text-symbols" "no-runtime-versions" + "no-empty-icons" "pure-preset" "pastel-powerline" + "tokyo-night" "jetpack" "catppuccin-powerline" ) EZA_THEMES=( - "gruvbox-dark.yml" - "black.yml" - "catppuccin-frappe.yml" - "catppuccin-latte.yml" - "catppuccin-macchiato.yml" - "catppuccin-mocha.yml" - "default.yml" - "dracula.yml" - "frosty.yml" - "gruvbox-light.yml" - "one_dark.yml" - "rose-pine-dawn.yml" - "rose-pine-moon.yml" - "rose-pine.yml" - "solarized-dark.yml" - "tokyonight.yml" - "white.yml" + "gruvbox-dark.yml" "black.yml" "catppuccin-frappe.yml" + "catppuccin-latte.yml" "catppuccin-macchiato.yml" "catppuccin-mocha.yml" + "default.yml" "dracula.yml" "frosty.yml" "gruvbox-light.yml" + "one_dark.yml" "rose-pine-dawn.yml" "rose-pine-moon.yml" + "rose-pine.yml" "solarized-dark.yml" "tokyonight.yml" "white.yml" ) -# --- Interactive Menu Logic --- -# Defaults SELECTED_STARSHIP="gruvbox-rainbow" SELECTED_EZA="gruvbox-dark.yml" -if [ "$CI_ENV" != "true" ]; then +if [ "${CI_ENV:-}" != "true" ]; then + if [ ! -t 0 ]; then + exec < /dev/tty + fi + echo -e "\n${CYAN}::: THEME SELECTION :::${NC}" echo "1) Default (Starship: Gruvbox-Rainbow | Eza: Gruvbox-Dark)" echo "2) Custom Selection" @@ -258,7 +244,6 @@ if [ "$CI_ENV" != "true" ]; then read -r THEME_OPT if [[ "$THEME_OPT" == "2" ]]; then - # 1. Select Starship Theme echo -e "\n${BLUE}:: Select Starship Prompt Theme ::${NC}" PS3="Enter number (1-${#STARSHIP_THEMES[@]}): " select s_theme in "${STARSHIP_THEMES[@]}"; do @@ -270,7 +255,6 @@ if [ "$CI_ENV" != "true" ]; then fi done - # 2. Select Eza Theme echo -e "\n${BLUE}:: Select Eza (ls) Theme ::${NC}" PS3="Enter number (1-${#EZA_THEMES[@]}): " select e_theme in "${EZA_THEMES[@]}"; do @@ -286,53 +270,73 @@ if [ "$CI_ENV" != "true" ]; then fi fi -# --- Apply Configuration --- echo -e "${YELLOW}Applying Starship Preset: ${SELECTED_STARSHIP}...${NC}" -starship preset "$SELECTED_STARSHIP" -o ~/.config/starship.toml || \ +starship preset "$SELECTED_STARSHIP" -o "$HOME/.config/starship.toml" > /dev/null 2>&1 || \ echo -e "${RED}Warning: Failed to load preset '$SELECTED_STARSHIP'. Check starship version.${NC}" echo -e "${YELLOW}Applying Eza Theme: ${SELECTED_EZA}...${NC}" ln -sf "$HOME/.config/eza-themes/themes/${SELECTED_EZA}" "$HOME/.config/eza/theme.yml" +echo -e "${YELLOW}Optimizing Zsh startup...${NC}" +if ! grep -q "skip_global_compinit=1" "$HOME/.zshenv" 2>/dev/null; then + echo 'skip_global_compinit=1' >> "$HOME/.zshenv" +fi + # ============================================================================= # 5. FONTS (FiraCode Nerd Font) # ============================================================================= -if [ "$CI_ENV" = "true" ]; then +if [ "${CI_ENV:-}" = "true" ]; then echo -e "${YELLOW}>> CI Environment detected. Skipping Font Installation.${NC}" else - echo -e "\n${CYAN}::: FONT INSTALLATION :::${NC}" - echo -ne "${YELLOW}Install FiraCode Nerd Font? [Y/n] (default: Y): ${NC}" - read -r FONT_OPT + if [ ! -t 0 ]; then + exec < /dev/tty + fi + + if [[ "$OS_TYPE" == "macos" ]]; then + FONT_DIR="$HOME/Library/Fonts" + else + FONT_DIR="$HOME/.local/share/fonts" + fi - # Check for 'n' or 'N'. All other inputs (including empty Enter) trigger installation. - if [[ "$FONT_OPT" =~ ^[Nn]$ ]]; then - echo -e "${BLUE}>> Skipping Font Installation.${NC}" + if ls "$FONT_DIR"/FiraCode* >/dev/null 2>&1; then + echo -e "${GREEN}:: FiraCode Nerd Font already installed. Skipping.${NC}" else - echo -e "${YELLOW}Installing FiraCode Nerd Font...${NC}" - - # Determine Font Directory - if [[ "$OS_TYPE" == "macos" ]]; then - FONT_DIR="$HOME/Library/Fonts" + echo -e "\n${CYAN}::: FONT INSTALLATION :::${NC}" + echo -ne "${YELLOW}Install FiraCode Nerd Font? [Y/n] (default: Y): ${NC}" + read -r FONT_OPT + + if [[ "$FONT_OPT" =~ ^[Nn]$ ]]; then + echo -e "${BLUE}>> Skipping Font Installation.${NC}" else - FONT_DIR="$HOME/.local/share/fonts" - fi - - mkdir -p "$FONT_DIR" - TEMP_DIR=$(mktemp -d) - - echo -e "${BLUE}Downloading FiraCode.zip...${NC}" - - wget -q --show-progress -O "$TEMP_DIR/FiraCode.zip" "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/FiraCode.zip" - unzip -q "$TEMP_DIR/FiraCode.zip" -d "$TEMP_DIR" - find "$TEMP_DIR" -name "*.ttf" -exec mv -f {} "$FONT_DIR/" \; 2>/dev/null || true - rm -rf "$TEMP_DIR" - - # Refresh Cache (Linux only) - if [[ "$OS_TYPE" == "linux" ]] && command -v fc-cache &> /dev/null; then - echo -e "${BLUE}Updating font cache...${NC}" - fc-cache -f "$FONT_DIR" + echo -e "${YELLOW}Installing FiraCode Nerd Font...${NC}" + + mkdir -p "$FONT_DIR" + + FONT_VER=$(curl -fsSL "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" \ + | grep '"tag_name"' | cut -d'"' -f4) + if [ -z "$FONT_VER" ]; then + echo -e "${YELLOW}Warning: Could not determine latest font version. Falling back to v3.4.0.${NC}" + FONT_VER="v3.4.0" + fi + + FONT_TMP=$(mktemp -d) + TEMP_DIRS+=("$FONT_TMP") + + wget -q -O "$FONT_TMP/FiraCode.zip" \ + "https://github.com/ryanoasis/nerd-fonts/releases/download/${FONT_VER}/FiraCode.zip" || { + echo -e "${RED}Font download failed. Skipping.${NC}" + } + + if [ -f "$FONT_TMP/FiraCode.zip" ]; then + unzip -q "$FONT_TMP/FiraCode.zip" -d "$FONT_TMP" + find "$FONT_TMP" -name "*.ttf" -exec mv -f {} "$FONT_DIR/" \; 2>/dev/null || true + + if [[ "$OS_TYPE" == "linux" ]] && command -v fc-cache &> /dev/null; then + fc-cache -f -q "$FONT_DIR" > /dev/null 2>&1 + fi + echo -e "${GREEN}✔ Font installation complete (${FONT_VER}).${NC}" + fi fi - echo -e "${GREEN}✔ Font installation complete.${NC}" fi fi @@ -340,36 +344,49 @@ fi # 6. DOWNLOAD .ZSHRC # ============================================================================= echo -e "${YELLOW}Downloading .zshrc from GitHub...${NC}" -[ -f ~/.zshrc ] && mv ~/.zshrc ~/.zshrc.bak +[ -f "$HOME/.zshrc" ] && mv "$HOME/.zshrc" "$HOME/.zshrc.bak" -if curl -fsSL -o ~/.zshrc "$ZSHRC_URL"; then +if curl -fsSL -o "$HOME/.zshrc" "$ZSHRC_URL"; then echo -e "${GREEN}Downloaded .zshrc successfully.${NC}" else echo -e "${RED}Failed to download .zshrc!${NC}" - [ -f ~/.zshrc.bak ] && mv ~/.zshrc.bak ~/.zshrc + [ -f "$HOME/.zshrc.bak" ] && mv "$HOME/.zshrc.bak" "$HOME/.zshrc" exit 1 fi # ============================================================================= # 7. FINALIZE # ============================================================================= -ZSH_PATH=$(which zsh) +ZSH_PATH=$(command -v zsh || true) -if [ "$CI_ENV" = "true" ]; then +if [ -z "$ZSH_PATH" ]; then + echo -e "${RED}Error: Zsh binary not found. Please verify the installation.${NC}" + exit 1 +fi + +if [ "${CI_ENV:-}" = "true" ]; then echo -e "\n${GREEN}✔ CI Environment detected. Installation Verified!${NC}" exit 0 fi if ! grep -q "$ZSH_PATH" /etc/shells; then - echo "$ZSH_PATH" | sudo tee -a /etc/shells + echo "$ZSH_PATH" | sudo tee -a /etc/shells > /dev/null fi if [ "$SHELL" != "$ZSH_PATH" ]; then if [[ "$OS_TYPE" == "macos" ]]; then - chsh -s "$ZSH_PATH" + if ! chsh -s "$ZSH_PATH" 2>&1; then + echo -e "${YELLOW}Warning: Could not set Zsh as default shell automatically.${NC}" + echo -e "${YELLOW}Run manually: chsh -s ${ZSH_PATH}${NC}" + fi else - sudo usermod --shell "$ZSH_PATH" "$USER" || chsh -s "$ZSH_PATH" + if ! sudo usermod --shell "$ZSH_PATH" "$USER" > /dev/null 2>&1; then + if ! chsh -s "$ZSH_PATH"; then + echo -e "${YELLOW}Warning: Could not set Zsh as default shell automatically.${NC}" + echo -e "${YELLOW}Run manually: chsh -s ${ZSH_PATH}${NC}" + fi + fi fi fi -echo -e "\n${GREEN}✔ Z-Shift Installation Complete!${NC}" \ No newline at end of file +echo -e "\n${GREEN}✔ Z-Shift Installation Complete! Please restart your terminal.${NC}" \ No newline at end of file diff --git a/tests/local_test.sh b/tests/local_test.sh index cece7ef..e3d9dc3 100644 --- a/tests/local_test.sh +++ b/tests/local_test.sh @@ -1,17 +1,19 @@ #!/bin/bash # tests/local_test.sh -# Colors +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' -echo "Running Z-Shift Local Tests..." +echo "Running Z-Shift Local Tests (repo root: $REPO_ROOT)..." -# Check if files exist -REQUIRED_FILES=("install.sh" ".zshrc" "README.md") +# Check required files exist +REQUIRED_FILES=("install.sh" ".zshrc" "README.md" "theme.sh" "uninstall.sh") for file in "${REQUIRED_FILES[@]}"; do - if [ -f "../$file" ] || [ -f "$file" ]; then + if [ -f "$REPO_ROOT/$file" ]; then echo -e "[${GREEN}PASS${NC}] File $file exists" else echo -e "[${RED}FAIL${NC}] File $file missing" @@ -19,14 +21,9 @@ for file in "${REQUIRED_FILES[@]}"; do fi done -# Syntax Check (Zsh) +# Zsh syntax check if command -v zsh >/dev/null; then - # We look for .zshrc in parent dir or current dir - ZSHRC_PATH="" - [ -f ".zshrc" ] && ZSHRC_PATH=".zshrc" - [ -f "../.zshrc" ] && ZSHRC_PATH="../.zshrc" - - if zsh -n "$ZSHRC_PATH"; then + if zsh -n "$REPO_ROOT/.zshrc"; then echo -e "[${GREEN}PASS${NC}] .zshrc syntax is valid" else echo -e "[${RED}FAIL${NC}] .zshrc contains syntax errors" @@ -37,18 +34,18 @@ else fi # Shellcheck (Bash) +# from the repo root automatically, keeping rules in sync with CI. if command -v shellcheck >/dev/null; then - INSTALL_PATH="" - [ -f "install.sh" ] && INSTALL_PATH="install.sh" - [ -f "../install.sh" ] && INSTALL_PATH="../install.sh" - - # We exclude warning SC2181 (checking $? directly) as it's common in install scripts - if shellcheck -e SC2181 "$INSTALL_PATH"; then - echo -e "[${GREEN}PASS${NC}] install.sh passed shellcheck" - else - echo -e "[${RED}FAIL${NC}] install.sh failed shellcheck" - exit 1 - fi + FAILED=0 + for script in install.sh theme.sh uninstall.sh; do + if shellcheck "$REPO_ROOT/$script"; then + echo -e "[${GREEN}PASS${NC}] $script passed shellcheck" + else + echo -e "[${RED}FAIL${NC}] $script failed shellcheck" + FAILED=1 + fi + done + [ "$FAILED" -eq 1 ] && exit 1 else echo -e "[${RED}WARN${NC}] shellcheck not installed, skipping linting" fi diff --git a/uninstall.sh b/uninstall.sh index 926ddbe..807e214 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,21 +1,36 @@ #!/bin/bash # ============================================================================= -# Z-SHIFT: Cleanup & Restoration Script (Final Hardened Version) +# Z-SHIFT: Cleanup & Restoration Script # ============================================================================= -# Colors for output -RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' +CYAN='\033[0;36m' +RED='\033[0;31m' NC='\033[0m' -echo -e "${BLUE}>>> Starting Z-Shift Removal...${NC}" +echo -e "${CYAN}>>> Initiating Z-Shift Environment Removal...${NC}" + +# ============================================================================= +# 0. OS & PACKAGE MANAGER DETECTION +# ============================================================================= +OS_TYPE="unknown" +DISTRO="unknown" + +if [[ "$OSTYPE" == "darwin"* ]]; then + OS_TYPE="macos" + DISTRO="macos" +elif [ -f /etc/os-release ]; then + OS_TYPE="linux" + # shellcheck disable=SC1091 + . /etc/os-release + DISTRO="$ID" +fi + +echo -e "${BLUE}Detected OS: ${OS_TYPE} (${DISTRO})${NC}" -# ----------------------------------------------------------------------------- -# Helper: Detect current shell safely -# ----------------------------------------------------------------------------- get_current_shell() { if command -v getent >/dev/null 2>&1; then getent passwd "$USER" | cut -d: -f7 @@ -27,7 +42,7 @@ get_current_shell() { # ----------------------------------------------------------------------------- # 1. RESTORE PREVIOUS SHELL # ----------------------------------------------------------------------------- -if [[ "$OSTYPE" != "darwin"* ]]; then +if [[ "$OS_TYPE" != "macos" ]]; then CURRENT_SHELL=$(get_current_shell) if [[ "$CURRENT_SHELL" == *"zsh"* ]]; then @@ -35,13 +50,16 @@ if [[ "$OSTYPE" != "darwin"* ]]; then if [ -n "$BASH_PATH" ]; then echo -e "${YELLOW}:: Reverting default shell to Bash...${NC}" - # Try usermod first, fallback to chsh - sudo usermod --shell "$BASH_PATH" "$USER" 2>/dev/null || chsh -s "$BASH_PATH" - echo -e "${GREEN}✔ Default shell reverted to $BASH_PATH.${NC}" + if sudo usermod --shell "$BASH_PATH" "$USER" 2>/dev/null || chsh -s "$BASH_PATH"; then + echo -e "${GREEN}✔ Default shell reverted to $BASH_PATH.${NC}" + else + echo -e "${RED}✘ Failed to revert default shell. You may need to do this manually.${NC}" + fi + else + echo -e "${RED}✘ Bash not found in PATH. Cannot automatically revert shell.${NC}" fi fi else - # On Mac, Zsh is the system default. We leave it as is. echo -e "${GREEN}:: macOS detected. Keeping Zsh as system default.${NC}" fi @@ -50,7 +68,6 @@ fi # ----------------------------------------------------------------------------- echo -e "${YELLOW}:: Cleaning up configuration files...${NC}" -# Restore .zshrc backup if available, otherwise just delete the Z-Shift one if [ -f "$HOME/.zshrc.bak" ]; then mv "$HOME/.zshrc.bak" "$HOME/.zshrc" echo -e "${GREEN}✔ Restored previous .zshrc from backup.${NC}" @@ -59,40 +76,77 @@ else echo -e "${YELLOW}:: No backup found. Removed Z-Shift .zshrc.${NC}" fi -# Remove Z-Shift specific config files +if [ -f "$HOME/.zshenv" ]; then + if grep -q "skip_global_compinit=1" "$HOME/.zshenv"; then + grep -v "skip_global_compinit=1" "$HOME/.zshenv" > "$HOME/.zshenv.tmp" && mv "$HOME/.zshenv.tmp" "$HOME/.zshenv" + echo -e "${GREEN}✔ Cleaned up ~/.zshenv.${NC}" + fi + [ ! -s "$HOME/.zshenv" ] && rm -f "$HOME/.zshenv" +fi + rm -f "$HOME/.config/starship.toml" rm -rf "$HOME/.config/eza" rm -rf "$HOME/.config/eza-themes" -# Remove Zinit plugins and data ZINIT_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zinit" -rm -rf "$ZINIT_DIR" -echo -e "${GREEN}✔ Configuration and plugin data removed.${NC}" +if [ -d "$ZINIT_DIR" ]; then + rm -rf "$ZINIT_DIR" + echo -e "${GREEN}✔ Plugin data removed (includes Zinit-managed tool binaries: bat, fd, fzf, rg, zoxide, eza).${NC}" +fi # ----------------------------------------------------------------------------- # 3. OPTIONAL: REMOVE BINARIES # ----------------------------------------------------------------------------- -if [ "$CI_ENV" != "true" ]; then - echo -ne "${CYAN}Do you want to uninstall the CLI tools (eza, bat, fd, etc.)? [y/N]: ${NC}" - read -r REPLY - if [[ "$REPLY" =~ ^[Yy]$ ]]; then +if [ "${CI_ENV:-}" != "true" ]; then + if [ ! -t 0 ]; then exec < /dev/tty; fi + + echo -ne "${CYAN}Do you want to uninstall the system CLI tools (eza, starship)? [y/N]: ${NC}" + read -r REPLY_BINS + if [[ "$REPLY_BINS" =~ ^[Yy]$ ]]; then echo -e "${YELLOW}:: Removing binaries...${NC}" - if [[ "$OSTYPE" == "darwin"* ]]; then - brew uninstall eza bat fd fzf ripgrep tealdeer zoxide starship 2>/dev/null || true - elif [ -f /etc/debian_version ]; then - sudo apt remove -y eza bat fd-find fzf ripgrep starship 2>/dev/null || true - elif [ -f /etc/fedora-release ]; then - sudo dnf remove -y eza bat fd fzf ripgrep starship 2>/dev/null || true - elif [ -f /etc/arch-release ]; then - sudo pacman -Rs --noconfirm eza bat fd fzf ripgrep starship 2>/dev/null || true - fi + case $DISTRO in + ubuntu|debian|pop|kali|linuxmint) + export DEBIAN_FRONTEND=noninteractive + sudo apt-get remove -qq -y eza starship > /dev/null 2>&1 || echo -e "${RED}! Some packages could not be removed via APT.${NC}" + sudo rm -f /etc/apt/sources.list.d/gierens.list + sudo rm -f /etc/apt/keyrings/gierens.gpg + sudo apt-get update -qq -y > /dev/null 2>&1 || true + ;; + arch|manjaro|endeavouros) + sudo pacman -Rs --noconfirm eza starship > /dev/null 2>&1 || echo -e "${RED}! Some packages could not be removed via Pacman.${NC}" + ;; + fedora|rhel|centos) + # Remove the manual binary first; only attempt dnf for older Fedora releases + # where dnf was actually used — this avoids a confusing "not installed" error. + FEDORA_VERSION=$(rpm -E %fedora 2>/dev/null || echo "0") + if [ "$FEDORA_VERSION" -ge 42 ]; then + if [ -f /usr/local/bin/eza ]; then + sudo rm -f /usr/local/bin/eza + echo -e "${GREEN}✔ Removed manually-installed eza binary.${NC}" + fi + # Starship may still be in /usr/local/bin from its install script + if [ -f /usr/local/bin/starship ]; then + sudo rm -f /usr/local/bin/starship + echo -e "${GREEN}✔ Removed starship binary.${NC}" + fi + else + sudo dnf remove -q -y eza starship > /dev/null 2>&1 || echo -e "${RED}! Some packages could not be removed via DNF.${NC}" + fi + ;; + macos) + brew uninstall -q eza starship > /dev/null 2>&1 || echo -e "${RED}! Some packages could not be removed via Brew.${NC}" + ;; + esac + + # Catch-all for any remaining manual installs in /usr/local/bin + # (e.g. starship installed via its curl script on non-Fedora distros) + for bin in /usr/local/bin/eza /usr/local/bin/starship; do + if [ -f "$bin" ]; then + sudo rm -f "$bin" && echo -e "${GREEN}✔ Removed $bin${NC}" || echo -e "${RED}✘ Failed to remove $bin${NC}" + fi + done - # IMPORTANT: Remove manual script-installed binaries - # These are often missed by package managers - [ -f /usr/local/bin/eza ] && sudo rm -f /usr/local/bin/eza - [ -f /usr/local/bin/starship ] && sudo rm -f /usr/local/bin/starship - echo -e "${GREEN}✔ Binary cleanup complete.${NC}" fi fi @@ -100,21 +154,24 @@ fi # ----------------------------------------------------------------------------- # 4. OPTIONAL: REMOVE FONTS # ----------------------------------------------------------------------------- -if [ "$CI_ENV" != "true" ]; then +if [ "${CI_ENV:-}" != "true" ]; then echo -ne "${CYAN}Do you want to remove FiraCode Nerd Fonts? [y/N]: ${NC}" - read -r REPLY - if [[ "$REPLY" =~ ^[Yy]$ ]]; then + read -r REPLY_FONTS + if [[ "$REPLY_FONTS" =~ ^[Yy]$ ]]; then echo -e "${YELLOW}:: Removing fonts...${NC}" - FONT_DIR="$HOME/.local/share/fonts" - [[ "$OSTYPE" == "darwin"* ]] && FONT_DIR="$HOME/Library/Fonts" - rm -f "$FONT_DIR"/FiraCode* 2>/dev/null - - # Refresh font cache if the tool exists - if command -v fc-cache >/dev/null 2>&1; then - fc-cache -f "$FONT_DIR" || true + FONT_DIR="$HOME/.local/share/fonts" + [[ "$OS_TYPE" == "macos" ]] && FONT_DIR="$HOME/Library/Fonts" + + if ls "$FONT_DIR"/FiraCode* >/dev/null 2>&1; then + rm -f "$FONT_DIR"/FiraCode* + if [[ "$OS_TYPE" == "linux" ]] && command -v fc-cache >/dev/null 2>&1; then + fc-cache -f -q "$FONT_DIR" > /dev/null 2>&1 || true + fi + echo -e "${GREEN}✔ Fonts removed.${NC}" + else + echo -e "${YELLOW}:: No FiraCode fonts found in $FONT_DIR.${NC}" fi - echo -e "${GREEN}✔ Fonts removed.${NC}" fi fi