1959 lines
77 KiB
Bash
1959 lines
77 KiB
Bash
#!/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 "$@"
|