Files
ProjectAGiPrompt/35-黑苹果DELL/3-WSL系统优化/wsl-dev-setup.sh
2026-06-24 17:20:42 +08:00

1959 lines
77 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#===============================================================================
#
# WSL2 Development Environment Setup Script
#
# Description : One-click setup to transform WSL2 Ubuntu into a full
# development workstation with modern terminal, languages,
# and CLI tools.
#
# Supports : Ubuntu 20.04 / 22.04 / 24.04 / 26.04
# Terminal : Zsh + Zinit (plugin manager) + Powerlevel10k (prompt)
# Languages : Go (latest) + Node.js LTS (via fnm)
# Proxy : Clash Verge TUN mode compatible
#
# Usage : sudo bash wsl-dev-setup.sh [OPTIONS]
# sudo bash wsl-dev-setup.sh --help
#
# Version : 1.0.0
# License : MIT
#
#===============================================================================
set -uo pipefail
#===============================================================================
# Global Configuration
#===============================================================================
readonly SCRIPT_VERSION="1.0.0"
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_START_TIME="$(date +%s)"
# --- Network / Proxy ---
WINDOWS_HOST_IP="${WINDOWS_HOST_IP:-192.168.1.20}"
CLASH_PROXY_PORT="${CLASH_PROXY_PORT:-7890}"
ENABLE_PROXY="${ENABLE_PROXY:-true}"
# --- User ---
INSTALL_USER="${SUDO_USER:-$(whoami)}"
USER_HOME="$(eval echo ~"${INSTALL_USER}" 2>/dev/null || echo "/home/${INSTALL_USER}")"
# --- Installation Versions ---
GO_VERSION="${GO_VERSION:-latest}"
NODE_VERSION="${NODE_VERSION:-lts}"
NERD_FONT="${NERD_FONT:-JetBrainsMono}"
# --- APT Mirror ---
APT_MIRROR="${APT_MIRROR:-tuna}" # tuna | ustc | aliyun | default
# --- Detected at runtime ---
UBUNTU_VERSION=""
UBUNTU_CODENAME=""
ARCH=""
GO_ARCH=""
IS_WSL=false
WIN_USER=""
# --- Module control ---
ALL_MODULES="proxy mirror locale base-tools zsh fonts modern-cli golang nodejs"
SKIP_MODULES=""
ONLY_MODULES=""
FAILED_MODULES=""
#===============================================================================
# Colors & Logging
#===============================================================================
if [[ -t 1 ]]; then
readonly C_RED='\033[0;31m'
readonly C_GREEN='\033[0;32m'
readonly C_YELLOW='\033[0;33m'
readonly C_BLUE='\033[0;34m'
readonly C_MAGENTA='\033[0;35m'
readonly C_CYAN='\033[0;36m'
readonly C_WHITE='\033[1;37m'
readonly C_BOLD='\033[1m'
readonly C_DIM='\033[2m'
readonly C_NC='\033[0m'
else
readonly C_RED='' C_GREEN='' C_YELLOW='' C_BLUE=''
readonly C_MAGENTA='' C_CYAN='' C_WHITE='' C_BOLD='' C_DIM='' C_NC=''
fi
log_info() { echo -e "${C_BLUE}[INFO]${C_NC} $*"; }
log_success() { echo -e "${C_GREEN}[ OK ]${C_NC} $*"; }
log_warn() { echo -e "${C_YELLOW}[WARN]${C_NC} $*"; }
log_error() { echo -e "${C_RED}[ERROR]${C_NC} $*"; }
log_step() { echo -e "${C_MAGENTA}${C_NC} $*"; }
section_header() {
local title="$1"
local width=58
echo ""
echo -e "${C_CYAN}$(printf '─%.0s' $(seq 1 $width))${C_NC}"
printf "${C_CYAN}${C_NC} %-$((width - 2))s${C_CYAN}${C_NC}\n" "$title"
echo -e "${C_CYAN}$(printf '─%.0s' $(seq 1 $width))${C_NC}"
echo ""
}
print_banner() {
echo ""
echo -e "${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_NC}"
echo -e "${C_CYAN}${C_NC} ${C_BOLD}🚀 WSL2 Development Environment Setup${C_NC} v${SCRIPT_VERSION} ${C_CYAN}${C_NC}"
echo -e "${C_CYAN}${C_NC} ${C_DIM}Zinit + Powerlevel10k · Go · Node.js · Modern CLI${C_NC} ${C_CYAN}${C_NC}"
echo -e "${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_NC}"
echo ""
}
#===============================================================================
# Usage / Help
#===============================================================================
print_usage() {
cat << USAGE
${C_BOLD}WSL2 Development Environment Setup Script${C_NC} v${SCRIPT_VERSION}
${C_BOLD}USAGE:${C_NC}
sudo bash ${SCRIPT_NAME} [OPTIONS]
${C_BOLD}OPTIONS:${C_NC}
-h, --help Show this help message
-v, --version Show script version
--only <modules> Only run specified modules (comma-separated)
--skip <modules> Skip specified modules (comma-separated)
--no-proxy Disable proxy configuration
--proxy-ip <ip> Windows host IP (default: 192.168.1.20)
--proxy-port <port> Clash proxy port (default: 7890)
--mirror <name> APT mirror: tuna|ustc|aliyun|default (default: tuna)
--go-version <ver> Go version (default: latest)
--node-version <ver> Node.js version (default: lts)
--font <name> Nerd Font name (default: JetBrainsMono)
--dry-run Show what would be done without executing
${C_BOLD}MODULES:${C_NC}
proxy Proxy configuration (Clash Verge)
mirror APT mirror source configuration
locale Locale & UTF-8 setup
base-tools Essential system tools
zsh Zsh + Zinit + Powerlevel10k
fonts Nerd Font installation
modern-cli Modern CLI replacements (bat, eza, fzf, etc.)
golang Go programming language
nodejs Node.js via fnm
${C_BOLD}EXAMPLES:${C_NC}
# Full installation (recommended)
sudo bash ${SCRIPT_NAME}
# Install only Go and Node.js
sudo bash ${SCRIPT_NAME} --only golang,nodejs
# Skip fonts and modern CLI tools
sudo bash ${SCRIPT_NAME} --skip fonts,modern-cli
# Use USTC mirror with custom proxy IP
sudo bash ${SCRIPT_NAME} --mirror ustc --proxy-ip 192.168.1.100
${C_BOLD}ENVIRONMENT VARIABLES:${C_NC}
WINDOWS_HOST_IP Windows host IP address
CLASH_PROXY_PORT Clash proxy port
ENABLE_PROXY Enable proxy (true/false)
GO_VERSION Go version to install
NODE_VERSION Node.js version to install
APT_MIRROR APT mirror name
USAGE
}
#===============================================================================
# Argument Parsing
#===============================================================================
DRY_RUN=false
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) print_usage; exit 0 ;;
-v|--version) echo "${SCRIPT_NAME} v${SCRIPT_VERSION}"; exit 0 ;;
--only) ONLY_MODULES="${2//,/ }"; shift 2 ;;
--skip) SKIP_MODULES="${2//,/ }"; shift 2 ;;
--no-proxy) ENABLE_PROXY="false"; shift ;;
--proxy-ip) WINDOWS_HOST_IP="$2"; shift 2 ;;
--proxy-port) CLASH_PROXY_PORT="$2"; shift 2 ;;
--mirror) APT_MIRROR="$2"; shift 2 ;;
--go-version) GO_VERSION="$2"; shift 2 ;;
--node-version) NODE_VERSION="$2"; shift 2 ;;
--font) NERD_FONT="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
*)
log_error "Unknown option: $1"
print_usage
exit 1
;;
esac
done
}
#===============================================================================
# Utility Functions
#===============================================================================
# Check if a command exists
check_cmd() {
command -v "$1" &>/dev/null
}
# Run command as the installation user (not root)
run_as_user() {
local cmd="$1"
if [[ "$(id -u)" -eq 0 ]] && [[ "${INSTALL_USER}" != "root" ]]; then
su "${INSTALL_USER}" -s /bin/bash -c "
export HOME='${USER_HOME}'
export http_proxy='${http_proxy:-}'
export https_proxy='${https_proxy:-}'
export HTTP_PROXY='${HTTP_PROXY:-}'
export HTTPS_PROXY='${HTTPS_PROXY:-}'
export no_proxy='${no_proxy:-}'
export NO_PROXY='${NO_PROXY:-}'
export PATH='${PATH}'
cd '${USER_HOME}'
${cmd}
"
else
eval "${cmd}"
fi
}
# Backup a file before modification
backup_file() {
local file="$1"
if [[ -f "$file" ]]; then
local backup="${file}.bak.$(date +%Y%m%d%H%M%S)"
cp "$file" "$backup"
log_step "Backed up: $(basename "$file")$(basename "$backup")"
fi
}
# Add a block to a shell config file (idempotent)
add_to_rc() {
local file="$1"
local marker="$2"
local content="$3"
# Create file if it doesn't exist
if [[ ! -f "$file" ]]; then
touch "$file"
chown "${INSTALL_USER}:${INSTALL_USER}" "$file" 2>/dev/null || true
fi
# Remove old block if exists, then add new
if grep -q "# >>> ${marker} >>>" "$file" 2>/dev/null; then
# Remove old block
sed -i "/# >>> ${marker} >>>/,/# <<< ${marker} <<</d" "$file"
fi
{
echo ""
echo "# >>> ${marker} >>>"
echo "${content}"
echo "# <<< ${marker} <<<"
} >> "$file"
}
# Compare versions: returns 0 if $1 >= $2
version_gte() {
[[ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" == "$2" ]]
}
# Ensure required commands are available
ensure_deps() {
local missing=()
local check_cmd_name
for dep in "$@"; do
check_cmd_name="$dep"
if [[ "$dep" == "fontconfig" ]]; then
check_cmd_name="fc-cache"
fi
if ! check_cmd "$check_cmd_name"; then
missing+=("$dep")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_step "Installing dependencies: ${missing[*]}"
if ! apt-get install -y -qq "${missing[@]}" >/dev/null 2>&1; then
log_error "Failed to install dependencies: ${missing[*]}"
return 1
fi
# Double check if dependencies were successfully installed
local still_missing=()
for dep in "${missing[@]}"; do
check_cmd_name="$dep"
if [[ "$dep" == "fontconfig" ]]; then
check_cmd_name="fc-cache"
fi
if ! check_cmd "$check_cmd_name"; then
still_missing+=("$dep")
fi
done
if [[ ${#still_missing[@]} -gt 0 ]]; then
log_error "Dependencies still missing after installation attempt: ${still_missing[*]}"
return 1
fi
fi
return 0
}
# Download a GitHub release asset
download_github_asset() {
local repo="$1"
local pattern="$2"
local output="$3"
ensure_deps curl jq || return 1
local api_url="https://api.github.com/repos/${repo}/releases/latest"
local url
url=$(curl -sL --connect-timeout 20 "${api_url}" \
| jq -r --arg pat "${pattern}" '.assets[] | select(.name | test($pat; "i")) | .browser_download_url' \
| head -1)
if [[ -z "$url" || "$url" == "null" ]]; then
log_error "Failed to find GitHub asset: ${repo} matching '${pattern}'"
return 1
fi
log_step "Downloading: $(basename "$url")"
curl -fsSL --connect-timeout 30 --retry 3 -L "$url" -o "$output"
}
# Install a .deb package from GitHub releases
install_github_deb() {
local repo="$1"
local pattern="$2"
local tmp_deb
tmp_deb=$(mktemp /tmp/wsl-setup-XXXXXX.deb)
download_github_asset "$repo" "$pattern" "$tmp_deb" || return 1
dpkg -i "$tmp_deb" >/dev/null 2>&1 || apt-get install -f -y -qq >/dev/null 2>&1
rm -f "$tmp_deb"
}
# Install a binary from a GitHub release tarball
install_github_tar_binary() {
local repo="$1"
local pattern="$2"
local binary_name="$3"
local tmp_dir
tmp_dir=$(mktemp -d /tmp/wsl-setup-XXXXXX)
local tmp_file="${tmp_dir}/download.tar.gz"
download_github_asset "$repo" "$pattern" "$tmp_file" || { rm -rf "$tmp_dir"; return 1; }
tar -xzf "$tmp_file" -C "$tmp_dir" 2>/dev/null
local found
found=$(find "$tmp_dir" -name "$binary_name" -type f 2>/dev/null | head -1)
if [[ -n "$found" ]]; then
install -m 755 "$found" "/usr/local/bin/${binary_name}"
else
log_error "Binary '${binary_name}' not found in archive"
rm -rf "$tmp_dir"
return 1
fi
rm -rf "$tmp_dir"
}
# Check if a module should run
should_run_module() {
local module="$1"
if [[ -n "$ONLY_MODULES" ]]; then
[[ " $ONLY_MODULES " == *" $module "* ]]
else
[[ " $SKIP_MODULES " != *" $module "* ]]
fi
}
#===============================================================================
# System Detection
#===============================================================================
detect_system() {
section_header "🖥️ System Detection"
# --- OS Detection ---
if [[ ! -f /etc/os-release ]]; then
log_error "/etc/os-release not found — is this Ubuntu?"
exit 1
fi
# shellcheck source=/dev/null
source /etc/os-release
UBUNTU_VERSION="${VERSION_ID:-unknown}"
UBUNTU_CODENAME="${VERSION_CODENAME:-unknown}"
# Fallback codename detection
if [[ "$UBUNTU_CODENAME" == "unknown" ]]; then
UBUNTU_CODENAME=$(lsb_release -cs 2>/dev/null || echo "unknown")
fi
case "$UBUNTU_VERSION" in
20.04) UBUNTU_CODENAME="focal" ;;
22.04) UBUNTU_CODENAME="jammy" ;;
24.04) UBUNTU_CODENAME="noble" ;;
26.04) : ;; # Use auto-detected codename
esac
case "$UBUNTU_VERSION" in
20.04|22.04|24.04|26.04)
log_success "Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — Supported ✓"
;;
*)
log_warn "Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — Not officially tested"
log_warn "Will attempt best-effort compatibility"
;;
esac
# --- Architecture ---
ARCH=$(dpkg --print-architecture 2>/dev/null || echo "amd64")
case "$ARCH" in
amd64) GO_ARCH="amd64" ;;
arm64) GO_ARCH="arm64" ;;
*) log_error "Unsupported architecture: ${ARCH}"; exit 1 ;;
esac
log_info "Architecture: ${ARCH}"
# --- WSL Detection ---
if grep -qi microsoft /proc/version 2>/dev/null; then
IS_WSL=true
log_info "Environment: WSL2"
WIN_USER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r\n' || echo "")
if [[ -n "$WIN_USER" ]]; then
log_info "Windows user: ${WIN_USER}"
fi
else
IS_WSL=false
log_info "Environment: Native Linux"
fi
# --- User ---
log_info "Install user: ${INSTALL_USER} (HOME=${USER_HOME})"
}
#===============================================================================
# Proxy Configuration
#===============================================================================
setup_proxy() {
section_header "🌐 Proxy Configuration (Clash Verge)"
local proxy_http="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}"
local proxy_socks="socks5://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}"
local no_proxy_list="localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"
log_info "HTTP proxy: ${proxy_http}"
log_info "SOCKS proxy: ${proxy_socks}"
# 1. Unset any proxy environment variables first to test direct connectivity
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
log_step "Testing direct connectivity (checking if TUN mode is handling traffic)..."
local direct_ok=false
if curl -sI --connect-timeout 5 https://github.com >/dev/null 2>&1 || curl -sI --connect-timeout 5 https://www.google.com >/dev/null 2>&1; then
direct_ok=true
log_success "Direct connection working (TUN mode or native routing active) ✓"
fi
local proxy_ok=false
if [[ "$direct_ok" == "false" ]]; then
log_step "Direct connection unavailable. Testing manual proxy connectivity..."
export http_proxy="$proxy_http"
export https_proxy="$proxy_http"
export HTTP_PROXY="$proxy_http"
export HTTPS_PROXY="$proxy_http"
export all_proxy="$proxy_socks"
export ALL_PROXY="$proxy_socks"
export no_proxy="$no_proxy_list"
export NO_PROXY="$no_proxy_list"
if curl -sI --connect-timeout 5 https://github.com >/dev/null 2>&1 || curl -sI --connect-timeout 5 https://www.google.com >/dev/null 2>&1; then
proxy_ok=true
log_success "Proxy connection working ✓"
else
log_warn "Proxy connectivity test failed — proxy port may be unreachable"
# Unset broken proxy variables so we don't break subsequent commands
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
fi
fi
# --- Persist proxy_on/proxy_off to .bashrc and .profile ---
local proxy_shell_block
proxy_shell_block=$(cat << EOFPROXYBLOCK
proxy_on() {
export http_proxy="${proxy_http}"
export https_proxy="${proxy_http}"
export HTTP_PROXY="\${http_proxy}"
export HTTPS_PROXY="\${https_proxy}"
export all_proxy="${proxy_socks}"
export ALL_PROXY="\${all_proxy}"
export no_proxy="${no_proxy_list}"
export NO_PROXY="\${no_proxy}"
echo "🌐 Proxy ON: ${proxy_http}"
}
proxy_off() {
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
echo "🚫 Proxy OFF"
}
EOFPROXYBLOCK
)
if [[ "$proxy_ok" == "true" ]]; then
proxy_shell_block+=$'\n\n# Auto-enable proxy on shell start\nproxy_on >/dev/null 2>&1'
# Git proxy
run_as_user "git config --global http.proxy '${proxy_http}'" 2>/dev/null || true
run_as_user "git config --global https.proxy '${proxy_http}'" 2>/dev/null || true
AUTO_ENABLE_PROXY="true"
log_success "Proxy configured and enabled (auto-enable on shell start)"
else
proxy_shell_block+=$'\n\n# Proxy auto-enable skipped (tested offline or TUN mode active)'
# Remove git proxy since direct mode is used or proxy is unreachable
run_as_user "git config --global --unset http.proxy" 2>/dev/null || true
run_as_user "git config --global --unset https.proxy" 2>/dev/null || true
AUTO_ENABLE_PROXY="false"
if [[ "$direct_ok" == "true" ]]; then
log_success "Using transparent direct routing (helper functions written to shell rc)"
else
log_warn "Network seems offline — proxy configuration written but disabled"
fi
fi
add_to_rc "${USER_HOME}/.bashrc" "WSL-PROXY" "$proxy_shell_block"
add_to_rc "${USER_HOME}/.profile" "WSL-PROXY" "$proxy_shell_block"
}
#===============================================================================
# APT Mirror Configuration
#===============================================================================
setup_apt_mirror() {
section_header "📦 APT Mirror Configuration"
local mirror_url
case "$APT_MIRROR" in
tuna) mirror_url="https://mirrors.tuna.tsinghua.edu.cn" ;;
ustc) mirror_url="https://mirrors.ustc.edu.cn" ;;
aliyun) mirror_url="https://mirrors.aliyun.com" ;;
default) log_info "Using default APT sources — skipping"; return 0 ;;
*) log_warn "Unknown mirror '${APT_MIRROR}', falling back to tuna"
mirror_url="https://mirrors.tuna.tsinghua.edu.cn" ;;
esac
log_info "Mirror: ${mirror_url}"
log_info "Codename: ${UBUNTU_CODENAME}"
if version_gte "$UBUNTU_VERSION" "24.04"; then
_setup_apt_deb822 "$mirror_url"
else
_setup_apt_traditional "$mirror_url"
fi
log_step "Updating package lists..."
apt-get update -qq >/dev/null 2>&1
log_success "APT mirror configured ✓"
}
_setup_apt_traditional() {
local mirror_url="$1"
local sources_file="/etc/apt/sources.list"
backup_file "$sources_file"
cat > "$sources_file" << EOF
# Ubuntu APT Sources — Generated by wsl-dev-setup.sh
# Mirror: ${mirror_url}
# Date: $(date -Iseconds)
deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME} main restricted universe multiverse
deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-updates main restricted universe multiverse
deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-backports main restricted universe multiverse
deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-security main restricted universe multiverse
# deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME} main restricted universe multiverse
# deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-updates main restricted universe multiverse
# deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-backports main restricted universe multiverse
# deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-security main restricted universe multiverse
EOF
log_step "Wrote traditional sources.list"
}
_setup_apt_deb822() {
local mirror_url="$1"
local sources_file="/etc/apt/sources.list.d/ubuntu.sources"
backup_file "$sources_file"
# Disable legacy sources.list if it exists
[[ -f /etc/apt/sources.list ]] && \
mv /etc/apt/sources.list /etc/apt/sources.list.disabled 2>/dev/null || true
cat > "$sources_file" << EOF
## Ubuntu APT Sources (DEB822) — Generated by wsl-dev-setup.sh
## Mirror: ${mirror_url}
## Date: $(date -Iseconds)
Types: deb
URIs: ${mirror_url}/ubuntu/
Suites: ${UBUNTU_CODENAME} ${UBUNTU_CODENAME}-updates ${UBUNTU_CODENAME}-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Types: deb
URIs: ${mirror_url}/ubuntu/
Suites: ${UBUNTU_CODENAME}-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF
log_step "Wrote DEB822 format ubuntu.sources"
}
#===============================================================================
# Locale Configuration
#===============================================================================
setup_locale() {
section_header "🌍 Locale & UTF-8 Configuration"
if ! apt-get install -y -qq locales >/dev/null 2>&1; then
log_warn "Failed to install locales package. Skipping locale configuration."
return 0
fi
sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen 2>/dev/null || true
sed -i 's/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen 2>/dev/null || true
if locale-gen en_US.UTF-8 zh_CN.UTF-8 >/dev/null 2>&1; then
update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LANGUAGE=en_US:en 2>/dev/null || true
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
log_success "Locale: en_US.UTF-8 + zh_CN.UTF-8 ✓"
else
log_warn "locale-gen failed to generate locales"
fi
}
#===============================================================================
# Base Tools Installation
#===============================================================================
install_base_tools() {
section_header "🛠️ Base Tools Installation"
# Common packages for all versions
local packages=(
# Core
curl wget git
# Network diagnostics
net-tools iputils-ping mtr-tiny dnsutils traceroute
# Build essentials
build-essential gcc g++ make cmake pkg-config
# Security / certificates
ca-certificates gnupg lsb-release
software-properties-common apt-transport-https
# Compression
unzip zip tar gzip bzip2 xz-utils
# JSON / text
jq tree
# System monitoring
htop ncdu
# SSH
openssh-client
# Terminal multiplexer
tmux
# Fonts
fontconfig
)
# telnet: package name varies by version
if apt-cache show inetutils-telnet >/dev/null 2>&1; then
packages+=(inetutils-telnet)
elif apt-cache show telnet >/dev/null 2>&1; then
packages+=(telnet)
fi
# p7zip
if apt-cache show p7zip-full >/dev/null 2>&1; then
packages+=(p7zip-full)
fi
log_info "Installing ${#packages[@]} packages..."
apt-get install -y -qq "${packages[@]}" >/dev/null 2>&1
# Verify key tools
local tools_ok=true
for tool in curl git wget ping mtr; do
if check_cmd "$tool"; then
log_step "${tool}"
else
log_warn "${tool} — not found after install"
tools_ok=false
fi
done
# Check telnet specifically (might be inetutils-telnet)
if check_cmd telnet; then
log_step "telnet ✓"
else
log_warn "telnet — not found (try: apt install telnet)"
fi
if $tools_ok; then
log_success "Base tools installed ✓"
else
log_warn "Some tools may require manual installation"
fi
}
#===============================================================================
# Zsh + Zinit + Powerlevel10k
#===============================================================================
install_zsh_terminal() {
section_header "💻 Zsh + Zinit + Powerlevel10k"
# --- Install Zsh ---
if ! check_cmd zsh; then
log_step "Installing Zsh..."
if ! apt-get install -y -qq zsh >/dev/null 2>&1; then
log_error "Failed to install Zsh"
return 1
fi
fi
if ! check_cmd zsh; then
log_error "Zsh command not found after installation attempt"
return 1
fi
log_success "Zsh $(zsh --version 2>/dev/null | head -c 20)"
# --- Install Zinit ---
local zinit_home="${USER_HOME}/.local/share/zinit/zinit.git"
if [[ ! -d "$zinit_home" ]]; then
log_step "Installing Zinit plugin manager..."
run_as_user "mkdir -p '${USER_HOME}/.local/share/zinit'"
if ! run_as_user "git clone --depth=1 https://github.com/zdharma-continuum/zinit.git '${zinit_home}'"; then
log_error "Failed to clone Zinit plugin manager from GitHub"
return 1
fi
log_success "Zinit installed ✓"
else
log_info "Zinit already installed — pulling updates..."
run_as_user "cd '${zinit_home}' && git pull --quiet" || true
fi
# --- Generate .zshrc ---
_generate_zshrc
# --- Generate .p10k.zsh ---
_generate_p10k
# --- Set Zsh as default shell ---
local zsh_path
zsh_path=$(which zsh)
if [[ -n "$zsh_path" ]]; then
local current_shell
current_shell=$(getent passwd "${INSTALL_USER}" 2>/dev/null | cut -d: -f7)
if [[ "$current_shell" != "$zsh_path" ]]; then
if chsh -s "$zsh_path" "$INSTALL_USER" 2>/dev/null; then
log_success "Default shell → Zsh ✓"
else
log_warn "Failed to set default shell to Zsh using chsh"
fi
else
log_info "Zsh already set as default shell"
fi
else
log_warn "Could not determine Zsh path — skipping shell change"
fi
}
_generate_zshrc() {
local zshrc="${USER_HOME}/.zshrc"
backup_file "$zshrc"
cat > "$zshrc" << 'EOFZSHRC'
# Ensure UTF-8 locale is fully active at the very start of the shell session
# (Important for CJK character width calculations in P10k and fzf-tab)
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export LC_CTYPE=en_US.UTF-8
# ╔══════════════════════════════════════════════════════════════╗
# ║ Zsh Configuration ║
# ║ Powered by Zinit + Powerlevel10k ║
# ║ Generated by wsl-dev-setup.sh ║
# ╚══════════════════════════════════════════════════════════════╝
# ┌──────────────────────────────────────────┐
# │ Powerlevel10k Instant Prompt │
# │ (must stay near the top of .zshrc) │
# └──────────────────────────────────────────┘
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
# ┌──────────────────────────────────────────┐
# │ Zinit Initialization │
# └──────────────────────────────────────────┘
ZINIT_HOME="${HOME}/.local/share/zinit/zinit.git"
if [[ -f "${ZINIT_HOME}/zinit.zsh" ]]; then
source "${ZINIT_HOME}/zinit.zsh"
autoload -Uz _zinit
(( ${+_comps} )) && _comps[zinit]=_zinit
fi
# ┌──────────────────────────────────────────┐
# │ Completion System Setup │
# └──────────────────────────────────────────┘
# Initialize compinit synchronously so completion functions and plugins (like fzf-tab)
# hook into the completion system correctly and reliably.
mkdir -p "${HOME}/.cache"
autoload -Uz compinit
compinit -d "${HOME}/.cache/zcompdump"
# ┌──────────────────────────────────────────┐
# │ Theme: Powerlevel10k │
# └──────────────────────────────────────────┘
zinit ice depth=1
zinit light romkatv/powerlevel10k
# ┌──────────────────────────────────────────┐
# │ Plugins (Zinit Turbo Mode) │
# └──────────────────────────────────────────┘
# Core plugins — loaded asynchronously for fast startup
zinit wait lucid for \
atinit"ZINIT[COMPINIT_OPTS]=-C; zicompinit; zicdreplay" \
zdharma-continuum/fast-syntax-highlighting \
blockf \
zsh-users/zsh-completions \
atload"!_zsh_autosuggest_start" \
zsh-users/zsh-autosuggestions
# History substring search (↑↓ arrow key filtering)
# Loaded synchronously so widgets are registered before keybindings and syntax highlighting
zinit light zsh-users/zsh-history-substring-search
# fzf-tab: replace default tab completion with fzf
zinit wait lucid for \
Aloxaf/fzf-tab
# Oh-My-Zsh snippets (cherry-picked essentials, NOT the whole framework)
zinit wait lucid for \
OMZL::history.zsh \
OMZL::key-bindings.zsh \
OMZL::completion.zsh \
OMZL::directories.zsh \
OMZP::git \
OMZP::sudo \
OMZP::extract \
OMZP::command-not-found
# ┌──────────────────────────────────────────┐
# │ History │
# └──────────────────────────────────────────┘
HISTFILE="${HOME}/.zsh_history"
HISTSIZE=50000
SAVEHIST=50000
setopt EXTENDED_HISTORY # Record timestamp
setopt HIST_EXPIRE_DUPS_FIRST # Expire duplicates first
setopt HIST_IGNORE_DUPS # Ignore contiguous duplicates
setopt HIST_IGNORE_ALL_DUPS # Remove older duplicates
setopt HIST_IGNORE_SPACE # Ignore commands starting with space
setopt HIST_FIND_NO_DUPS # No duplicates in search
setopt HIST_SAVE_NO_DUPS # No duplicates on save
setopt SHARE_HISTORY # Share history across sessions
setopt INC_APPEND_HISTORY # Append immediately
# ┌──────────────────────────────────────────┐
# │ Completion │
# └──────────────────────────────────────────┘
zstyle ':completion:*' matcher-list \
'm:{a-zA-Z}={A-Za-z}' \
'r:|[._-]=* r:|=*'
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}"
zstyle ':completion:*' menu no
zstyle ':completion:*:descriptions' format '[%d]'
zstyle ':completion:*' special-dirs true
zstyle ':fzf-tab:complete:cd:*' fzf-preview \
'eza -1 --color=always --icons $realpath 2>/dev/null || ls -1 --color=always $realpath 2>/dev/null'
zstyle ':fzf-tab:*' switch-group '<' '>'
# ┌──────────────────────────────────────────┐
# │ Key Bindings │
# └──────────────────────────────────────────┘
bindkey '^[[A' history-substring-search-up 2>/dev/null
bindkey '^[[B' history-substring-search-down 2>/dev/null
bindkey '^P' history-substring-search-up 2>/dev/null
bindkey '^N' history-substring-search-down 2>/dev/null
bindkey '^[OA' history-substring-search-up 2>/dev/null
bindkey '^[OB' history-substring-search-down 2>/dev/null
# ┌──────────────────────────────────────────┐
# │ Aliases │
# └──────────────────────────────────────────┘
# ── Modern tool replacements ──
if command -v eza &>/dev/null; then
alias ls='eza --icons --group-directories-first'
alias ll='eza -alh --icons --group-directories-first --git'
alias la='eza -a --icons --group-directories-first'
alias lt='eza --tree --icons --level=3'
else
alias ll='ls -alh --color=auto'
alias la='ls -A --color=auto'
fi
if command -v bat &>/dev/null; then
alias cat='bat --paging=never --style=plain'
alias catn='bat --paging=never'
elif command -v batcat &>/dev/null; then
alias cat='batcat --paging=never --style=plain'
alias catn='batcat --paging=never'
fi
command -v fdfind &>/dev/null && ! command -v fd &>/dev/null && alias fd='fdfind'
# ── Navigation ──
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
# ── Utilities ──
alias cls='clear'
alias ports='ss -tulnp'
alias myip='curl -s ifconfig.me && echo'
alias df='df -h'
alias free='free -h'
alias grep='grep --color=auto'
alias mkdir='mkdir -pv'
alias reload='exec zsh'
# ── Git extras ──
alias glog='git log --oneline --graph --decorate -20'
alias gd='git diff'
alias gs='git status -sb'
# ┌──────────────────────────────────────────┐
# │ Environment — Go │
# └──────────────────────────────────────────┘
if [[ -d /usr/local/go ]]; then
export GOROOT=/usr/local/go
export GOPATH="${HOME}/go"
export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}"
export GOPROXY=https://goproxy.cn,direct
export GONOSUMDB="*"
fi
# ┌──────────────────────────────────────────┐
# │ Environment — fnm (Node.js) │
# └──────────────────────────────────────────┘
if [[ -d "${HOME}/.local/share/fnm" ]]; then
export FNM_DIR="${HOME}/.local/share/fnm"
export PATH="${FNM_DIR}:${PATH}"
fi
if command -v fnm &>/dev/null; then
eval "$(fnm env --use-on-cd --shell zsh)"
fi
# ┌──────────────────────────────────────────┐
# │ Tool Integrations │
# └──────────────────────────────────────────┘
# fzf
if [[ -f "${HOME}/.fzf.zsh" ]]; then
source "${HOME}/.fzf.zsh"
elif command -v fzf &>/dev/null; then
eval "$(fzf --zsh 2>/dev/null)" || true
fi
# zoxide (smart cd)
if command -v zoxide &>/dev/null; then
eval "$(zoxide init zsh)"
fi
# delta as git pager
if command -v delta &>/dev/null; then
export GIT_PAGER='delta'
fi
# ┌──────────────────────────────────────────┐
# │ Powerlevel10k Config │
# └──────────────────────────────────────────┘
# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
EOFZSHRC
# --- Append proxy config if enabled ---
if [[ "${ENABLE_PROXY}" == "true" ]]; then
cat >> "$zshrc" << EOFPROXY
# >>> WSL-PROXY >>>
proxy_on() {
export http_proxy="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}"
export https_proxy="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}"
export HTTP_PROXY="\${http_proxy}"
export HTTPS_PROXY="\${https_proxy}"
export all_proxy="socks5://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}"
export ALL_PROXY="\${all_proxy}"
export no_proxy="localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"
export NO_PROXY="\${no_proxy}"
echo "🌐 Proxy ON: http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}"
}
proxy_off() {
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
echo "🚫 Proxy OFF"
}
EOFPROXY
if [[ "${AUTO_ENABLE_PROXY:-false}" == "true" ]]; then
cat >> "$zshrc" << EOFPROXY_AUTO
# Auto-enable proxy
proxy_on >/dev/null 2>&1
# <<< WSL-PROXY <<<
EOFPROXY_AUTO
else
cat >> "$zshrc" << EOFPROXY_AUTO
# Proxy auto-enable skipped (tested offline or TUN mode active)
# <<< WSL-PROXY <<<
EOFPROXY_AUTO
fi
fi
chown "${INSTALL_USER}:${INSTALL_USER}" "$zshrc"
log_success ".zshrc generated ✓"
}
_generate_p10k() {
local p10k_file="${USER_HOME}/.p10k.zsh"
log_step "Generating Powerlevel10k config (Rainbow style)..."
cat > "$p10k_file" << 'EOFP10K'
# ╔══════════════════════════════════════════════════════════════╗
# ║ Powerlevel10k Configuration ║
# ║ Style: Rainbow with Nerd Font v3 ║
# ║ Generated by wsl-dev-setup.sh ║
# ║ To reconfigure interactively: p10k configure ║
# ╚══════════════════════════════════════════════════════════════╝
'builtin' 'local' '-a' 'p10k_config_opts'
[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases')
[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob')
[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand')
'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand'
() {
emulate -L zsh -o extended_glob
# Reset all P10k settings
unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR'
# ══════════════════════════════════════
# Prompt Layout
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
os_icon # OS icon (Ubuntu/Linux)
dir # Current directory
vcs # Git status
newline # ── new line ──
prompt_char # or ✘
)
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
status # Exit code on error
command_execution_time # Duration of last command
background_jobs # Background job indicator
direnv # direnv status
go_version # Go version (in Go projects)
node_version # Node.js version (in Node projects)
context # user@hostname (SSH only)
time # Current time HH:MM
)
# ══════════════════════════════════════
# General
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_MODE=nerdfont-v3
typeset -g POWERLEVEL9K_ICON_PADDING=moderate
typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT=
typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=true
# Powerline separators
typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='\uE0B0'
typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='\uE0B2'
typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='\uE0B1'
typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='\uE0B3'
typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0'
typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2'
typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL=''
typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL=''
typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL=
# ══════════════════════════════════════
# Prompt Char ()
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76
typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION=''
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION=''
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='V'
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶'
typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true
typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL=
typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL=
# ══════════════════════════════════════
# OS Icon
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=255
typeset -g POWERLEVEL9K_OS_ICON_BACKGROUND=24
# ══════════════════════════════════════
# Directory
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_DIR_FOREGROUND=255
typeset -g POWERLEVEL9K_DIR_BACKGROUND=31
typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=153
typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=255
typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true
typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique
typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=3
typeset -g POWERLEVEL9K_SHORTEN_DELIMITER=
typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80
typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40
typeset -g POWERLEVEL9K_DIR_HYPERLINK=false
typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=v3
typeset -g POWERLEVEL9K_DIR_NOT_WRITABLE_FOREGROUND=220
typeset -g POWERLEVEL9K_DIR_NOT_WRITABLE_BACKGROUND=88
typeset -g POWERLEVEL9K_DIR_CLASSES=()
# ══════════════════════════════════════
# VCS (Git)
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=0
typeset -g POWERLEVEL9K_VCS_CLEAN_BACKGROUND=76
typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=0
typeset -g POWERLEVEL9K_VCS_MODIFIED_BACKGROUND=178
typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=0
typeset -g POWERLEVEL9K_VCS_UNTRACKED_BACKGROUND=166
typeset -g POWERLEVEL9K_VCS_CONFLICTED_FOREGROUND=255
typeset -g POWERLEVEL9K_VCS_CONFLICTED_BACKGROUND=196
typeset -g POWERLEVEL9K_VCS_LOADING_FOREGROUND=244
typeset -g POWERLEVEL9K_VCS_LOADING_BACKGROUND=240
typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 '
typeset -g POWERLEVEL9K_VCS_COMMIT_ICON='@'
typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1
typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION=
typeset -g POWERLEVEL9K_VCS_BACKENDS=(git)
typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~'
# ══════════════════════════════════════
# Status (Exit Code)
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true
# OK — hidden by default (no noise)
typeset -g POWERLEVEL9K_STATUS_OK=false
typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=70
typeset -g POWERLEVEL9K_STATUS_OK_BACKGROUND=238
typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔'
# OK pipe
typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true
typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=70
typeset -g POWERLEVEL9K_STATUS_OK_PIPE_BACKGROUND=238
typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔'
# Error
typeset -g POWERLEVEL9K_STATUS_ERROR=true
typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=255
typeset -g POWERLEVEL9K_STATUS_ERROR_BACKGROUND=124
typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘'
# Error signal
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=255
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_BACKGROUND=124
typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘'
# Error pipe
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=255
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_BACKGROUND=124
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘'
# ══════════════════════════════════════
# Command Execution Time
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=248
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_BACKGROUND=240
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s'
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION=
# ══════════════════════════════════════
# Background Jobs
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=37
typeset -g POWERLEVEL9K_BACKGROUND_JOBS_BACKGROUND=238
typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false
# ══════════════════════════════════════
# Direnv
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=178
typeset -g POWERLEVEL9K_DIRENV_BACKGROUND=238
# ══════════════════════════════════════
# Go Version
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=255
typeset -g POWERLEVEL9K_GO_VERSION_BACKGROUND=37
typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='󰟓'
# ══════════════════════════════════════
# Node Version
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=255
typeset -g POWERLEVEL9K_NODE_VERSION_BACKGROUND=70
typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='󰎙'
# ══════════════════════════════════════
# Context (user@hostname)
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=255
typeset -g POWERLEVEL9K_CONTEXT_BACKGROUND=240
typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=255
typeset -g POWERLEVEL9K_CONTEXT_ROOT_BACKGROUND=124
typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT_EXPANSION,TEMPLATE}='%n@%m'
typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%B%n@%m'
# Only show in SSH sessions or as root
typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_CONTENT_EXPANSION=
typeset -g POWERLEVEL9K_ALWAYS_SHOW_CONTEXT=false
typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_CONTENT_EXPANSION='%n@%m'
# ══════════════════════════════════════
# Time
# ══════════════════════════════════════
typeset -g POWERLEVEL9K_TIME_FOREGROUND=255
typeset -g POWERLEVEL9K_TIME_BACKGROUND=236
typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M}'
typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false
typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION=
# ══════════════════════════════════════
# Transient Prompt
# ══════════════════════════════════════
# Replace previous prompts with a minimal version to keep terminal clean
typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always
# ══════════════════════════════════════
# Instant Prompt
# ══════════════════════════════════════
# Show prompt immediately while plugins load in background
typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose
# ══════════════════════════════════════
# Hot Reload
# ══════════════════════════════════════
(( ! $+functions[p10k] )) || p10k reload
}
(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]}
'builtin' 'unset' 'p10k_config_opts'
EOFP10K
chown "${INSTALL_USER}:${INSTALL_USER}" "$p10k_file"
log_success ".p10k.zsh generated (Rainbow + Nerd Font v3) ✓"
log_info "To reconfigure interactively: ${C_BOLD}p10k configure${C_NC}"
}
#===============================================================================
# Nerd Font Installation
#===============================================================================
install_nerd_fonts() {
section_header "🔤 Nerd Font: ${NERD_FONT}"
ensure_deps curl jq unzip fontconfig || return 1
local font_dir="${USER_HOME}/.local/share/fonts/NerdFonts"
# --- Check if already installed ---
if [[ -d "$font_dir" ]] && compgen -G "${font_dir}/*.ttf" >/dev/null 2>&1; then
local count
count=$(find "$font_dir" -name '*.ttf' 2>/dev/null | wc -l)
log_info "Nerd Fonts already installed (${count} files in ${font_dir})"
else
log_step "Downloading ${NERD_FONT} Nerd Font from GitHub..."
local tmp_dir
tmp_dir=$(mktemp -d /tmp/nerd-font-XXXXXX)
download_github_asset "ryanoasis/nerd-fonts" \
"${NERD_FONT}\\.zip" \
"${tmp_dir}/${NERD_FONT}.zip" || {
log_error "Failed to download Nerd Font"
rm -rf "$tmp_dir"
return 1
}
run_as_user "mkdir -p '${font_dir}'"
unzip -o -q "${tmp_dir}/${NERD_FONT}.zip" '*.ttf' -d "$font_dir" 2>/dev/null || \
unzip -o -q "${tmp_dir}/${NERD_FONT}.zip" -d "$font_dir" 2>/dev/null
# Clean up non-essential files
rm -f "${font_dir}"/*Windows*.ttf 2>/dev/null || true
rm -f "${font_dir}"/*.md "${font_dir}"/*.txt "${font_dir}"/*.json 2>/dev/null || true
chown -R "${INSTALL_USER}:${INSTALL_USER}" "${font_dir}"
fc-cache -f "${font_dir}" >/dev/null 2>&1
rm -rf "$tmp_dir"
local count
count=$(find "$font_dir" -name '*.ttf' 2>/dev/null | wc -l)
log_success "Installed ${count} font files → ${font_dir}"
fi
# --- Copy to Windows side (for Windows Terminal) ---
if [[ "$IS_WSL" == "true" && -n "${WIN_USER}" ]]; then
local win_font_dir="/mnt/c/Users/${WIN_USER}/AppData/Local/Microsoft/Windows/Fonts"
if [[ -d "/mnt/c/Users/${WIN_USER}" ]]; then
mkdir -p "$win_font_dir" 2>/dev/null || true
if cp "${font_dir}"/*.ttf "$win_font_dir/" 2>/dev/null; then
log_success "Fonts copied to Windows → ${win_font_dir}"
else
log_warn "Could not copy fonts to Windows side (non-critical)"
fi
fi
fi
echo ""
echo -e " ${C_YELLOW}╔══════════════════════════════════════════════════════╗${C_NC}"
echo -e " ${C_YELLOW}${C_NC} ${C_BOLD}📌 Windows Terminal 字体配置:${C_NC} ${C_YELLOW}${C_NC}"
echo -e " ${C_YELLOW}${C_NC} ${C_YELLOW}${C_NC}"
echo -e " ${C_YELLOW}${C_NC} Settings → Profiles → Defaults → Appearance ${C_YELLOW}${C_NC}"
echo -e " ${C_YELLOW}${C_NC} → Font face: ${C_BOLD}JetBrainsMono Nerd Font${C_NC} ${C_YELLOW}${C_NC}"
echo -e " ${C_YELLOW}${C_NC} ${C_YELLOW}${C_NC}"
echo -e " ${C_YELLOW}${C_NC} 如字体未出现,请在 Windows 中手动安装: ${C_YELLOW}${C_NC}"
echo -e " ${C_YELLOW}${C_NC} 双击 .ttf 文件 → Install ${C_YELLOW}${C_NC}"
echo -e " ${C_YELLOW}╚══════════════════════════════════════════════════════╝${C_NC}"
echo ""
}
#===============================================================================
# Modern CLI Tools
#===============================================================================
install_modern_cli() {
section_header "⚡ Modern CLI Tools"
_install_bat
_install_eza
_install_fd
_install_ripgrep
_install_fzf
_install_zoxide
_install_delta
_install_lazygit
echo ""
log_success "Modern CLI tools installation complete ✓"
}
# ── bat (better cat) ──
_install_bat() {
if check_cmd bat || check_cmd batcat; then
log_info "bat — already installed ✓"
return 0
fi
log_step "Installing bat..."
if version_gte "$UBUNTU_VERSION" "22.04"; then
apt-get install -y -qq bat >/dev/null 2>&1
# Create symlink: batcat → bat
[[ -f /usr/bin/batcat ]] && ln -sf /usr/bin/batcat /usr/local/bin/bat 2>/dev/null || true
else
case "$ARCH" in
amd64) install_github_deb "sharkdp/bat" "bat_.*_amd64\\.deb" ;;
arm64) install_github_deb "sharkdp/bat" "bat_.*_arm64\\.deb" ;;
esac
fi
if check_cmd bat || check_cmd batcat; then
log_success "bat installed ✓"
else
log_warn "bat — installation failed"
fi
}
# ── eza (better ls) ──
_install_eza() {
if check_cmd eza; then
log_info "eza — already installed ✓"
return 0
fi
log_step "Installing eza..."
if version_gte "$UBUNTU_VERSION" "24.04"; then
apt-get install -y -qq eza >/dev/null 2>&1
else
# Install from GitHub release
case "$ARCH" in
amd64) install_github_tar_binary "eza-community/eza" "eza_x86_64-unknown-linux-gnu\\.tar\\.gz" "eza" ;;
arm64) install_github_tar_binary "eza-community/eza" "eza_aarch64-unknown-linux-gnu\\.tar\\.gz" "eza" ;;
esac
fi
if check_cmd eza; then
log_success "eza installed ✓"
else
log_warn "eza — installation failed"
fi
}
# ── fd (better find) ──
_install_fd() {
if check_cmd fd || check_cmd fdfind; then
log_info "fd — already installed ✓"
return 0
fi
log_step "Installing fd-find..."
apt-get install -y -qq fd-find >/dev/null 2>&1 || {
case "$ARCH" in
amd64) install_github_deb "sharkdp/fd" "fd_.*_amd64\\.deb" ;;
arm64) install_github_deb "sharkdp/fd" "fd_.*_arm64\\.deb" ;;
esac
}
# Create symlink: fdfind → fd
[[ -f /usr/bin/fdfind ]] && ln -sf /usr/bin/fdfind /usr/local/bin/fd 2>/dev/null || true
if check_cmd fd || check_cmd fdfind; then
log_success "fd installed ✓"
else
log_warn "fd — installation failed"
fi
}
# ── ripgrep (better grep) ──
_install_ripgrep() {
if check_cmd rg; then
log_info "ripgrep — already installed ✓"
return 0
fi
log_step "Installing ripgrep..."
apt-get install -y -qq ripgrep >/dev/null 2>&1 || {
case "$ARCH" in
amd64) install_github_deb "BurntSushi/ripgrep" "ripgrep_.*_amd64\\.deb" ;;
arm64) install_github_deb "BurntSushi/ripgrep" "ripgrep_.*_arm64\\.deb" ;;
esac
}
if check_cmd rg; then
log_success "ripgrep installed ✓"
else
log_warn "ripgrep — installation failed"
fi
}
# ── fzf (fuzzy finder) ──
_install_fzf() {
if check_cmd fzf; then
log_info "fzf — already installed ✓"
return 0
fi
log_step "Installing fzf..."
# Try apt first for newer Ubuntu
if version_gte "$UBUNTU_VERSION" "22.04"; then
apt-get install -y -qq fzf >/dev/null 2>&1
fi
# Fallback to git installation (gets latest version + shell integrations)
if ! check_cmd fzf; then
run_as_user "git clone --depth 1 https://github.com/junegunn/fzf.git '${USER_HOME}/.fzf'" 2>/dev/null || true
if [[ -d "${USER_HOME}/.fzf" ]]; then
run_as_user "'${USER_HOME}/.fzf/install' --all --no-update-rc --no-bash --no-zsh" 2>/dev/null || true
# Add to path
ln -sf "${USER_HOME}/.fzf/bin/fzf" /usr/local/bin/fzf 2>/dev/null || true
fi
fi
if check_cmd fzf; then
log_success "fzf installed ✓"
else
log_warn "fzf — installation failed"
fi
}
# ── zoxide (better cd) ──
_install_zoxide() {
if check_cmd zoxide; then
log_info "zoxide — already installed ✓"
return 0
fi
log_step "Installing zoxide..."
if version_gte "$UBUNTU_VERSION" "24.04"; then
apt-get install -y -qq zoxide >/dev/null 2>&1
else
case "$ARCH" in
amd64) install_github_deb "ajeetdsouza/zoxide" "zoxide_.*_amd64\\.deb" ;;
arm64) install_github_deb "ajeetdsouza/zoxide" "zoxide_.*_arm64\\.deb" ;;
esac
fi
if check_cmd zoxide; then
log_success "zoxide installed ✓"
else
log_warn "zoxide — installation failed"
fi
}
# ── delta (better diff / git pager) ──
_install_delta() {
if check_cmd delta; then
log_info "delta — already installed ✓"
return 0
fi
log_step "Installing git-delta..."
case "$ARCH" in
amd64) install_github_deb "dandavison/delta" "git-delta_.*_amd64\\.deb" ;;
arm64) install_github_deb "dandavison/delta" "git-delta_.*_arm64\\.deb" ;;
esac
# Configure git to use delta
if check_cmd delta; then
run_as_user "git config --global core.pager delta" 2>/dev/null || true
run_as_user "git config --global interactive.diffFilter 'delta --color-only'" 2>/dev/null || true
run_as_user "git config --global delta.navigate true" 2>/dev/null || true
run_as_user "git config --global delta.light false" 2>/dev/null || true
run_as_user "git config --global delta.line-numbers true" 2>/dev/null || true
run_as_user "git config --global merge.conflictstyle diff3" 2>/dev/null || true
run_as_user "git config --global diff.colorMoved default" 2>/dev/null || true
log_success "delta installed + git configured ✓"
else
log_warn "delta — installation failed"
fi
}
# ── lazygit (git TUI) ──
_install_lazygit() {
if check_cmd lazygit; then
log_info "lazygit — already installed ✓"
return 0
fi
log_step "Installing lazygit..."
case "$ARCH" in
amd64) install_github_tar_binary "jesseduffield/lazygit" "lazygit_.*_linux_x86_64\\.tar\\.gz" "lazygit" ;;
arm64) install_github_tar_binary "jesseduffield/lazygit" "lazygit_.*_linux_arm64\\.tar\\.gz" "lazygit" ;;
esac
if check_cmd lazygit; then
log_success "lazygit installed ✓"
else
log_warn "lazygit — installation failed"
fi
}
#===============================================================================
# Go Installation
#===============================================================================
install_golang() {
section_header "🐹 Go Installation"
ensure_deps curl jq || return 1
# --- Resolve version ---
local target_version="$GO_VERSION"
if [[ "$target_version" == "latest" ]]; then
log_step "Detecting latest Go version..."
target_version=$(curl -sL --connect-timeout 15 'https://golang.google.cn/dl/?mode=json' \
| jq -r '.[0].version' 2>/dev/null \
| sed 's/^go//')
if [[ -z "$target_version" || "$target_version" == "null" ]]; then
# Fallback to go.dev
target_version=$(curl -sL --connect-timeout 15 'https://go.dev/dl/?mode=json' \
| jq -r '.[0].version' 2>/dev/null \
| sed 's/^go//')
fi
if [[ -z "$target_version" || "$target_version" == "null" ]]; then
log_error "Failed to detect latest Go version"
return 1
fi
fi
log_info "Target version: Go ${target_version}"
# --- Check current installation ---
if check_cmd go; then
local current
current=$(go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "")
if [[ "$current" == "$target_version" ]]; then
log_success "Go ${target_version} already installed ✓"
_setup_go_env
return 0
fi
log_info "Current Go: ${current} → Upgrading to ${target_version}"
fi
# --- Download ---
local go_tarball="go${target_version}.linux-${GO_ARCH}.tar.gz"
local download_url="https://golang.google.cn/dl/${go_tarball}"
local tmp_file="/tmp/${go_tarball}"
log_step "Downloading: ${go_tarball}"
if ! curl -fsSL --connect-timeout 30 --retry 3 "$download_url" -o "$tmp_file"; then
# Fallback to go.dev
download_url="https://go.dev/dl/${go_tarball}"
log_step "Retrying from go.dev..."
curl -fsSL --connect-timeout 30 --retry 3 "$download_url" -o "$tmp_file" || {
log_error "Failed to download Go"
return 1
}
fi
# --- Install ---
log_step "Installing to /usr/local/go..."
rm -rf /usr/local/go
tar -C /usr/local -xzf "$tmp_file"
rm -f "$tmp_file"
# --- Create GOPATH ---
run_as_user "mkdir -p '${USER_HOME}/go/bin' '${USER_HOME}/go/src' '${USER_HOME}/go/pkg'"
# --- Setup environment ---
_setup_go_env
# --- Verify ---
local installed_version
installed_version=$(/usr/local/go/bin/go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "unknown")
log_success "Go ${installed_version} installed ✓"
log_info "GOROOT=/usr/local/go GOPATH=\${HOME}/go"
log_info "GOPROXY=https://goproxy.cn,direct"
}
_setup_go_env() {
# System-wide (all users, all login shells)
cat > /etc/profile.d/golang.sh << 'EOFGO'
# Go environment — managed by wsl-dev-setup.sh
export GOROOT=/usr/local/go
export GOPATH="${HOME}/go"
export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}"
export GOPROXY=https://goproxy.cn,direct
export GONOSUMDB="*"
EOFGO
chmod 644 /etc/profile.d/golang.sh
# .bashrc
local go_bashrc='
if [[ -d /usr/local/go ]]; then
export GOROOT=/usr/local/go
export GOPATH="${HOME}/go"
export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}"
export GOPROXY=https://goproxy.cn,direct
export GONOSUMDB="*"
fi'
add_to_rc "${USER_HOME}/.bashrc" "GOLANG" "$go_bashrc"
# .profile (for sh / login shells)
local go_profile='
if [ -d /usr/local/go ]; then
export GOROOT=/usr/local/go
export GOPATH="${HOME}/go"
export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}"
export GOPROXY=https://goproxy.cn,direct
export GONOSUMDB="*"
fi'
add_to_rc "${USER_HOME}/.profile" "GOLANG" "$go_profile"
# .zshrc already includes Go config via _generate_zshrc
log_step "Go env configured: /etc/profile.d, .bashrc, .profile, .zshrc"
}
#===============================================================================
# Node.js Installation (via fnm)
#===============================================================================
install_nodejs() {
section_header "📗 Node.js LTS via fnm (Fast Node Manager)"
ensure_deps curl jq unzip || return 1
local fnm_dir="${USER_HOME}/.local/share/fnm"
local fnm_bin="${fnm_dir}/fnm"
# --- Install fnm ---
if [[ -x "$fnm_bin" ]]; then
log_info "fnm already installed ✓"
else
log_step "Installing fnm from GitHub..."
local tmp_dir
tmp_dir=$(mktemp -d /tmp/fnm-XXXXXX)
local fnm_pattern
case "$ARCH" in
amd64) fnm_pattern="fnm-linux\\.zip" ;;
arm64) fnm_pattern="fnm-arm64\\.zip" ;;
esac
download_github_asset "Schniz/fnm" "$fnm_pattern" "${tmp_dir}/fnm.zip" || {
log_error "Failed to download fnm"
rm -rf "$tmp_dir"
return 1
}
run_as_user "mkdir -p '${fnm_dir}'"
unzip -o -q "${tmp_dir}/fnm.zip" -d "$fnm_dir"
chmod +x "$fnm_bin"
chown -R "${INSTALL_USER}:${INSTALL_USER}" "$fnm_dir"
# Also link to /usr/local/bin for root access
ln -sf "$fnm_bin" /usr/local/bin/fnm 2>/dev/null || true
rm -rf "$tmp_dir"
log_success "fnm installed → ${fnm_dir}"
fi
# --- Setup environment for fnm ---
_setup_fnm_env
# --- Install Node.js LTS ---
log_step "Installing Node.js LTS..."
# We need fnm in the PATH for the user, and specify shell bash to prevent "Can't infer shell!" warning
local install_cmd="export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; fnm install --lts"
if ! run_as_user "$install_cmd"; then
log_warn "First attempt to install Node.js LTS failed. Clearing partial downloads and retrying..."
run_as_user "rm -rf '${fnm_dir}/node-versions/.downloads'" 2>/dev/null || true
if ! run_as_user "$install_cmd"; then
log_error "Failed to install Node.js LTS"
return 1
fi
fi
run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; fnm default lts-latest" 2>/dev/null || true
# --- Get installed version ---
local node_version
node_version=$(run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; node --version" 2>/dev/null || echo "unknown")
local npm_version
npm_version=$(run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm --version" 2>/dev/null || echo "unknown")
log_success "Node.js ${node_version} (npm ${npm_version}) installed ✓"
# --- Set npm China mirror ---
log_step "Setting npm registry → npmmirror.com"
run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm config set registry https://registry.npmmirror.com" 2>/dev/null || true
# --- Install global npm tools ---
log_step "Installing tldr..."
run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm install -g tldr" 2>/dev/null || {
log_warn "tldr installation failed (non-critical)"
}
log_success "Node.js environment configured ✓"
log_info "npm registry: https://registry.npmmirror.com"
}
_setup_fnm_env() {
local fnm_dir="${USER_HOME}/.local/share/fnm"
# .bashrc
local fnm_bashrc="
if [[ -d \"${fnm_dir}\" ]]; then
export FNM_DIR=\"${fnm_dir}\"
export PATH=\"\${FNM_DIR}:\${PATH}\"
eval \"\$(fnm env --use-on-cd --shell bash)\"
fi"
add_to_rc "${USER_HOME}/.bashrc" "FNM-NODEJS" "$fnm_bashrc"
# .profile
local fnm_profile="
if [ -d \"${fnm_dir}\" ]; then
export FNM_DIR=\"${fnm_dir}\"
export PATH=\"\${FNM_DIR}:\${PATH}\"
eval \"\$(fnm env --use-on-cd)\"
fi"
add_to_rc "${USER_HOME}/.profile" "FNM-NODEJS" "$fnm_profile"
# .zshrc already includes fnm config via _generate_zshrc
log_step "fnm env configured: .bashrc, .profile, .zshrc"
}
#===============================================================================
# Verification & Summary
#===============================================================================
print_summary() {
local elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
local minutes=$(( elapsed / 60 ))
local seconds=$(( elapsed % 60 ))
echo ""
echo -e "${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_NC}"
echo -e "${C_CYAN}${C_NC} ${C_BOLD}📊 Installation Summary${C_NC} ${C_CYAN}${C_NC}"
echo -e "${C_CYAN}╠══════════════════════════════════════════════════════════════╣${C_NC}"
# System info
printf "${C_CYAN}${C_NC} %-58s${C_CYAN}${C_NC}\n" "🖥️ Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — ${ARCH}"
[[ "$IS_WSL" == "true" ]] && \
printf "${C_CYAN}${C_NC} %-58s${C_CYAN}${C_NC}\n" "📦 WSL2 — Windows user: ${WIN_USER:-N/A}"
echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}"
# Module results
_check_result "APT Mirror" "[[ -n '${APT_MIRROR}' ]]"
_check_result "Locale" "locale 2>/dev/null | grep -q 'UTF-8'"
_check_result "curl" "check_cmd curl"
_check_result "git" "check_cmd git"
_check_result "wget" "check_cmd wget"
_check_result "ping" "check_cmd ping"
_check_result "mtr" "check_cmd mtr"
_check_result "Zsh" "check_cmd zsh"
_check_result "Zinit" "[[ -d '${USER_HOME}/.local/share/zinit/zinit.git' ]]"
_check_result "P10k Config" "[[ -f '${USER_HOME}/.p10k.zsh' ]]"
_check_result "Nerd Font" "compgen -G '${USER_HOME}/.local/share/fonts/NerdFonts/*.ttf' >/dev/null 2>&1"
_check_result "bat" "check_cmd bat || check_cmd batcat"
_check_result "eza" "check_cmd eza"
_check_result "fd" "check_cmd fd || check_cmd fdfind"
_check_result "ripgrep" "check_cmd rg"
_check_result "fzf" "check_cmd fzf"
_check_result "zoxide" "check_cmd zoxide"
_check_result "delta" "check_cmd delta"
_check_result "lazygit" "check_cmd lazygit"
_check_result "Go" "check_cmd go || [[ -x /usr/local/go/bin/go ]]"
_check_result "fnm" "[[ -x '${USER_HOME}/.local/share/fnm/fnm' ]]"
_check_result "Node.js" "run_as_user \"export PATH='${USER_HOME}/.local/share/fnm:\${PATH}'; eval \\\"\\\$(fnm env)\\\"; node --version\" >/dev/null 2>&1"
echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}"
# Versions
local go_ver="N/A"
[[ -x /usr/local/go/bin/go ]] && go_ver=$(/usr/local/go/bin/go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "N/A")
local node_ver="N/A"
node_ver=$(run_as_user "export PATH='${USER_HOME}/.local/share/fnm:\${PATH}'; eval \"\$(fnm env)\" 2>/dev/null; node --version" 2>/dev/null || echo "N/A")
printf "${C_CYAN}${C_NC} %-58s${C_CYAN}${C_NC}\n" "Go: ${go_ver} Node.js: ${node_ver}"
printf "${C_CYAN}${C_NC} %-58s${C_CYAN}${C_NC}\n" "⏱️ Completed in ${minutes}m ${seconds}s"
if [[ -n "$FAILED_MODULES" ]]; then
echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}"
printf "${C_CYAN}${C_NC} ${C_RED}⚠️ Failed modules: %-39s${C_NC}${C_CYAN}${C_NC}\n" "$FAILED_MODULES"
fi
echo -e "${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_NC}"
# Post-install instructions
echo ""
echo -e "${C_BOLD}📌 Next Steps:${C_NC}"
echo -e " 1. Set Windows Terminal font: ${C_BOLD}JetBrainsMono Nerd Font${C_NC}"
echo -e " 2. Restart WSL: ${C_DIM}wsl --shutdown${C_NC} then reopen terminal"
echo -e " 3. (Optional) Reconfigure prompt: ${C_DIM}p10k configure${C_NC}"
echo -e " 4. (Optional) Toggle proxy: ${C_DIM}proxy_on${C_NC} / ${C_DIM}proxy_off${C_NC}"
echo ""
}
_check_result() {
local label="$1"
local test_cmd="$2"
if eval "$test_cmd" 2>/dev/null; then
printf "${C_CYAN}${C_NC} ${C_GREEN}${C_NC} %-55s${C_CYAN}${C_NC}\n" "$label"
else
printf "${C_CYAN}${C_NC} ${C_RED}${C_NC} %-55s${C_CYAN}${C_NC}\n" "$label"
fi
}
#===============================================================================
# Main Entry Point
#===============================================================================
main() {
parse_args "$@"
# --- Root check ---
if [[ "$(id -u)" -ne 0 ]]; then
log_error "This script must be run as root."
echo ""
echo -e " Usage: ${C_BOLD}sudo bash ${SCRIPT_NAME}${C_NC}"
echo ""
exit 1
fi
# --- Banner ---
print_banner
# --- Dry run ---
if [[ "$DRY_RUN" == "true" ]]; then
log_info "DRY RUN — showing what would be executed:"
for module in $ALL_MODULES; do
if should_run_module "$module"; then
echo -e " ${C_GREEN}${C_NC} $module"
else
echo -e " ${C_DIM}$module (skipped)${C_NC}"
fi
done
exit 0
fi
# ── Phase 0: System Detection ──
detect_system
# ── Phase 1: Network & Sources (needed for downloads) ──
if should_run_module "proxy" && [[ "$ENABLE_PROXY" == "true" ]]; then
setup_proxy || FAILED_MODULES="${FAILED_MODULES} proxy"
fi
if should_run_module "mirror"; then
setup_apt_mirror || FAILED_MODULES="${FAILED_MODULES} mirror"
fi
# ── Phase 2: System Foundation ──
if should_run_module "locale"; then
setup_locale || FAILED_MODULES="${FAILED_MODULES} locale"
fi
if should_run_module "base-tools"; then
install_base_tools || FAILED_MODULES="${FAILED_MODULES} base-tools"
fi
# ── Phase 3: Terminal Environment ──
if should_run_module "zsh"; then
install_zsh_terminal || FAILED_MODULES="${FAILED_MODULES} zsh"
fi
if should_run_module "fonts"; then
install_nerd_fonts || FAILED_MODULES="${FAILED_MODULES} fonts"
fi
if should_run_module "modern-cli"; then
install_modern_cli || FAILED_MODULES="${FAILED_MODULES} modern-cli"
fi
# ── Phase 4: Development Languages ──
if should_run_module "golang"; then
install_golang || FAILED_MODULES="${FAILED_MODULES} golang"
fi
if should_run_module "nodejs"; then
install_nodejs || FAILED_MODULES="${FAILED_MODULES} nodejs"
fi
# ── Phase 5: Summary ──
print_summary
}
# ── Execute ──
main "$@"