#!/usr/bin/env bash # ============================================================================= # install.sh — Configuration zsh portable (Linux / macOS, sans sudo) # Sert aussi d'updater : seules les actions réellement effectuées sont affichées # (TERMINAL_VERBOSE=true pour voir aussi les "déjà à jour / déjà installé"). # Usage : curl -fsSL https://raw.githubusercontent.com/Coxyz/Terminal/main/install.sh | bash # ============================================================================= set -euo pipefail REPO="Coxyz/Terminal" LOCAL_BIN="$HOME/.local/bin" CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/terminal-zsh" # Empêche update.zsh (sourcé par les `zsh -ic` ci-dessous) de proposer/déclencher # une mise à jour pendant que l'installeur tourne. export TERMINAL_DISABLE_AUTO_UPDATE=true # ── Affichage ───────────────────────────────────────────────────────────────── # On ne montre que ce qui est RÉELLEMENT fait : # - step() diffère l'en-tête de section ; il n'est imprimé (_flush_step) que si # une action (info/warn/error) suit — les sections sans action disparaissent ; # - skip() = "déjà fait / à jour", silencieux sauf si TERMINAL_VERBOSE=true. RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' VERBOSE="${TERMINAL_VERBOSE:-false}" _pending_step="" _did_something=false _flush_step() { [ -n "$_pending_step" ] || return 0 echo -e "\n${BLUE}──────────────────────────────${NC}\n${BLUE}$_pending_step${NC}" _pending_step="" } step() { _pending_step="$1"; } info() { _flush_step; _did_something=true; echo -e "${GREEN}[✔]${NC} $1"; } warn() { _flush_step; echo -e "${YELLOW}[!]${NC} $1"; } error() { _flush_step; echo -e "${RED}[✘]${NC} $1"; exit 1; } skip() { [ "$VERBOSE" = "true" ] && { _flush_step; echo -e "${YELLOW}[=]${NC} $1"; }; return 0; } _has_sudo() { groups | tr ' ' '\n' | grep -qE '^(sudo|wheel|admin)$' } # ── Résolution du commit cible ──────────────────────────────────────────────── # On épingle les téléchargements sur le SHA exact (pas sur `main`) : le CDN de # raw.githubusercontent peut servir un `main` périmé, ce qui désynchronise les # fichiers du SHA enregistré (l'auto-update se croit alors à jour à tort). # Un chemin // est immuable → fichiers toujours cohérents avec la version. REMOTE_SHA="$(curl -fsSL --max-time 8 -H 'Accept: application/vnd.github.sha' \ "https://api.github.com/repos/$REPO/commits/main" 2>/dev/null || true)" if [ -n "$REMOTE_SHA" ]; then RAW_BASE="https://raw.githubusercontent.com/$REPO/$REMOTE_SHA/zsh" else RAW_BASE="https://raw.githubusercontent.com/$REPO/main/zsh" warn "SHA distant indisponible — repli sur la branche main" fi # ── Détection OS ────────────────────────────────────────────────────────────── OS="$(uname -s)" case "$OS" in Linux*) PLATFORM="linux" ;; Darwin*) PLATFORM="macos" ;; *) error "OS non supporté : $OS" ;; esac skip "Plateforme détectée : $PLATFORM" # ── Vérification zsh ────────────────────────────────────────────────────────── step "Vérification de zsh" if ! command -v zsh &>/dev/null; then error "zsh n'est pas installé.\n → Linux : sudo apt install zsh\n → macOS : brew install zsh" fi skip "zsh $(zsh --version | awk '{print $2}') trouvé" # ── Oh My Zsh ───────────────────────────────────────────────────────────────── step "Oh My Zsh" if [ -d "$HOME/.oh-my-zsh" ]; then skip "Oh My Zsh déjà installé" else info "Installation silencieuse de Oh My Zsh..." if curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh | \ RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -s -- --unattended >/dev/null 2>&1; then info "Oh My Zsh installé" else error "Échec de l'installation silencieuse de Oh My Zsh" fi fi ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" # ── Plugins externes ────────────────────────────────────────────────────────── step "Plugins externes" clone_plugin() { local name="$1" url="$2" dest="$ZSH_CUSTOM/plugins/$1" if [ -d "$dest" ]; then skip "$name déjà présent" else info "Clonage de $name..." git clone --depth=1 -q "$url" "$dest" fi } clone_plugin "zsh-autosuggestions" "https://github.com/zsh-users/zsh-autosuggestions" clone_plugin "zsh-syntax-highlighting" "https://github.com/zsh-users/zsh-syntax-highlighting" # ── fzf ─────────────────────────────────────────────────────────────────────── step "fzf" if [ -d "$HOME/.fzf" ]; then skip "fzf déjà installé" else info "Installation de fzf..." git clone --depth=1 -q https://github.com/junegunn/fzf.git "$HOME/.fzf" "$HOME/.fzf/install" --all --no-bash --no-fish --no-update-rc >/dev/null 2>&1 info "fzf installé" fi # ── fd (find amélioré) ──────────────────────────────────────────────────────── # Nom du binaire : `fd` (brew/cargo) ou `fdfind` (apt Debian/Ubuntu). step "fd (find amélioré)" if command -v fd &>/dev/null || command -v fdfind &>/dev/null; then skip "fd déjà installé" elif [ "$PLATFORM" = "macos" ] && command -v brew &>/dev/null; then brew install fd >/dev/null 2>&1 && info "fd installé (brew)" || warn "Échec installation fd" elif [ "$PLATFORM" = "linux" ] && _has_sudo && command -v apt-get &>/dev/null; then sudo apt-get install -y fd-find >/dev/null 2>&1 && info "fd installé (apt)" || warn "Échec installation fd" else warn "fd non installé (pas de gestionnaire de paquets dispo) — skip" fi # ── eza (ls amélioré) ───────────────────────────────────────────────────────── install_eza_prebuilt() { # Télécharge un binaire eza pré-compilé dans ~/.local/bin (sans sudo). local arch target url tmp arch="$(uname -m)" case "$arch" in x86_64|amd64) target="x86_64-unknown-linux-musl" ;; aarch64|arm64) target="aarch64-unknown-linux-gnu" ;; *) warn "Architecture non gérée pour eza pré-compilé : $arch"; return 1 ;; esac url="https://github.com/eza-community/eza/releases/latest/download/eza_${target}.tar.gz" mkdir -p "$LOCAL_BIN" tmp="$(mktemp -d)" || return 1 if curl -fsSL "$url" -o "$tmp/eza.tar.gz" && tar -xzf "$tmp/eza.tar.gz" -C "$tmp" && [ -f "$tmp/eza" ]; then mv "$tmp/eza" "$LOCAL_BIN/eza" chmod +x "$LOCAL_BIN/eza" rm -rf "$tmp" return 0 fi rm -rf "$tmp" return 1 } step "eza (ls amélioré)" if command -v eza &>/dev/null; then skip "eza déjà installé" else case "$PLATFORM" in macos) if command -v brew &>/dev/null; then brew install eza >/dev/null 2>&1 && info "eza installé (brew)" || warn "Échec installation eza via brew" else warn "Homebrew absent — installe eza manuellement : brew install eza" fi ;; linux) if _has_sudo && command -v apt-get &>/dev/null && apt-cache show eza &>/dev/null; then sudo apt-get install -y eza >/dev/null 2>&1 && info "eza installé (apt)" || warn "Échec installation eza via apt" else info "Installation d'eza (binaire pré-compilé, sans sudo)..." if install_eza_prebuilt; then info "eza installé dans $LOCAL_BIN" else warn "Échec installation eza — les alias retomberont sur ls" fi fi ;; esac fi # ── bat (cat amélioré) ──────────────────────────────────────────────────────── install_bat_prebuilt() { local arch target url tmp arch="$(uname -m)" case "$arch" in x86_64|amd64) target="x86_64-unknown-linux-musl" ;; aarch64|arm64) target="aarch64-unknown-linux-gnu" ;; *) warn "Architecture non gérée pour bat pré-compilé : $arch"; return 1 ;; esac local version version="$(curl -fsSL --max-time 8 "https://api.github.com/repos/sharkdp/bat/releases/latest" 2>/dev/null \ | grep '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/')" || true [ -z "$version" ] && { warn "Impossible de récupérer la version de bat"; return 1; } url="https://github.com/sharkdp/bat/releases/download/v${version}/bat-v${version}-${target}.tar.gz" mkdir -p "$LOCAL_BIN" tmp="$(mktemp -d)" || return 1 if curl -fsSL "$url" -o "$tmp/bat.tar.gz" \ && tar -xzf "$tmp/bat.tar.gz" -C "$tmp" --strip-components=1 \ && [ -f "$tmp/bat" ]; then mv "$tmp/bat" "$LOCAL_BIN/bat" chmod +x "$LOCAL_BIN/bat" rm -rf "$tmp" return 0 fi rm -rf "$tmp" return 1 } step "bat (cat amélioré)" if command -v bat &>/dev/null || command -v batcat &>/dev/null; then skip "bat déjà installé" else case "$PLATFORM" in macos) if command -v brew &>/dev/null; then brew install bat >/dev/null 2>&1 && info "bat installé (brew)" || warn "Échec installation bat via brew" else warn "Homebrew absent — installe bat manuellement : brew install bat" fi ;; linux) if _has_sudo && command -v apt-get &>/dev/null; then sudo apt-get install -y bat >/dev/null 2>&1 && info "bat installé (apt)" || warn "Échec installation bat via apt" else info "Installation de bat (binaire pré-compilé, sans sudo)..." if install_bat_prebuilt; then info "bat installé dans $LOCAL_BIN" else warn "Échec installation bat — les alias retomberont sur cat" fi fi ;; esac fi # ── atuin (historique shell synchronisé) ────────────────────────────────────── # Version épinglée : le protocole de sync évolue, client et serveur doivent # rester alignés (serveur : ghcr.io/atuinsh/atuin sur Boxyz). # ⚠️ Les tags d'IMAGE Docker n'ont pas de préfixe "v" (18.18.1), les tags Git # des releases si (v18.18.1) — c'est ce dernier qui sert ici. ATUIN_VERSION="18.18.1" install_atuin_prebuilt() { # Binaire pré-compilé dans ~/.local/bin (sans sudo). local arch target url tmp arch="$(uname -m)" case "$PLATFORM" in linux) case "$arch" in x86_64|amd64) target="x86_64-unknown-linux-musl" ;; aarch64|arm64) target="aarch64-unknown-linux-gnu" ;; *) warn "Architecture non gérée pour atuin : $arch"; return 1 ;; esac ;; macos) case "$arch" in x86_64|amd64) target="x86_64-apple-darwin" ;; arm64|aarch64) target="aarch64-apple-darwin" ;; *) warn "Architecture non gérée pour atuin : $arch"; return 1 ;; esac ;; esac url="https://github.com/atuinsh/atuin/releases/download/v${ATUIN_VERSION}/atuin-${target}.tar.gz" mkdir -p "$LOCAL_BIN" tmp="$(mktemp -d)" || return 1 if curl -fsSL "$url" -o "$tmp/atuin.tar.gz" \ && tar -xzf "$tmp/atuin.tar.gz" -C "$tmp" --strip-components=1 \ && [ -f "$tmp/atuin" ]; then mv "$tmp/atuin" "$LOCAL_BIN/atuin" chmod +x "$LOCAL_BIN/atuin" rm -rf "$tmp" return 0 fi rm -rf "$tmp" return 1 } step "atuin (historique synchronisé)" _atuin_installed="" if command -v atuin &>/dev/null; then _atuin_installed="$(atuin --version 2>/dev/null | awk '{print $2}')" || true _atuin_path="$(command -v atuin)" case "$_atuin_path" in "$LOCAL_BIN"/*) ;; *) warn "atuin détecté hors de $LOCAL_BIN ($_atuin_path) — c'est lui qui sera utilisé si son chemin est prioritaire dans le PATH" ;; esac fi if [ "$_atuin_installed" = "$ATUIN_VERSION" ]; then skip "atuin $ATUIN_VERSION déjà installé" else if [ -n "$_atuin_installed" ]; then info "Mise à jour d'atuin ($_atuin_installed → $ATUIN_VERSION)..." else info "Installation d'atuin $ATUIN_VERSION (binaire pré-compilé, sans sudo)..." fi if install_atuin_prebuilt; then info "atuin $ATUIN_VERSION installé dans $LOCAL_BIN" else warn "Échec installation atuin — Ctrl+R retombera sur fzf/l'historique zsh" fi fi # ── Téléchargement des fichiers zsh ─────────────────────────────────────────── step "Téléchargement des fichiers zsh" download() { local src="$1" dest="$2" tmp_file="" mkdir -p "$(dirname "$dest")" tmp_file="$(mktemp)" || error "Impossible de créer un fichier temporaire" if curl -fsSL "$src" -o "$tmp_file"; then if [ -f "$dest" ] && cmp -s "$dest" "$tmp_file"; then rm -f "$tmp_file" skip "$(basename "$dest") déjà à jour" return 0 fi mv "$tmp_file" "$dest" info "$(basename "$dest") mis à jour" return 0 fi rm -f "$tmp_file" error "Impossible de télécharger : $src" } download_if_missing() { local src="$1" dest="$2" label="${3:-$(basename "$dest")}" if [ -f "$dest" ]; then skip "$label déjà présent — conservé" return 0 fi download "$src" "$dest" } # Fichiers partagés : toujours synchronisés avec le dépôt download "$RAW_BASE/.zshrc" "$HOME/.zshrc" download "$RAW_BASE/aliases/default.zsh" "$ZSH_CUSTOM/aliases/default.zsh" download "$RAW_BASE/macros/default.zsh" "$ZSH_CUSTOM/macros/default.zsh" download "$RAW_BASE/fzf.zsh" "$ZSH_CUSTOM/fzf.zsh" download "$RAW_BASE/atuin.zsh" "$ZSH_CUSTOM/atuin.zsh" download "$RAW_BASE/update.zsh" "$ZSH_CUSTOM/update.zsh" download "$RAW_BASE/aussiegeek-custom.zsh-theme" "$ZSH_CUSTOM/themes/aussiegeek-custom.zsh-theme" # Fichiers locaux : créés une fois puis jamais écrasés download_if_missing "$RAW_BASE/aliases/local.zsh" "$ZSH_CUSTOM/aliases/local.zsh" "aliases/local.zsh" download_if_missing "$RAW_BASE/macros/local.zsh" "$ZSH_CUSTOM/macros/local.zsh" "macros/local.zsh" download_if_missing "$RAW_BASE/export.zsh" "$ZSH_CUSTOM/export.zsh" "export.zsh" # Config atuin : fichier partagé (hors ZSH_CUSTOM, chemin imposé par atuin). # Les réglages propres à une machine passent par les variables ATUIN_* de # export.zsh, qui ont la priorité sur ce fichier. download "$RAW_BASE/atuin/config.toml" \ "${XDG_CONFIG_HOME:-$HOME/.config}/atuin/config.toml" # ── Éditeur par défaut ──────────────────────────────────────────────────────── step "Configuration éditeur" EXPORT_ZSH="$ZSH_CUSTOM/export.zsh" MACROS_DEFAULT_FILE="$ZSH_CUSTOM/macros/default.zsh" MACROS_LOCAL_FILE="$ZSH_CUSTOM/macros/local.zsh" zsh_editor_runner="export ZSH_CUSTOM='$ZSH_CUSTOM'; [ -f '$MACROS_DEFAULT_FILE' ] && source '$MACROS_DEFAULT_FILE'; [ -f '$MACROS_LOCAL_FILE' ] && source '$MACROS_LOCAL_FILE'; typeset -f zsh-editor >/dev/null && zsh-editor" zsh_editor_check_runner="export ZSH_CUSTOM='$ZSH_CUSTOM'; [ -f '$MACROS_DEFAULT_FILE' ] && source '$MACROS_DEFAULT_FILE'; [ -f '$MACROS_LOCAL_FILE' ] && source '$MACROS_LOCAL_FILE'; [ -f '$EXPORT_ZSH' ] && source '$EXPORT_ZSH'; print -r -- \"\${EDITOR%% *}\"" current_editor="$(zsh -ic "$zsh_editor_check_runner" 2>/dev/null | tail -n 1)" if [ -n "$current_editor" ] && command -v "$current_editor" >/dev/null 2>&1; then skip "Éditeur déjà configuré : $current_editor" elif zsh -ic "$zsh_editor_runner"; then info "Configuration de l'éditeur effectuée via zsh-editor" else warn "Impossible d'exécuter zsh-editor, fallback sur vim" _tmp="$(grep -v '^export EDITOR=' "$EXPORT_ZSH" 2>/dev/null || true)" printf '%s\nexport EDITOR='"'"'%s'"'"'\n' "$_tmp" "vim" > "$EXPORT_ZSH" info "EDITOR='vim' enregistré dans $EXPORT_ZSH" fi # ── atuin : connexion au serveur de sync ────────────────────────────────────── # Volontairement interactif : aucun identifiant n'est stocké dans le dépôt ni # passé en argument (les args de `atuin login -p …` finiraient dans la liste des # process et dans l'historique). C'est atuin lui-même qui pose les questions et # masque la saisie ; on se contente de lui brancher /dev/tty, indispensable car # stdin est occupé par le pipe de `curl | bash`. step "atuin — connexion au serveur" _atuin_just_logged_in=false if ! command -v atuin &>/dev/null; then skip "atuin non installé — connexion ignorée" elif [ "${TERMINAL_SKIP_ATUIN_LOGIN:-false}" = "true" ]; then skip "Connexion atuin ignorée (TERMINAL_SKIP_ATUIN_LOGIN=true)" elif atuin status >/dev/null 2>&1; then skip "atuin déjà connecté" elif [ ! -e /dev/tty ]; then warn "Pas de terminal interactif — connecte-toi ensuite avec : atuin login" else _flush_step _atuin_server="$(grep -E '^\s*sync_address' \ "${XDG_CONFIG_HOME:-$HOME/.config}/atuin/config.toml" 2>/dev/null \ | sed 's/.*"\(.*\)".*/\1/')" echo -e " Serveur : ${BLUE}${_atuin_server:-non configuré}${NC}" echo " [1] J'ai déjà un compte → login (demande la clé de chiffrement)" echo " [2] Créer un compte → register (1re machine uniquement)" echo " [3] Plus tard" printf ' Choix [1/2/3] : ' read -r _atuin_choice < /dev/tty || _atuin_choice="3" case "${_atuin_choice:-3}" in 1) if atuin login < /dev/tty; then info "Connecté au serveur atuin" _atuin_just_logged_in=true else warn "Échec de la connexion — réessaie plus tard avec : atuin login" fi ;; 2) if atuin register < /dev/tty; then info "Compte atuin créé" _atuin_just_logged_in=true echo warn "Clé de chiffrement — À SAUVEGARDER MAINTENANT (Bitwarden) :" atuin key < /dev/tty || warn "Récupère-la avec : atuin key" warn "Sans cette clé, l'historique synchronisé est IRRÉCUPÉRABLE," warn "même avec un accès au serveur et à sa base." echo else warn "Échec de l'inscription — réessaie plus tard avec : atuin register" fi ;; *) skip "Connexion atuin reportée" warn "Pour te connecter plus tard : atuin login (ou atuin register)" ;; esac if [ "$_atuin_just_logged_in" = "true" ]; then if atuin import auto >/dev/null 2>&1; then info "Historique shell existant importé dans atuin" else warn "Import de l'historique échoué — relance : atuin import auto" fi if atuin sync >/dev/null 2>&1; then info "Synchronisation initiale effectuée" else warn "Sync initiale échouée — vérifie l'accès à ${_atuin_server:-ton serveur}" fi fi fi # ── Version & cache (pour l'auto-update) ────────────────────────────────────── # REMOTE_SHA a été résolu en début de script (et a servi à épingler RAW_BASE), # donc la version enregistrée correspond exactement aux fichiers téléchargés. step "Version & cache" mkdir -p "$CACHE_DIR" if [ -n "$REMOTE_SHA" ]; then _old_sha="" [ -f "$CACHE_DIR/version" ] && _old_sha="$(<"$CACHE_DIR/version")" printf '%s\n' "$REMOTE_SHA" > "$CACHE_DIR/version" date +%s > "$CACHE_DIR/last_check" rm -f "$CACHE_DIR/update_available" if [ "$_old_sha" = "$REMOTE_SHA" ]; then skip "Version déjà à jour : ${REMOTE_SHA:0:7}" else info "Version installée : ${REMOTE_SHA:0:7}" fi else warn "Version distante indisponible — l'auto-update se calera plus tard" fi # ── Shell par défaut ────────────────────────────────────────────────────────── step "Shell par défaut" if [ "$(basename "$SHELL")" != "zsh" ]; then warn "Shell actuel : $SHELL" warn "Pour passer à zsh : chsh -s $(command -v zsh)" else skip "zsh est déjà ton shell par défaut" fi # ── Fin ─────────────────────────────────────────────────────────────────────── echo -e "\n${GREEN}════════════════════════════════════${NC}" if [ "$_did_something" != "true" ]; then echo -e "${GREEN} ✅ Déjà à jour — rien à faire${NC}" echo -e "${GREEN}════════════════════════════════════${NC}" exit 0 fi echo -e "${GREEN} ✅ Terminé !${NC}" echo -e "${GREEN}════════════════════════════════════${NC}" # Appelé par l'updater (zsh-update) ? C'est lui qui rechargera le shell. [ "${TERMINAL_FROM_UPDATER:-0}" = "1" ] && exit 0 # Recharge automatiquement le shell pour appliquer la nouvelle config. # (TERMINAL_NO_AUTO_EXEC=1 pour désactiver ce comportement) if [ "${TERMINAL_NO_AUTO_EXEC:-0}" != "1" ] && command -v zsh >/dev/null 2>&1; then unset TERMINAL_DISABLE_AUTO_UPDATE TERMINAL_FROM_UPDATER # ne pas fuiter dans le nouveau shell echo -e " ${BLUE}Rechargement du shell...${NC}" source ~/.zshrc fi