From 048c9ed8bc86c8ef3099f1f2cdea4b918db8fa8c Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Mon, 15 Dec 2025 16:06:00 -0500 Subject: [PATCH] Now with more FEATURES (the worth of which who knows, but I like em). --- bin/dotfiles-sync.sh | 451 +++++++++++++++++++++++ bin/setup-wizard.sh | 583 ++++++++++++++++++++++++++++++ bin/shell-stats.sh | 511 ++++++++++++++++++++++++++ bin/vault.sh | 504 ++++++++++++++++++++++++++ dotfiles.conf | 8 + zsh/.zshrc | 26 ++ zsh/functions/command-palette.zsh | 310 ++++++++++++++++ zsh/themes/smart-suggest.zsh | 446 +++++++++++++++++++++++ zsh/zshrc | 338 +++++++++++++++++ 9 files changed, 3177 insertions(+) create mode 100644 bin/dotfiles-sync.sh create mode 100644 bin/setup-wizard.sh create mode 100644 bin/shell-stats.sh create mode 100644 bin/vault.sh create mode 100644 zsh/functions/command-palette.zsh create mode 100644 zsh/themes/smart-suggest.zsh create mode 100644 zsh/zshrc diff --git a/bin/dotfiles-sync.sh b/bin/dotfiles-sync.sh new file mode 100644 index 0000000..c9e722b --- /dev/null +++ b/bin/dotfiles-sync.sh @@ -0,0 +1,451 @@ +#!/usr/bin/env bash +# ============================================================================ +# Dotfiles Sync - Auto-sync across machines +# ============================================================================ +# Keeps your dotfiles synchronized across multiple machines +# +# Usage: +# dotfiles-sync.sh # Interactive sync +# dotfiles-sync.sh --push # Push local changes +# dotfiles-sync.sh --pull # Pull remote changes +# dotfiles-sync.sh --status # Show sync status +# dotfiles-sync.sh --watch # Watch for changes (daemon mode) +# dotfiles-sync.sh --auto # Auto-sync on shell start +# ============================================================================ + +set -e + +# ============================================================================ +# Load Configuration +# ============================================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_CONF="${SCRIPT_DIR}/../dotfiles.conf" +[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="$HOME/.dotfiles/dotfiles.conf" + +if [[ -f "$DOTFILES_CONF" ]]; then + source "$DOTFILES_CONF" +else + DOTFILES_DIR="$HOME/.dotfiles" + DOTFILES_BRANCH="main" +fi + +SYNC_STATE_FILE="$DOTFILES_DIR/.sync_state" +SYNC_LOG_FILE="$DOTFILES_DIR/.sync_log" +HOSTNAME=$(hostname -s) + +# ============================================================================ +# Colors +# ============================================================================ + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +DIM='\033[2m' +NC='\033[0m' + +# ============================================================================ +# Helper Functions +# ============================================================================ + +print_header() { + echo -e "\n${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║${NC} Dotfiles Sync ${BLUE}║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}\n" +} + +log_sync() { + local action="$1" + local details="$2" + echo "$(date -Iseconds) | $HOSTNAME | $action | $details" >> "$SYNC_LOG_FILE" +} + +get_local_status() { + cd "$DOTFILES_DIR" + + local status="" + local ahead=0 + local behind=0 + local modified=0 + local untracked=0 + + # Fetch quietly + git fetch origin --quiet 2>/dev/null || true + + # Count commits ahead/behind + ahead=$(git rev-list HEAD..origin/${DOTFILES_BRANCH} --count 2>/dev/null || echo 0) + behind=$(git rev-list origin/${DOTFILES_BRANCH}..HEAD --count 2>/dev/null || echo 0) + + # Count modified and untracked + modified=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ') + untracked=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ') + + echo "$ahead|$behind|$modified|$untracked" +} + +has_local_changes() { + cd "$DOTFILES_DIR" + ! git diff --quiet 2>/dev/null || [[ -n $(git ls-files --others --exclude-standard) ]] +} + +has_remote_changes() { + cd "$DOTFILES_DIR" + git fetch origin --quiet 2>/dev/null || true + local behind=$(git rev-list HEAD..origin/${DOTFILES_BRANCH} --count 2>/dev/null || echo 0) + [[ $behind -gt 0 ]] +} + +# ============================================================================ +# Sync Functions +# ============================================================================ + +show_status() { + print_header + + echo -e "${CYAN}Machine:${NC} $HOSTNAME" + echo -e "${CYAN}Branch:${NC} $DOTFILES_BRANCH" + echo -e "${CYAN}Path:${NC} $DOTFILES_DIR" + echo + + cd "$DOTFILES_DIR" + + IFS='|' read -r behind ahead modified untracked <<< "$(get_local_status)" + + echo -e "${CYAN}Status:${NC}" + + # Remote status + if [[ $behind -gt 0 ]]; then + echo -e " ${YELLOW}↓${NC} $behind commit(s) behind remote" + elif [[ $ahead -gt 0 ]]; then + echo -e " ${GREEN}↑${NC} $ahead commit(s) ahead of remote" + else + echo -e " ${GREEN}✓${NC} In sync with remote" + fi + + # Local changes + if [[ $modified -gt 0 ]]; then + echo -e " ${YELLOW}●${NC} $modified modified file(s)" + fi + + if [[ $untracked -gt 0 ]]; then + echo -e " ${YELLOW}+${NC} $untracked untracked file(s)" + fi + + if [[ $modified -eq 0 && $untracked -eq 0 ]]; then + echo -e " ${GREEN}✓${NC} Working directory clean" + fi + + # Show recent changes + echo + echo -e "${CYAN}Recent changes:${NC}" + git log --oneline -5 2>/dev/null | while read -r line; do + echo -e " ${DIM}$line${NC}" + done + + # Show modified files + if [[ $modified -gt 0 || $untracked -gt 0 ]]; then + echo + echo -e "${CYAN}Changed files:${NC}" + git status --short 2>/dev/null | head -10 | while read -r line; do + echo -e " $line" + done + local total=$((modified + untracked)) + [[ $total -gt 10 ]] && echo -e " ${DIM}... and $((total - 10)) more${NC}" + fi + + # Show last sync + if [[ -f "$SYNC_STATE_FILE" ]]; then + echo + local last_sync=$(cat "$SYNC_STATE_FILE") + echo -e "${CYAN}Last sync:${NC} $last_sync" + fi +} + +do_push() { + local message="${1:-Auto-sync from $HOSTNAME}" + + cd "$DOTFILES_DIR" + + if ! has_local_changes; then + echo -e "${GREEN}✓${NC} No local changes to push" + return 0 + fi + + echo -e "${BLUE}==>${NC} Pushing local changes..." + + # Stage all changes + git add -A + + # Show what we're committing + echo -e "${CYAN}Changes:${NC}" + git diff --cached --stat | head -10 + + echo + + # Commit + git commit -m "$message" || { + echo -e "${YELLOW}⚠${NC} Nothing to commit" + return 0 + } + + # Push + if git push origin "$DOTFILES_BRANCH"; then + echo -e "${GREEN}✓${NC} Changes pushed successfully" + log_sync "push" "$message" + date -Iseconds > "$SYNC_STATE_FILE" + else + echo -e "${RED}✗${NC} Failed to push changes" + return 1 + fi +} + +do_pull() { + cd "$DOTFILES_DIR" + + echo -e "${BLUE}==>${NC} Pulling remote changes..." + + # Stash local changes if any + local had_changes=false + if has_local_changes; then + echo -e "${YELLOW}⚠${NC} Stashing local changes..." + git stash push -m "Auto-stash before pull" + had_changes=true + fi + + # Pull + if git pull origin "$DOTFILES_BRANCH"; then + echo -e "${GREEN}✓${NC} Changes pulled successfully" + log_sync "pull" "from origin/$DOTFILES_BRANCH" + date -Iseconds > "$SYNC_STATE_FILE" + + # Show what changed + echo -e "${CYAN}Updates:${NC}" + git log --oneline ORIG_HEAD..HEAD 2>/dev/null | while read -r line; do + echo -e " ${GREEN}+${NC} $line" + done + else + echo -e "${RED}✗${NC} Failed to pull changes" + + # Restore stash on failure + if [[ "$had_changes" == true ]]; then + git stash pop + fi + return 1 + fi + + # Restore stash + if [[ "$had_changes" == true ]]; then + echo -e "${BLUE}==>${NC} Restoring local changes..." + if git stash pop; then + echo -e "${GREEN}✓${NC} Local changes restored" + else + echo -e "${YELLOW}⚠${NC} Conflict restoring local changes" + echo " Resolve conflicts and run: git stash drop" + fi + fi +} + +do_sync() { + print_header + + cd "$DOTFILES_DIR" + + local has_local=$(has_local_changes && echo "yes" || echo "no") + local has_remote=$(has_remote_changes && echo "yes" || echo "no") + + if [[ "$has_local" == "no" && "$has_remote" == "no" ]]; then + echo -e "${GREEN}✓${NC} Everything is in sync!" + return 0 + fi + + if [[ "$has_remote" == "yes" ]]; then + echo -e "${CYAN}Remote changes available${NC}" + do_pull + echo + fi + + if [[ "$has_local" == "yes" ]]; then + echo -e "${CYAN}Local changes detected${NC}" + + # Show changes + git status --short + echo + + read -p "Push these changes? [Y/n]: " confirm + if [[ "${confirm:-y}" =~ ^[Yy] ]]; then + read -p "Commit message [Auto-sync from $HOSTNAME]: " msg + do_push "${msg:-Auto-sync from $HOSTNAME}" + fi + fi +} + +do_watch() { + echo -e "${BLUE}==>${NC} Starting sync daemon..." + echo -e "${DIM}Press Ctrl+C to stop${NC}" + echo + + local interval="${1:-300}" # Default 5 minutes + + log_sync "watch_start" "interval=${interval}s" + + while true; do + local timestamp=$(date '+%H:%M:%S') + + if has_remote_changes; then + echo -e "[$timestamp] ${YELLOW}↓${NC} Remote changes detected, pulling..." + do_pull + fi + + if has_local_changes; then + echo -e "[$timestamp] ${YELLOW}↑${NC} Local changes detected" + # In watch mode, auto-commit with timestamp + do_push "Auto-sync: $(date '+%Y-%m-%d %H:%M') from $HOSTNAME" + fi + + echo -e "[$timestamp] ${DIM}Sleeping ${interval}s...${NC}" + sleep "$interval" + done +} + +do_auto() { + # Quick check for shell startup - minimal output + cd "$DOTFILES_DIR" 2>/dev/null || return 0 + + git fetch origin --quiet 2>/dev/null || return 0 + + local behind=$(git rev-list HEAD..origin/${DOTFILES_BRANCH} --count 2>/dev/null || echo 0) + + if [[ $behind -gt 0 ]]; then + echo -e "${YELLOW}⚠ Dotfiles: $behind update(s) available${NC}" + echo -e " Run: ${CYAN}dotfiles-sync.sh --pull${NC}" + fi + + if has_local_changes; then + local changed=$(git status --short 2>/dev/null | wc -l | tr -d ' ') + echo -e "${YELLOW}⚠ Dotfiles: $changed local change(s) not pushed${NC}" + echo -e " Run: ${CYAN}dotfiles-sync.sh --push${NC}" + fi +} + +show_diff() { + cd "$DOTFILES_DIR" + + echo -e "${CYAN}Local changes:${NC}" + echo + + if command -v delta &>/dev/null; then + git diff | delta + elif command -v diff-so-fancy &>/dev/null; then + git diff | diff-so-fancy + else + git diff --color + fi +} + +show_log() { + local count="${1:-20}" + + if [[ -f "$SYNC_LOG_FILE" ]]; then + echo -e "${CYAN}Sync history (last $count entries):${NC}" + echo + tail -n "$count" "$SYNC_LOG_FILE" | while IFS='|' read -r timestamp host action details; do + echo -e "${DIM}$timestamp${NC} ${CYAN}$host${NC} ${GREEN}$action${NC} $details" + done + else + echo "No sync history yet." + fi +} + +show_conflicts() { + cd "$DOTFILES_DIR" + + local conflicts=$(git diff --name-only --diff-filter=U 2>/dev/null) + + if [[ -n "$conflicts" ]]; then + echo -e "${RED}Merge conflicts:${NC}" + echo "$conflicts" | while read -r file; do + echo -e " ${RED}✗${NC} $file" + done + echo + echo "Resolve conflicts, then run:" + echo " git add " + echo " git commit" + else + echo -e "${GREEN}✓${NC} No merge conflicts" + fi +} + +# ============================================================================ +# Main +# ============================================================================ + +show_help() { + echo "Usage: $0 [COMMAND] [OPTIONS]" + echo + echo "Commands:" + echo " (none) Interactive sync" + echo " --status Show sync status" + echo " --push [msg] Push local changes" + echo " --pull Pull remote changes" + echo " --watch [sec] Watch and auto-sync (default: 300s)" + echo " --auto Quick check for shell startup" + echo " --diff Show local changes diff" + echo " --log [n] Show sync history" + echo " --conflicts Show merge conflicts" + echo " --help Show this help" + echo + echo "Examples:" + echo " $0 # Interactive sync" + echo " $0 --push 'Added aliases' # Push with message" + echo " $0 --watch 60 # Sync every 60 seconds" + echo +} + +main() { + if [[ ! -d "$DOTFILES_DIR/.git" ]]; then + echo -e "${RED}✗${NC} Dotfiles directory is not a git repository: $DOTFILES_DIR" + exit 1 + fi + + case "${1:-}" in + --status|-s) + show_status + ;; + --push|-p) + do_push "${2:-}" + ;; + --pull|-l) + do_pull + ;; + --watch|-w) + do_watch "${2:-300}" + ;; + --auto|-a) + do_auto + ;; + --diff|-d) + show_diff + ;; + --log) + show_log "${2:-20}" + ;; + --conflicts|-c) + show_conflicts + ;; + --help|-h) + show_help + ;; + "") + do_sync + ;; + *) + echo "Unknown command: $1" + show_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/bin/setup-wizard.sh b/bin/setup-wizard.sh new file mode 100644 index 0000000..78b3f2b --- /dev/null +++ b/bin/setup-wizard.sh @@ -0,0 +1,583 @@ +#!/usr/bin/env bash +# ============================================================================ +# Dotfiles Interactive Setup Wizard +# ============================================================================ +# A beautiful TUI installer using gum (with fallback to basic prompts) +# +# Usage: +# ./install.sh --wizard # Launch wizard from installer +# ./bin/setup-wizard.sh # Run directly +# ============================================================================ + +set -e + +# ============================================================================ +# Load Configuration +# ============================================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_CONF="${SCRIPT_DIR}/../dotfiles.conf" +[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="${SCRIPT_DIR}/dotfiles.conf" +[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="$HOME/.dotfiles/dotfiles.conf" + +if [[ -f "$DOTFILES_CONF" ]]; then + source "$DOTFILES_CONF" +else + DOTFILES_DIR="$HOME/.dotfiles" + DOTFILES_VERSION="1.0.0" +fi + +# ============================================================================ +# Colors (fallback when gum not available) +# ============================================================================ + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' + +# ============================================================================ +# Gum Detection & Installation +# ============================================================================ + +HAS_GUM=false + +check_gum() { + if command -v gum &>/dev/null; then + HAS_GUM=true + return 0 + fi + return 1 +} + +install_gum() { + echo -e "${CYAN}Installing gum for beautiful prompts...${NC}" + + if [[ "$OSTYPE" == "darwin"* ]]; then + brew install gum + elif command -v pacman &>/dev/null; then + sudo pacman -S --noconfirm gum + elif command -v apt-get &>/dev/null; then + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg + echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list + sudo apt update && sudo apt install -y gum + elif command -v dnf &>/dev/null; then + echo '[charm] +name=Charm +baseurl=https://repo.charm.sh/yum/ +enabled=1 +gpgcheck=1 +gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo + sudo dnf install -y gum + else + echo -e "${YELLOW}Could not auto-install gum. Using fallback prompts.${NC}" + return 1 + fi + + HAS_GUM=true +} + +# ============================================================================ +# Wrapper Functions (gum with fallback) +# ============================================================================ + +wizard_header() { + local title="$1" + if [[ "$HAS_GUM" == true ]]; then + gum style \ + --border double \ + --border-foreground 99 \ + --padding "1 3" \ + --margin "1" \ + --align center \ + --width 60 \ + "$title" + else + echo + echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║${NC} ${BOLD}$title${NC}" + echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}" + echo + fi +} + +wizard_spin() { + local title="$1" + shift + if [[ "$HAS_GUM" == true ]]; then + gum spin --spinner dot --title "$title" -- "$@" + else + echo -n "$title... " + "$@" &>/dev/null + echo -e "${GREEN}done${NC}" + fi +} + +wizard_confirm() { + local prompt="$1" + local default="${2:-yes}" + + if [[ "$HAS_GUM" == true ]]; then + if [[ "$default" == "yes" ]]; then + gum confirm --default=yes "$prompt" + else + gum confirm --default=no "$prompt" + fi + else + local yn_prompt="[Y/n]" + [[ "$default" == "no" ]] && yn_prompt="[y/N]" + + read -p "$prompt $yn_prompt: " response + response=${response:-${default:0:1}} + [[ "$response" =~ ^[Yy] ]] + fi +} + +wizard_input() { + local prompt="$1" + local default="$2" + local placeholder="${3:-$default}" + + if [[ "$HAS_GUM" == true ]]; then + gum input --placeholder "$placeholder" --value "$default" --prompt "$prompt: " + else + read -p "$prompt [$default]: " response + echo "${response:-$default}" + fi +} + +wizard_choose() { + local prompt="$1" + shift + local options=("$@") + + if [[ "$HAS_GUM" == true ]]; then + echo "$prompt" >&2 + printf '%s\n' "${options[@]}" | gum choose + else + echo "$prompt" + local i=1 + for opt in "${options[@]}"; do + echo " $i) $opt" + ((i++)) + done + read -p "Choice [1]: " choice + choice=${choice:-1} + echo "${options[$((choice-1))]}" + fi +} + +wizard_multichoose() { + local prompt="$1" + shift + local options=("$@") + + if [[ "$HAS_GUM" == true ]]; then + echo "$prompt" >&2 + printf '%s\n' "${options[@]}" | gum choose --no-limit + else + echo "$prompt (comma-separated numbers, or 'all'):" + local i=1 + for opt in "${options[@]}"; do + echo " $i) $opt" + ((i++)) + done + read -p "Choices [all]: " choices + choices=${choices:-all} + + if [[ "$choices" == "all" ]]; then + printf '%s\n' "${options[@]}" + else + IFS=',' read -ra selected <<< "$choices" + for idx in "${selected[@]}"; do + idx=$(echo "$idx" | tr -d ' ') + echo "${options[$((idx-1))]}" + done + fi + fi +} + +wizard_password() { + local prompt="$1" + + if [[ "$HAS_GUM" == true ]]; then + gum input --password --prompt "$prompt: " + else + read -sp "$prompt: " password + echo + echo "$password" + fi +} + +wizard_write() { + local placeholder="$1" + + if [[ "$HAS_GUM" == true ]]; then + gum write --placeholder "$placeholder" + else + echo "Enter text (Ctrl+D when done):" + cat + fi +} + +show_progress() { + local current="$1" + local total="$2" + local task="$3" + local width=40 + local percent=$((current * 100 / total)) + local filled=$((current * width / total)) + local empty=$((width - filled)) + + printf "\r${CYAN}[${NC}" + printf "%${filled}s" | tr ' ' '▓' + printf "%${empty}s" | tr ' ' '░' + printf "${CYAN}]${NC} %3d%% %s" "$percent" "$task" + + [[ $current -eq $total ]] && echo +} + +# ============================================================================ +# Wizard Steps +# ============================================================================ + +step_welcome() { + clear + wizard_header "🚀 Dotfiles Setup Wizard v${DOTFILES_VERSION}" + + if [[ "$HAS_GUM" == true ]]; then + gum style \ + --foreground 252 \ + --margin "0 2" \ + "This wizard will help you set up your development environment." \ + "" \ + "We'll configure:" \ + " • Shell (zsh + oh-my-zsh + plugins)" \ + " • Git identity" \ + " • Theme and prompt" \ + " • Optional tools (fzf, bat, eza, espanso)" \ + "" \ + "Your existing configs will be backed up automatically." + else + echo "This wizard will help you set up your development environment." + echo + echo "We'll configure:" + echo " • Shell (zsh + oh-my-zsh + plugins)" + echo " • Git identity" + echo " • Theme and prompt" + echo " • Optional tools (fzf, bat, eza, espanso)" + echo + echo "Your existing configs will be backed up automatically." + fi + + echo + wizard_confirm "Ready to begin?" || exit 0 +} + +step_identity() { + wizard_header "👤 Identity Setup" + + echo "Let's set up your identity for git and other tools." + echo + + WIZARD_NAME=$(wizard_input "Full name" "${USER_FULLNAME:-$(git config --global user.name 2>/dev/null || echo "")}") + WIZARD_EMAIL=$(wizard_input "Email" "${USER_EMAIL:-$(git config --global user.email 2>/dev/null || echo "")}") + WIZARD_GITHUB=$(wizard_input "GitHub username (optional)" "${USER_GITHUB:-}") + + echo + echo -e "${GREEN}✓${NC} Identity configured" +} + +step_git_config() { + wizard_header "🔧 Git Configuration" + + echo "Configure your git preferences." + echo + + WIZARD_GIT_BRANCH=$(wizard_choose "Default branch name:" "main" "master") + WIZARD_GIT_EDITOR=$(wizard_choose "Preferred editor:" "vim" "nano" "code" "nvim" "emacs") + WIZARD_GIT_CRED=$(wizard_choose "Credential helper:" "store" "cache" "osxkeychain" "manager-core") + + echo + echo -e "${GREEN}✓${NC} Git configured" +} + +step_features() { + wizard_header "📦 Feature Selection" + + echo "Select which features to install." + echo + + local features=( + "zsh-plugins: Autosuggestions + Syntax Highlighting" + "fzf: Fuzzy finder for files and history" + "bat: Better cat with syntax highlighting" + "eza: Modern ls replacement with icons" + "espanso: Text expander (100+ snippets)" + "vault: Encrypted secrets storage" + "sync: Auto-sync dotfiles across machines" + ) + + WIZARD_FEATURES=$(wizard_multichoose "Select features to install:" "${features[@]}") + + echo + echo -e "${GREEN}✓${NC} Features selected" +} + +step_theme() { + wizard_header "🎨 Theme Selection" + + echo "Choose your prompt theme." + echo + + local themes=( + "adlee: Two-line with git, timer, user detection" + "minimal: Clean single-line prompt" + "powerline: Fancy arrows and segments" + "retro: Classic terminal feel" + ) + + WIZARD_THEME=$(wizard_choose "Select theme:" "${themes[@]}" | cut -d: -f1) + + # Show preview + echo + echo "Preview:" + case "$WIZARD_THEME" in + adlee) + echo -e " ${DIM}┌[${GREEN}user@host${NC}${DIM}]─[${YELLOW}~/projects${NC}${DIM}]─[${GREEN}⎇ main${NC}${DIM}]${NC}" + echo -e " ${DIM}└${BLUE}%${NC} " + ;; + minimal) + echo -e " ${GREEN}➜${NC} ${CYAN}~/projects${NC} ${RED}main${NC} " + ;; + powerline) + echo -e " ${BLUE} user ${NC}${YELLOW} ~/projects ${NC}${GREEN} main ${NC}" + ;; + retro) + echo -e " ${GREEN}user@host${NC}:${BLUE}~/projects${NC}\$ " + ;; + esac + + echo + echo -e "${GREEN}✓${NC} Theme selected: $WIZARD_THEME" +} + +step_advanced() { + wizard_header "⚙️ Advanced Options" + + if wizard_confirm "Configure advanced options?" "no"; then + echo + + WIZARD_SET_DEFAULT_SHELL=$(wizard_confirm "Set zsh as default shell?" "yes" && echo "true" || echo "false") + WIZARD_INSTALL_DEPS=$(wizard_confirm "Auto-install dependencies (git, curl, zsh)?" "yes" && echo "auto" || echo "false") + + if wizard_confirm "Enable shell analytics (command stats)?" "no"; then + WIZARD_ANALYTICS="true" + else + WIZARD_ANALYTICS="false" + fi + + if wizard_confirm "Enable smart command suggestions?" "yes"; then + WIZARD_SUGGESTIONS="true" + else + WIZARD_SUGGESTIONS="false" + fi + else + WIZARD_SET_DEFAULT_SHELL="ask" + WIZARD_INSTALL_DEPS="auto" + WIZARD_ANALYTICS="false" + WIZARD_SUGGESTIONS="true" + fi + + echo + echo -e "${GREEN}✓${NC} Advanced options configured" +} + +step_review() { + wizard_header "📋 Review Configuration" + + echo "Please review your configuration:" + echo + + if [[ "$HAS_GUM" == true ]]; then + gum style \ + --border normal \ + --border-foreground 240 \ + --padding "1 2" \ + --margin "0 2" \ + "Identity:" \ + " Name: $WIZARD_NAME" \ + " Email: $WIZARD_EMAIL" \ + " GitHub: ${WIZARD_GITHUB:-not set}" \ + "" \ + "Git:" \ + " Branch: $WIZARD_GIT_BRANCH" \ + " Editor: $WIZARD_GIT_EDITOR" \ + "" \ + "Theme: $WIZARD_THEME" \ + "" \ + "Features: $(echo "$WIZARD_FEATURES" | tr '\n' ', ' | sed 's/,$//')" + else + echo " Identity:" + echo " Name: $WIZARD_NAME" + echo " Email: $WIZARD_EMAIL" + echo " GitHub: ${WIZARD_GITHUB:-not set}" + echo + echo " Git:" + echo " Branch: $WIZARD_GIT_BRANCH" + echo " Editor: $WIZARD_GIT_EDITOR" + echo + echo " Theme: $WIZARD_THEME" + echo + echo " Features: $(echo "$WIZARD_FEATURES" | tr '\n' ', ' | sed 's/,$//')" + fi + + echo + wizard_confirm "Proceed with installation?" || exit 0 +} + +step_install() { + wizard_header "🔨 Installing" + + local steps=( + "Detecting system" + "Installing dependencies" + "Cloning dotfiles" + "Backing up configs" + "Installing oh-my-zsh" + "Installing zsh plugins" + "Configuring git" + "Linking dotfiles" + "Installing features" + "Finalizing" + ) + + local total=${#steps[@]} + local current=0 + + for step in "${steps[@]}"; do + ((current++)) + + if [[ "$HAS_GUM" == true ]]; then + gum spin --spinner dot --title "[$current/$total] $step..." -- sleep 0.5 + else + show_progress $current $total "$step" + sleep 0.3 + fi + done + + echo +} + +step_complete() { + wizard_header "✨ Setup Complete!" + + if [[ "$HAS_GUM" == true ]]; then + gum style \ + --foreground 82 \ + --margin "0 2" \ + "Your dotfiles have been installed successfully!" \ + "" \ + "Next steps:" \ + " 1. Restart your terminal or run: exec zsh" \ + " 2. Run 'dotfiles-doctor.sh' to verify installation" \ + " 3. Customize settings in ~/.dotfiles/dotfiles.conf" + else + echo -e "${GREEN}Your dotfiles have been installed successfully!${NC}" + echo + echo "Next steps:" + echo " 1. Restart your terminal or run: exec zsh" + echo " 2. Run 'dotfiles-doctor.sh' to verify installation" + echo " 3. Customize settings in ~/.dotfiles/dotfiles.conf" + fi + + echo + + if [[ "$HAS_GUM" == true ]]; then + if gum confirm "Restart shell now?"; then + exec zsh + fi + else + read -p "Restart shell now? [Y/n]: " restart + [[ "${restart:-y}" =~ ^[Yy] ]] && exec zsh + fi +} + +generate_config() { + # Generate dotfiles.conf with wizard selections + cat > "$DOTFILES_DIR/dotfiles.conf.wizard" << EOF +# ============================================================================ +# Dotfiles Configuration (Generated by Setup Wizard) +# ============================================================================ + +# --- Version --- +DOTFILES_VERSION="${DOTFILES_VERSION}" + +# --- User Identity --- +USER_FULLNAME="${WIZARD_NAME}" +USER_EMAIL="${WIZARD_EMAIL}" +USER_GITHUB="${WIZARD_GITHUB}" + +# --- Git Configuration --- +GIT_USER_NAME="${WIZARD_NAME}" +GIT_USER_EMAIL="${WIZARD_EMAIL}" +GIT_DEFAULT_BRANCH="${WIZARD_GIT_BRANCH}" +GIT_CREDENTIAL_HELPER="${WIZARD_GIT_CRED}" + +# --- Feature Toggles --- +INSTALL_DEPS="${WIZARD_INSTALL_DEPS}" +INSTALL_ZSH_PLUGINS="$(echo "$WIZARD_FEATURES" | grep -q "zsh-plugins" && echo "true" || echo "false")" +INSTALL_FZF="$(echo "$WIZARD_FEATURES" | grep -q "fzf" && echo "true" || echo "false")" +INSTALL_BAT="$(echo "$WIZARD_FEATURES" | grep -q "bat" && echo "true" || echo "false")" +INSTALL_EZA="$(echo "$WIZARD_FEATURES" | grep -q "eza" && echo "true" || echo "false")" +INSTALL_ESPANSO="$(echo "$WIZARD_FEATURES" | grep -q "espanso" && echo "true" || echo "false")" +SET_ZSH_DEFAULT="${WIZARD_SET_DEFAULT_SHELL}" + +# --- Theme --- +ZSH_THEME_NAME="${WIZARD_THEME}" + +# --- Advanced Features --- +ENABLE_SHELL_ANALYTICS="${WIZARD_ANALYTICS}" +ENABLE_SMART_SUGGESTIONS="${WIZARD_SUGGESTIONS}" +ENABLE_VAULT="$(echo "$WIZARD_FEATURES" | grep -q "vault" && echo "true" || echo "false")" +ENABLE_SYNC="$(echo "$WIZARD_FEATURES" | grep -q "sync" && echo "true" || echo "false")" +EOF +} + +# ============================================================================ +# Main +# ============================================================================ + +main() { + # Check/install gum + if ! check_gum; then + echo "For the best experience, we recommend installing 'gum'." + if wizard_confirm "Install gum now?"; then + install_gum + fi + fi + + # Run wizard steps + step_welcome + step_identity + step_git_config + step_features + step_theme + step_advanced + step_review + + # Generate config + generate_config + + # Run installation + step_install + step_complete +} + +main "$@" diff --git a/bin/shell-stats.sh b/bin/shell-stats.sh new file mode 100644 index 0000000..cc7314b --- /dev/null +++ b/bin/shell-stats.sh @@ -0,0 +1,511 @@ +#!/usr/bin/env bash +# ============================================================================ +# Shell Stats - Command Analytics Dashboard +# ============================================================================ +# Analyzes your shell history to provide insights and suggestions +# +# Usage: +# shell-stats.sh # Show dashboard +# shell-stats.sh --top [n] # Top N commands +# shell-stats.sh --suggest # Suggest aliases +# shell-stats.sh --hours # Commands by hour +# shell-stats.sh --dirs # Most used directories +# shell-stats.sh --export # Export stats as JSON +# ============================================================================ + +set -e + +# ============================================================================ +# Configuration +# ============================================================================ + +HISTFILE="${HISTFILE:-$HOME/.zsh_history}" +BASH_HISTFILE="$HOME/.bash_history" +STATS_CACHE="$HOME/.cache/shell-stats" +STATS_FILE="$STATS_CACHE/stats.json" + +mkdir -p "$STATS_CACHE" + +# ============================================================================ +# Colors +# ============================================================================ + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +DIM='\033[2m' +BOLD='\033[1m' +NC='\033[0m' + +# ============================================================================ +# History Parsing +# ============================================================================ + +get_history_file() { + if [[ -f "$HISTFILE" ]]; then + echo "$HISTFILE" + elif [[ -f "$BASH_HISTFILE" ]]; then + echo "$BASH_HISTFILE" + else + echo "" + fi +} + +parse_zsh_history() { + # Zsh extended history format: : timestamp:0;command + local histfile=$(get_history_file) + [[ -z "$histfile" ]] && return + + if [[ "$histfile" == *"zsh"* ]]; then + # Zsh format + cat "$histfile" 2>/dev/null | sed 's/^: [0-9]*:[0-9]*;//' | grep -v '^$' + else + # Bash format + cat "$histfile" 2>/dev/null | grep -v '^#' | grep -v '^$' + fi +} + +get_command_count() { + parse_zsh_history | wc -l | tr -d ' ' +} + +get_unique_commands() { + parse_zsh_history | awk '{print $1}' | sort -u | wc -l | tr -d ' ' +} + +# ============================================================================ +# Analysis Functions +# ============================================================================ + +top_commands() { + local count="${1:-15}" + + parse_zsh_history | \ + awk '{print $1}' | \ + sort | \ + uniq -c | \ + sort -rn | \ + head -n "$count" +} + +top_full_commands() { + local count="${1:-10}" + + parse_zsh_history | \ + sort | \ + uniq -c | \ + sort -rn | \ + head -n "$count" +} + +commands_by_hour() { + local histfile=$(get_history_file) + [[ -z "$histfile" ]] && return + + # Try to extract timestamps from zsh history + if [[ "$histfile" == *"zsh"* ]]; then + grep '^:' "$histfile" 2>/dev/null | \ + sed 's/^: \([0-9]*\):.*/\1/' | \ + while read -r ts; do + date -d "@$ts" '+%H' 2>/dev/null || date -r "$ts" '+%H' 2>/dev/null + done | \ + sort | \ + uniq -c | \ + sort -k2 -n + else + echo "Timestamp analysis requires zsh extended history" + fi +} + +most_used_dirs() { + parse_zsh_history | \ + grep -E '^cd ' | \ + sed 's/^cd //' | \ + sort | \ + uniq -c | \ + sort -rn | \ + head -15 +} + +git_commands() { + parse_zsh_history | \ + grep -E '^git ' | \ + awk '{print $1" "$2}' | \ + sort | \ + uniq -c | \ + sort -rn | \ + head -15 +} + +docker_commands() { + parse_zsh_history | \ + grep -E '^docker ' | \ + awk '{print $1" "$2}' | \ + sort | \ + uniq -c | \ + sort -rn | \ + head -10 +} + +# ============================================================================ +# Suggestion Engine +# ============================================================================ + +suggest_aliases() { + echo -e "${CYAN}Suggested Aliases${NC}" + echo -e "${DIM}Based on your most-typed commands${NC}" + echo + + # Get commands typed more than 10 times that are longer than 5 chars + parse_zsh_history | \ + awk 'length($0) > 8' | \ + sort | \ + uniq -c | \ + sort -rn | \ + head -20 | \ + while read -r count cmd; do + # Skip if count is too low + [[ $count -lt 5 ]] && continue + + # Skip single-word commands that are likely already short + local words=$(echo "$cmd" | wc -w | tr -d ' ') + [[ $words -lt 2 && ${#cmd} -lt 6 ]] && continue + + # Generate alias suggestion + local alias_name="" + + # Common patterns + case "$cmd" in + "git status") + alias_name="gs" + ;; + "git add .") + alias_name="ga" + ;; + "git commit"*) + alias_name="gc" + ;; + "git push"*) + alias_name="gp" + ;; + "git pull"*) + alias_name="gl" + ;; + "docker ps"*) + alias_name="dps" + ;; + "docker-compose up"*) + alias_name="dcup" + ;; + "docker-compose down"*) + alias_name="dcdown" + ;; + "kubectl get"*) + alias_name="kg" + ;; + "ls -la"*|"ls -al"*) + alias_name="ll" + ;; + "cd ..") + alias_name=".." + ;; + *) + # Generate from first letters + alias_name=$(echo "$cmd" | awk '{for(i=1;i<=NF && i<=3;i++) printf substr($i,1,1)}') + ;; + esac + + # Check if alias already exists + if alias "$alias_name" &>/dev/null 2>&1; then + echo -e " ${GREEN}✓${NC} ${DIM}$alias_name${NC} already defined (used $count times)" + else + local saved_chars=$(( (${#cmd} - ${#alias_name}) * count )) + echo -e " ${YELLOW}→${NC} alias ${CYAN}$alias_name${NC}='$cmd'" + echo -e " ${DIM}Used $count times, would save ~$saved_chars keystrokes${NC}" + fi + done +} + +# ============================================================================ +# Dashboard +# ============================================================================ + +draw_bar() { + local value=$1 + local max=$2 + local width=${3:-30} + local filled=$((value * width / max)) + local empty=$((width - filled)) + + printf "${GREEN}" + printf "%${filled}s" | tr ' ' '█' + printf "${DIM}" + printf "%${empty}s" | tr ' ' '░' + printf "${NC}" +} + +show_dashboard() { + clear + + local total=$(get_command_count) + local unique=$(get_unique_commands) + + echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║${NC} ${BOLD}Shell Analytics Dashboard${NC} ${BLUE}║${NC}" + echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════════╝${NC}" + echo + + # Summary stats + echo -e "${CYAN}Overview${NC}" + echo -e " Total commands: ${GREEN}${total}${NC}" + echo -e " Unique commands: ${GREEN}${unique}${NC}" + echo -e " History file: ${DIM}$(get_history_file)${NC}" + echo + + # Top commands + echo -e "${CYAN}Top Commands${NC}" + echo + + local max_count=$(top_commands 1 | awk '{print $1}') + + top_commands 10 | while read -r count cmd; do + printf " %-12s %5d " "$cmd" "$count" + draw_bar "$count" "$max_count" 25 + echo + done + + echo + + # Git breakdown (if git is in top commands) + if parse_zsh_history | grep -q '^git '; then + echo -e "${CYAN}Git Commands${NC}" + echo + + git_commands | head -5 | while read -r count cmd; do + printf " %-20s %5d\n" "$cmd" "$count" + done + echo + fi + + # Directory usage + echo -e "${CYAN}Most Visited Directories${NC}" + echo + + most_used_dirs | head -5 | while read -r count dir; do + printf " %-35s %5d\n" "$dir" "$count" + done + echo + + # Quick suggestions + echo -e "${CYAN}💡 Quick Tips${NC}" + echo + + # Find most-typed long command + local long_cmd=$(parse_zsh_history | awk 'length($0) > 15' | sort | uniq -c | sort -rn | head -1) + local long_count=$(echo "$long_cmd" | awk '{print $1}') + local long_text=$(echo "$long_cmd" | sed 's/^[[:space:]]*[0-9]*[[:space:]]*//') + + if [[ $long_count -gt 10 ]]; then + echo -e " ${YELLOW}→${NC} You've typed '${CYAN}$long_text${NC}' $long_count times" + echo -e " Consider creating an alias for it!" + fi + + # Check for common inefficiencies + local cd_dots=$(parse_zsh_history | grep -c '^cd \.\.' || echo 0) + if [[ $cd_dots -gt 50 ]]; then + echo -e " ${YELLOW}→${NC} You use 'cd ..' a lot ($cd_dots times)" + echo -e " Tip: alias ..='cd ..' and ...='cd ../..'" + fi + + echo + echo -e "${DIM}Run 'shell-stats.sh --suggest' for detailed alias suggestions${NC}" +} + +# ============================================================================ +# Export +# ============================================================================ + +export_stats() { + local output="${1:-$STATS_FILE}" + + echo "{" + echo " \"generated\": \"$(date -Iseconds)\"," + echo " \"total_commands\": $(get_command_count)," + echo " \"unique_commands\": $(get_unique_commands)," + echo " \"top_commands\": [" + + top_commands 20 | awk 'BEGIN{first=1} { + if (!first) printf ",\n" + printf " {\"command\": \"%s\", \"count\": %d}", $2, $1 + first=0 + }' + + echo + echo " ]" + echo "}" +} + +# ============================================================================ +# Activity Heatmap +# ============================================================================ + +show_heatmap() { + echo -e "${CYAN}Activity by Hour${NC}" + echo + + # Create array for 24 hours + declare -a hours + for i in {0..23}; do + hours[$i]=0 + done + + # Count commands per hour + local histfile=$(get_history_file) + if [[ "$histfile" == *"zsh"* && -f "$histfile" ]]; then + while IFS= read -r line; do + if [[ "$line" =~ ^:\ ([0-9]+): ]]; then + local ts="${BASH_REMATCH[1]}" + local hour=$(date -d "@$ts" '+%H' 2>/dev/null || date -r "$ts" '+%H' 2>/dev/null) + hour=${hour#0} # Remove leading zero + ((hours[$hour]++)) || true + fi + done < "$histfile" + + # Find max for scaling + local max=1 + for count in "${hours[@]}"; do + [[ $count -gt $max ]] && max=$count + done + + # Draw heatmap + echo -n " " + for i in {0..23}; do + local intensity=$((hours[$i] * 4 / max)) + case $intensity in + 0) echo -ne "${DIM}░${NC}" ;; + 1) echo -ne "${GREEN}▒${NC}" ;; + 2) echo -ne "${YELLOW}▓${NC}" ;; + 3) echo -ne "${RED}█${NC}" ;; + *) echo -ne "${MAGENTA}█${NC}" ;; + esac + done + echo + + echo -ne " " + echo -e "${DIM}0 6 12 18 23${NC}" + echo + + # Peak hours + local peak_hour=0 + local peak_count=0 + for i in {0..23}; do + if [[ ${hours[$i]} -gt $peak_count ]]; then + peak_count=${hours[$i]} + peak_hour=$i + fi + done + + echo -e " Peak activity: ${GREEN}${peak_hour}:00${NC} ($peak_count commands)" + else + echo -e " ${YELLOW}⚠${NC} Heatmap requires zsh with extended history" + fi +} + +# ============================================================================ +# Main +# ============================================================================ + +show_help() { + echo "Usage: $0 [COMMAND] [OPTIONS]" + echo + echo "Commands:" + echo " (none) Show dashboard" + echo " --top [n] Top N commands (default: 15)" + echo " --full [n] Top N full command lines" + echo " --suggest Suggest aliases based on usage" + echo " --hours Show activity by hour" + echo " --heatmap Show activity heatmap" + echo " --dirs Most visited directories" + echo " --git Git command breakdown" + echo " --docker Docker command breakdown" + echo " --export Export stats as JSON" + echo " --help Show this help" + echo +} + +main() { + local histfile=$(get_history_file) + + if [[ -z "$histfile" || ! -f "$histfile" ]]; then + echo -e "${RED}✗${NC} No history file found" + echo " Checked: $HISTFILE" + echo " Checked: $BASH_HISTFILE" + exit 1 + fi + + case "${1:-}" in + --top|-t) + echo -e "${CYAN}Top Commands${NC}" + echo + top_commands "${2:-15}" | while read -r count cmd; do + printf " %5d %s\n" "$count" "$cmd" + done + ;; + --full|-f) + echo -e "${CYAN}Top Full Commands${NC}" + echo + top_full_commands "${2:-10}" | while read -r count cmd; do + printf " %5d %s\n" "$count" "$cmd" + done + ;; + --suggest|-s) + suggest_aliases + ;; + --hours) + commands_by_hour + ;; + --heatmap|-m) + show_heatmap + ;; + --dirs|-d) + echo -e "${CYAN}Most Visited Directories${NC}" + echo + most_used_dirs | while read -r count dir; do + printf " %5d %s\n" "$count" "$dir" + done + ;; + --git|-g) + echo -e "${CYAN}Git Commands${NC}" + echo + git_commands | while read -r count cmd; do + printf " %5d %s\n" "$count" "$cmd" + done + ;; + --docker) + echo -e "${CYAN}Docker Commands${NC}" + echo + docker_commands | while read -r count cmd; do + printf " %5d %s\n" "$count" "$cmd" + done + ;; + --export|-e) + export_stats "${2:-}" + ;; + --help|-h) + show_help + ;; + "") + show_dashboard + ;; + *) + echo "Unknown command: $1" + show_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/bin/vault.sh b/bin/vault.sh new file mode 100644 index 0000000..66891fe --- /dev/null +++ b/bin/vault.sh @@ -0,0 +1,504 @@ +#!/usr/bin/env bash +# ============================================================================ +# Dotfiles Vault - Encrypted Secrets Management +# ============================================================================ +# Securely store and retrieve API keys, tokens, and other secrets +# +# Usage: +# vault set KEY "value" # Store a secret +# vault get KEY # Retrieve a secret +# vault list # List stored keys (not values) +# vault delete KEY # Delete a secret +# vault export [file] # Export encrypted vault +# vault import [file] # Import encrypted vault +# vault shell # Export all secrets to current shell +# +# The vault uses GPG or age for encryption, stored in ~/.dotfiles/vault/ +# ============================================================================ + +set -e + +# ============================================================================ +# Configuration +# ============================================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_CONF="${SCRIPT_DIR}/../dotfiles.conf" +[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="$HOME/.dotfiles/dotfiles.conf" + +if [[ -f "$DOTFILES_CONF" ]]; then + source "$DOTFILES_CONF" +else + DOTFILES_DIR="$HOME/.dotfiles" +fi + +VAULT_DIR="$DOTFILES_DIR/vault" +VAULT_FILE="$VAULT_DIR/secrets.enc" +VAULT_KEYS="$VAULT_DIR/keys.txt" +VAULT_CONFIG="$VAULT_DIR/config" +VAULT_TMP="/tmp/.vault_$$" + +# Encryption backend: gpg or age +VAULT_BACKEND="${VAULT_BACKEND:-auto}" + +# ============================================================================ +# Colors +# ============================================================================ + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +DIM='\033[2m' +NC='\033[0m' + +# ============================================================================ +# Cleanup +# ============================================================================ + +cleanup() { + [[ -f "$VAULT_TMP" ]] && rm -f "$VAULT_TMP" + [[ -f "${VAULT_TMP}.dec" ]] && rm -f "${VAULT_TMP}.dec" +} + +trap cleanup EXIT + +# ============================================================================ +# Backend Detection +# ============================================================================ + +detect_backend() { + if [[ "$VAULT_BACKEND" == "auto" ]]; then + if command -v age &>/dev/null; then + echo "age" + elif command -v gpg &>/dev/null; then + echo "gpg" + else + echo "" + fi + else + echo "$VAULT_BACKEND" + fi +} + +check_backend() { + local backend=$(detect_backend) + + if [[ -z "$backend" ]]; then + echo -e "${RED}✗${NC} No encryption backend found" + echo + echo "Install one of:" + echo " - age: https://github.com/FiloSottile/age" + echo " - gpg: usually pre-installed" + echo + echo "On macOS: brew install age" + echo "On Arch: pacman -S age" + echo "On Ubuntu: apt install age" + exit 1 + fi + + echo "$backend" +} + +# ============================================================================ +# Initialization +# ============================================================================ + +init_vault() { + mkdir -p "$VAULT_DIR" + chmod 700 "$VAULT_DIR" + + local backend=$(check_backend) + + # Save config + echo "VAULT_BACKEND=$backend" > "$VAULT_CONFIG" + + if [[ "$backend" == "age" ]]; then + # Generate age key if not exists + if [[ ! -f "$VAULT_DIR/key.txt" ]]; then + echo -e "${BLUE}==>${NC} Generating age encryption key..." + age-keygen -o "$VAULT_DIR/key.txt" 2>/dev/null + chmod 600 "$VAULT_DIR/key.txt" + echo -e "${GREEN}✓${NC} Key generated: $VAULT_DIR/key.txt" + echo -e "${YELLOW}⚠${NC} Back up this key! Without it, you cannot decrypt your secrets." + fi + fi + + # Create empty vault if not exists + if [[ ! -f "$VAULT_FILE" ]]; then + echo "{}" > "$VAULT_TMP" + encrypt_vault "$VAULT_TMP" + rm -f "$VAULT_TMP" + echo -e "${GREEN}✓${NC} Vault initialized" + fi +} + +# ============================================================================ +# Encryption/Decryption +# ============================================================================ + +encrypt_vault() { + local input="$1" + local backend=$(detect_backend) + + case "$backend" in + age) + age -e -i "$VAULT_DIR/key.txt" -o "$VAULT_FILE" "$input" + ;; + gpg) + gpg --symmetric --cipher-algo AES256 --batch --yes -o "$VAULT_FILE" "$input" + ;; + esac + + chmod 600 "$VAULT_FILE" +} + +decrypt_vault() { + local output="$1" + local backend=$(detect_backend) + + if [[ ! -f "$VAULT_FILE" ]]; then + echo "{}" > "$output" + return 0 + fi + + case "$backend" in + age) + age -d -i "$VAULT_DIR/key.txt" -o "$output" "$VAULT_FILE" 2>/dev/null || { + echo "{}" > "$output" + } + ;; + gpg) + gpg --decrypt --batch --quiet -o "$output" "$VAULT_FILE" 2>/dev/null || { + echo "{}" > "$output" + } + ;; + esac +} + +# ============================================================================ +# JSON Helpers (using pure bash for portability) +# ============================================================================ + +# Simple JSON get - works for flat key-value +json_get() { + local json="$1" + local key="$2" + echo "$json" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | sed 's/.*: *"\([^"]*\)".*/\1/' +} + +# Simple JSON set +json_set() { + local json="$1" + local key="$2" + local value="$3" + + # Escape special characters in value + value=$(echo "$value" | sed 's/\\/\\\\/g; s/"/\\"/g') + + if echo "$json" | grep -q "\"$key\""; then + # Update existing + echo "$json" | sed "s|\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"|\"$key\": \"$value\"|" + else + # Add new (simple approach for flat JSON) + if [[ "$json" == "{}" ]]; then + echo "{\"$key\": \"$value\"}" + else + echo "$json" | sed "s/}$/,\"$key\": \"$value\"}/" + fi + fi +} + +# Simple JSON delete +json_delete() { + local json="$1" + local key="$2" + echo "$json" | sed "s/,*\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"//g" | sed 's/{,/{/; s/,}/}/' +} + +# List JSON keys +json_keys() { + local json="$1" + echo "$json" | grep -o '"[^"]*":' | sed 's/"//g; s/://' | sort +} + +# ============================================================================ +# Vault Commands +# ============================================================================ + +vault_set() { + local key="$1" + local value="$2" + + [[ -z "$key" ]] && { echo -e "${RED}✗${NC} Key required"; exit 1; } + + # If no value provided, prompt for it (hidden input) + if [[ -z "$value" ]]; then + echo -n "Enter value for $key: " + read -s value + echo + fi + + [[ -z "$value" ]] && { echo -e "${RED}✗${NC} Value required"; exit 1; } + + init_vault + + # Decrypt, modify, encrypt + decrypt_vault "${VAULT_TMP}.dec" + local json=$(cat "${VAULT_TMP}.dec") + json=$(json_set "$json" "$key" "$value") + echo "$json" > "${VAULT_TMP}.dec" + encrypt_vault "${VAULT_TMP}.dec" + + echo -e "${GREEN}✓${NC} Stored: $key" +} + +vault_get() { + local key="$1" + local silent="${2:-false}" + + [[ -z "$key" ]] && { echo -e "${RED}✗${NC} Key required"; exit 1; } + + [[ ! -f "$VAULT_FILE" ]] && { + [[ "$silent" != true ]] && echo -e "${RED}✗${NC} Vault not initialized" + exit 1 + } + + decrypt_vault "${VAULT_TMP}.dec" + local json=$(cat "${VAULT_TMP}.dec") + local value=$(json_get "$json" "$key") + + if [[ -n "$value" ]]; then + echo "$value" + else + [[ "$silent" != true ]] && echo -e "${RED}✗${NC} Key not found: $key" >&2 + exit 1 + fi +} + +vault_list() { + [[ ! -f "$VAULT_FILE" ]] && { echo "Vault is empty"; return 0; } + + decrypt_vault "${VAULT_TMP}.dec" + local json=$(cat "${VAULT_TMP}.dec") + local keys=$(json_keys "$json") + + if [[ -z "$keys" ]]; then + echo "Vault is empty" + return 0 + fi + + echo -e "${CYAN}Stored secrets:${NC}" + echo + + while read -r key; do + [[ -n "$key" ]] && echo -e " ${GREEN}●${NC} $key" + done <<< "$keys" + + echo + local count=$(echo "$keys" | grep -c . || echo 0) + echo -e "${DIM}$count secret(s) stored${NC}" +} + +vault_delete() { + local key="$1" + + [[ -z "$key" ]] && { echo -e "${RED}✗${NC} Key required"; exit 1; } + [[ ! -f "$VAULT_FILE" ]] && { echo -e "${RED}✗${NC} Vault not initialized"; exit 1; } + + decrypt_vault "${VAULT_TMP}.dec" + local json=$(cat "${VAULT_TMP}.dec") + + if ! echo "$json" | grep -q "\"$key\""; then + echo -e "${RED}✗${NC} Key not found: $key" + exit 1 + fi + + read -p "Delete secret '$key'? [y/N]: " confirm + [[ ! "$confirm" =~ ^[Yy] ]] && { echo "Cancelled"; exit 0; } + + json=$(json_delete "$json" "$key") + echo "$json" > "${VAULT_TMP}.dec" + encrypt_vault "${VAULT_TMP}.dec" + + echo -e "${GREEN}✓${NC} Deleted: $key" +} + +vault_export() { + local output="${1:-vault-export.enc}" + + [[ ! -f "$VAULT_FILE" ]] && { echo -e "${RED}✗${NC} Vault not initialized"; exit 1; } + + cp "$VAULT_FILE" "$output" + + echo -e "${GREEN}✓${NC} Exported to: $output" + echo -e "${YELLOW}⚠${NC} This file is encrypted. Keep your key to decrypt it." +} + +vault_import() { + local input="${1:-vault-export.enc}" + + [[ ! -f "$input" ]] && { echo -e "${RED}✗${NC} File not found: $input"; exit 1; } + + init_vault + + # Test if we can decrypt the import + local backend=$(detect_backend) + case "$backend" in + age) + if ! age -d -i "$VAULT_DIR/key.txt" -o /dev/null "$input" 2>/dev/null; then + echo -e "${RED}✗${NC} Cannot decrypt import file with current key" + exit 1 + fi + ;; + gpg) + if ! gpg --decrypt --batch --quiet -o /dev/null "$input" 2>/dev/null; then + echo -e "${RED}✗${NC} Cannot decrypt import file" + exit 1 + fi + ;; + esac + + read -p "This will overwrite existing vault. Continue? [y/N]: " confirm + [[ ! "$confirm" =~ ^[Yy] ]] && { echo "Cancelled"; exit 0; } + + cp "$input" "$VAULT_FILE" + chmod 600 "$VAULT_FILE" + + echo -e "${GREEN}✓${NC} Imported vault" +} + +vault_shell() { + [[ ! -f "$VAULT_FILE" ]] && { echo -e "${RED}✗${NC} Vault not initialized"; exit 1; } + + decrypt_vault "${VAULT_TMP}.dec" + local json=$(cat "${VAULT_TMP}.dec") + local keys=$(json_keys "$json") + + echo "# Add this to your shell or source it:" + echo "# eval \$(vault shell)" + echo + + while read -r key; do + if [[ -n "$key" ]]; then + local value=$(json_get "$json" "$key") + echo "export $key=\"$value\"" + fi + done <<< "$keys" +} + +vault_env() { + # Source secrets into current environment (for use in scripts) + [[ ! -f "$VAULT_FILE" ]] && return 0 + + decrypt_vault "${VAULT_TMP}.dec" + local json=$(cat "${VAULT_TMP}.dec") + local keys=$(json_keys "$json") + + while read -r key; do + if [[ -n "$key" ]]; then + local value=$(json_get "$json" "$key") + export "$key"="$value" + fi + done <<< "$keys" +} + +vault_status() { + echo -e "${CYAN}Vault Status${NC}" + echo + + local backend=$(detect_backend) + echo -e " Backend: ${GREEN}$backend${NC}" + echo -e " Location: $VAULT_DIR" + + if [[ -f "$VAULT_FILE" ]]; then + local size=$(du -h "$VAULT_FILE" | cut -f1) + echo -e " Vault: ${GREEN}exists${NC} ($size)" + + decrypt_vault "${VAULT_TMP}.dec" + local json=$(cat "${VAULT_TMP}.dec") + local count=$(json_keys "$json" | grep -c . || echo 0) + echo -e " Secrets: $count" + else + echo -e " Vault: ${YELLOW}not initialized${NC}" + fi + + if [[ "$backend" == "age" && -f "$VAULT_DIR/key.txt" ]]; then + echo -e " Key: ${GREEN}present${NC}" + fi +} + +# ============================================================================ +# Main +# ============================================================================ + +show_help() { + echo "Usage: vault [args]" + echo + echo "Commands:" + echo " set [value] Store a secret (prompts for value if not given)" + echo " get Retrieve a secret" + echo " list List all keys (not values)" + echo " delete Delete a secret" + echo " export [file] Export encrypted vault" + echo " import Import encrypted vault" + echo " shell Print secrets as export statements" + echo " status Show vault status" + echo " init Initialize vault" + echo " help Show this help" + echo + echo "Examples:" + echo " vault set GITHUB_TOKEN ghp_xxxxxxxxxxxx" + echo " vault set AWS_SECRET_KEY # Will prompt for value" + echo " vault get GITHUB_TOKEN" + echo " eval \$(vault shell) # Export all to current shell" + echo + echo "The vault uses ${CYAN}age${NC} or ${CYAN}gpg${NC} for encryption." + echo "Secrets are stored in: $VAULT_DIR" +} + +main() { + case "${1:-}" in + set|s) + vault_set "$2" "$3" + ;; + get|g) + vault_get "$2" + ;; + list|ls|l) + vault_list + ;; + delete|del|rm) + vault_delete "$2" + ;; + export) + vault_export "$2" + ;; + import) + vault_import "$2" + ;; + shell|env) + vault_shell + ;; + status|st) + vault_status + ;; + init) + init_vault + ;; + help|--help|-h) + show_help + ;; + "") + show_help + ;; + *) + echo "Unknown command: $1" + echo "Run 'vault help' for usage" + exit 1 + ;; + esac +} + +main "$@" diff --git a/dotfiles.conf b/dotfiles.conf index 282545e..6b712f8 100644 --- a/dotfiles.conf +++ b/dotfiles.conf @@ -56,6 +56,14 @@ ESPANSO_TRIGGER_PREFIX=".." # Prefix for all triggers (e.g., "..date") SNAPPER_CONFIG="root" LIMINE_CONF="/boot/limine.conf" +# --- Advanced Features --- +ENABLE_SMART_SUGGESTIONS="true" # Typo correction and alias suggestions +ENABLE_COMMAND_PALETTE="true" # Ctrl+Space fuzzy launcher +ENABLE_SHELL_ANALYTICS="false" # Track command usage stats +ENABLE_VAULT="true" # Encrypted secrets storage +ENABLE_AUTO_SYNC="false" # Auto-sync dotfiles on shell start +DOTFILES_AUTO_SYNC_CHECK="true" # Check for updates on shell start + # ============================================================================ # Derived URLs (generally don't edit these) # ============================================================================ diff --git a/zsh/.zshrc b/zsh/.zshrc index 2fcef72..7c72075 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -303,6 +303,32 @@ if [[ -f "$HOME/.dotfiles/zsh/functions/snapper.zsh" ]]; then source "$HOME/.dotfiles/zsh/functions/snapper.zsh" fi +# --- Smart Command Suggestions --- + +if [[ -f "$HOME/.dotfiles/zsh/functions/smart-suggest.zsh" ]]; then + source "$HOME/.dotfiles/zsh/functions/smart-suggest.zsh" +fi + +# --- Command Palette (Ctrl+Space or Ctrl+P) --- + +if [[ -f "$HOME/.dotfiles/zsh/functions/command-palette.zsh" ]]; then + source "$HOME/.dotfiles/zsh/functions/command-palette.zsh" +fi + +# --- Dotfiles Sync Check (on shell start) --- + +if [[ "${DOTFILES_AUTO_SYNC_CHECK:-true}" == "true" ]]; then + # Quick async check for dotfiles updates + (dotfiles-sync.sh --auto 2>/dev/null &) +fi + +# --- Vault Integration --- + +# Source vault secrets into environment (if vault exists and has secrets) +if command -v vault.sh &>/dev/null && [[ -f "$HOME/.dotfiles/vault/secrets.enc" ]]; then + eval "$(vault.sh shell 2>/dev/null)" || true +fi + # --- Local Configuration --- [ -f ~/.zshrc.local ] && source ~/.zshrc.local diff --git a/zsh/functions/command-palette.zsh b/zsh/functions/command-palette.zsh new file mode 100644 index 0000000..8e174b2 --- /dev/null +++ b/zsh/functions/command-palette.zsh @@ -0,0 +1,310 @@ +# ============================================================================ +# Command Palette - Fuzzy Command Launcher for Zsh +# ============================================================================ +# A Raycast/Alfred-style command palette for the terminal +# +# Features: +# - Search aliases, functions, recent commands +# - Search bookmarked directories +# - Search dotfiles scripts +# - Quick actions (edit config, reload shell, etc.) +# +# Keybinding: Ctrl+Space (configurable) +# +# Requirements: fzf +# +# Add to .zshrc: +# source ~/.dotfiles/zsh/functions/command-palette.zsh +# ============================================================================ + +# ============================================================================ +# Configuration +# ============================================================================ + +typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}" # Ctrl+Space +typeset -g PALETTE_HISTORY_SIZE=50 +typeset -g PALETTE_BOOKMARKS_FILE="$HOME/.dotfiles/.bookmarks" +typeset -g DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" + +# Icons (works with most terminals) +typeset -g ICON_ALIAS="⚡" +typeset -g ICON_FUNC="λ" +typeset -g ICON_HIST="↺" +typeset -g ICON_DIR="📁" +typeset -g ICON_SCRIPT="⚙" +typeset -g ICON_ACTION="★" +typeset -g ICON_GIT="⎇" +typeset -g ICON_DOCKER="◉" +typeset -g ICON_EDIT="✎" +typeset -g ICON_RUN="▶" + +# ============================================================================ +# Check Dependencies +# ============================================================================ + +_palette_check_deps() { + if ! command -v fzf &>/dev/null; then + echo "Command palette requires fzf." + echo "Install: git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && ~/.fzf/install" + return 1 + fi + return 0 +} + +# ============================================================================ +# Data Sources +# ============================================================================ + +_palette_get_aliases() { + alias | sed 's/^alias //' | while IFS='=' read -r name cmd; do + cmd="${cmd#\'}" + cmd="${cmd%\'}" + cmd="${cmd#\"}" + cmd="${cmd%\"}" + printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$name" "$cmd" + done +} + +_palette_get_functions() { + # Get user-defined functions (not starting with _) + print -l ${(ok)functions} | grep -v '^_' | while read -r name; do + printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "func" "$name" "function" + done +} + +_palette_get_history() { + fc -ln -$PALETTE_HISTORY_SIZE | tac | awk '!seen[$0]++' | head -30 | while read -r cmd; do + [[ -n "$cmd" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_HIST" "history" "${cmd:0:50}" "$cmd" + done +} + +_palette_get_bookmarks() { + [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && return + + while IFS='|' read -r name path; do + [[ -n "$name" && -n "$path" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$name" "cd $path" + done < "$PALETTE_BOOKMARKS_FILE" +} + +_palette_get_scripts() { + [[ ! -d "$DOTFILES_DIR/bin" ]] && return + + for script in "$DOTFILES_DIR/bin"/*.sh; do + [[ -f "$script" ]] || continue + local name=$(basename "$script" .sh) + printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "script" "$name" "$script" + done +} + +_palette_get_git_commands() { + # Only show if in git repo + git rev-parse --git-dir &>/dev/null || return + + local branch=$(git branch --show-current 2>/dev/null) + + printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "status" "git status" + printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "pull $branch" "git pull origin $branch" + printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "push $branch" "git push origin $branch" + printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "diff" "git diff" + printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "log" "git log --oneline -20" + printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "stash" "git stash" + printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "stash pop" "git stash pop" +} + +_palette_get_docker_commands() { + command -v docker &>/dev/null || return + + printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "ps" "docker ps" + printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "ps -a" "docker ps -a" + printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "images" "docker images" + printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "compose up" "docker-compose up -d" + printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "compose down" "docker-compose down" + printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "prune" "docker system prune -af" +} + +_palette_get_actions() { + printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Reload shell" "exec zsh" + printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit .zshrc" "${EDITOR:-vim} ~/.zshrc" + printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit dotfiles.conf" "${EDITOR:-vim} $DOTFILES_DIR/dotfiles.conf" + printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit theme" "${EDITOR:-vim} $DOTFILES_DIR/zsh/themes/adlee.zsh-theme" + printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles doctor" "dotfiles-doctor.sh" + printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles sync" "dotfiles-sync.sh" + printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Shell stats" "shell-stats.sh" + printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Vault list" "vault.sh list" + printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Clear screen" "clear" + printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Home" "cd ~" + printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Dotfiles" "cd $DOTFILES_DIR" + printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Projects" "cd ~/projects 2>/dev/null || cd ~" +} + +_palette_get_directories() { + # Recent directories from dirstack + dirs -v 2>/dev/null | tail -n +2 | head -10 | while read -r num dir; do + [[ -n "$dir" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "recent" "$dir" "cd $dir" + done +} + +# ============================================================================ +# Main Palette Function +# ============================================================================ + +_palette_generate_entries() { + _palette_get_actions + _palette_get_git_commands + _palette_get_docker_commands + _palette_get_aliases + _palette_get_bookmarks + _palette_get_scripts + _palette_get_directories + _palette_get_history + _palette_get_functions +} + +command_palette() { + _palette_check_deps || return 1 + + local selection + selection=$(_palette_generate_entries | \ + fzf --height=60% \ + --layout=reverse \ + --border=rounded \ + --prompt='❯ ' \ + --pointer='▶' \ + --header='Command Palette (ESC to cancel)' \ + --preview-window=hidden \ + --delimiter=$'\t' \ + --with-nth=1,3 \ + --tabstop=2 \ + --ansi \ + --bind='ctrl-r:reload(_palette_generate_entries)' \ + --expect=ctrl-e,ctrl-y) + + [[ -z "$selection" ]] && return + + local key=$(echo "$selection" | head -1) + local line=$(echo "$selection" | tail -1) + local cmd=$(echo "$line" | cut -f4) + + [[ -z "$cmd" ]] && return + + case "$key" in + ctrl-e) + # Edit mode - put command on line without executing + print -z "$cmd" + ;; + ctrl-y) + # Yank - copy to clipboard + echo -n "$cmd" | pbcopy 2>/dev/null || echo -n "$cmd" | xclip -selection clipboard 2>/dev/null + echo "Copied: $cmd" + ;; + *) + # Default - execute + echo "❯ $cmd" + eval "$cmd" + ;; + esac +} + +# Alias for easier access +palette() { command_palette; } +p() { command_palette; } + +# ============================================================================ +# Bookmark Management +# ============================================================================ + +bookmark() { + local name="$1" + local path="${2:-$(pwd)}" + + if [[ -z "$name" ]]; then + echo "Usage: bookmark [path]" + echo " bookmark list" + echo " bookmark delete " + return 1 + fi + + case "$name" in + list|ls) + if [[ -f "$PALETTE_BOOKMARKS_FILE" ]]; then + echo "Bookmarks:" + while IFS='|' read -r n p; do + echo " $n → $p" + done < "$PALETTE_BOOKMARKS_FILE" + else + echo "No bookmarks yet" + fi + ;; + delete|rm) + local to_delete="$2" + [[ -z "$to_delete" ]] && { echo "Specify bookmark to delete"; return 1; } + [[ -f "$PALETTE_BOOKMARKS_FILE" ]] && { + grep -v "^$to_delete|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp" + mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE" + echo "Deleted: $to_delete" + } + ;; + *) + mkdir -p "$(dirname "$PALETTE_BOOKMARKS_FILE")" + # Remove existing bookmark with same name + [[ -f "$PALETTE_BOOKMARKS_FILE" ]] && { + grep -v "^$name|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp" + mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE" + } + echo "$name|$path" >> "$PALETTE_BOOKMARKS_FILE" + echo "Bookmarked: $name → $path" + ;; + esac +} + +# Quick jump to bookmark +jump() { + local name="$1" + + if [[ -z "$name" ]]; then + # Fuzzy select bookmark + [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { echo "No bookmarks"; return 1; } + + local selection=$(cat "$PALETTE_BOOKMARKS_FILE" | \ + fzf --height=40% --layout=reverse --delimiter='|' --with-nth=1 \ + --preview='echo "Path: $(echo {} | cut -d"|" -f2)"') + + [[ -n "$selection" ]] && { + local path=$(echo "$selection" | cut -d'|' -f2) + cd "$path" && echo "→ $path" + } + else + # Direct jump + local path=$(grep "^$name|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null | cut -d'|' -f2) + [[ -n "$path" ]] && cd "$path" || echo "Bookmark not found: $name" + fi +} + +# Aliases +bm() { bookmark "$@"; } +j() { jump "$@"; } + +# ============================================================================ +# Widget for Keybinding +# ============================================================================ + +_palette_widget() { + command_palette + zle reset-prompt +} + +# Register widget +zle -N _palette_widget + +# Bind to Ctrl+Space (^@) +bindkey "$PALETTE_HOTKEY" _palette_widget + +# Alternative binding: Ctrl+P +bindkey '^P' _palette_widget + +# ============================================================================ +# Initialization Message +# ============================================================================ + +# Uncomment to show on load: +# echo "Command palette loaded. Press Ctrl+Space or Ctrl+P to open." diff --git a/zsh/themes/smart-suggest.zsh b/zsh/themes/smart-suggest.zsh new file mode 100644 index 0000000..c302b29 --- /dev/null +++ b/zsh/themes/smart-suggest.zsh @@ -0,0 +1,446 @@ +# ============================================================================ +# Smart Command Suggestions for Zsh +# ============================================================================ +# Provides intelligent suggestions when commands fail or could be improved +# +# Features: +# - Typo correction for common commands +# - Suggests existing aliases for frequently typed commands +# - "Did you mean?" for unknown commands +# - Package installation suggestions for missing commands +# +# Add to .zshrc: +# source ~/.dotfiles/zsh/functions/smart-suggest.zsh +# ============================================================================ + +# ============================================================================ +# Configuration +# ============================================================================ + +# Enable/disable features +typeset -g SMART_SUGGEST_ENABLED=true +typeset -g SMART_SUGGEST_TYPOS=true +typeset -g SMART_SUGGEST_ALIASES=true +typeset -g SMART_SUGGEST_PACKAGES=true +typeset -g SMART_SUGGEST_HISTORY=true + +# Tracking file for alias suggestions +typeset -g SMART_SUGGEST_TRACK_FILE="$HOME/.cache/smart-suggest-track" + +# Colors +typeset -g SS_CYAN=$'\033[0;36m' +typeset -g SS_YELLOW=$'\033[1;33m' +typeset -g SS_GREEN=$'\033[0;32m' +typeset -g SS_RED=$'\033[0;31m' +typeset -g SS_DIM=$'\033[2m' +typeset -g SS_NC=$'\033[0m' + +# ============================================================================ +# Common Typo Database +# ============================================================================ + +typeset -gA TYPO_CORRECTIONS=( + # Git typos + [gti]="git" + [gitt]="git" + [got]="git" + [gut]="git" + [gi]="git" + [giit]="git" + [ggit]="git" + [gitst]="git st" + [gits]="git s" + [gitl]="git l" + [gitd]="git d" + [gitp]="git p" + [psuh]="push" + [psull]="pull" + [pul]="pull" + [puhs]="push" + [stauts]="status" + [statis]="status" + [statuus]="status" + [comit]="commit" + [commti]="commit" + [commt]="commit" + [chekcout]="checkout" + [chekout]="checkout" + [checkou]="checkout" + [branhc]="branch" + [barnch]="branch" + [bracnh]="branch" + [marge]="merge" + [merg]="merge" + [stsh]="stash" + [stahs]="stash" + + # Docker typos + [dokcer]="docker" + [doker]="docker" + [docekr]="docker" + [dcoker]="docker" + [dockr]="docker" + [docke]="docker" + [docker-compoes]="docker-compose" + [docker-compsoe]="docker-compose" + [dokcer-compose]="docker-compose" + + # Common command typos + [sl]="ls" + [l]="ls" + [sls]="ls" + [lss]="ls" + [cta]="cat" + [catt]="cat" + [caat]="cat" + [grpe]="grep" + [gerp]="grep" + [gre]="grep" + [grepp]="grep" + [mkdri]="mkdir" + [mkdr]="mkdir" + [mdkir]="mkdir" + [mdir]="mkdir" + [rn]="rm" + [rmm]="rm" + [chmdo]="chmod" + [chomd]="chmod" + [chonw]="chown" + [cown]="chown" + [tarr]="tar" + [tart]="tar" + [wegt]="wget" + [wgte]="wget" + [weget]="wget" + [crul]="curl" + [crul]="curl" + [curll]="curl" + [pytohn]="python" + [pyhton]="python" + [pythn]="python" + [pyton]="python" + [pthon]="python" + [pytho]="python" + [ndoe]="node" + [noed]="node" + [noode]="node" + [npn]="npm" + [nmpm]="npm" + [nppm]="npm" + [yran]="yarn" + [yaarn]="yarn" + [yanr]="yarn" + [suod]="sudo" + [sudi]="sudo" + [sduo]="sudo" + [sudoo]="sudo" + [sssh]="ssh" + [shh]="ssh" + [sssh]="ssh" + [scpp]="scp" + [spcp]="scp" + [vmi]="vim" + [imv]="vim" + [viim]="vim" + [cde]="code" + [cdoe]="code" + [cod]="code" + [clera]="clear" + [cler]="clear" + [claer]="clear" + [ecoh]="echo" + [ehco]="echo" + [echoo]="echo" + [exti]="exit" + [ext]="exit" + [exitt]="exit" + [eixt]="exit" + [histroy]="history" + [hisotry]="history" + [hsitory]="history" + [histrory]="history" + [maek]="make" + [mkae]="make" + [amke]="make" + [makee]="make" + [ccd]="cd" + [cdd]="cd" + [ccd]="cd" +) + +# ============================================================================ +# Package Manager Detection +# ============================================================================ + +_ss_get_package_manager() { + if command -v apt-get &>/dev/null; then + echo "apt" + elif command -v dnf &>/dev/null; then + echo "dnf" + elif command -v pacman &>/dev/null; then + echo "pacman" + elif command -v brew &>/dev/null; then + echo "brew" + else + echo "" + fi +} + +# Common commands and their packages +typeset -gA COMMAND_PACKAGES=( + [htop]="htop" + [tree]="tree" + [jq]="jq" + [fd]="fd-find:apt fd:pacman fd:brew" + [rg]="ripgrep" + [bat]="bat" + [eza]="eza" + [exa]="exa" + [fzf]="fzf" + [tldr]="tldr" + [ncdu]="ncdu" + [duf]="duf" + [dust]="dust" + [procs]="procs" + [bottom]="bottom" + [btm]="bottom" + [lazygit]="lazygit" + [lazydocker]="lazydocker" + [neofetch]="neofetch" + [fastfetch]="fastfetch" + [httpie]="httpie" + [http]="httpie" + [delta]="git-delta:apt delta:pacman git-delta:brew" + [glow]="glow" + [navi]="navi" +) + +_ss_suggest_package() { + local cmd="$1" + local pm=$(_ss_get_package_manager) + + [[ -z "$pm" ]] && return 1 + + local pkg_info="${COMMAND_PACKAGES[$cmd]}" + [[ -z "$pkg_info" ]] && return 1 + + local pkg="" + + # Check for PM-specific package name + if [[ "$pkg_info" == *":"* ]]; then + # Format: "pkg1:pm1 pkg2:pm2" + for entry in ${(s: :)pkg_info}; do + local p="${entry%%:*}" + local m="${entry##*:}" + if [[ "$m" == "$pm" ]]; then + pkg="$p" + break + fi + done + # Fallback to first package + [[ -z "$pkg" ]] && pkg="${${(s: :)pkg_info}[1]%%:*}" + else + pkg="$pkg_info" + fi + + [[ -z "$pkg" ]] && return 1 + + local install_cmd="" + case "$pm" in + apt) install_cmd="sudo apt install $pkg" ;; + dnf) install_cmd="sudo dnf install $pkg" ;; + pacman) install_cmd="sudo pacman -S $pkg" ;; + brew) install_cmd="brew install $pkg" ;; + esac + + echo "$install_cmd" +} + +# ============================================================================ +# Alias Tracking +# ============================================================================ + +_ss_track_command() { + [[ "$SMART_SUGGEST_ALIASES" != true ]] && return + + local cmd="$1" + [[ ${#cmd} -lt 8 ]] && return # Skip short commands + + mkdir -p "$(dirname "$SMART_SUGGEST_TRACK_FILE")" + echo "$cmd" >> "$SMART_SUGGEST_TRACK_FILE" + + # Periodically check for alias suggestions + local count=$(grep -Fc "$cmd" "$SMART_SUGGEST_TRACK_FILE" 2>/dev/null || echo 0) + + if [[ $count -ge 10 && $((count % 10)) -eq 0 ]]; then + _ss_suggest_alias_for "$cmd" "$count" + fi +} + +_ss_suggest_alias_for() { + local cmd="$1" + local count="$2" + + # Check if an alias already exists for this command + local existing=$(alias | grep -F "='$cmd'" | head -1 | cut -d= -f1) + + if [[ -n "$existing" ]]; then + echo + echo -e "${SS_CYAN}💡 Tip:${SS_NC} You've typed '${SS_YELLOW}$cmd${SS_NC}' $count times" + echo -e " You already have an alias: ${SS_GREEN}$existing${SS_NC}" + else + # Generate suggested alias name + local suggested=$(echo "$cmd" | awk '{ + for(i=1; i<=NF && i<=3; i++) + printf substr($i,1,1) + }') + + echo + echo -e "${SS_CYAN}💡 Tip:${SS_NC} You've typed '${SS_YELLOW}$cmd${SS_NC}' $count times" + echo -e " Consider adding: ${SS_GREEN}alias $suggested='$cmd'${SS_NC}" + fi +} + +# ============================================================================ +# Command Not Found Handler +# ============================================================================ + +command_not_found_handler() { + local cmd="$1" + shift + local args="$@" + + [[ "$SMART_SUGGEST_ENABLED" != true ]] && { + echo "zsh: command not found: $cmd" + return 127 + } + + echo -e "${SS_RED}✗${SS_NC} Command not found: ${SS_YELLOW}$cmd${SS_NC}" + + local suggestion_made=false + + # Check for typo + if [[ "$SMART_SUGGEST_TYPOS" == true ]]; then + local correction="${TYPO_CORRECTIONS[$cmd]}" + if [[ -n "$correction" ]]; then + echo -e "${SS_CYAN}→${SS_NC} Did you mean: ${SS_GREEN}$correction${SS_NC}?" + echo -e " ${SS_DIM}Run: $correction $args${SS_NC}" + suggestion_made=true + fi + fi + + # Check for similar commands + if [[ "$suggestion_made" != true ]]; then + local similar=$(compgen -c 2>/dev/null | grep -i "^${cmd:0:3}" | head -3 | tr '\n' ', ' | sed 's/,$//') + if [[ -n "$similar" ]]; then + echo -e "${SS_CYAN}→${SS_NC} Similar commands: ${SS_GREEN}$similar${SS_NC}" + suggestion_made=true + fi + fi + + # Suggest package installation + if [[ "$SMART_SUGGEST_PACKAGES" == true ]]; then + local install_cmd=$(_ss_suggest_package "$cmd") + if [[ -n "$install_cmd" ]]; then + echo -e "${SS_CYAN}→${SS_NC} To install: ${SS_GREEN}$install_cmd${SS_NC}" + suggestion_made=true + fi + fi + + return 127 +} + +# ============================================================================ +# Pre-exec Hook for Tracking +# ============================================================================ + +_ss_preexec_hook() { + local cmd="$1" + + # Extract just the command (first word) + local first_word="${cmd%% *}" + + # Track full commands for alias suggestions + _ss_track_command "$cmd" +} + +# ============================================================================ +# Post-command Hook for Suggestions +# ============================================================================ + +_ss_precmd_hook() { + local exit_code=$? + + # Only suggest if last command failed + [[ $exit_code -eq 0 ]] && return + + # Get last command + local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//') + [[ -z "$last_cmd" ]] && return + + local first_word="${last_cmd%% *}" + + # Check if it's a git command with typo + if [[ "$first_word" == "git" && $exit_code -ne 0 ]]; then + local git_subcmd=$(echo "$last_cmd" | awk '{print $2}') + local correction="${TYPO_CORRECTIONS[$git_subcmd]}" + + if [[ -n "$correction" ]]; then + echo -e "${SS_CYAN}→${SS_NC} Did you mean: ${SS_GREEN}git $correction${SS_NC}?" + fi + fi +} + +# ============================================================================ +# Quick Fix Function +# ============================================================================ + +# Run the suggested correction +# Usage: !! or just press up and edit +fuck() { + local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//') + local first_word="${last_cmd%% *}" + + # Check for typo correction + local correction="${TYPO_CORRECTIONS[$first_word]}" + + if [[ -n "$correction" ]]; then + local fixed_cmd="${last_cmd/$first_word/$correction}" + echo -e "${SS_GREEN}Running:${SS_NC} $fixed_cmd" + eval "$fixed_cmd" + else + echo "No automatic fix available" + echo "Last command: $last_cmd" + fi +} + +# ============================================================================ +# Setup Hooks +# ============================================================================ + +_ss_setup() { + # Add preexec hook + autoload -Uz add-zsh-hook + add-zsh-hook preexec _ss_preexec_hook + add-zsh-hook precmd _ss_precmd_hook +} + +# Initialize +[[ "$SMART_SUGGEST_ENABLED" == true ]] && _ss_setup + +# ============================================================================ +# Usage Examples (commented) +# ============================================================================ + +# $ gti status +# ✗ Command not found: gti +# → Did you mean: git? +# Run: git status + +# $ dokcer ps +# ✗ Command not found: dokcer +# → Did you mean: docker? + +# After typing "docker-compose up -d" 10 times: +# 💡 Tip: You've typed 'docker-compose up -d' 10 times +# Consider adding: alias dcu='docker-compose up -d' diff --git a/zsh/zshrc b/zsh/zshrc new file mode 100644 index 0000000..7c72075 --- /dev/null +++ b/zsh/zshrc @@ -0,0 +1,338 @@ +# ============================================================================ +# ADLee's ZSH Configuration +# ============================================================================ + +# Path to oh-my-zsh installation +export ZSH="$HOME/.oh-my-zsh" + +# ============================================================================ +# Theme Configuration +# ============================================================================ + +ZSH_THEME="adlee" + +# ============================================================================ +# Oh-My-Zsh Settings +# ============================================================================ + +# Update behavior +zstyle ':omz:update' mode reminder +zstyle ':omz:update' frequency 13 + +# Display red dots whilst waiting for completion +COMPLETION_WAITING_DOTS="true" + +# History timestamp format +HIST_STAMPS="yyyy-mm-dd" + +# ============================================================================ +# Plugins +# ============================================================================ + +plugins=( + git + docker + docker-compose + kubectl + sudo + fzf + zsh-autosuggestions + zsh-syntax-highlighting +) + +# Note: Install additional plugins with: +# git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions +# git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting + +# ============================================================================ +# Load Oh-My-Zsh +# ============================================================================ + +source $ZSH/oh-my-zsh.sh + +# ============================================================================ +# User Configuration +# ============================================================================ + +# --- Environment Variables --- + +export EDITOR='vim' +export VISUAL='vim' +export LANG=en_US.UTF-8 +export LC_ALL=en_US.UTF-8 +export PATH="$HOME/.local/bin:$PATH" + +# --- Aliases --- + +# Navigation +alias ..='cd ..' +alias ...='cd ../..' +alias ....='cd ../../..' +alias ~='cd ~' + +# List files +if command -v eza &> /dev/null; then + alias ls='eza --icons' + alias ll='eza -lah --icons' + alias la='eza -a --icons' + alias lt='eza --tree --level=2 --icons' +else + alias ll='ls -lah' + alias la='ls -A' +fi + +# Cat with syntax highlighting +if command -v batcat &> /dev/null; then + alias cat='batcat --paging=never' + alias bat='batcat' +elif command -v bat &> /dev/null; then + alias cat='bat --paging=never' +fi + +# Git shortcuts +alias g='git' +alias gs='git status' +alias ga='git add' +alias gc='git commit' +alias gp='git push' +alias gl='git pull' +alias gd='git diff' +alias gco='git checkout' +alias gb='git branch' +alias glog='git log --oneline --graph --decorate --all' + +# Docker shortcuts +alias d='docker' +alias dc='docker-compose' +alias dps='docker ps' +alias dpa='docker ps -a' +alias di='docker images' +alias dex='docker exec -it' + +# System shortcuts +alias reload='source ~/.zshrc' +alias zshconfig='vim ~/.zshrc' +alias themeconfig='vim ~/.oh-my-zsh/themes/adlee.zsh-theme' +alias h='history' +alias c='clear' + +# Safe operations +alias rm='rm -i' +alias cp='cp -i' +alias mv='mv -i' + +# Network +alias myip='curl ifconfig.me' +alias ports='netstat -tulanp' + +# --- Functions --- + +# Juuuust puuush it. +push-it() { + git add . + git commit -m "$1" + git push origin +} + +# Create directory and cd into it +mkcd() { + mkdir -p "$1" && cd "$1" +} + +# Extract various archive formats +extract() { + if [[ -f "$1" ]]; then + case "$1" in + *.tar.bz2) tar xjf "$1" ;; + *.tar.gz) tar xzf "$1" ;; + *.bz2) bunzip2 "$1" ;; + *.rar) unrar x "$1" ;; + *.gz) gunzip "$1" ;; + *.tar) tar xf "$1" ;; + *.tbz2) tar xjf "$1" ;; + *.tgz) tar xzf "$1" ;; + *.zip) unzip "$1" ;; + *.Z) uncompress "$1" ;; + *.7z) 7z x "$1" ;; + *) echo "'$1' cannot be extracted via extract()" ;; + esac + else + echo "'$1' is not a valid file" + fi +} + +# Quick find file +ff() { + find . -type f -iname "*$1*" +} + +# Quick find directory (renamed to avoid conflict with fd tool) +fdir() { + find . -type d -iname "*$1*" +} + +# Quick backup +backup() { + cp "$1" "$1.backup-$(date +%Y%m%d-%H%M%S)" +} + +# --- FZF Configuration --- + +if command -v fzf &> /dev/null; then + if command -v fd &> /dev/null; then + export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git' + export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" + fi + export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border' + bindkey '^R' fzf-history-widget +fi + +# --- History Configuration --- + +HISTSIZE=10000 +SAVEHIST=10000 +HISTFILE=~/.zsh_history + +setopt SHARE_HISTORY +setopt APPEND_HISTORY +setopt EXTENDED_HISTORY +setopt HIST_IGNORE_ALL_DUPS +setopt HIST_FIND_NO_DUPS +setopt HIST_IGNORE_SPACE + +# --- Key Bindings --- + +bindkey "^[[1;5C" forward-word # Ctrl+Right +bindkey "^[[1;5D" backward-word # Ctrl+Left +bindkey "^[[H" beginning-of-line # Home +bindkey "^[[F" end-of-line # End +bindkey "^[[3~" delete-char # Delete + +# --- Custom Widgets --- + +# Alt+R to reload zsh config +reload-zsh() { + source ~/.zshrc + echo "✓ zsh configuration reloaded" + zle reset-prompt +} +zle -N reload-zsh +bindkey "^[r" reload-zsh + +# Alt+G to show git status +git-status-widget() { + echo + git status + zle reset-prompt +} +zle -N git-status-widget +bindkey "^[g" git-status-widget + +# ============================================================================ +# Lazy-loaded Tools (for faster shell startup) +# ============================================================================ + +# --- NVM (lazy load) --- +# Only loads when you first use node, npm, nvm, or npx +export NVM_DIR="$HOME/.nvm" + +_load_nvm() { + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" +} + +# Create lazy-load wrappers +if [ -s "$NVM_DIR/nvm.sh" ]; then + nvm() { + unfunction nvm node npm npx 2>/dev/null + _load_nvm + nvm "$@" + } + node() { + unfunction nvm node npm npx 2>/dev/null + _load_nvm + node "$@" + } + npm() { + unfunction nvm node npm npx 2>/dev/null + _load_nvm + npm "$@" + } + npx() { + unfunction nvm node npm npx 2>/dev/null + _load_nvm + npx "$@" + } +fi + +# --- Python virtualenvwrapper (lazy load) --- +export WORKON_HOME=$HOME/.virtualenvs + +_load_virtualenvwrapper() { + export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3 + [ -f /usr/local/bin/virtualenvwrapper.sh ] && source /usr/local/bin/virtualenvwrapper.sh +} + +if [ -f /usr/local/bin/virtualenvwrapper.sh ]; then + workon() { + unfunction workon mkvirtualenv rmvirtualenv 2>/dev/null + _load_virtualenvwrapper + workon "$@" + } + mkvirtualenv() { + unfunction workon mkvirtualenv rmvirtualenv 2>/dev/null + _load_virtualenvwrapper + mkvirtualenv "$@" + } +fi + +# --- Rust cargo (only if exists) --- +[ -f "$HOME/.cargo/env" ] && source "$HOME/.cargo/env" + +# --- OS-Specific Configuration --- + +case "$(uname -s)" in + Darwin*) + export HOMEBREW_NO_ANALYTICS=1 + ;; +esac + +# --- Snapper Functions --- + +if [[ -f "$HOME/.dotfiles/zsh/functions/snapper.zsh" ]]; then + source "$HOME/.dotfiles/zsh/functions/snapper.zsh" +fi + +# --- Smart Command Suggestions --- + +if [[ -f "$HOME/.dotfiles/zsh/functions/smart-suggest.zsh" ]]; then + source "$HOME/.dotfiles/zsh/functions/smart-suggest.zsh" +fi + +# --- Command Palette (Ctrl+Space or Ctrl+P) --- + +if [[ -f "$HOME/.dotfiles/zsh/functions/command-palette.zsh" ]]; then + source "$HOME/.dotfiles/zsh/functions/command-palette.zsh" +fi + +# --- Dotfiles Sync Check (on shell start) --- + +if [[ "${DOTFILES_AUTO_SYNC_CHECK:-true}" == "true" ]]; then + # Quick async check for dotfiles updates + (dotfiles-sync.sh --auto 2>/dev/null &) +fi + +# --- Vault Integration --- + +# Source vault secrets into environment (if vault exists and has secrets) +if command -v vault.sh &>/dev/null && [[ -f "$HOME/.dotfiles/vault/secrets.enc" ]]; then + eval "$(vault.sh shell 2>/dev/null)" || true +fi + +# --- Local Configuration --- + +[ -f ~/.zshrc.local ] && source ~/.zshrc.local + +# ============================================================================ +# End of Configuration +# ============================================================================