Now with more FEATURES (the worth of which who knows, but I like em).
This commit is contained in:
451
bin/dotfiles-sync.sh
Normal file
451
bin/dotfiles-sync.sh
Normal file
@@ -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 <file>"
|
||||||
|
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 "$@"
|
||||||
583
bin/setup-wizard.sh
Normal file
583
bin/setup-wizard.sh
Normal file
@@ -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 "$@"
|
||||||
511
bin/shell-stats.sh
Normal file
511
bin/shell-stats.sh
Normal file
@@ -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 "$@"
|
||||||
504
bin/vault.sh
Normal file
504
bin/vault.sh
Normal file
@@ -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 <command> [args]"
|
||||||
|
echo
|
||||||
|
echo "Commands:"
|
||||||
|
echo " set <key> [value] Store a secret (prompts for value if not given)"
|
||||||
|
echo " get <key> Retrieve a secret"
|
||||||
|
echo " list List all keys (not values)"
|
||||||
|
echo " delete <key> Delete a secret"
|
||||||
|
echo " export [file] Export encrypted vault"
|
||||||
|
echo " import <file> 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 "$@"
|
||||||
@@ -56,6 +56,14 @@ ESPANSO_TRIGGER_PREFIX=".." # Prefix for all triggers (e.g., "..date")
|
|||||||
SNAPPER_CONFIG="root"
|
SNAPPER_CONFIG="root"
|
||||||
LIMINE_CONF="/boot/limine.conf"
|
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)
|
# Derived URLs (generally don't edit these)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
26
zsh/.zshrc
26
zsh/.zshrc
@@ -303,6 +303,32 @@ if [[ -f "$HOME/.dotfiles/zsh/functions/snapper.zsh" ]]; then
|
|||||||
source "$HOME/.dotfiles/zsh/functions/snapper.zsh"
|
source "$HOME/.dotfiles/zsh/functions/snapper.zsh"
|
||||||
fi
|
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 ---
|
# --- Local Configuration ---
|
||||||
|
|
||||||
[ -f ~/.zshrc.local ] && source ~/.zshrc.local
|
[ -f ~/.zshrc.local ] && source ~/.zshrc.local
|
||||||
|
|||||||
310
zsh/functions/command-palette.zsh
Normal file
310
zsh/functions/command-palette.zsh
Normal file
@@ -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 <name> [path]"
|
||||||
|
echo " bookmark list"
|
||||||
|
echo " bookmark delete <name>"
|
||||||
|
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."
|
||||||
446
zsh/themes/smart-suggest.zsh
Normal file
446
zsh/themes/smart-suggest.zsh
Normal file
@@ -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: !! <correction> 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'
|
||||||
338
zsh/zshrc
Normal file
338
zsh/zshrc
Normal file
@@ -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
|
||||||
|
# ============================================================================
|
||||||
Reference in New Issue
Block a user